(function (global) {
"use strict";
function escapeHtml(s) {
return String(s ?? "")
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """);
}
function createLabMasterPanel() {
let root = null;
let recipeId = null;
let state = null;
let components = [];
let profiles = [];
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 || "Ошибка API");
return data;
}
async function ensureCatalogs(rationType) {
const [compResp, profData] = await Promise.all([
components.length ? Promise.resolve(components) : fetch("/api/components").then((r) => r.json()),
api(`/api/lab/animal-profiles${rationType ? `?ration_type=${encodeURIComponent(rationType)}` : ""}`),
]);
if (Array.isArray(compResp)) components = compResp;
profiles = profData.profiles || [];
}
async function loadRecipes() {
const data = await api("/api/lab/recipes");
return data.recipes || [];
}
async function loadRation(id) {
return api(`/api/lab/rations/${encodeURIComponent(id)}`);
}
function componentOptionsHtml(selectedId) {
const opts = [''];
components.forEach((c) => {
const sel = String(c.id) === String(selectedId) ? " selected" : "";
opts.push(``);
});
return opts.join("");
}
function profileOptionsHtml(selectedId) {
const opts = [''];
profiles.forEach((p) => {
const sel = String(p.id) === String(selectedId) ? " selected" : "";
opts.push(
``
);
});
return opts.join("");
}
function render() {
if (!root) return;
const indicators = (state?.rationResults?.indicators || []).slice(0, 8);
const lines = state?.lines || [];
root.innerHTML =
`
` +
`
` +
`` +
`` +
`` +
`` +
`` +
`` +
`` +
`` +
`
` +
`
Зоотехнический мастер · ${escapeHtml(state?.recipeName || "")}
` +
`
` +
`
` +
indicators
.map(
(ind) =>
`${escapeHtml(ind.label)}: ${escapeHtml(ind.content)} ${escapeHtml(ind.unit)}`
)
.join("") +
`
`;
const sel = root.querySelector("[data-lab-recipe-select]");
if (sel && sel.options.length === 0) {
loadRecipes().then((recipes) => {
sel.innerHTML =
'' +
recipes
.map(
(r) =>
``
)
.join("");
});
}
}
async function reload() {
if (!recipeId) return;
try {
state = await loadRation(recipeId);
await ensureCatalogs(state.rationType);
if (!state.exists) {
const ok = await global.WespDialog?.confirm?.(
"Создать мастер из текущего рецепта?",
{ title: "Мастер рациона" }
);
if (ok) {
await api(`/api/lab/rations/${encodeURIComponent(recipeId)}/seed-from-execution`, {
method: "POST",
});
state = await loadRation(recipeId);
await ensureCatalogs(state.rationType);
} else {
await api(`/api/lab/rations/${encodeURIComponent(recipeId)}/ensure-empty`, {
method: "POST",
});
state = await loadRation(recipeId);
await ensureCatalogs(state.rationType);
}
}
render();
} catch (e) {
global.WespZootechNotify?.createAdapter?.()?.error?.(e.message, { fallback: "Не удалось выполнить операцию" });
}
}
function collectLines() {
if (!root || !state) return [];
return (state.lines || []).map((line, i) => {
const row = root.querySelector(`tr[data-line-idx="${i}"]`);
if (!row) return line;
const componentId = row.querySelector('[data-field="componentId"]')?.value || null;
const comp = components.find((c) => String(c.id) === String(componentId));
return {
...line,
componentId,
ingredientName: comp?.name || line.ingredientName,
dailyKg: parseFloat(row.querySelector('[data-field="dailyKg"]')?.value) || null,
inRation: row.querySelector('[data-field="inRation"]')?.checked,
inCompound: row.querySelector('[data-field="inCompound"]')?.checked,
};
});
}
function collectPayload() {
const rationType = root.querySelector("[data-lab-ration-type]")?.value || state?.rationType;
return {
lines: collectLines(),
rationType,
animalProfileId: root.querySelector("[data-lab-profile-select]")?.value || null,
};
}
return {
mount(el) {
root = el;
root.addEventListener("click", async (ev) => {
const btn = ev.target.closest("[data-action]");
if (!btn || !recipeId) return;
const action = btn.getAttribute("data-action");
try {
if (action === "lab-add-line") {
state.lines = [...(state?.lines || []), {
componentId: "",
ingredientName: "",
dailyKg: 0,
inRation: true,
inCompound: false,
}];
render();
return;
}
if (action === "lab-remove-line") {
const idx = parseInt(btn.getAttribute("data-line-idx"), 10);
if (!Number.isNaN(idx)) {
state.lines = (state.lines || []).filter((_, i) => i !== idx);
render();
}
return;
}
if (action === "lab-recalculate") {
await api(`/api/lab/rations/${encodeURIComponent(recipeId)}/recalculate`, {
method: "POST",
});
await reload();
}
if (action === "lab-save") {
await api(`/api/lab/rations/${encodeURIComponent(recipeId)}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(collectPayload()),
});
await reload();
global.WespZootechNotify?.createAdapter?.()?.success?.("Мастер сохранён");
}
if (action === "lab-print-ration" || action === "lab-print-compound") {
const mode = action === "lab-print-compound" ? "compound" : "ration";
global.open?.(
`/api/lab/rations/${encodeURIComponent(recipeId)}/print?mode=${mode}`,
"_blank",
"noopener"
);
}
} catch (e) {
global.WespZootechNotify?.createAdapter?.()?.error?.(e.message, { fallback: "Не удалось выполнить операцию" });
}
});
root.addEventListener("change", async (ev) => {
if (ev.target.matches("[data-lab-recipe-select]")) {
recipeId = ev.target.value;
reload();
return;
}
if (ev.target.matches("[data-lab-ration-type]")) {
const rt = ev.target.value;
state = { ...state, rationType: rt };
profiles = [];
await ensureCatalogs(rt);
render();
return;
}
if (ev.target.matches("[data-lab-profile-select]")) {
const profile = profiles.find((p) => String(p.id) === String(ev.target.value));
if (profile?.rationType) {
state = { ...state, rationType: profile.rationType, animalProfileId: profile.id };
const typeSel = root.querySelector("[data-lab-ration-type]");
if (typeSel) typeSel.value = profile.rationType;
}
}
});
},
destroy() {
root = null;
state = null;
},
reload,
setRecipeId(id) {
recipeId = id;
reload();
},
};
}
global.WespLabMasterPanel = { createLabMasterPanel };
})(window);