300 lines
12 KiB
JavaScript
300 lines
12 KiB
JavaScript
(function (global) {
|
||
"use strict";
|
||
|
||
let diffByComponent = new Map();
|
||
let hooked = false;
|
||
|
||
function getActiveRecipeId() {
|
||
const fromEdit = document.getElementById("recipeEdit")?.dataset?.recipeId;
|
||
if (fromEdit) return fromEdit;
|
||
return document.querySelector(".list-item.active[data-recipe-id]")?.dataset?.recipeId || null;
|
||
}
|
||
|
||
function ensureToolbarButtons() {
|
||
const toolbar = document.querySelector("#recipeEdit .recipe-section-toolbar");
|
||
if (!toolbar) return;
|
||
if (!toolbar.querySelector('[data-action="sync-to-master"]')) {
|
||
const syncBtn = document.createElement("button");
|
||
syncBtn.type = "button";
|
||
syncBtn.className = "btn btn-outline-secondary";
|
||
syncBtn.setAttribute("data-action", "sync-to-master");
|
||
syncBtn.innerHTML = '<i class="fas fa-arrow-up me-1"></i>В мастер';
|
||
toolbar.insertBefore(syncBtn, toolbar.firstChild);
|
||
}
|
||
if (!toolbar.querySelector('[data-action="apply-from-master"]')) {
|
||
const btn = document.createElement("button");
|
||
btn.type = "button";
|
||
btn.className = "btn btn-outline-primary";
|
||
btn.setAttribute("data-action", "apply-from-master");
|
||
btn.hidden = true;
|
||
btn.innerHTML = '<i class="fas fa-arrow-down me-1"></i>Из мастера';
|
||
const addBtn = toolbar.querySelector('[data-action="add-ingredient"]');
|
||
if (addBtn) toolbar.insertBefore(btn, addBtn);
|
||
else toolbar.appendChild(btn);
|
||
}
|
||
}
|
||
|
||
function ensureKHubButton() {
|
||
const actions = document.querySelector("#recipeEdit .recipe-edit-header-actions .d-flex");
|
||
if (!actions || actions.querySelector('[data-action="open-lab-k-hub"]')) return;
|
||
const btn = document.createElement("button");
|
||
btn.type = "button";
|
||
btn.className = "btn btn-outline-secondary recipe-edit-header-icon-btn";
|
||
btn.setAttribute("data-action", "open-lab-k-hub");
|
||
btn.title = "Открыть зоотех-мастер в K-hub";
|
||
btn.setAttribute("aria-label", "K-hub");
|
||
btn.innerHTML = '<i class="fas fa-flask" aria-hidden="true"></i>';
|
||
actions.insertBefore(btn, actions.firstChild);
|
||
}
|
||
|
||
function ensureRationTypeField() {
|
||
const grid = document.querySelector("#recipeEdit .recipe-form-grid");
|
||
if (!grid || document.getElementById("rationType")) return;
|
||
const wrap = document.createElement("div");
|
||
wrap.className = "form-group";
|
||
wrap.innerHTML =
|
||
'<label for="rationType">Тип стада</label>' +
|
||
'<select class="form-control" id="rationType">' +
|
||
'<option value="">—</option><option value="DAIRY">Дойное</option><option value="BEEF">Мясное</option>' +
|
||
"</select>";
|
||
const tripWrap = document.getElementById("tripPercent")?.closest(".form-group");
|
||
if (tripWrap?.parentElement === grid) {
|
||
tripWrap.insertAdjacentElement("afterend", wrap);
|
||
} else {
|
||
grid.appendChild(wrap);
|
||
}
|
||
}
|
||
|
||
function ensureDiffStyles() {
|
||
if (document.getElementById("wesp-lab-diff-styles")) return;
|
||
const style = document.createElement("style");
|
||
style.id = "wesp-lab-diff-styles";
|
||
style.textContent =
|
||
".lab-diff-badge{margin-left:6px;font-size:.7rem;vertical-align:middle;}" +
|
||
".lab-diff-badge--warn{background:#fff3cd;color:#664d03;}" +
|
||
".lab-diff-badge--miss{background:#f8d7da;color:#842029;}" +
|
||
".lab-recipe-quality{margin-top:4px;font-size:.78rem;color:var(--bs-secondary-color,#6c757d);}" +
|
||
".lab-recipe-quality__label{font-weight:600;margin-right:4px;}";
|
||
document.head.appendChild(style);
|
||
}
|
||
|
||
function diffBadgeHtml(reasons) {
|
||
if (!reasons?.length) return "";
|
||
const title = reasons.join(", ");
|
||
const cls = reasons.includes("missing_in_master") || reasons.includes("missing_in_execution")
|
||
? "lab-diff-badge--miss"
|
||
: "lab-diff-badge--warn";
|
||
return `<span class="badge lab-diff-badge ${cls}" title="${title}">мастер</span>`;
|
||
}
|
||
|
||
function applyDiffBadges() {
|
||
document.querySelectorAll("#ingredientsTableBody .ingredient-row").forEach((row) => {
|
||
const select = row.querySelector(".ingredient-select");
|
||
const cell = row.querySelector('.ingredient-detail-cell[data-label="Ингредиент"] .recipe-row-name-with-skip');
|
||
if (!select || !cell) return;
|
||
cell.querySelectorAll(".lab-diff-badge").forEach((el) => el.remove());
|
||
const compId = select.value;
|
||
if (!compId) return;
|
||
const reasons = diffByComponent.get(String(compId));
|
||
if (!reasons?.length) return;
|
||
cell.insertAdjacentHTML("beforeend", diffBadgeHtml(reasons));
|
||
});
|
||
}
|
||
|
||
function ensureQualityBlock() {
|
||
const headerText = document.querySelector("#recipeEdit .recipe-edit-header-text");
|
||
if (!headerText || document.getElementById("labRecipeQuality")) return;
|
||
const block = document.createElement("div");
|
||
block.id = "labRecipeQuality";
|
||
block.className = "lab-recipe-quality";
|
||
block.hidden = true;
|
||
block.innerHTML =
|
||
'<span class="lab-recipe-quality__label">Зоотех:</span>' +
|
||
'<span class="lab-recipe-quality__items" data-lab-quality-items></span>';
|
||
headerText.appendChild(block);
|
||
}
|
||
|
||
async function refreshQuality(recipeId) {
|
||
ensureQualityBlock();
|
||
const block = document.getElementById("labRecipeQuality");
|
||
const items = block?.querySelector("[data-lab-quality-items]");
|
||
if (!block || !items || !recipeId) return;
|
||
try {
|
||
const resp = await fetch(`/api/lab/rations/${encodeURIComponent(recipeId)}`);
|
||
if (!resp.ok) throw new Error("no ration");
|
||
const data = await resp.json();
|
||
const indicators = (data.rationResults?.indicators || []).slice(0, 4);
|
||
if (!data.exists || !indicators.length) {
|
||
block.hidden = true;
|
||
return;
|
||
}
|
||
items.textContent = indicators
|
||
.map((ind) => `${ind.label}: ${ind.content ?? "—"} ${ind.unit || ""}`.trim())
|
||
.join(" · ");
|
||
block.hidden = false;
|
||
} catch (_) {
|
||
block.hidden = true;
|
||
}
|
||
}
|
||
|
||
async function refreshDiff(recipeId) {
|
||
const btn = document.querySelector('[data-action="apply-from-master"]');
|
||
if (!recipeId) return;
|
||
await refreshQuality(recipeId);
|
||
if (!btn) return;
|
||
try {
|
||
const resp = await fetch(`/api/lab/rations/${encodeURIComponent(recipeId)}/diff`);
|
||
const data = await resp.json();
|
||
diffByComponent = new Map();
|
||
(data.lines || []).forEach((line) => {
|
||
if (line.componentId) diffByComponent.set(String(line.componentId), line.reasons || []);
|
||
});
|
||
btn.hidden = !data.hasChanges;
|
||
applyDiffBadges();
|
||
} catch (_) {
|
||
btn.hidden = true;
|
||
diffByComponent = new Map();
|
||
}
|
||
}
|
||
|
||
function patchRecipeLoad() {
|
||
if (hooked) return;
|
||
const orig = global.loadRecipeDetails;
|
||
if (typeof orig !== "function") return;
|
||
global.loadRecipeDetails = async function (recipeId, options) {
|
||
const recipeEdit = document.getElementById("recipeEdit");
|
||
if (recipeEdit && recipeId) recipeEdit.dataset.recipeId = String(recipeId);
|
||
await orig(recipeId, options);
|
||
await refreshDiff(recipeId);
|
||
global.WespLabRecipesPlanOverlay?.decorateRows?.();
|
||
};
|
||
hooked = true;
|
||
}
|
||
|
||
function patchRecipeSave() {
|
||
const orig = global.doSaveRecipe;
|
||
if (typeof orig !== "function" || orig.__labPatched) return;
|
||
global.doSaveRecipe = async function (closeAfterSave) {
|
||
const rationEl = document.getElementById("rationType");
|
||
const prev = global.__wespLabRationTypeForSave;
|
||
if (rationEl) {
|
||
global.__wespLabRationTypeForSave = rationEl.value || null;
|
||
}
|
||
try {
|
||
return await orig(closeAfterSave);
|
||
} finally {
|
||
global.__wespLabRationTypeForSave = prev;
|
||
}
|
||
};
|
||
const origFetch = global.fetch.bind(global);
|
||
global.fetch = async function (input, init) {
|
||
const url = typeof input === "string" ? input : input?.url;
|
||
const method = (init?.method || "GET").toUpperCase();
|
||
if (
|
||
method === "PUT" &&
|
||
url &&
|
||
/\/api\/recipes\/[^/]+$/.test(url) &&
|
||
init?.body &&
|
||
global.__wespLabRationTypeForSave !== undefined
|
||
) {
|
||
try {
|
||
const body = JSON.parse(init.body);
|
||
body.ration_type = global.__wespLabRationTypeForSave || null;
|
||
init = { ...init, body: JSON.stringify(body) };
|
||
} catch (_) {
|
||
/* ignore */
|
||
}
|
||
}
|
||
return origFetch(input, init);
|
||
};
|
||
global.doSaveRecipe.__labPatched = true;
|
||
}
|
||
|
||
document.addEventListener("click", async (ev) => {
|
||
const btn = ev.target.closest("[data-action]");
|
||
if (!btn) return;
|
||
const action = btn.getAttribute("data-action");
|
||
const recipeId = getActiveRecipeId();
|
||
|
||
if (action === "open-lab-k-hub") {
|
||
global.WespZootechNotificationCenter?.openDailyPlan?.(recipeId || undefined);
|
||
return;
|
||
}
|
||
|
||
if (action === "sync-to-master") {
|
||
if (!recipeId) return;
|
||
const ok = await global.WespDialog?.confirm?.(
|
||
"Сохранить текущий рецепт в зоотех-мастер?",
|
||
{ title: "В мастер" }
|
||
);
|
||
if (!ok) return;
|
||
try {
|
||
const resp = await fetch(
|
||
`/api/lab/rations/${encodeURIComponent(recipeId)}/sync-from-execution`,
|
||
{ method: "POST" }
|
||
);
|
||
const data = await resp.json();
|
||
if (!resp.ok) throw new Error(data.message || "Ошибка");
|
||
global.WespZootechNotify?.createAdapter?.()?.success?.("Мастер обновлён из рецепта");
|
||
await refreshDiff(recipeId);
|
||
} catch (e) {
|
||
global.WespZootechNotify?.createAdapter?.()?.error?.(e.message, { fallback: "Не удалось выполнить операцию" });
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (action !== "apply-from-master") return;
|
||
if (!recipeId) return;
|
||
const ok = await global.WespDialog?.confirm?.("Перенести ингредиенты из зоотех-мастера?", {
|
||
title: "Из мастера",
|
||
});
|
||
if (!ok) return;
|
||
try {
|
||
const resp = await fetch(`/api/lab/rations/${encodeURIComponent(recipeId)}/apply-from-master`, {
|
||
method: "POST",
|
||
});
|
||
const data = await resp.json();
|
||
if (!resp.ok) throw new Error(data.message || "Ошибка");
|
||
if (typeof global.reloadRecipeEditor === "function") {
|
||
await global.reloadRecipeEditor(recipeId);
|
||
} else if (typeof global.loadRecipeDetails === "function") {
|
||
await global.loadRecipeDetails(recipeId);
|
||
}
|
||
await refreshDiff(recipeId);
|
||
global.WespZootechNotify?.createAdapter?.()?.success?.("Мастер применён к рецепту");
|
||
} catch (e) {
|
||
global.WespZootechNotify?.createAdapter?.()?.error?.(e.message, { fallback: "Не удалось применить мастер" });
|
||
}
|
||
});
|
||
|
||
document.addEventListener("change", (ev) => {
|
||
if (!ev.target?.matches?.(".ingredient-select")) return;
|
||
applyDiffBadges();
|
||
});
|
||
|
||
global.WespLabRecipesOverlay = {
|
||
init() {
|
||
ensureDiffStyles();
|
||
ensureToolbarButtons();
|
||
ensureKHubButton();
|
||
ensureRationTypeField();
|
||
patchRecipeLoad();
|
||
patchRecipeSave();
|
||
},
|
||
refreshDiff,
|
||
refreshQuality,
|
||
applyDiffBadges,
|
||
};
|
||
|
||
function boot() {
|
||
global.WespLabRecipesOverlay.init();
|
||
if (typeof global.loadRecipeDetails === "function") patchRecipeLoad();
|
||
}
|
||
|
||
if (document.readyState === "loading") {
|
||
document.addEventListener("DOMContentLoaded", boot);
|
||
} else {
|
||
boot();
|
||
}
|
||
})(window);
|