Интегрирован 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,665 @@
/**
* Коррекция СВ% компонентов в плане — overlay в стиле K-hub.
* Редактируется только СВ%; превью через /api/recipes/calculate.
*/
(function (global) {
function todayIso() {
return global.WespDailyPlanSkipDuration?.todayIso?.() || new Date().toISOString().slice(0, 10);
}
function escapeHtml(value) {
return (
global.WespDailyPlanSkipDuration?.escapeHtml?.(value) ||
String(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;")
);
}
function fmtNum(value, digits) {
const n = Number(value);
if (!Number.isFinite(n)) return "—";
return n.toFixed(digits).replace(/\.?0+$/, "");
}
function roundWphNum(value) {
const n = Number(value);
if (!Number.isFinite(n)) return NaN;
if (n > 0 && n < 0.01) return 0.01;
return Math.round(n * 100) / 100;
}
function fmtRange(values, digits) {
const nums = values.filter((n) => Number.isFinite(n));
if (!nums.length) return "—";
const min = Math.min(...nums);
const max = Math.max(...nums);
const a = fmtNum(min, digits);
const b = fmtNum(max, digits);
return a === b ? a : `${a}${b}`;
}
function usageEntries(item) {
const usages = Array.isArray(item.usages) ? item.usages : [];
if (usages.length) {
return usages.map((u) => ({
wph: Number(u.masterWeightPerHead),
dmPh: Number(u.masterDryMatterPerHead),
dryMatterLocked: !!u.dryMatterLocked,
}));
}
return [
{
wph: Number(item.masterWeightPerHead),
dmPh: Number(item.masterDryMatterPerHead),
dryMatterLocked: false,
},
];
}
const PHYS_WEIGHT_HINT = "Этот компонент выдается строго по физическому весу";
function itemIsDmEditable(item) {
return itemHasLockedUsage(item);
}
function componentCellHtml(item) {
const editable = itemIsDmEditable(item);
const lockedCount = lockedUsageEntries(item).length;
const trips = Number(item.usageCount);
let meta = "";
if (editable && lockedCount > 0) {
meta = `<div class="zt-k-hub-plan-norms__meta">${lockedCount} рец. с замком СВ</div>`;
} else if (!editable) {
meta = `<div class="zt-k-hub-plan-norms__meta zt-k-hub-plan-norms__meta--weight">${escapeHtml(PHYS_WEIGHT_HINT)}</div>`;
} else if (Number.isFinite(trips) && trips > 1) {
meta = `<div class="zt-k-hub-plan-norms__meta">${trips} рейсов в плане</div>`;
}
const lockIcon = editable
? ""
: `<i class="fas fa-lock zt-k-hub-plan-norms__weight-lock" title="${escapeHtml(PHYS_WEIGHT_HINT)}" aria-hidden="true"></i>`;
return (
`<div class="zt-k-hub-plan-norms__component">` +
`<div class="zt-k-hub-plan-norms__name-row">` +
lockIcon +
`<div class="zt-k-hub-plan-norms__name">${escapeHtml(item.componentName)}</div>` +
`</div>` +
meta +
"</div>"
);
}
function flashWeightLockedHint(input) {
const cell = input?.closest(".zt-k-hub-plan-norms__dm-cell");
if (!cell) return;
let hint = cell.querySelector("[data-norms-weight-hint]");
if (!hint) {
hint = document.createElement("span");
hint.className = "zt-k-hub-plan-norms__weight-hint";
hint.dataset.normsWeightHint = "1";
hint.textContent = PHYS_WEIGHT_HINT;
cell.appendChild(hint);
}
hint.classList.add("is-visible");
clearTimeout(hint._hideTimer);
hint._hideTimer = setTimeout(() => hint.classList.remove("is-visible"), 2800);
}
function notify(msg, type) {
const n = global.WespZootechNotify?.createAdapter?.();
if (!n) return;
if (type === "error") n.error(msg, { skipRecord: true });
else n.success(msg, { skipRecord: true });
}
function overlayHost() {
return document.getElementById("zootechNotificationCenterModal") || document.body;
}
let overlayEl = null;
let items = [];
const rowPreviewTimers = new Map();
let recalcSeq = 0;
let lockedOnlyFilter = true;
function itemHasLockedUsage(item) {
return usageEntries(item).some((entry) => entry.dryMatterLocked);
}
function lockedUsageEntries(item) {
return usageEntries(item).filter((entry) => entry.dryMatterLocked);
}
function clearRowPreviewTimers() {
rowPreviewTimers.forEach((timer) => clearTimeout(timer));
rowPreviewTimers.clear();
}
function closeModal() {
clearRowPreviewTimers();
recalcSeq += 1;
if (overlayEl?.isConnected) overlayEl.remove();
overlayEl = null;
}
function actionBtn(action, attrs, title, icon) {
return (
`<button type="button" class="btn btn-sm recipe-table-delete-btn zt-k-hub-plan-row__skip" ` +
`data-${action}="1" ${attrs} title="${escapeHtml(title)}">` +
`<i class="fas ${icon}" aria-hidden="true"></i></button>`
);
}
function masterPreviewForItem(item) {
const wphs = [];
const dmPhs = [];
for (const entry of usageEntries(item)) {
if (Number.isFinite(entry.wph)) wphs.push(roundWphNum(entry.wph));
if (Number.isFinite(entry.dmPh)) dmPhs.push(entry.dmPh);
}
return {
dmPh: fmtRange(dmPhs, 4),
wph: fmtRange(wphs, 2),
};
}
function previewCompareHtml(before, after) {
const was = before || "—";
const now = after || "—";
if (was === "—" && now === "—") return "—";
if (was === now) {
return `<span class="zt-k-hub-plan-norms-preview__now">${escapeHtml(now)}</span>`;
}
return (
`<span class="zt-k-hub-plan-norms-preview zt-k-hub-plan-norms-preview--compare">` +
`<span class="zt-k-hub-plan-norms-preview__was">${escapeHtml(was)}</span>` +
`<span class="zt-k-hub-plan-norms-preview__now">${escapeHtml(now)}</span>` +
`</span>`
);
}
async function previewForItem(item, planPct) {
const pct = Number(planPct);
if (!Number.isFinite(pct) || pct < 0) return { dmPh: "—", wph: "—" };
const lockedEntries = lockedUsageEntries(item);
const wphs = [];
const dmPhs = [];
await Promise.all(
lockedEntries.map(async (entry) => {
if (!Number.isFinite(entry.dmPh)) return;
try {
const response = await fetch("/api/recipes/calculate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
ingredients: [
{
component_id: item.componentId,
dryMatter: pct,
dryMatterPerHead: entry.dmPh,
},
],
headsCount: 1,
tripPercent: 100,
calculateFromDryMatter: true,
}),
});
if (!response.ok) return;
const result = await response.json();
const calc = result.ingredients?.[0];
if (!calc) return;
dmPhs.push(Number(entry.dmPh));
const rawWph = calc.weightPerHead !== undefined ? calc.weightPerHead : calc.weight_per_head;
if (rawWph != null) wphs.push(roundWphNum(rawWph));
} catch (_) {
/* ignore */
}
})
);
for (const entry of usageEntries(item)) {
if (entry.dryMatterLocked) continue;
if (!Number.isFinite(entry.wph)) continue;
wphs.push(roundWphNum(entry.wph));
if (pct >= 0) dmPhs.push(entry.wph * (pct / 100));
}
return {
dmPh: fmtRange(dmPhs, 4),
wph: fmtRange(wphs, 2),
};
}
function planEditValue(item) {
if (item.planDryMatterPct != null) return item.planDryMatterPct;
return item.masterDryMatterPct ?? item.dryMatterPct;
}
function masterBaseDryMatterLabel(item) {
const catalog = Number(item.dryMatterPct);
if (Number.isFinite(catalog)) {
return `${fmtNum(catalog, 2)}%`;
}
const min = Number(item.masterDryMatterPctMin);
const max = Number(item.masterDryMatterPctMax);
if (Number.isFinite(min) && Number.isFinite(max)) {
const a = fmtNum(min, 2);
const b = fmtNum(max, 2);
return a === b ? `${a}%` : `${a}${b}%`;
}
const master = Number(item.masterDryMatterPct);
if (Number.isFinite(master)) return `${fmtNum(master, 2)}%`;
return null;
}
function baseDryMatterHintHtml(item) {
const label = masterBaseDryMatterLabel(item);
if (!label) return "";
return (
`<span class="zt-k-hub-plan-norms__base" data-norms-base title="СВ% в справочнике компонентов">` +
`База: ${escapeHtml(label)}</span>`
);
}
function setRowPreviewLoading(row) {
const dmPhEl = row.querySelector("[data-norms-preview-dmph]");
const wphEl = row.querySelector("[data-norms-preview-wph]");
if (dmPhEl) {
dmPhEl.textContent = "…";
dmPhEl.classList.remove("zt-k-hub-plan-norms-preview-cell--compare");
}
if (wphEl) {
wphEl.textContent = "…";
wphEl.classList.remove("zt-k-hub-plan-norms-preview-cell--compare");
}
}
function setPreviewCell(el, before, after) {
if (!el) return;
const was = before || "—";
const now = after || "—";
if (was === now) {
el.classList.remove("zt-k-hub-plan-norms-preview-cell--compare");
el.innerHTML = `<span class="zt-k-hub-plan-norms-preview">${escapeHtml(now)}</span>`;
return;
}
el.classList.add("zt-k-hub-plan-norms-preview-cell--compare");
el.innerHTML = previewCompareHtml(was, now);
}
async function updateRowPreview(row, seq) {
const componentId = row.dataset.componentId;
const item = items.find((x) => String(x.componentId) === String(componentId));
if (!item) return;
const input = row.querySelector("[data-norms-input]");
const dmPhEl = row.querySelector("[data-norms-preview-dmph]");
const wphEl = row.querySelector("[data-norms-preview-wph]");
const masterPrev = masterPreviewForItem(item);
if (row.dataset.normsEditable !== "1") {
if (dmPhEl) {
dmPhEl.classList.remove("zt-k-hub-plan-norms-preview-cell--compare");
dmPhEl.innerHTML = `<span class="zt-k-hub-plan-norms-preview">${escapeHtml(masterPrev.dmPh)}</span>`;
}
if (wphEl) {
wphEl.classList.remove("zt-k-hub-plan-norms-preview-cell--compare");
wphEl.innerHTML = `<span class="zt-k-hub-plan-norms-preview">${escapeHtml(masterPrev.wph)}</span>`;
}
return;
}
const raw = input?.value;
if (raw === "" || raw == null) {
setPreviewCell(dmPhEl, null, null);
setPreviewCell(wphEl, null, null);
return;
}
setRowPreviewLoading(row);
const next = await previewForItem(item, raw);
if (seq !== recalcSeq || !overlayEl?.contains(row)) return;
setPreviewCell(dmPhEl, masterPrev.dmPh, next.dmPh);
setPreviewCell(wphEl, masterPrev.wph, next.wph);
}
function scheduleRowPreview(row, debounceMs) {
if (!row) return;
const componentId = row.dataset.componentId;
if (!componentId) return;
const existing = rowPreviewTimers.get(componentId);
if (existing) clearTimeout(existing);
const seq = recalcSeq;
const delay = debounceMs == null ? 300 : debounceMs;
if (delay <= 0) {
rowPreviewTimers.delete(componentId);
updateRowPreview(row, seq).catch((err) => console.error("Ошибка при расчете:", err));
return;
}
rowPreviewTimers.set(
componentId,
setTimeout(() => {
rowPreviewTimers.delete(componentId);
updateRowPreview(row, seq).catch((err) => console.error("Ошибка при расчете:", err));
}, delay)
);
}
function updateAllPreviews() {
overlayEl?.querySelectorAll("[data-norms-row]").forEach((row) => scheduleRowPreview(row, 0));
}
function buildRowHtml(item) {
const editable = itemIsDmEditable(item);
const val = planEditValue(item);
const inputVal = val != null && Number.isFinite(Number(val)) ? Number(val) : "";
const undo = item.adjustedToday
? actionBtn("norms-undo", "", "Вернуть как в рецепте", "fa-undo")
: "";
const replaceBtn = actionBtn(
"norms-replace",
"",
"Заменить компонент во всех рецептах",
"fa-exchange-alt"
);
const applyBtn = editable ? actionBtn("norms-apply", "", "Сохранить для плана", "fa-check") : "";
const rowMods = [
item.adjustedToday ? "ingredient-row--adjusted" : "",
editable ? "" : "ingredient-row--weight-locked",
]
.filter(Boolean)
.join(" ");
const inputClass = editable
? "form-control zt-k-hub-plan-norms-input dry-matter-percent-input"
: "form-control zt-k-hub-plan-norms-input zt-k-hub-plan-norms-input--locked dry-matter-percent-input";
const inputAttrs = editable
? `data-norms-input aria-label="СВ% на план"`
: `readonly tabindex="-1" data-norms-input data-norms-input-locked aria-label="СВ% — только чтение" aria-disabled="true" title="${escapeHtml(PHYS_WEIGHT_HINT)}"`;
return (
`<div class="zt-data-grid__row ingredient-row${rowMods ? " " + rowMods : ""}" ` +
`data-norms-row data-component-id="${escapeHtml(item.componentId)}" ` +
`data-norms-editable="${editable ? "1" : "0"}" role="row">` +
`<div class="zt-data-grid__cell ingredient-detail-cell zt-k-hub-plan-norms__component-cell" data-label="Компонент" role="gridcell">` +
componentCellHtml(item) +
"</div>" +
`<div class="zt-data-grid__cell ingredient-detail-cell dry-matter-percent zt-data-grid__cell--num" data-label="СВ, %" role="gridcell">` +
`<div class="zt-k-hub-plan-norms__dm-cell">` +
`<input type="number" class="${inputClass}" step="0.01" min="0" max="100" ` +
`value="${inputVal === "" ? "" : escapeHtml(String(inputVal))}" inputmode="decimal" ${inputAttrs}>` +
baseDryMatterHintHtml(item) +
`</div></div>` +
`<div class="zt-data-grid__cell ingredient-detail-cell zt-data-grid__cell--num" data-label="СВ/гол, кг" role="gridcell">` +
`<span class="text-cell zt-k-hub-plan-norms-preview-cell" data-norms-preview-dmph>…</span></div>` +
`<div class="zt-data-grid__cell ingredient-detail-cell zt-data-grid__cell--num" data-label="Вес/гол, кг" role="gridcell">` +
`<span class="text-cell zt-k-hub-plan-norms-preview-cell" data-norms-preview-wph>…</span></div>` +
`<div class="zt-data-grid__cell ingredient-detail-cell zt-data-grid__cell--actions recipe-table-actions-cell" data-label="Действия" role="gridcell">` +
`<div class="d-flex align-items-center justify-content-end flex-nowrap recipe-table-row-actions" role="group">` +
applyBtn +
replaceBtn +
undo +
"</div></div></div>"
);
}
function ensureOverlay() {
closeModal();
overlayEl = document.createElement("div");
overlayEl.className = "zt-k-hub-plan-overlay zt-k-hub-plan-norms";
overlayEl.innerHTML =
'<div class="zt-k-hub-plan-subdialog zt-k-hub-plan-norms__dialog" role="dialog" aria-modal="true">' +
'<div class="zt-k-hub-plan-subdialog__header">' +
'<h3 class="zt-k-hub-plan-subdialog__title">Коррекция СВ% в плане</h3>' +
'<button type="button" class="wesp-shell-bsmodal-close" data-norms-close aria-label="Закрыть">×</button>' +
"</div>" +
'<p class="zt-k-hub-plan-norms__lead">Только на выбранный срок. Рецепты в справочнике не меняются. Пересчёт кг/гол — только где в рецепте включён замок СВ.</p>' +
'<label class="zt-k-hub-plan-replace__search-label">Поиск компонента</label>' +
'<input type="search" class="form-control zt-k-hub-plan-replace__search" data-norms-search placeholder="Начните вводить название…" autocomplete="off">' +
'<label class="zt-k-hub-plan-norms__filter">' +
'<span class="zt-checkbox">' +
'<input type="checkbox" class="zt-checkbox__input" data-norms-locked-only checked>' +
'<span class="zt-checkbox__box" aria-hidden="true"></span>' +
"</span>" +
'<span class="zt-k-hub-plan-norms__filter-text">Только компоненты в рецептах с замком СВ</span></label>' +
'<div class="zt-k-hub-plan-norms__table-wrap">' +
'<div class="zt-data-grid zt-data-grid--norms" id="planNormsGrid" role="grid" aria-label="Коррекция норм">' +
'<div class="zt-data-grid__head" role="row">' +
'<div class="zt-data-grid__cell zt-data-grid__cell--head" role="columnheader">Компонент</div>' +
'<div class="zt-data-grid__cell zt-data-grid__cell--head zt-data-grid__cell--num" role="columnheader">СВ, %</div>' +
'<div class="zt-data-grid__cell zt-data-grid__cell--head zt-data-grid__cell--num" role="columnheader">СВ/гол, кг</div>' +
'<div class="zt-data-grid__cell zt-data-grid__cell--head zt-data-grid__cell--num" role="columnheader">Вес/гол, кг</div>' +
'<div class="zt-data-grid__cell zt-data-grid__cell--head zt-data-grid__cell--actions" role="columnheader">Действия</div>' +
"</div>" +
'<div class="zt-data-grid__body" data-norms-body role="rowgroup"></div>' +
"</div></div></div>";
overlayEl.querySelector("[data-norms-close]")?.addEventListener("click", closeModal);
overlayEl.addEventListener("click", (e) => {
if (e.target === overlayEl) closeModal();
});
overlayEl.querySelector("[data-norms-search]")?.addEventListener("input", renderRows);
overlayEl.querySelector("[data-norms-locked-only]")?.addEventListener("change", (event) => {
lockedOnlyFilter = !!event.target.checked;
renderRows();
});
overlayEl.querySelector("[data-norms-body]")?.addEventListener("input", (event) => {
const row = event.target.closest("[data-norms-row]");
if (row && event.target.matches("[data-norms-input]:not([data-norms-input-locked])")) {
scheduleRowPreview(row);
}
});
overlayEl.querySelector("[data-norms-body]")?.addEventListener("click", onTableClick);
overlayEl.setAttribute("data-daily-plan-overlay", "1");
const host = overlayHost();
host.querySelectorAll(".zt-k-hub-plan-overlay").forEach((el) => {
if (el !== overlayEl) el.remove();
});
host.appendChild(overlayEl);
return overlayEl;
}
function filteredItems() {
let list = items;
if (lockedOnlyFilter) {
list = list.filter(itemHasLockedUsage);
}
const q = (overlayEl?.querySelector("[data-norms-search]")?.value || "").trim().toLowerCase();
if (!q) return list;
return list.filter((item) => {
if ((item.componentName || "").toLowerCase().includes(q)) return true;
return (item.recipes || []).some((name) => String(name).toLowerCase().includes(q));
});
}
function renderRows() {
const body = overlayEl?.querySelector("[data-norms-body]");
if (!body) return;
const rows = filteredItems();
if (!rows.length) {
body.innerHTML =
'<div class="zt-k-hub-plan-replace__empty zt-k-hub-plan-norms__empty">' +
(lockedOnlyFilter
? "Нет компонентов в рецептах с замком СВ на эту дату"
: "Нет компонентов в плане на эту дату") +
"</div>";
return;
}
body.innerHTML = rows.map((item) => buildRowHtml(item)).join("");
updateAllPreviews();
}
async function loadItems() {
const dispenserId = global.WespDailyPlanPanel?.getSelectedDispenserId?.() || "";
if (!dispenserId) {
notify("Сначала выберите кормораздатчик в плане", "error");
return false;
}
const date = global.WespDailyPlanPanel?.getSelectedPlanDate?.() || todayIso();
const r = await fetch(
"/api/daily-plan/component-norms?dispenser_id=" +
encodeURIComponent(dispenserId) +
"&date=" +
encodeURIComponent(date),
{ credentials: "same-origin" }
);
if (!r.ok) {
notify("Не удалось загрузить компоненты плана", "error");
return false;
}
const data = await r.json();
items = Array.isArray(data.items) ? data.items : [];
return true;
}
async function applyRow(row) {
if (row.dataset.normsEditable !== "1") {
flashWeightLockedHint(row.querySelector("[data-norms-input-locked]"));
return;
}
const componentId = row.dataset.componentId;
const input = row.querySelector("[data-norms-input]");
const raw = input?.value;
if (raw === "" || raw == null) {
notify("Введите СВ%", "error");
return;
}
const value = Number(raw);
if (!Number.isFinite(value) || value < 0 || value > 100) {
notify("СВ% должно быть от 0 до 100", "error");
return;
}
const skip = global.WespDailyPlanSkipDuration;
if (!skip) return;
const planDate = global.WespDailyPlanPanel?.getSelectedPlanDate?.() || todayIso();
const payload = await skip.skipDurationBody(
planDate,
{ componentId, dryMatter: value },
"На какой срок сохранить коррекцию?",
{
overlayHost: overlayHost(),
keepOverlays: [".zt-k-hub-plan-norms"],
onError: (msg) => notify(msg, "error"),
}
);
if (!payload) return;
const r = await fetch("/api/daily-plan/adjustments/components", {
method: "POST",
credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const data = await r.json().catch(() => ({}));
if (!r.ok) {
notify(data.message || "Ошибка сохранения", "error");
return;
}
notify("Коррекция сохранена");
await refreshAfterChange();
}
async function undoRow(row) {
const componentId = row.dataset.componentId;
const planDate = global.WespDailyPlanPanel?.getSelectedPlanDate?.() || todayIso();
const r = await fetch(
"/api/daily-plan/adjustments/components?component_id=" +
encodeURIComponent(componentId) +
"&date=" +
encodeURIComponent(planDate),
{ method: "DELETE", credentials: "same-origin" }
);
if (!r.ok) {
const data = await r.json().catch(() => ({}));
notify(data.message || "Не удалось сбросить", "error");
return;
}
notify("СВ% как в рецепте");
await refreshAfterChange();
}
async function replaceRow(row) {
const componentId = row.dataset.componentId;
const item = items.find((x) => String(x.componentId) === String(componentId));
const label = item?.componentName || "Компонент";
const replaceModal = global.WespDailyPlanReplaceModal;
const skip = global.WespDailyPlanSkipDuration;
if (!replaceModal || !skip) return;
const planDate = global.WespDailyPlanPanel?.getSelectedPlanDate?.() || todayIso();
const dispenserId = global.WespDailyPlanPanel?.getSelectedDispenserId?.() || "";
if (!dispenserId) {
notify("Сначала выберите кормораздатчик в плане", "error");
return;
}
const durationHost = overlayHost();
await replaceModal.open({
componentId,
label,
overlayHost: overlayHost(),
keepOverlays: [".zt-k-hub-plan-norms"],
subtitle: "Замена применится во всех рецептах плана, где используется этот компонент.",
onError: (msg) => notify(msg, "error"),
onPick: async (replacementComponentId, replacementName) => {
const body = await skip.skipDurationBody(
planDate,
{ componentId, replacementComponentId, dispenserId },
"На какой срок заменить?",
{
overlayHost: durationHost,
keepOverlays: [".zt-k-hub-plan-norms"],
onError: (msg) => notify(msg, "error"),
}
);
if (!body) return;
const resp = await fetch("/api/daily-plan/replacements/components", {
method: "POST",
credentials: "same-origin",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify(body),
});
const data = await resp.json().catch(() => ({}));
if (!resp.ok) {
throw new Error(data.message || "Не удалось заменить компонент");
}
const count = Number(data.count) || 0;
notify(
count > 1
? `Компонент заменён на «${replacementName}» в ${count} рецептах`
: `Компонент заменён на «${replacementName}»`
);
await refreshAfterChange();
},
});
}
async function refreshAfterChange() {
await loadItems();
renderRows();
global.WespDailyPlanPanel?.reload?.();
if (typeof global.reloadAfterSkipRestore === "function") global.reloadAfterSkipRestore();
}
function onTableClick(event) {
const lockedInput = event.target.closest("[data-norms-input-locked]");
if (lockedInput) {
event.preventDefault();
flashWeightLockedHint(lockedInput);
return;
}
const row = event.target.closest("[data-norms-row]");
if (!row) return;
if (event.target.closest("[data-norms-apply]")) {
event.preventDefault();
applyRow(row);
} else if (event.target.closest("[data-norms-replace]")) {
event.preventDefault();
replaceRow(row);
} else if (event.target.closest("[data-norms-undo]")) {
event.preventDefault();
undoRow(row);
}
}
async function openModal() {
lockedOnlyFilter = true;
ensureOverlay();
const ok = await loadItems();
if (ok) renderRows();
}
global.openDailyPlanComponentNormsModal = openModal;
global.closeDailyPlanComponentNormsModal = closeModal;
})(window);