(function (global) { "use strict"; const DEFAULT_OPTIMIZE_KEYS = [ "dry_matter", "usp", "nel", "crude_protein", "rnb", "nfc_pct_dm_uk", ]; let catalog = []; let groups = []; let profiles = []; let currentStep = 1; const selections = {}; function formulateError(message) { global.WespLabHeisenbergPrime?.error?.(message, "formulate"); } 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 formatNum(v) { if (v == null || v === "") return "—"; const n = Number(v); if (Number.isNaN(n)) return "—"; return Math.abs(n) >= 100 ? n.toFixed(1) : n.toFixed(3).replace(/\.?0+$/, ""); } function diffClass(diff) { if (diff == null || Number.isNaN(Number(diff))) return "lab-sandbox-diff--na"; const d = Number(diff); if (Math.abs(d) < 0.001) return "lab-sandbox-diff--ok"; return "lab-sandbox-diff--bad"; } function totalSteps() { return groups.length + 1; } function groupById(id) { return groups.find((g) => g.id === id); } function selectedForGroup(gid) { return Array.from(document.querySelectorAll(`.lab-formulate-gcb[data-group="${gid}"]:checked`)).map( (el) => el.value ); } function allSelections() { const out = {}; groups.forEach((g) => { out[g.id] = selectedForGroup(g.id); }); return out; } function totalPicked() { return Object.values(allSelections()).reduce((n, arr) => n + arr.length, 0); } function componentsForGroup(gid, filterText) { const q = (filterText || "").trim().toLowerCase(); return catalog.filter((c) => { if (!c.eligible || c.feedGroup !== gid) return false; if (q && !String(c.name || "").toLowerCase().includes(q)) return false; return true; }); } function renderGroupTable(gid, filterText) { const body = document.querySelector(`#labFormulatePane-${gid} tbody`); if (!body) return; const picked = new Set(selectedForGroup(gid)); const rows = componentsForGroup(gid, filterText); if (!rows.length) { body.innerHTML = 'В этой фракции пусто — нечего готовить'; return; } body.innerHTML = rows .map((c) => { const checked = picked.has(c.id) ? " checked" : ""; return ( `` + `` + `${escapeHtml(c.name)}` + `${formatNum(c.mainFeedDmGPerKg)}` + `${formatNum(c.dryMatterPct)}%` + `` ); }) .join(""); } function buildWizard() { const stepsEl = document.getElementById("labFormulateSteps"); const panesEl = document.getElementById("labFormulatePanes"); if (!stepsEl || !panesEl) return; stepsEl.innerHTML = groups .map( (g, i) => `` ) .join("") + ``; panesEl.innerHTML = groups .map((g) => { const req = g.required ? `без этого никак` : `не мешай`; return ( `
` + `
` + `
${escapeHtml(g.label)}
${req}
` + `

${escapeHtml(g.hint)}

` + `

В партии: 0

` + `
` + `` + `
` + `
` + `` + `` + `
РеагентОсн.СВ%
` + `
` + (g.step > 1 ? `` : "") + (!g.required ? `` : "") + `` + `
` ); }) .join(""); groups.forEach((g) => renderGroupTable(g.id, "")); } function updateCounters() { const sel = allSelections(); groups.forEach((g) => { const el = document.querySelector(`.lab-formulate-group-count[data-group="${g.id}"]`); if (el) { const n = (sel[g.id] || []).length; el.textContent = g.required ? `В партии: ${n} (мин. ${g.minPick})` : `В партии: ${n}`; } }); const total = totalPicked(); const summaryEl = document.getElementById("labFormulateSummary"); const prefilter = document.getElementById("labFormulatePrefilterNote"); const roughOk = (sel.rough || []).length >= 1; if (summaryEl) { summaryEl.textContent = roughOk && total >= 3 ? `Партия из ${total} реагентов — можно готовить` : `Нужна база (≥1) и ≥3 реагента всего — сейчас ${total}. Нам нужно готовить.`; } if (prefilter) prefilter.classList.toggle("d-none", total <= 18); const runBtn = document.getElementById("labFormulateRun"); if (runBtn) runBtn.disabled = !(roughOk && total >= 3 && currentStep === totalSteps()); renderReview(); } function renderReview() { const el = document.getElementById("labFormulateReview"); if (!el) return; const sel = allSelections(); let html = ""; groups.forEach((g) => { const ids = sel[g.id] || []; html += `
  • ${escapeHtml(g.shortLabel)} (${ids.length})
  • `; ids.forEach((id) => { const name = catalog.find((c) => c.id === id)?.name || id; html += `
  • ${escapeHtml(name)}
  • `; }); }); el.innerHTML = html || "
  • "; } function setStep(step) { currentStep = step; const calcStep = totalSteps(); groups.forEach((g) => { document.getElementById(`labFormulatePane-${g.id}`)?.classList.toggle("d-none", g.step !== step); }); document.getElementById("labFormulateReviewPane")?.classList.toggle("d-none", step !== calcStep); document.querySelector(".lab-formulate-step-panel--calc")?.classList.toggle("d-none", step !== calcStep); document.querySelectorAll(".lab-formulate-step").forEach((btn) => { btn.classList.toggle("lab-formulate-step--active", Number(btn.dataset.step) === step); }); updateCounters(); } function canGoToStep(step) { if (step <= 1) return true; const sel = allSelections(); for (const g of groups) { if (g.step >= step) break; if (g.required && (sel[g.id] || []).length < (g.minPick || 1)) return false; } if (step === totalSteps()) { return (sel.rough || []).length >= 1 && totalPicked() >= 3; } return true; } function readNumInput(id) { const el = document.getElementById(id); if (!el) return null; const raw = String(el.value ?? "").trim(); if (!raw) return null; const n = Number(raw); return Number.isFinite(n) ? n : null; } function readNormsMethod() { return document.getElementById("labFormulateNormsMethod")?.value || "wesp"; } function readNormsParams() { const method = readNormsMethod(); if (method === "wesp") return {}; const params = { milkFatPct: readNumInput("labFormulateFat") ?? 4, lactationNo: readNumInput("labFormulateLactation") ?? 2, }; if (method === "racion_piter") { params.koncOeSv = readNumInput("labFormulateKonc") ?? 10.3; } return params; } function syncNormsMethodUi() { const method = readNormsMethod(); const racionBox = document.getElementById("labFormulateRacionFields"); const piterOnly = document.querySelectorAll(".lab-formulate-piter-only"); if (racionBox) racionBox.classList.toggle("d-none", method === "wesp"); piterOnly.forEach((el) => el.classList.toggle("d-none", method !== "racion_piter")); } function readFormulateParams() { return { profileId: document.getElementById("labFormulateProfile")?.value || "", massKg: readNumInput("labFormulateMass"), milkYieldKg: readNumInput("labFormulateMilk"), totalKgPerHead: readNumInput("labFormulateTotalKg") ?? 7.3, costWeight: readNumInput("labFormulateCostWeight") ?? 100, optimizeKeys: Array.from(document.querySelectorAll(".lab-formulate-opt-cb:checked")).map((el) => el.value), groupSelections: allSelections(), normsMethod: readNormsMethod(), normsParams: readNormsParams(), }; } function resolveMassMilk(profile) { return { mass: readNumInput("labFormulateMass") ?? profile?.massKg ?? null, milk: readNumInput("labFormulateMilk") ?? profile?.milkYieldKg ?? null, }; } async function normsForOptimizeDisplay(profile) { const method = readNormsMethod(); let norms = method === "wesp" ? { ...(profile?.norms || profile?.normsData?.indicators || {}) } : {}; const { mass, milk } = resolveMassMilk(profile); if (method !== "wesp" && (mass == null || milk == null)) return norms; if (mass == null && milk == null) return norms; try { const q = new URLSearchParams({ method }); const profileId = document.getElementById("labFormulateProfile")?.value; if (profileId) q.set("profile_id", profileId); if (mass != null) q.set("mass_kg", String(mass)); if (milk != null) q.set("milk_yield_kg", String(milk)); 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)); const preview = await api(`/api/lab/norms-preview?${q.toString()}`); const resolved = preview.resolvedIndicators || {}; const dynamic = preview.dynamicNorms || {}; const merged = { ...resolved }; Object.keys(dynamic).forEach((key) => { const d = dynamic[key]; if (d?.min != null) merged[key] = { ...(merged[key] || {}), min: d.min }; }); if (method === "wesp") { Object.assign(norms, merged); } else { norms = merged; } } catch (_err) { /* preview optional */ } return norms; } async function refreshOptimizeKeysForInputs() { const profileId = document.getElementById("labFormulateProfile")?.value; const profile = profiles.find((p) => String(p.id) === String(profileId)); if (!profile) return; const norms = await normsForOptimizeDisplay(profile); renderOptimizeKeys({ ...profile, norms }); } let optimizeKeysRefreshTimer = null; function scheduleOptimizeKeysRefresh() { if (optimizeKeysRefreshTimer) clearTimeout(optimizeKeysRefreshTimer); optimizeKeysRefreshTimer = setTimeout(() => { optimizeKeysRefreshTimer = null; refreshOptimizeKeysForInputs().catch(() => {}); }, 200); } function profileOptions(selectedId) { const opts = ['']; profiles.forEach((p) => { const sel = String(p.id) === String(selectedId) ? " selected" : ""; const key = p.profileKey ? `[${p.profileKey}] ` : ""; opts.push( `` ); }); return opts.join(""); } function fillProfileFields(profileId) { const profile = profiles.find((p) => String(p.id) === String(profileId)); if (!profile) return; const massEl = document.getElementById("labFormulateMass"); const milkEl = document.getElementById("labFormulateMilk"); if (massEl && profile.massKg != null) massEl.value = profile.massKg; if (milkEl && profile.milkYieldKg != null) milkEl.value = profile.milkYieldKg; refreshOptimizeKeysForInputs().catch(() => renderOptimizeKeys(profile)); } function renderOptimizeKeys(profile) { const box = document.getElementById("labFormulateOptimizeKeys"); if (!box) return; const norms = profile?.norms || profile?.normsData?.indicators || {}; const keys = Object.keys(norms).filter((k) => norms[k]?.min != null || norms[k]?.max != null); const useKeys = keys.length ? keys : DEFAULT_OPTIMIZE_KEYS; const prevChecked = new Set( Array.from(document.querySelectorAll(".lab-formulate-opt-cb:checked")).map((el) => el.value) ); const hasPrev = prevChecked.size > 0; box.innerHTML = useKeys .map((key) => { const checked = (hasPrev ? prevChecked.has(key) : DEFAULT_OPTIMIZE_KEYS.includes(key)) ? " checked" : ""; const b = norms[key] || {}; const bounds = b.min != null && b.max != null ? `${formatNum(b.min)}–${formatNum(b.max)}` : b.max != null ? `≤ ${formatNum(b.max)}` : b.min != null ? `≥ ${formatNum(b.min)}` : ""; return ( `` ); }) .join(""); } function renderResult(data) { const el = document.getElementById("labFormulateResult"); if (!el) return; if (!data) { el.innerHTML = '
    Скажи моё имя и жми кнопку
    '; return; } const stats = data.searchStats || {}; const optimizeSet = new Set(data.optimizeKeys || []); let html = `
    ` + `Реагентов ${data.candidatePoolSize}` + `Троек ${stats.tripletsEvaluated || 0}` + `${stats.durationMs || 0} мс — время деньги
    ` + `` + (data.lines || []) .map( (line) => `` ) .join("") + `
    Реагенткг/сут%
    ${escapeHtml(line.name)}${formatNum(line.dailyKg)}${formatNum(line.sharePct)}
    `; const rows = (data.indicators || []).filter( (ind) => ind.min != null || ind.max != null || ind.content != null ); html += `` + rows .map((ind) => { const target = optimizeSet.has(ind.key) ? " lab-formulate-row--target" : ""; const dc = ind.min != null || ind.max != null ? diffClass(ind.diff) : "lab-sandbox-diff--na"; const norm = ind.min != null && ind.max != null ? `${formatNum(ind.min)}–${formatNum(ind.max)}` : ind.max != null ? `≤ ${formatNum(ind.max)}` : ind.min != null ? `≥ ${formatNum(ind.min)}` : "—"; return ( `` + `` + `` ); }) .join("") + `
    ПоказательФактСтандартΔ
    ${escapeHtml(ind.label || ind.key)}${formatNum(ind.content)}${norm}${formatNum(ind.diff)}
    `; el.innerHTML = html; } async function loadCatalogs() { const [compData, profData] = await Promise.all([ api("/api/lab/formulate/components"), api("/api/lab/animal-profiles?ration_type=DAIRY"), ]); catalog = compData.components || []; groups = (compData.groups || []).slice().sort((a, b) => a.step - b.step); profiles = profData.profiles || []; buildWizard(); const profileSel = document.getElementById("labFormulateProfile"); if (profileSel) { const labDefault = profiles.find((p) => p.profileKey === "lab_math_dairy_01") || profiles[0]; profileSel.innerHTML = profileOptions(labDefault?.id); if (labDefault) fillProfileFields(labDefault.id); } setStep(1); } async function labFormulateRun() { const params = readFormulateParams(); const profile = profiles.find((p) => String(p.id) === String(params.profileId)); if (!params.profileId) throw new Error("Выбери стандарт чистоты — я предупреждал"); if (params.normsMethod !== "wesp") { const { mass, milk } = resolveMassMilk(profile); if (mass == null || milk == null) { throw new Error("Для Москва/Петербург нужны масса и удой — заполни поля или выбери профиль с удоем"); } params.massKg = mass; params.milkYieldKg = milk; } if ((params.groupSelections.rough || []).length < 1) { throw new Error("Без базы не готовим. Минимум один грубый корм"); } if (totalPicked() < 3) throw new Error("Минимум три реагента. Нам нужно готовить"); const data = await api("/api/lab/formulate", { method: "POST", headers: { "Content-Type": "application/json" }, cache: "no-store", body: JSON.stringify({ profileId: params.profileId, groupSelections: params.groupSelections, massKg: params.massKg, milkYieldKg: params.milkYieldKg, totalKgPerHead: params.totalKgPerHead, costWeight: params.costWeight, optimizeKeys: params.optimizeKeys, normsMethod: params.normsMethod, normsParams: params.normsParams, }), }); renderResult(data); return data; } function selectLabForGroup(gid) { catalog .filter((c) => c.eligible && c.feedGroup === gid && String(c.name || "").startsWith("LAB тест —")) .forEach((c) => { const cb = document.querySelector( `.lab-formulate-gcb[data-group="${CSS.escape(gid)}"][value="${CSS.escape(c.id)}"]` ); if (cb) cb.checked = true; }); updateCounters(); } function bindEvents() { document.getElementById("labFormulateRun")?.addEventListener("click", async () => { try { await labFormulateRun(); } catch (err) { formulateError(String(err.message || err)); } finally { updateCounters(); } }); document.getElementById("labFormulateProfile")?.addEventListener("change", (e) => { fillProfileFields(e.target.value); }); for (const id of [ "labFormulateMass", "labFormulateMilk", "labFormulateTotalKg", "labFormulateCostWeight", "labFormulateFat", "labFormulateLactation", "labFormulateKonc", ]) { document.getElementById(id)?.addEventListener("input", scheduleOptimizeKeysRefresh); } document.getElementById("labFormulateNormsMethod")?.addEventListener("change", () => { syncNormsMethodUi(); scheduleOptimizeKeysRefresh(); }); document.getElementById("labFormulatePanes")?.addEventListener("input", (e) => { if (e.target.classList.contains("lab-formulate-filter")) { renderGroupTable(e.target.dataset.group, e.target.value); } }); document.getElementById("labFormulatePanes")?.addEventListener("change", (e) => { if (e.target.classList.contains("lab-formulate-gcb")) updateCounters(); }); document.getElementById("labFormulatePanes")?.addEventListener("click", (e) => { const next = e.target.closest(".lab-formulate-next"); const back = e.target.closest(".lab-formulate-back"); const skip = e.target.closest(".lab-formulate-skip"); if (next) { const step = Number(next.dataset.step); const g = groups.find((x) => x.step === step); if (g?.required && selectedForGroup(g.id).length < (g.minPick || 1)) { formulateError(`В «${g.label}» нужно минимум ${g.minPick}. Всё под контролем — выбери`); return; } setStep(step + 1); } if (back) setStep(Number(back.dataset.step) - 1); if (skip) setStep(Number(skip.dataset.step) + 1); }); document.getElementById("labFormulateSteps")?.addEventListener("click", (e) => { const btn = e.target.closest(".lab-formulate-step"); if (!btn) return; const step = Number(btn.dataset.step); if (!canGoToStep(step)) return; setStep(step); }); document.addEventListener("keydown", (e) => { if (e.altKey && e.key === "l") { selectLabForGroup("rough"); selectLabForGroup("succulent"); selectLabForGroup("concentrate"); } }); } function init() { bindEvents(); syncNormsMethodUi(); loadCatalogs().catch((err) => { const msg = err.message || String(err); formulateError(msg); const panes = document.getElementById("labFormulatePanes"); if (panes) panes.innerHTML = `

    ${escapeHtml(msg)}

    `; }); } global.labFormulateRun = labFormulateRun; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })(window);