Интегрирован wesp в сайт
CI / quality (push) Canceled after 0s

This commit is contained in:
влад
2026-07-17 12:57:18 +03:00
parent 5dfa06ddbe
commit 355c0ef9f1
883 changed files with 194576 additions and 177 deletions
@@ -0,0 +1,930 @@
/**
* Панель «План на день» внутри hub-модалки «К».
*/
(function (global) {
const DAILY_PLAN_DISPENSER_KEY = "wesp-daily-plan-dispenser";
const ALL_DISPENSERS_ID = "__all_dispensers__";
const ALL_MILLS_ID = "__all_mills__";
function escapeHtml(value) {
return String(value ?? "")
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function formatDistribution(group) {
if (group?.distributionLabel) return group.distributionLabel;
const type = group?.distributionType || "percent";
const value = group?.value ?? "";
if (type === "heads") return `${value} гол.`;
return `${value}%`;
}
function todayIso() {
const d = new Date();
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
}
function createDailyPlanPanel() {
let rootEl = null;
let dateEl = null;
let dispenserEl = null;
let contentEl = null;
let dispensers = [];
let undoToastTimer = null;
function getSelectedDispenserId() {
return dispenserEl?.value || "";
}
function getSelectedPlanDate() {
return planDateValue();
}
function notifySuccess(message) {
try {
global.WespZootechNotify?.createAdapter?.()?.success?.(message, { skipRecord: true });
} catch {
/* ignore */
}
}
function notifyError(message, fallback) {
try {
global.WespZootechNotify?.createAdapter?.()?.error?.(message, {
fallback: fallback || message,
skipRecord: true,
});
} catch {
/* ignore */
}
}
function renderToolbar() {
return (
'<div class="zt-k-hub-plan__toolbar">' +
'<div class="zt-k-hub-plan__field">' +
'<label class="form-label">Дата</label>' +
'<input type="date" class="form-control form-control-sm" data-daily-plan-date>' +
"</div>" +
'<div class="zt-k-hub-plan__field">' +
'<label class="form-label">Кормораздатчик</label>' +
'<select class="form-control form-control-sm" data-daily-plan-dispenser>' +
'<option value="">Загрузка…</option></select>' +
"</div>" +
'<button type="button" class="btn btn-primary btn-sm zt-k-hub-plan__pdf" data-action="daily-plan-pdf">' +
'<i class="fas fa-file-pdf"></i> Скачать PDF</button>' +
"</div>"
);
}
function mount(container) {
rootEl = container;
rootEl.innerHTML =
'<div class="zt-k-hub-plan">' +
renderToolbar() +
'<div class="zt-k-hub-plan__content" data-daily-plan-content>' +
'<div class="zt-k-hub-plan__loading text-muted small">Загрузка плана…</div>' +
"</div></div>";
dateEl = rootEl.querySelector("[data-daily-plan-date]");
dispenserEl = rootEl.querySelector("[data-daily-plan-dispenser]");
contentEl = rootEl.querySelector("[data-daily-plan-content]");
if (dateEl) dateEl.value = todayIso();
dateEl?.addEventListener("change", () => loadPlan());
dispenserEl?.addEventListener("change", () => {
if (dispenserEl.value) {
try {
localStorage.setItem(DAILY_PLAN_DISPENSER_KEY, dispenserEl.value);
} catch {
/* ignore */
}
}
loadPlan();
});
rootEl.querySelector("[data-action='daily-plan-pdf']")?.addEventListener("click", downloadPdf);
rootEl.addEventListener("click", onPanelClick);
loadDispensers();
}
function planDateValue() {
return dateEl?.value || todayIso();
}
async function onPanelClick(event) {
const btn = event.target.closest("[data-action]");
if (!btn || !rootEl?.contains(btn)) return;
const action = btn.dataset.action;
if (action === "daily-plan-skip-trip") {
event.preventDefault();
const recipeId = btn.dataset.recipeId;
const recipeName = btn.dataset.recipeName || "Рейс";
if (recipeId) await skipTrip(recipeId, recipeName);
return;
}
if (action === "daily-plan-unskip-trip") {
event.preventDefault();
const recipeId = btn.dataset.recipeId;
if (recipeId) await unskipTrip(recipeId);
return;
}
if (action === "daily-plan-skip-ingredient") {
event.preventDefault();
const recipeId = btn.dataset.recipeId;
const ingredientId = btn.dataset.ingredientId;
const label = btn.dataset.label || "Компонент";
if (recipeId && ingredientId) await skipIngredient(recipeId, ingredientId, label);
return;
}
if (action === "daily-plan-unskip-ingredient") {
event.preventDefault();
const recipeId = btn.dataset.recipeId;
const ingredientId = btn.dataset.ingredientId;
if (recipeId && ingredientId) await unskipIngredient(recipeId, ingredientId);
return;
}
if (action === "daily-plan-skip-unloading-group") {
event.preventDefault();
const recipeId = btn.dataset.recipeId;
const groupId = btn.dataset.groupId;
const label = btn.dataset.label || "Группа";
if (recipeId && groupId) await skipUnloadingGroup(recipeId, groupId, label);
return;
}
if (action === "daily-plan-unskip-unloading-group") {
event.preventDefault();
const recipeId = btn.dataset.recipeId;
const groupId = btn.dataset.groupId;
if (recipeId && groupId) await unskipUnloadingGroup(recipeId, groupId);
return;
}
if (action === "daily-plan-replace-ingredient") {
event.preventDefault();
const recipeId = btn.dataset.recipeId;
const ingredientId = btn.dataset.ingredientId;
const componentId = btn.dataset.componentId;
const label = btn.dataset.label || "Компонент";
if (recipeId && ingredientId && componentId) {
await openReplaceIngredientModal(recipeId, ingredientId, componentId, label);
}
return;
}
if (action === "daily-plan-undo-replace-ingredient") {
event.preventDefault();
const recipeId = btn.dataset.recipeId;
const ingredientId = btn.dataset.ingredientId;
if (recipeId && ingredientId) await undoReplaceIngredient(recipeId, ingredientId);
return;
}
if (action === "daily-plan-undo-adjust-norm") {
event.preventDefault();
const componentId = btn.dataset.componentId;
if (componentId) await undoComponentNormAdjustment(componentId);
}
}
function formatKg(value) {
const n = Number(value);
if (!Number.isFinite(n)) return "—";
if (Number.isInteger(n)) return String(n);
return n.toFixed(2).replace(/\.?0+$/, "");
}
function replacementRowTitle(ing) {
if (!ing?.replacedToday) return "";
if (ing.recalculationMode === "dry_matter") {
return (
`Пересчёт по СВ: ${formatKg(ing.originalDryMatterPerHead)} кг/гол сохранено, ` +
`вес ${formatKg(ing.originalWeightPerHead)}${formatKg(ing.weightPerHead)} кг/гол ` +
`(СВ ${formatKg(ing.originalDryMatterPct)}% → ${formatKg(ing.dryMatterPct)}%)`
);
}
return (
`Пересчёт по весу: ${formatKg(ing.weightPerHead)} кг/гол сохранено, ` +
`СВ/гол ${formatKg(ing.originalDryMatterPerHead)}${formatKg(ing.dryMatterPerHead)} кг/гол`
);
}
function adjustmentRowTitle(ing) {
if (!ing?.adjustedToday) return "";
return (
`Норма в плане: ${formatKg(ing.originalWeightPerHead)}${formatKg(ing.weightPerHead)} кг/гол, ` +
`СВ/гол ${formatKg(ing.originalDryMatterPerHead)}${formatKg(ing.dryMatterPerHead)}`
);
}
function skippedIngredientHint(ing) {
const wph = Number(ing?.baselineWeightPerHead ?? ing?.weightPerHead);
const total = Number(ing?.baselineTotalKg);
if (!Number.isFinite(wph) || !Number.isFinite(total)) return "";
return `Было: ${formatKg(wph)} кг/гол (${formatKg(total)} кг)`;
}
function renderSkippedValueCell(hint) {
if (!hint) {
return '<td class="zt-k-hub-plan-row--skipped text-muted">—</td>';
}
return (
`<td class="zt-k-hub-plan-row--skipped text-muted zt-k-hub-plan-cell--hint" ` +
`data-hint="${escapeHtml(hint)}" title="${escapeHtml(hint)}" aria-label="${escapeHtml(hint)}">—</td>`
);
}
function renderPlanDot(title) {
return (
`<span class="recipe-skip-badge__dot zt-k-hub-plan-row__skip-dot" ` +
`title="${escapeHtml(title)}" aria-hidden="true"></span>`
);
}
function renderSkipDot() {
return renderPlanDot("Исключён из плана на этот день");
}
function renderReplaceDot(title) {
return renderPlanDot(title || "Сегодня заменили компонент в плане");
}
function renderAdjustDot(title = "Сегодня изменили норму в плане") {
return `<span class="zt-k-hub-plan-row__adjust-dot" title="${escapeHtml(title)}" aria-hidden="true"></span>`;
}
function renderRowActionBtn(action, attrs, title, iconClass) {
return (
`<button type="button" class="btn btn-sm recipe-table-delete-btn zt-k-hub-plan-row__skip" ` +
`data-action="${action}" ${attrs} title="${escapeHtml(title)}">` +
`<i class="fas ${iconClass}"></i></button>`
);
}
function renderSkipActionBtn(action, attrs, title) {
return renderRowActionBtn(action, attrs, title, "fa-times");
}
function hubModalEl() {
return (
rootEl?.closest(".modal.wesp-shell-modal") ||
document.getElementById("zootechNotificationCenterModal")
);
}
function planOverlayHost() {
return hubModalEl() || rootEl?.closest(".modal-content") || rootEl;
}
function dismissAllPlanOverlays() {
hubModalEl()
?.querySelectorAll(".zt-k-hub-plan-overlay")
.forEach((el) => el.remove());
}
function appendPlanOverlay(overlay) {
dismissAllPlanOverlays();
overlay.setAttribute("data-daily-plan-overlay", "1");
planOverlayHost()?.appendChild(overlay);
return overlay;
}
function promptSkipDuration(planDate, title = "На какой срок исключить?") {
const skip = global.WespDailyPlanSkipDuration;
if (!skip) return Promise.resolve(null);
return skip.promptSkipDuration({
planDate,
title,
overlayHost: planOverlayHost() || document.body,
beforeAppend: dismissAllPlanOverlays,
onError: notifyError,
});
}
function skipDurationBody(planDate, extra = {}, title) {
const skip = global.WespDailyPlanSkipDuration;
if (!skip) return Promise.resolve(null);
return skip.skipDurationBody(planDate, extra, title);
}
async function openReplaceIngredientModal(recipeId, ingredientId, componentId, label) {
const planDate = planDateValue();
const replaceModal = global.WespDailyPlanReplaceModal;
if (!replaceModal) return;
await replaceModal.open({
componentId,
label,
overlayHost: planOverlayHost() || document.body,
onError: notifyError,
onPick: async (replacementComponentId, replacementName) => {
const body = await skipDurationBody(
planDate,
{ recipeId, ingredientId, replacementComponentId },
"На какой срок заменить?"
);
if (!body) return;
const resp = await fetch("/api/daily-plan/replacements/ingredients", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify(body),
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.message || "Не удалось заменить компонент");
}
await loadPlan();
notifySuccess(`Компонент заменён на «${replacementName}»`);
},
});
}
async function undoReplaceIngredient(recipeId, ingredientId) {
const planDate = planDateValue();
try {
const params = new URLSearchParams({
recipe_id: recipeId,
ingredient_id: ingredientId,
date: planDate,
});
const resp = await fetch(`/api/daily-plan/replacements/ingredients?${params.toString()}`, {
method: "DELETE",
headers: { Accept: "application/json" },
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.message || "Не удалось отменить замену");
}
await loadPlan();
notifySuccess("Замена отменена");
} catch (err) {
notifyError(err.message, "Не удалось отменить замену");
}
}
async function undoComponentNormAdjustment(componentId) {
const planDate = planDateValue();
try {
const params = new URLSearchParams({
component_id: componentId,
date: planDate,
});
const resp = await fetch(`/api/daily-plan/adjustments/components?${params.toString()}`, {
method: "DELETE",
headers: { Accept: "application/json" },
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.message || "Не удалось вернуть норму");
}
await loadPlan();
notifySuccess("Норма возвращена к мастеру");
if (typeof global.reloadAfterSkipRestore === "function") {
global.reloadAfterSkipRestore();
}
} catch (err) {
notifyError(err.message, "Не удалось вернуть норму");
}
}
async function skipTrip(recipeId, recipeName) {
const planDate = planDateValue();
const body = await skipDurationBody(planDate, { recipeId });
if (!body) return;
try {
const resp = await fetch("/api/daily-plan/skips", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify(body),
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.message || "Не удалось исключить рейс");
}
await loadPlan();
showUndoToast(
"daily-plan-unskip-trip",
`data-recipe-id="${escapeHtml(recipeId)}"`,
`Рейс «${escapeHtml(recipeName)}» убран из плана.`
);
} catch (err) {
notifyError(err.message, "Не удалось исключить рейс");
}
}
async function unskipTrip(recipeId) {
const planDate = planDateValue();
try {
const params = new URLSearchParams({ recipe_id: recipeId, date: planDate });
const resp = await fetch(`/api/daily-plan/skips?${params.toString()}`, {
method: "DELETE",
headers: { Accept: "application/json" },
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.message || "Не удалось вернуть рейс");
}
dismissUndoToast();
await loadPlan();
notifySuccess("Рейс снова в плане");
} catch (err) {
notifyError(err.message, "Не удалось вернуть рейс");
}
}
async function skipIngredient(recipeId, ingredientId, label) {
const planDate = planDateValue();
const body = await skipDurationBody(planDate, { recipeId, ingredientId });
if (!body) return;
try {
const resp = await fetch("/api/daily-plan/skips/ingredients", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify(body),
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.message || "Не удалось убрать компонент");
}
await loadPlan();
showUndoToast(
"daily-plan-unskip-ingredient",
`data-recipe-id="${escapeHtml(recipeId)}" data-ingredient-id="${escapeHtml(ingredientId)}"`,
`Компонент «${escapeHtml(label)}» убран из плана.`
);
} catch (err) {
notifyError(err.message, "Не удалось убрать компонент");
}
}
async function unskipIngredient(recipeId, ingredientId) {
const planDate = planDateValue();
try {
const params = new URLSearchParams({
recipe_id: recipeId,
ingredient_id: ingredientId,
date: planDate,
});
const resp = await fetch(`/api/daily-plan/skips/ingredients?${params.toString()}`, {
method: "DELETE",
headers: { Accept: "application/json" },
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.message || "Не удалось вернуть компонент");
}
dismissUndoToast();
await loadPlan();
notifySuccess("Компонент снова в плане");
} catch (err) {
notifyError(err.message, "Не удалось вернуть компонент");
}
}
async function skipUnloadingGroup(recipeId, groupId, label) {
const planDate = planDateValue();
const body = await skipDurationBody(planDate, { recipeId, unloadingGroupId: groupId });
if (!body) return;
try {
const resp = await fetch("/api/daily-plan/skips/unloading-groups", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify(body),
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.message || "Не удалось убрать группу");
}
await loadPlan();
showUndoToast(
"daily-plan-unskip-unloading-group",
`data-recipe-id="${escapeHtml(recipeId)}" data-group-id="${escapeHtml(groupId)}"`,
`Группа «${escapeHtml(label)}» убрана из плана.`
);
} catch (err) {
notifyError(err.message, "Не удалось убрать группу");
}
}
async function unskipUnloadingGroup(recipeId, groupId) {
const planDate = planDateValue();
try {
const params = new URLSearchParams({
recipe_id: recipeId,
unloading_group_id: groupId,
date: planDate,
});
const resp = await fetch(
`/api/daily-plan/skips/unloading-groups?${params.toString()}`,
{ method: "DELETE", headers: { Accept: "application/json" } }
);
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.message || "Не удалось вернуть группу");
}
dismissUndoToast();
await loadPlan();
notifySuccess("Группа снова в плане");
} catch (err) {
notifyError(err.message, "Не удалось вернуть группу");
}
}
function dismissUndoToast() {
if (undoToastTimer) {
clearTimeout(undoToastTimer);
undoToastTimer = null;
}
rootEl?.querySelector("[data-daily-plan-undo]")?.remove();
}
function showUndoToast(unskipAction, dataAttrs, message) {
dismissUndoToast();
const toast = document.createElement("div");
toast.className = "zt-k-hub-plan__undo-toast";
toast.dataset.dailyPlanUndo = "1";
toast.innerHTML =
`<span>${message}</span>` +
`<button type="button" class="btn btn-sm btn-link zt-k-hub-plan__undo-btn" ` +
`data-action="${unskipAction}" ${dataAttrs}>Отменить</button>`;
rootEl?.querySelector(".zt-k-hub-plan")?.appendChild(toast);
undoToastTimer = setTimeout(() => dismissUndoToast(), 8000);
}
async function loadDispensers() {
try {
const resp = await fetch("/api/feed_dispensers?limit=200");
if (!resp.ok) throw new Error("dispensers");
const data = await resp.json();
dispensers = Array.isArray(data) ? data : [];
if (!dispenserEl) return;
if (!dispensers.length) {
dispenserEl.innerHTML = '<option value="">Нет кормораздатчиков</option>';
renderEmpty("Добавьте кормораздатчик в разделе «Кормораздатчики».");
return;
}
const saved = (() => {
try {
return localStorage.getItem(DAILY_PLAN_DISPENSER_KEY) || "";
} catch {
return "";
}
})();
const dispenserItems = dispensers.filter((d) => d.type !== "mill");
const millItems = dispensers.filter((d) => d.type === "mill");
let options =
`<option value="${ALL_DISPENSERS_ID}"${saved === ALL_DISPENSERS_ID ? " selected" : ""}>Все кормораздатчики</option>`;
if (dispenserItems.length) {
options += '<optgroup label="Кормораздатчики">';
options += dispenserItems
.map((d) => {
const id = escapeHtml(d.id);
const name = escapeHtml(d.name || d.id);
const selected = d.id === saved ? " selected" : "";
return `<option value="${id}"${selected}>${name}</option>`;
})
.join("");
options += "</optgroup>";
}
options += `<option value="${ALL_MILLS_ID}"${saved === ALL_MILLS_ID ? " selected" : ""}>Все кормоцеха</option>`;
if (millItems.length) {
options += '<optgroup label="Кормоцеха">';
options += millItems
.map((d) => {
const id = escapeHtml(d.id);
const name = escapeHtml(d.name || d.id);
const selected = d.id === saved ? " selected" : "";
return `<option value="${id}"${selected}>${name}</option>`;
})
.join("");
options += "</optgroup>";
}
dispenserEl.innerHTML = options;
if (!dispenserEl.value) {
dispenserEl.value = saved || ALL_DISPENSERS_ID;
}
loadPlan();
} catch {
if (dispenserEl) {
dispenserEl.innerHTML = '<option value="">Ошибка загрузки</option>';
}
renderEmpty("Не удалось загрузить список кормораздатчиков.");
}
}
function buildPlanUrl(pdf) {
const params = new URLSearchParams();
if (dateEl?.value) params.set("date", dateEl.value);
if (dispenserEl?.value) params.set("dispenser_id", dispenserEl.value);
const base = pdf ? "/api/daily-plan/pdf" : "/api/daily-plan";
return `${base}?${params.toString()}`;
}
async function loadPlan() {
if (!contentEl) return;
if (!dispenserEl?.value) {
renderEmpty("Выберите кормораздатчик.");
return;
}
contentEl.innerHTML =
'<div class="zt-k-hub-plan__loading text-muted small">Загрузка плана…</div>';
try {
const resp = await fetch(buildPlanUrl(false));
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.message || "Ошибка загрузки");
}
const plan = await resp.json();
renderPlan(plan);
} catch (err) {
renderEmpty(global.WespUserMessages?.safe?.(err.message, "Не удалось загрузить план.") || "Не удалось загрузить план.");
}
}
function renderEmpty(message) {
if (!contentEl) return;
contentEl.innerHTML =
'<div class="zt-k-hub-plan__empty">' +
'<i class="fas fa-calendar-day"></i>' +
`<p>${escapeHtml(message)}</p></div>`;
}
function renderReplaceBtn(recipeId, ing) {
if (!ing.componentId) return "";
const label = ing.originalName || ing.name || "Компонент";
return renderRowActionBtn(
"daily-plan-replace-ingredient",
`data-recipe-id="${escapeHtml(recipeId)}" data-ingredient-id="${escapeHtml(ing.id)}" ` +
`data-component-id="${escapeHtml(ing.componentId)}" data-label="${escapeHtml(label)}"`,
"Заменить компонент",
"fa-exchange-alt"
);
}
function renderTotalsColumn(totals, grandTotalKg) {
if (!totals.length) {
return (
'<aside class="zt-k-hub-plan__totals-col">' +
'<h3 class="zt-k-hub-plan__col-title">Итого по компонентам</h3>' +
'<p class="zt-k-hub-plan__col-empty text-muted">Нет данных</p></aside>'
);
}
let rows = "";
totals.forEach((row) => {
rows += `<tr><td>${escapeHtml(row.name)}</td><td>${escapeHtml(formatKg(row.totalKg))}</td></tr>`;
});
const grand =
grandTotalKg ??
totals.reduce((sum, row) => sum + Number(row.totalKg || 0), 0);
return (
'<aside class="zt-k-hub-plan__totals-col">' +
'<h3 class="zt-k-hub-plan__col-title">Итого по компонентам</h3>' +
'<div class="zt-k-hub-plan__col-scroll">' +
'<table class="zt-k-hub-plan-table zt-k-hub-plan-table--totals">' +
"<thead><tr><th>Компонент</th><th>Всего, кг</th></tr></thead>" +
`<tbody>${rows}</tbody>` +
`<tfoot><tr class="zt-k-hub-plan-table__total-row"><th>Итого</th><th>${escapeHtml(formatKg(grand))}</th></tr></tfoot>` +
"</table></div></aside>"
);
}
function renderSkippedSection(plan) {
const skippedTrips = plan.skippedTrips || [];
if (!skippedTrips.length) return "";
let rows = "";
skippedTrips.forEach((trip) => {
rows +=
'<li class="zt-k-hub-plan-skipped__item">' +
`<span>${renderSkipDot()}<span class="text-muted">Рейс:</span> ${escapeHtml(trip.recipeName)}</span>` +
'<button type="button" class="btn btn-sm btn-outline-success" ' +
'data-action="daily-plan-unskip-trip" ' +
`data-recipe-id="${escapeHtml(trip.recipeId)}" title="Вернуть в план">` +
'<i class="fas fa-undo"></i> Вернуть</button></li>';
});
return (
'<section class="zt-k-hub-plan-skipped">' +
'<h3 class="zt-k-hub-plan-skipped__title">Исключено из плана</h3>' +
`<ul class="zt-k-hub-plan-skipped__list">${rows}</ul></section>`
);
}
function renderTripsColumn(periods) {
let html = '<div class="zt-k-hub-plan__trips-col"><div class="zt-k-hub-plan__col-scroll">';
periods.forEach((period) => {
const trips = period.trips || [];
if (!trips.length) return;
html +=
`<section class="zt-k-hub-plan__period"><h3 class="zt-k-hub-plan__period-title">${escapeHtml(period.name)}</h3><div class="zt-k-hub-plan__trips">`;
trips.forEach((trip) => {
const recipeId = escapeHtml(trip.recipeId);
const hasAdjustedNorm = (trip.ingredients || []).some((ing) => ing.adjustedToday);
const hasReplacedIngredient = (trip.ingredients || []).some((ing) => ing.replacedToday);
const adjustBadge = hasAdjustedNorm
? ' <span class="recipe-adjust-badge" title="Сегодня изменили норму в плане">' +
'<span class="zt-k-hub-plan-row__adjust-dot" aria-hidden="true"></span>сегодня изменили норму</span>'
: "";
const replaceBadge = hasReplacedIngredient
? ' <span class="recipe-skip-badge" title="Сегодня заменили компонент в плане">' +
'<span class="recipe-skip-badge__dot" aria-hidden="true"></span>заменён компонент</span>'
: "";
const ings = (trip.ingredients || [])
.map((ing) => {
const ingId = escapeHtml(ing.id);
const name = escapeHtml(ing.name);
const skipped = Boolean(ing.skippedToday);
const replaced = Boolean(ing.replacedToday);
const adjusted = Boolean(ing.adjustedToday);
const rowClass = skipped
? "zt-k-hub-plan-row--skipped"
: replaced
? "zt-k-hub-plan-row--replaced"
: adjusted
? "zt-k-hub-plan-row--adjusted"
: "";
const replaceTitle = replacementRowTitle(ing);
const adjustTitle = adjustmentRowTitle(ing);
const skipHint = skipped ? skippedIngredientHint(ing) : "";
const nameCell = skipped
? `<td class="zt-k-hub-plan-row--skipped">${renderSkipDot()}${name}</td>`
: replaced
? `<td class="zt-k-hub-plan-row--replaced" title="${escapeHtml(replaceTitle)}">${renderReplaceDot(replaceTitle)}${name}</td>`
: adjusted
? `<td class="zt-k-hub-plan-row--adjusted" title="${escapeHtml(adjustTitle)}">${renderAdjustDot(adjustTitle)}${name}</td>`
: `<td>${name}</td>`;
const weightCell = skipped
? renderSkippedValueCell(skipHint)
: `<td>${escapeHtml(formatKg(ing.weightPerHead))}</td>`;
const totalCell = skipped
? renderSkippedValueCell(skipHint)
: `<td>${escapeHtml(formatKg(ing.totalKg))}</td>`;
let actionCell = "";
if (ing.id) {
if (skipped) {
actionCell = renderRowActionBtn(
"daily-plan-unskip-ingredient",
`data-recipe-id="${recipeId}" data-ingredient-id="${ingId}"`,
"Вернуть в план",
"fa-undo"
);
} else {
if (replaced) {
actionCell += renderRowActionBtn(
"daily-plan-undo-replace-ingredient",
`data-recipe-id="${recipeId}" data-ingredient-id="${ingId}"`,
"Отменить замену",
"fa-undo"
);
} else if (adjusted && ing.componentId) {
actionCell += renderRowActionBtn(
"daily-plan-undo-adjust-norm",
`data-component-id="${escapeHtml(ing.componentId)}"`,
"Вернуть норму",
"fa-undo"
);
} else {
actionCell += renderReplaceBtn(recipeId, ing);
}
actionCell += renderSkipActionBtn(
"daily-plan-skip-ingredient",
`data-recipe-id="${recipeId}" data-ingredient-id="${ingId}" data-label="${escapeHtml(ing.originalName || ing.name)}"`,
"Убрать компонент из плана на этот день"
);
}
}
return (
`<tr class="${rowClass}">` +
`${nameCell}${weightCell}${totalCell}` +
`<td class="zt-k-hub-plan-table__actions zt-k-hub-plan-table__actions--multi">${actionCell}</td></tr>`
);
})
.join("");
const groups = (trip.unloadingGroups || [])
.map((g) => {
const groupId = escapeHtml(g.id);
const name = escapeHtml(g.name);
const skipped = Boolean(g.skippedToday);
const nameCell = skipped
? `<td class="zt-k-hub-plan-row--skipped">${renderSkipDot()}${name}</td>`
: `<td>${name}</td>`;
const weightCell = skipped
? '<td class="zt-k-hub-plan-row--skipped text-muted">—</td>'
: `<td>${escapeHtml(g.weightKg)}</td>`;
const distCell = skipped
? '<td class="zt-k-hub-plan-row--skipped text-muted">—</td>'
: `<td>${escapeHtml(formatDistribution(g))}</td>`;
let actionCell = "";
if (g.id) {
if (skipped) {
actionCell = renderRowActionBtn(
"daily-plan-unskip-unloading-group",
`data-recipe-id="${recipeId}" data-group-id="${groupId}"`,
"Вернуть в план",
"fa-undo"
);
} else {
actionCell = renderSkipActionBtn(
"daily-plan-skip-unloading-group",
`data-recipe-id="${recipeId}" data-group-id="${groupId}" data-label="${name}"`,
"Убрать группу из плана на этот день"
);
}
}
return (
`<tr class="${skipped ? "zt-k-hub-plan-row--skipped" : ""}">` +
`${nameCell}${weightCell}${distCell}` +
`<td class="zt-k-hub-plan-table__actions">${actionCell}</td></tr>`
);
})
.join("");
html +=
'<article class="zt-k-hub-plan-trip zt-card">' +
'<div class="zt-k-hub-plan-trip__head">' +
`<h4 class="zt-k-hub-plan-trip__title">${escapeHtml(trip.recipeName)}${replaceBadge}${adjustBadge}` +
` <span class="text-muted">(${escapeHtml(trip.headsPerTrip)} гол., ${escapeHtml(trip.mixingTimeSec)} с)</span></h4>` +
'<button type="button" class="btn btn-sm recipe-table-delete-btn zt-k-hub-plan-trip__skip" ' +
'data-action="daily-plan-skip-trip" ' +
`data-recipe-id="${recipeId}" ` +
`data-recipe-name="${escapeHtml(trip.recipeName)}" ` +
'title="Убрать из плана на этот день">' +
'<i class="fas fa-trash"></i></button></div>' +
'<table class="zt-k-hub-plan-table"><thead><tr><th>Компонент</th><th>кг/гол</th><th>Всего, кг</th><th></th></tr></thead>' +
`<tbody>${ings || '<tr><td colspan="4" class="text-muted">—</td></tr>'}</tbody>` +
`<tfoot><tr class="zt-k-hub-plan-table__total-row"><td>Итого по рейсу</td><td></td><td>${escapeHtml(formatKg(trip.totalWeightKg))}</td><td></td></tr></tfoot></table>`;
if (groups) {
html +=
'<p class="zt-k-hub-plan-trip__sub">Выгрузка</p>' +
'<table class="zt-k-hub-plan-table"><thead><tr><th>Группа</th><th>кг</th><th>Распределение</th><th></th></tr></thead>' +
`<tbody>${groups}</tbody></table>`;
}
html += "</article>";
});
html += "</div></section>";
});
html += "</div></div>";
return html;
}
function renderPlan(plan) {
if (!contentEl) return;
const periods = plan.periods || [];
const skippedTrips = plan.skippedTrips || [];
const hasTrips = periods.some((p) => (p.trips || []).length);
const hasSkipped = skippedTrips.length;
if (!hasTrips && !hasSkipped) {
renderEmpty("Нет периодов или рейсов для выбранного кормораздатчика.");
return;
}
contentEl.innerHTML =
'<div class="zt-k-hub-plan__split">' +
renderTotalsColumn(plan.ingredientTotals || [], plan.ingredientGrandTotalKg) +
renderTripsColumn(periods) +
"</div>" +
renderSkippedSection(plan);
}
function downloadPdf() {
if (!dispenserEl?.value) return;
global.open(buildPlanUrl(true), "_blank", "noopener");
}
function destroy() {
dismissUndoToast();
dismissAllPlanOverlays();
if (rootEl) {
rootEl.removeEventListener("click", onPanelClick);
rootEl.innerHTML = "";
}
rootEl = null;
dateEl = null;
dispenserEl = null;
contentEl = null;
}
return {
mount,
destroy,
dismissAllPlanOverlays,
reload: loadPlan,
getSelectedDispenserId,
getSelectedPlanDate,
};
}
let activePanel = null;
global.WespDailyPlanPanel = {
createDailyPlanPanel,
_setActivePanel(panel) {
activePanel = panel || null;
},
getSelectedDispenserId() {
return activePanel?.getSelectedDispenserId?.() || "";
},
getSelectedPlanDate() {
return activePanel?.getSelectedPlanDate?.() || todayIso();
},
reload() {
return activePanel?.reload?.();
},
};
})(typeof window !== "undefined" ? window : globalThis);