602 lines
22 KiB
JavaScript
602 lines
22 KiB
JavaScript
(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, ">")
|
||
.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 =
|
||
'<tr><td colspan="4" class="lab-sandbox-empty">В этой фракции пусто — нечего готовить</td></tr>';
|
||
return;
|
||
}
|
||
body.innerHTML = rows
|
||
.map((c) => {
|
||
const checked = picked.has(c.id) ? " checked" : "";
|
||
return (
|
||
`<tr>` +
|
||
`<td><input type="checkbox" class="form-check-input lab-formulate-gcb" data-group="${escapeHtml(gid)}" value="${escapeHtml(c.id)}"${checked}></td>` +
|
||
`<td>${escapeHtml(c.name)}</td>` +
|
||
`<td>${formatNum(c.mainFeedDmGPerKg)}</td>` +
|
||
`<td>${formatNum(c.dryMatterPct)}%</td>` +
|
||
`</tr>`
|
||
);
|
||
})
|
||
.join("");
|
||
}
|
||
|
||
function buildWizard() {
|
||
const stepsEl = document.getElementById("labFormulateSteps");
|
||
const panesEl = document.getElementById("labFormulatePanes");
|
||
if (!stepsEl || !panesEl) return;
|
||
|
||
stepsEl.innerHTML =
|
||
groups
|
||
.map(
|
||
(g, i) =>
|
||
`<button type="button" class="lab-formulate-step${i === 0 ? " lab-formulate-step--active" : ""}" data-step="${i + 1}">${i + 1}. ${escapeHtml(g.shortLabel)}</button>`
|
||
)
|
||
.join("") +
|
||
`<button type="button" class="lab-formulate-step" data-step="${groups.length + 1}">${groups.length + 1}. Кристалл</button>`;
|
||
|
||
panesEl.innerHTML = groups
|
||
.map((g) => {
|
||
const req = g.required
|
||
? `<span class="lab-formulate-badge lab-formulate-badge--ok">без этого никак</span>`
|
||
: `<span class="lab-formulate-badge">не мешай</span>`;
|
||
return (
|
||
`<div id="labFormulatePane-${g.id}" class="lab-formulate-pane${g.step === 1 ? "" : " d-none"}" data-group="${g.id}">` +
|
||
`<div class="d-flex justify-content-between align-items-center mb-2">` +
|
||
`<div class="lab-sandbox-panel__title mb-0">${escapeHtml(g.label)}</div>${req}</div>` +
|
||
`<p class="small text-muted mb-2">${escapeHtml(g.hint)}</p>` +
|
||
`<p class="small mb-2 lab-formulate-group-count" data-group="${g.id}">В партии: 0</p>` +
|
||
`<div class="lab-sandbox-field">` +
|
||
`<input type="search" class="form-control form-control-sm lab-formulate-filter" data-group="${g.id}" placeholder="Найти реагент">` +
|
||
`</div>` +
|
||
`<div class="table-responsive lab-formulate-pool-wrap">` +
|
||
`<table class="table table-sm lab-sandbox-table mb-0">` +
|
||
`<thead><tr><th></th><th>Реагент</th><th>Осн.</th><th>СВ%</th></tr></thead>` +
|
||
`<tbody></tbody></table></div>` +
|
||
`<div class="lab-sandbox-actions mt-2">` +
|
||
(g.step > 1
|
||
? `<button type="button" class="btn btn-sm btn-outline-secondary lab-formulate-back" data-step="${g.step}">← Откат</button>`
|
||
: "") +
|
||
(!g.required
|
||
? `<button type="button" class="btn btn-sm btn-outline-secondary lab-formulate-skip" data-step="${g.step}">Не мешай</button>`
|
||
: "") +
|
||
`<button type="button" class="btn btn-sm btn-primary lab-formulate-next" data-step="${g.step}">Вперёд →</button>` +
|
||
`</div></div>`
|
||
);
|
||
})
|
||
.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 += `<li><strong>${escapeHtml(g.shortLabel)}</strong> (${ids.length})</li>`;
|
||
ids.forEach((id) => {
|
||
const name = catalog.find((c) => c.id === id)?.name || id;
|
||
html += `<li class="ms-3">${escapeHtml(name)}</li>`;
|
||
});
|
||
});
|
||
el.innerHTML = html || "<li>—</li>";
|
||
}
|
||
|
||
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 = ['<option value="">— стандарт чистоты —</option>'];
|
||
profiles.forEach((p) => {
|
||
const sel = String(p.id) === String(selectedId) ? " selected" : "";
|
||
const key = p.profileKey ? `[${p.profileKey}] ` : "";
|
||
opts.push(
|
||
`<option value="${escapeHtml(p.id)}"${sel}>${escapeHtml(key)}${escapeHtml(p.label)}</option>`
|
||
);
|
||
});
|
||
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 (
|
||
`<label class="lab-formulate-opt-key">` +
|
||
`<input type="checkbox" class="form-check-input lab-formulate-opt-cb" value="${escapeHtml(key)}"${checked}>` +
|
||
`<span>${escapeHtml(key)}</span>` +
|
||
`<small class="text-muted">${escapeHtml(bounds)}</small>` +
|
||
`</label>`
|
||
);
|
||
})
|
||
.join("");
|
||
}
|
||
|
||
function renderResult(data) {
|
||
const el = document.getElementById("labFormulateResult");
|
||
if (!el) return;
|
||
if (!data) {
|
||
el.innerHTML = '<div class="lab-sandbox-empty">Скажи моё имя и жми кнопку</div>';
|
||
return;
|
||
}
|
||
const stats = data.searchStats || {};
|
||
const optimizeSet = new Set(data.optimizeKeys || []);
|
||
let html =
|
||
`<div class="lab-formulate-result-meta">` +
|
||
`<span>Реагентов ${data.candidatePoolSize}</span>` +
|
||
`<span>Троек ${stats.tripletsEvaluated || 0}</span>` +
|
||
`<span>${stats.durationMs || 0} мс — время деньги</span></div>` +
|
||
`<table class="table table-sm lab-sandbox-table mb-3"><thead><tr><th>Реагент</th><th>кг/сут</th><th>%</th></tr></thead><tbody>` +
|
||
(data.lines || [])
|
||
.map(
|
||
(line) =>
|
||
`<tr><td>${escapeHtml(line.name)}</td><td>${formatNum(line.dailyKg)}</td><td>${formatNum(line.sharePct)}</td></tr>`
|
||
)
|
||
.join("") +
|
||
`</tbody></table>`;
|
||
const rows = (data.indicators || []).filter(
|
||
(ind) => ind.min != null || ind.max != null || ind.content != null
|
||
);
|
||
html +=
|
||
`<table class="table table-sm lab-sandbox-table"><thead><tr><th>Показатель</th><th>Факт</th><th>Стандарт</th><th>Δ</th></tr></thead><tbody>` +
|
||
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 (
|
||
`<tr class="${target}"><td>${escapeHtml(ind.label || ind.key)}</td>` +
|
||
`<td>${formatNum(ind.content)}</td><td>${norm}</td>` +
|
||
`<td class="${dc}">${formatNum(ind.diff)}</td></tr>`
|
||
);
|
||
})
|
||
.join("") +
|
||
`</tbody></table>`;
|
||
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 = `<p class="text-danger">${escapeHtml(msg)}</p>`;
|
||
});
|
||
}
|
||
|
||
global.labFormulateRun = labFormulateRun;
|
||
if (document.readyState === "loading") {
|
||
document.addEventListener("DOMContentLoaded", init);
|
||
} else {
|
||
init();
|
||
}
|
||
})(window);
|