778 lines
31 KiB
JavaScript
778 lines
31 KiB
JavaScript
(function (global) {
|
||
"use strict";
|
||
|
||
const GFE_DYNAMIC_KEYS = ["usp", "nel"];
|
||
|
||
let catalog = [];
|
||
let profiles = [];
|
||
let selectedId = null;
|
||
let draftNorms = {};
|
||
let dynamicNorms = {};
|
||
let normCoverage = null;
|
||
let normSourceMap = {};
|
||
let resolvedNormsPreview = {};
|
||
let seedCatalog = [];
|
||
let seedCatalogRation = null;
|
||
let normsCoverageTimer = null;
|
||
|
||
async function api(path, opts) {
|
||
const resp = await fetch(path, opts);
|
||
const data = await resp.json().catch(() => ({}));
|
||
if (!resp.ok) throw new Error(data.message || `Наука, чёрт возьми — ошибка ${resp.status}`);
|
||
return data;
|
||
}
|
||
|
||
function escapeHtml(s) {
|
||
return String(s ?? "")
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """);
|
||
}
|
||
|
||
function notify() {
|
||
return global.WespZootechNotify?.createAdapter?.();
|
||
}
|
||
|
||
function readMass() {
|
||
const raw = document.getElementById("labProfileMass")?.value?.trim();
|
||
if (raw === "") return null;
|
||
const n = Number(raw);
|
||
return Number.isNaN(n) ? null : n;
|
||
}
|
||
|
||
function readMilk() {
|
||
const raw = document.getElementById("labProfileMilk")?.value?.trim();
|
||
if (raw === "") return null;
|
||
const n = Number(raw);
|
||
return Number.isNaN(n) ? null : n;
|
||
}
|
||
|
||
function readNumInput(id) {
|
||
const raw = document.getElementById(id)?.value?.trim();
|
||
if (raw === "") return null;
|
||
const n = Number(raw);
|
||
return Number.isNaN(n) ? null : n;
|
||
}
|
||
|
||
function readNormsMethod() {
|
||
return document.getElementById("labProfileNormsMethod")?.value || "wesp";
|
||
}
|
||
|
||
function readNormsParams() {
|
||
const method = readNormsMethod();
|
||
if (method === "wesp") return {};
|
||
const params = {
|
||
milkFatPct: readNumInput("labProfileFat") ?? 4,
|
||
lactationNo: readNumInput("labProfileLactation") ?? 2,
|
||
};
|
||
if (method === "racion_piter") {
|
||
params.koncOeSv = readNumInput("labProfileKonc") ?? 10.3;
|
||
}
|
||
return params;
|
||
}
|
||
|
||
function syncNormsMethodUi() {
|
||
const method = readNormsMethod();
|
||
const racionBox = document.getElementById("labProfileRacionFields");
|
||
const piterOnly = document.querySelectorAll(".lab-profile-piter-only");
|
||
if (racionBox) racionBox.classList.toggle("d-none", method === "wesp");
|
||
piterOnly.forEach((el) => el.classList.toggle("d-none", method !== "racion_piter"));
|
||
syncSourceFilterOptions(method);
|
||
renderNormsSummary();
|
||
scheduleNormsCoverageRefresh();
|
||
}
|
||
|
||
function syncSourceFilterOptions(method) {
|
||
const sel = document.getElementById("labProfilesSourceFilter");
|
||
if (!sel) return;
|
||
sel.querySelectorAll("option[data-racion-only]").forEach((opt) => {
|
||
opt.hidden = method === "wesp";
|
||
opt.disabled = method === "wesp";
|
||
});
|
||
if (method === "wesp" && ["racion", "racion_only", "derived", "new_racion"].includes(sel.value)) {
|
||
sel.value = "all";
|
||
}
|
||
}
|
||
|
||
function buildNormSourceMap(coverage, storedNorms) {
|
||
const map = {};
|
||
if (coverage) {
|
||
(coverage.racion || []).forEach((k) => {
|
||
map[k] = "racion";
|
||
});
|
||
(coverage.derived || []).forEach((k) => {
|
||
map[k] = "derived";
|
||
});
|
||
(coverage.fallback || []).forEach((k) => {
|
||
if (!map[k]) map[k] = "fallback";
|
||
});
|
||
(coverage.missing || []).forEach((k) => {
|
||
if (!map[k]) map[k] = "missing";
|
||
});
|
||
}
|
||
Object.keys(storedNorms || {}).forEach((k) => {
|
||
if (!map[k]) map[k] = "stored";
|
||
});
|
||
return map;
|
||
}
|
||
|
||
function normSourceLabel(src) {
|
||
const labels = {
|
||
racion: "RACION",
|
||
derived: "расч.",
|
||
fallback: "WESP",
|
||
stored: "БД",
|
||
missing: "—",
|
||
gfe: "GfE",
|
||
};
|
||
return labels[src] || "—";
|
||
}
|
||
|
||
function normSourceBadge(src) {
|
||
if (!src || src === "missing") return '<span class="norm-source-badge norm-source-badge--empty">—</span>';
|
||
const cls = `norm-source-badge norm-source-badge--${src}`;
|
||
return `<span class="${cls}" title="${escapeHtml(normSourceLabel(src))}">${escapeHtml(normSourceLabel(src))}</span>`;
|
||
}
|
||
|
||
function isInDb(key) {
|
||
const b = draftNorms[key] || {};
|
||
return b.min != null || b.max != null;
|
||
}
|
||
|
||
function matchesSourceFilter(key) {
|
||
const filter = document.getElementById("labProfilesSourceFilter")?.value || "all";
|
||
if (filter === "all") return true;
|
||
const src = normSourceMap[key];
|
||
if (filter === "in_db") return isInDb(key);
|
||
if (filter === "seed") return src === "fallback" || src === "stored";
|
||
if (filter === "racion") return src === "racion" || src === "derived";
|
||
if (filter === "racion_only") return src === "racion";
|
||
if (filter === "derived") return src === "derived";
|
||
if (filter === "new_racion") return src === "racion" || src === "derived";
|
||
return true;
|
||
}
|
||
|
||
function countNormSources() {
|
||
const counts = { inDb: 0, racion: 0, derived: 0, fallback: 0, stored: 0, missing: 0 };
|
||
const seen = new Set();
|
||
catalog.forEach((c) => seen.add(c.key));
|
||
Object.keys(draftNorms).forEach((k) => seen.add(k));
|
||
if (normCoverage) {
|
||
(normCoverage.racion || []).forEach((k) => seen.add(k));
|
||
(normCoverage.derived || []).forEach((k) => seen.add(k));
|
||
(normCoverage.fallback || []).forEach((k) => seen.add(k));
|
||
(normCoverage.missing || []).forEach((k) => seen.add(k));
|
||
}
|
||
seen.forEach((key) => {
|
||
if (isInDb(key)) counts.inDb += 1;
|
||
const src = normSourceMap[key];
|
||
if (src === "racion") counts.racion += 1;
|
||
else if (src === "derived") counts.derived += 1;
|
||
else if (src === "fallback") counts.fallback += 1;
|
||
else if (src === "stored") counts.stored += 1;
|
||
else if (src === "missing") counts.missing += 1;
|
||
});
|
||
return counts;
|
||
}
|
||
|
||
function renderNormsSummary() {
|
||
const panel = document.getElementById("labProfilesNormsSummary");
|
||
const text = document.getElementById("labProfilesNormsSummaryText");
|
||
const form = document.getElementById("labProfilesForm");
|
||
if (!panel || !text || !form || form.hidden) {
|
||
if (panel) panel.hidden = true;
|
||
return;
|
||
}
|
||
const method = readNormsMethod();
|
||
const counts = countNormSources();
|
||
if (method === "wesp") {
|
||
panel.hidden = false;
|
||
text.textContent = `В БД ${counts.inDb} показ. · справочник/ручные ${counts.stored + counts.fallback}`;
|
||
return;
|
||
}
|
||
if (!normCoverage) {
|
||
panel.hidden = false;
|
||
text.textContent = "Укажи массу и удой — разберём источники RACION";
|
||
return;
|
||
}
|
||
panel.hidden = false;
|
||
const racTotal = counts.racion + counts.derived;
|
||
const withMin = normCoverage.withMin != null ? normCoverage.withMin : racTotal + counts.fallback;
|
||
text.textContent =
|
||
`В БД ${counts.inDb} · RACION ${racTotal} (NPitV ${counts.racion} + расч. ${counts.derived}) · ` +
|
||
`WESP fallback ${counts.fallback} · с min ${withMin}/${normCoverage.total ?? "—"}`;
|
||
}
|
||
|
||
function applyNormsCoveragePayload(payload) {
|
||
normCoverage = payload.coverage || null;
|
||
resolvedNormsPreview = payload.resolvedIndicators || {};
|
||
if (payload.dynamicNorms) {
|
||
dynamicNorms = { ...dynamicNorms, ...payload.dynamicNorms };
|
||
}
|
||
normSourceMap = buildNormSourceMap(normCoverage, draftNorms);
|
||
renderNormsSummary();
|
||
renderNormsTable();
|
||
}
|
||
|
||
async function refreshNormsCoverage() {
|
||
const method = readNormsMethod();
|
||
const mass = readMass();
|
||
const milk = readMilk();
|
||
if (method === "wesp") {
|
||
normCoverage = null;
|
||
resolvedNormsPreview = {};
|
||
normSourceMap = buildNormSourceMap(null, draftNorms);
|
||
GFE_DYNAMIC_KEYS.forEach((key) => {
|
||
if (dynamicNorms[key]?.min != null && !isInDb(key)) normSourceMap[key] = "gfe";
|
||
});
|
||
renderNormsSummary();
|
||
renderNormsTable();
|
||
return;
|
||
}
|
||
if (mass == null || milk == null) {
|
||
normCoverage = null;
|
||
normSourceMap = buildNormSourceMap(null, draftNorms);
|
||
renderNormsSummary();
|
||
renderNormsTable();
|
||
return;
|
||
}
|
||
const q = new URLSearchParams({ method, mass_kg: String(mass), milk_yield_kg: String(milk) });
|
||
if (selectedId) q.set("profile_id", selectedId);
|
||
const np = readNormsParams();
|
||
if (np.milkFatPct != null) q.set("milk_fat_pct", String(np.milkFatPct));
|
||
if (np.lactationNo != null) q.set("lactation_no", String(np.lactationNo));
|
||
if (np.koncOeSv != null) q.set("konc_oe_sv", String(np.koncOeSv));
|
||
try {
|
||
const preview = await api(`/api/lab/norms-preview?${q.toString()}`);
|
||
applyNormsCoveragePayload(preview);
|
||
} catch (err) {
|
||
notify()?.error?.(err.message);
|
||
}
|
||
}
|
||
|
||
function scheduleNormsCoverageRefresh() {
|
||
if (normsCoverageTimer) clearTimeout(normsCoverageTimer);
|
||
normsCoverageTimer = setTimeout(() => {
|
||
refreshNormsCoverage().catch((e) => notify()?.error?.(e.message, { fallback: "Не удалось выполнить операцию" }));
|
||
}, 350);
|
||
}
|
||
|
||
function fillNormsParamsFromProfile(p) {
|
||
const method = p.normsMethod || "wesp";
|
||
const sel = document.getElementById("labProfileNormsMethod");
|
||
if (sel) sel.value = method;
|
||
const np = p.normsParams || {};
|
||
const fat = document.getElementById("labProfileFat");
|
||
const lact = document.getElementById("labProfileLactation");
|
||
const konc = document.getElementById("labProfileKonc");
|
||
if (fat) fat.value = np.milkFatPct != null ? np.milkFatPct : 4;
|
||
if (lact) lact.value = np.lactationNo != null ? np.lactationNo : 2;
|
||
if (konc) konc.value = np.koncOeSv != null ? np.koncOeSv : 10.3;
|
||
syncNormsMethodUi();
|
||
}
|
||
|
||
/** GfE 2001 — parity app/lab/calc/gfe_norms.py */
|
||
function gfeUspMinG(massKg, milkKg) {
|
||
if (!massKg || massKg <= 0) return null;
|
||
return 0.09 * massKg ** 0.75 * 6.25 + Math.max(milkKg || 0, 0) * 85;
|
||
}
|
||
|
||
function gfeNelMinMj(massKg, milkKg) {
|
||
if (!massKg || massKg <= 0) return null;
|
||
return 0.293 * massKg ** 0.75 + Math.max(milkKg || 0, 0) * 3.3;
|
||
}
|
||
|
||
function computeDynamicNormsLocal() {
|
||
const mass = readMass();
|
||
const milk = readMilk();
|
||
const out = {};
|
||
const usp = gfeUspMinG(mass, milk);
|
||
if (usp != null) {
|
||
out.usp = { min: usp, formula: "0.09×масса^0.75×6.25 + удой×85" };
|
||
}
|
||
const nel = gfeNelMinMj(mass, milk);
|
||
if (nel != null) {
|
||
out.nel = { min: nel, formula: "0.293×масса^0.75 + удой×3.3" };
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function formatGfe(v) {
|
||
const n = Number(v);
|
||
if (Number.isNaN(n)) return "—";
|
||
return Math.abs(n) >= 100 ? n.toFixed(1) : n.toFixed(2);
|
||
}
|
||
|
||
function refreshDynamicNorms() {
|
||
dynamicNorms = computeDynamicNormsLocal();
|
||
renderGfePanel();
|
||
renderNormsTable();
|
||
}
|
||
|
||
function renderGfePanel() {
|
||
const panel = document.getElementById("labProfilesGfePanel");
|
||
const vals = document.getElementById("labProfilesGfeValues");
|
||
const form = document.getElementById("labProfilesForm");
|
||
if (!panel || !form || form.hidden) {
|
||
if (panel) panel.hidden = true;
|
||
return;
|
||
}
|
||
const mass = readMass();
|
||
if (!mass || mass <= 0) {
|
||
panel.hidden = true;
|
||
return;
|
||
}
|
||
panel.hidden = false;
|
||
const parts = [];
|
||
if (dynamicNorms.usp?.min != null) {
|
||
parts.push(`min уСП ≈ ${formatGfe(dynamicNorms.usp.min)} г`);
|
||
}
|
||
if (dynamicNorms.nel?.min != null) {
|
||
parts.push(`min ЧЭЛ ≈ ${formatGfe(dynamicNorms.nel.min)} МДж`);
|
||
}
|
||
if (vals) vals.textContent = parts.join(" · ") || "—";
|
||
const enabled = parts.length > 0;
|
||
["labProfilesGfeApplyUsp", "labProfilesGfeApplyNel", "labProfilesGfeApplyAll"].forEach((id) => {
|
||
const btn = document.getElementById(id);
|
||
if (btn) btn.disabled = !enabled;
|
||
});
|
||
}
|
||
|
||
function applyGfeMin(keys) {
|
||
collectNormsFromTable();
|
||
let applied = 0;
|
||
keys.forEach((key) => {
|
||
const v = dynamicNorms[key]?.min;
|
||
if (v == null) return;
|
||
const cur = draftNorms[key] || {};
|
||
if (cur.min != null) return;
|
||
draftNorms[key] = { ...cur, min: v };
|
||
applied += 1;
|
||
});
|
||
renderNormsTable();
|
||
if (applied) {
|
||
notify()?.success?.("Подставлено по GfE — нажми «Запомни это»");
|
||
} else {
|
||
notify()?.error?.("Нижние границы уже заданы или нет массы");
|
||
}
|
||
}
|
||
|
||
function syncDeleteButton() {
|
||
const btn = document.getElementById("labProfilesDelete");
|
||
if (btn) btn.hidden = !selectedId;
|
||
}
|
||
|
||
function readRationType() {
|
||
return document.getElementById("labProfileType")?.value || "DAIRY";
|
||
}
|
||
|
||
function syncSeedPanelVisibility() {
|
||
const panel = document.getElementById("labProfilesSeedPanel");
|
||
const form = document.getElementById("labProfilesForm");
|
||
if (panel) panel.hidden = !form || form.hidden;
|
||
}
|
||
|
||
function filteredSeedCatalog() {
|
||
const q = (document.getElementById("labProfilesSeedSearch")?.value || "").trim().toLowerCase();
|
||
if (!q) return seedCatalog;
|
||
return seedCatalog.filter((e) => {
|
||
const hay = `${e.externalNo} ${e.label} ${e.massKg ?? ""}`.toLowerCase();
|
||
return hay.includes(q);
|
||
});
|
||
}
|
||
|
||
function renderSeedSelect() {
|
||
const sel = document.getElementById("labProfilesSeedSelect");
|
||
const applyBtn = document.getElementById("labProfilesSeedApply");
|
||
if (!sel) return;
|
||
const prev = sel.value;
|
||
const rows = filteredSeedCatalog();
|
||
const opts = ['<option value="">— выбери строку —</option>'].concat(
|
||
rows.map((e) => {
|
||
const mass = e.massKg != null ? ` · ${e.massKg} кг` : "";
|
||
const cnt = e.indicatorCount != null ? ` · ${e.indicatorCount} показ.` : "";
|
||
return `<option value="${escapeHtml(String(e.externalNo))}">№${escapeHtml(String(e.externalNo))} — ${escapeHtml(e.label)}${escapeHtml(mass)}${escapeHtml(cnt)}</option>`;
|
||
})
|
||
);
|
||
sel.innerHTML = opts.join("");
|
||
if (prev && rows.some((e) => String(e.externalNo) === prev)) {
|
||
sel.value = prev;
|
||
}
|
||
if (applyBtn) applyBtn.disabled = !sel.value;
|
||
}
|
||
|
||
async function loadSeedCatalog(rationType) {
|
||
const ration = rationType || readRationType();
|
||
if (seedCatalogRation === ration && seedCatalog.length) {
|
||
renderSeedSelect();
|
||
return;
|
||
}
|
||
const data = await api(`/api/lab/seed-norms-catalog?ration_type=${encodeURIComponent(ration)}`);
|
||
seedCatalog = data.entries || [];
|
||
seedCatalogRation = ration;
|
||
renderSeedSelect();
|
||
}
|
||
|
||
async function applySeedNorms() {
|
||
const sel = document.getElementById("labProfilesSeedSelect");
|
||
const externalNo = sel?.value;
|
||
if (!externalNo) {
|
||
notify()?.error?.("Выбери строку справочника");
|
||
return;
|
||
}
|
||
const filled = Object.values(draftNorms).filter((b) => b.min != null || b.max != null).length;
|
||
if (filled > 0) {
|
||
const ok = global.confirm("Заменить текущие границы нормами из справочника?");
|
||
if (!ok) return;
|
||
}
|
||
const ration = readRationType();
|
||
const entry = await api(
|
||
`/api/lab/seed-norms-catalog/${encodeURIComponent(externalNo)}?ration_type=${encodeURIComponent(ration)}`
|
||
);
|
||
draftNorms = { ...(entry.indicators || {}) };
|
||
if (entry.massKg != null) {
|
||
document.getElementById("labProfileMass").value = entry.massKg;
|
||
}
|
||
document.getElementById("labProfileExternal").value = entry.externalNo;
|
||
const labelEl = document.getElementById("labProfileLabel");
|
||
if (labelEl && !labelEl.value.trim()) {
|
||
labelEl.value = entry.label || "";
|
||
}
|
||
refreshDynamicNorms();
|
||
notify()?.success?.(`Загружено ${Object.keys(draftNorms).length} показателей — нажми «Запомни это»`);
|
||
}
|
||
|
||
function filteredProfiles() {
|
||
const type = document.getElementById("labProfilesType")?.value || "";
|
||
const q = (document.getElementById("labProfilesSearch")?.value || "").trim().toLowerCase();
|
||
return profiles.filter((p) => {
|
||
if (type && p.rationType !== type) return false;
|
||
if (!q) return true;
|
||
const hay = `${p.profileKey} ${p.label}`.toLowerCase();
|
||
return hay.includes(q);
|
||
});
|
||
}
|
||
|
||
function renderList() {
|
||
const el = document.getElementById("labProfilesList");
|
||
if (!el) return;
|
||
const rows = filteredProfiles();
|
||
if (!rows.length) {
|
||
el.innerHTML = '<div class="lab-sandbox-empty">Стандартов нет — создай новый</div>';
|
||
return;
|
||
}
|
||
el.innerHTML = rows
|
||
.map(
|
||
(p) =>
|
||
`<button type="button" class="${p.id === selectedId ? "is-active" : ""}" data-id="${escapeHtml(p.id)}">` +
|
||
`${escapeHtml(p.label)}` +
|
||
`<span class="profile-key">${escapeHtml(p.profileKey)} · ${escapeHtml(p.rationType)}</span>` +
|
||
`</button>`
|
||
)
|
||
.join("");
|
||
}
|
||
|
||
function indicatorRows() {
|
||
const onlyCalc = document.getElementById("labProfilesOnlyCalc")?.checked;
|
||
const onlyFilled = document.getElementById("labProfilesOnlyFilled")?.checked;
|
||
const q = (document.getElementById("labProfilesNormSearch")?.value || "").trim().toLowerCase();
|
||
const extraKeys = Object.keys(draftNorms).filter((k) => !catalog.some((c) => c.key === k));
|
||
const rows = [
|
||
...catalog.map((c) => ({ ...c })),
|
||
...extraKeys.map((k) => ({ key: k, label: k, unit: "", inCalc: false })),
|
||
];
|
||
return rows.filter((row) => {
|
||
if (onlyCalc && !row.inCalc) return false;
|
||
const b = draftNorms[row.key] || {};
|
||
const has = b.min != null || b.max != null;
|
||
if (onlyFilled && !has) return false;
|
||
if (!matchesSourceFilter(row.key)) return false;
|
||
if (q) {
|
||
const hay = `${row.key} ${row.label}`.toLowerCase();
|
||
if (!hay.includes(q)) return false;
|
||
}
|
||
return true;
|
||
});
|
||
}
|
||
|
||
function renderNormsTable() {
|
||
const tbody = document.getElementById("labProfilesNormsBody");
|
||
const countEl = document.getElementById("labProfilesNormCount");
|
||
if (!tbody) return;
|
||
const rows = indicatorRows();
|
||
tbody.innerHTML = rows
|
||
.map((row) => {
|
||
const b = draftNorms[row.key] || {};
|
||
const min = b.min != null ? b.min : "";
|
||
const max = b.max != null ? b.max : "";
|
||
const badge = row.inCalc ? '<span class="norm-calc-badge ms-1">в деле</span>' : "";
|
||
const unit = row.unit ? ` <span class="text-muted">${escapeHtml(row.unit)}</span>` : "";
|
||
const dyn = dynamicNorms[row.key];
|
||
const gfeHint =
|
||
dyn?.min != null && min === ""
|
||
? `<small class="text-muted d-block">GfE ≈ ${formatGfe(dyn.min)}${dyn.formula ? ` (${escapeHtml(dyn.formula)})` : ""}</small>`
|
||
: "";
|
||
const gfeBadge = GFE_DYNAMIC_KEYS.includes(row.key)
|
||
? '<span class="norm-calc-badge ms-1" title="min по GfE 2001, если нижняя пуста">GfE</span>'
|
||
: "";
|
||
let src = normSourceMap[row.key];
|
||
if (!src && isInDb(row.key)) src = "stored";
|
||
const dbBadge = isInDb(row.key) ? '<span class="norm-source-badge norm-source-badge--db" title="Сохранено в БД">БД</span>' : "";
|
||
const srcBadge = normSourceBadge(src);
|
||
const resolved = resolvedNormsPreview[row.key];
|
||
const resolvedHint =
|
||
resolved?.min != null && min !== "" && Number(min) !== Number(resolved.min)
|
||
? `<small class="text-muted d-block">RACION ≈ ${formatGfe(resolved.min)}</small>`
|
||
: resolved?.min != null && min === ""
|
||
? `<small class="text-muted d-block">RACION ≈ ${formatGfe(resolved.min)} (не в БД)</small>`
|
||
: "";
|
||
return (
|
||
`<tr data-key="${escapeHtml(row.key)}">` +
|
||
`<td>${escapeHtml(row.label)}${badge}${gfeBadge}${unit}${gfeHint}${resolvedHint}</td>` +
|
||
`<td class="lab-profiles-source-cell">${dbBadge}${srcBadge}</td>` +
|
||
`<td><input type="number" class="form-control form-control-sm" data-bound="min" step="any" value="${min}"` +
|
||
`${dyn?.min != null && min === "" ? ` placeholder="${formatGfe(dyn.min)}"` : ""}></td>` +
|
||
`<td><input type="number" class="form-control form-control-sm" data-bound="max" step="any" value="${max}"></td>` +
|
||
`</tr>`
|
||
);
|
||
})
|
||
.join("");
|
||
if (countEl) {
|
||
const filled = Object.values(draftNorms).filter((b) => b.min != null || b.max != null).length;
|
||
const counts = countNormSources();
|
||
const method = readNormsMethod();
|
||
const srcPart =
|
||
method === "wesp"
|
||
? ` · в БД ${counts.inDb}`
|
||
: ` · БД ${counts.inDb} · RACION ${counts.racion + counts.derived} · WESP ${counts.fallback}`;
|
||
countEl.textContent = `В таблице ${rows.length} · с границами ${filled}${srcPart}`;
|
||
}
|
||
renderNormsSummary();
|
||
}
|
||
|
||
function collectNormsFromTable() {
|
||
const out = { ...draftNorms };
|
||
document.querySelectorAll("#labProfilesNormsBody tr[data-key]").forEach((tr) => {
|
||
const key = tr.getAttribute("data-key");
|
||
if (!key) return;
|
||
const minIn = tr.querySelector('[data-bound="min"]');
|
||
const maxIn = tr.querySelector('[data-bound="max"]');
|
||
const minRaw = minIn?.value?.trim();
|
||
const maxRaw = maxIn?.value?.trim();
|
||
const min = minRaw === "" ? null : Number(minRaw);
|
||
const max = maxRaw === "" ? null : Number(maxRaw);
|
||
if (min == null && max == null) {
|
||
delete out[key];
|
||
} else {
|
||
out[key] = {
|
||
min: Number.isNaN(min) ? null : min,
|
||
max: Number.isNaN(max) ? null : max,
|
||
};
|
||
}
|
||
});
|
||
draftNorms = out;
|
||
}
|
||
|
||
async function selectProfile(id) {
|
||
selectedId = id;
|
||
syncDeleteButton();
|
||
renderList();
|
||
const empty = document.getElementById("labProfilesEmpty");
|
||
const form = document.getElementById("labProfilesForm");
|
||
if (!id) {
|
||
if (empty) empty.hidden = false;
|
||
if (form) form.hidden = true;
|
||
syncSeedPanelVisibility();
|
||
return;
|
||
}
|
||
const p = await api(`/api/lab/animal-profiles/${encodeURIComponent(id)}`);
|
||
if (empty) empty.hidden = true;
|
||
if (form) form.hidden = false;
|
||
document.getElementById("labProfileKey").value = p.profileKey || "";
|
||
document.getElementById("labProfileLabel").value = p.label || "";
|
||
document.getElementById("labProfileType").value = p.rationType || "BEEF";
|
||
document.getElementById("labProfileMass").value = p.massKg != null ? p.massKg : "";
|
||
document.getElementById("labProfileMilk").value = p.milkYieldKg != null ? p.milkYieldKg : "";
|
||
document.getElementById("labProfileExternal").value = p.externalNo != null ? p.externalNo : "";
|
||
fillNormsParamsFromProfile(p);
|
||
draftNorms = { ...(p.norms || p.normsData?.indicators || {}) };
|
||
normCoverage = p.coverage || p.normsData?.coverage || null;
|
||
resolvedNormsPreview = p.resolvedNorms || p.normsData?.resolvedIndicators || {};
|
||
normSourceMap = buildNormSourceMap(normCoverage, draftNorms);
|
||
refreshDynamicNorms();
|
||
syncSourceFilterOptions(readNormsMethod());
|
||
renderNormsSummary();
|
||
scheduleNormsCoverageRefresh();
|
||
syncSeedPanelVisibility();
|
||
loadSeedCatalog(p.rationType || "DAIRY").catch(() => notify()?.error?.(null, { fallback: "Не удалось выполнить операцию" }));
|
||
}
|
||
|
||
function newProfile() {
|
||
selectedId = null;
|
||
syncDeleteButton();
|
||
renderList();
|
||
document.getElementById("labProfilesEmpty").hidden = true;
|
||
document.getElementById("labProfilesForm").hidden = false;
|
||
document.getElementById("labProfileKey").value = "";
|
||
document.getElementById("labProfileLabel").value = "";
|
||
document.getElementById("labProfileType").value = "DAIRY";
|
||
document.getElementById("labProfileMass").value = "";
|
||
document.getElementById("labProfileMilk").value = "";
|
||
document.getElementById("labProfileExternal").value = "";
|
||
fillNormsParamsFromProfile({ normsMethod: "wesp", normsParams: {} });
|
||
draftNorms = {};
|
||
normCoverage = null;
|
||
resolvedNormsPreview = {};
|
||
normSourceMap = {};
|
||
refreshDynamicNorms();
|
||
syncSourceFilterOptions("wesp");
|
||
renderNormsSummary();
|
||
syncSeedPanelVisibility();
|
||
loadSeedCatalog("DAIRY").catch(() => notify()?.error?.(null, { fallback: "Не удалось выполнить операцию" }));
|
||
}
|
||
|
||
async function saveProfile() {
|
||
collectNormsFromTable();
|
||
const key = document.getElementById("labProfileKey")?.value?.trim();
|
||
const label = document.getElementById("labProfileLabel")?.value?.trim();
|
||
const rationType = document.getElementById("labProfileType")?.value || "BEEF";
|
||
const massRaw = document.getElementById("labProfileMass")?.value?.trim();
|
||
const milkRaw = document.getElementById("labProfileMilk")?.value?.trim();
|
||
const extRaw = document.getElementById("labProfileExternal")?.value?.trim();
|
||
if (!key || !label) {
|
||
notify()?.error?.("Укажи код и название — я предупреждал");
|
||
return;
|
||
}
|
||
const payload = {
|
||
profileKey: key,
|
||
label,
|
||
rationType,
|
||
indicators: draftNorms,
|
||
normsMethod: readNormsMethod(),
|
||
normsParams: readNormsParams(),
|
||
};
|
||
if (massRaw !== "") payload.massKg = Number(massRaw);
|
||
payload.milkYieldKg = milkRaw === "" ? null : Number(milkRaw);
|
||
if (extRaw !== "") payload.externalNo = Number(extRaw);
|
||
const method = selectedId ? "PUT" : "POST";
|
||
const url = selectedId
|
||
? `/api/lab/animal-profiles/${encodeURIComponent(selectedId)}`
|
||
: "/api/lab/animal-profiles";
|
||
await api(url, {
|
||
method,
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
notify()?.success?.("Стандарт сохранён. Ты чертовски прав");
|
||
const typeFilter = document.getElementById("labProfilesType")?.value || "";
|
||
const data = await api(
|
||
`/api/lab/animal-profiles${typeFilter ? `?ration_type=${encodeURIComponent(typeFilter)}` : ""}`
|
||
);
|
||
profiles = data.profiles || [];
|
||
if (!selectedId && profiles.length) {
|
||
const hit = profiles.find((p) => p.profileKey === key);
|
||
if (hit) selectedId = hit.id;
|
||
}
|
||
renderList();
|
||
if (selectedId) await selectProfile(selectedId);
|
||
}
|
||
|
||
async function deleteProfile() {
|
||
if (!selectedId) return;
|
||
const label = document.getElementById("labProfileLabel")?.value?.trim() || selectedId;
|
||
if (!global.confirm(`Удалить «${label}»? Никаких полумер.`)) return;
|
||
await api(`/api/lab/animal-profiles/${encodeURIComponent(selectedId)}`, { method: "DELETE" });
|
||
notify()?.success?.("Стандарт снесён. Say my name.");
|
||
selectedId = null;
|
||
syncDeleteButton();
|
||
const typeFilter = document.getElementById("labProfilesType")?.value || "";
|
||
const data = await api(
|
||
`/api/lab/animal-profiles${typeFilter ? `?ration_type=${encodeURIComponent(typeFilter)}` : ""}`
|
||
);
|
||
profiles = data.profiles || [];
|
||
document.getElementById("labProfilesEmpty").hidden = false;
|
||
document.getElementById("labProfilesForm").hidden = true;
|
||
renderList();
|
||
}
|
||
|
||
async function init() {
|
||
const [cat, prof] = await Promise.all([
|
||
api("/api/lab/norm-indicators"),
|
||
api("/api/lab/animal-profiles"),
|
||
]);
|
||
catalog = cat.indicators || [];
|
||
profiles = prof.profiles || [];
|
||
renderList();
|
||
}
|
||
|
||
function bind() {
|
||
document.getElementById("labProfilesList")?.addEventListener("click", (ev) => {
|
||
const btn = ev.target.closest("button[data-id]");
|
||
if (!btn) return;
|
||
selectProfile(btn.getAttribute("data-id")).catch(() => notify()?.error?.(null, { fallback: "Не удалось выполнить операцию" }));
|
||
});
|
||
document.getElementById("labProfilesType")?.addEventListener("change", async () => {
|
||
const type = document.getElementById("labProfilesType")?.value || "";
|
||
const data = await api(
|
||
`/api/lab/animal-profiles${type ? `?ration_type=${encodeURIComponent(type)}` : ""}`
|
||
);
|
||
profiles = data.profiles || [];
|
||
renderList();
|
||
});
|
||
document.getElementById("labProfilesSearch")?.addEventListener("input", renderList);
|
||
document.getElementById("labProfilesOnlyFilled")?.addEventListener("change", renderNormsTable);
|
||
document.getElementById("labProfilesOnlyCalc")?.addEventListener("change", renderNormsTable);
|
||
document.getElementById("labProfilesSourceFilter")?.addEventListener("change", renderNormsTable);
|
||
document.getElementById("labProfilesNormSearch")?.addEventListener("input", renderNormsTable);
|
||
document.getElementById("labProfilesNormsRefresh")?.addEventListener("click", () => {
|
||
refreshNormsCoverage().catch((e) => notify()?.error?.(e.message, { fallback: "Не удалось выполнить операцию" }));
|
||
});
|
||
document.getElementById("labProfilesNew")?.addEventListener("click", newProfile);
|
||
document.getElementById("labProfilesSave")?.addEventListener("click", () => {
|
||
saveProfile().catch(() => notify()?.error?.(null, { fallback: "Не удалось выполнить операцию" }));
|
||
});
|
||
document.getElementById("labProfilesDelete")?.addEventListener("click", () => {
|
||
deleteProfile().catch(() => notify()?.error?.(null, { fallback: "Не удалось выполнить операцию" }));
|
||
});
|
||
document.getElementById("labProfilesNormsBody")?.addEventListener("change", (ev) => {
|
||
if (ev.target.matches("input[data-bound]")) collectNormsFromTable();
|
||
});
|
||
document.getElementById("labProfileMass")?.addEventListener("input", () => {
|
||
refreshDynamicNorms();
|
||
scheduleNormsCoverageRefresh();
|
||
});
|
||
document.getElementById("labProfileMilk")?.addEventListener("input", () => {
|
||
refreshDynamicNorms();
|
||
scheduleNormsCoverageRefresh();
|
||
});
|
||
document.getElementById("labProfileNormsMethod")?.addEventListener("change", syncNormsMethodUi);
|
||
["labProfileFat", "labProfileLactation", "labProfileKonc"].forEach((id) => {
|
||
document.getElementById(id)?.addEventListener("input", scheduleNormsCoverageRefresh);
|
||
});
|
||
document.getElementById("labProfileType")?.addEventListener("change", () => {
|
||
seedCatalogRation = null;
|
||
loadSeedCatalog(readRationType()).catch(() => notify()?.error?.(null, { fallback: "Не удалось выполнить операцию" }));
|
||
});
|
||
document.getElementById("labProfilesSeedSearch")?.addEventListener("input", renderSeedSelect);
|
||
document.getElementById("labProfilesSeedSelect")?.addEventListener("change", () => {
|
||
const applyBtn = document.getElementById("labProfilesSeedApply");
|
||
const sel = document.getElementById("labProfilesSeedSelect");
|
||
if (applyBtn && sel) applyBtn.disabled = !sel.value;
|
||
});
|
||
document.getElementById("labProfilesSeedApply")?.addEventListener("click", () => {
|
||
applySeedNorms().catch(() => notify()?.error?.(null, { fallback: "Не удалось выполнить операцию" }));
|
||
});
|
||
document.getElementById("labProfilesGfeApplyUsp")?.addEventListener("click", () => applyGfeMin(["usp"]));
|
||
document.getElementById("labProfilesGfeApplyNel")?.addEventListener("click", () => applyGfeMin(["nel"]));
|
||
document.getElementById("labProfilesGfeApplyAll")?.addEventListener("click", () =>
|
||
applyGfeMin(GFE_DYNAMIC_KEYS)
|
||
);
|
||
}
|
||
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
bind();
|
||
init().catch(() => notify()?.error?.(null, { fallback: "Не удалось выполнить операцию" }));
|
||
});
|
||
})(window);
|