(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, """); } 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 '—'; const cls = `norm-source-badge norm-source-badge--${src}`; return `${escapeHtml(normSourceLabel(src))}`; } 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 = [''].concat( rows.map((e) => { const mass = e.massKg != null ? ` · ${e.massKg} кг` : ""; const cnt = e.indicatorCount != null ? ` · ${e.indicatorCount} показ.` : ""; return ``; }) ); 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 = '