/** * Модалка настроек контроля отклонений (глобальные пороги и уведомления). */ const MODAL_ID = "feedQualitySettingsModal"; function escapeHtml(value) { return String(value ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function sectionHtml(id, title, fieldsHtml, checksHtml) { return ( `
` + '
' + `

${escapeHtml(title)}

` + '
" + `
${fieldsHtml}
` + `
${checksHtml}
` ); } function fieldHtml(section, key, label, value, step) { return ( '
' + `` + `` + "
" ); } function notifyChecks(section) { return ( '" + '" ); } function modalTemplate() { const loading = sectionHtml( "loading", "Загрузка компонентов", fieldHtml("loading", "warning_pct", "Порог предупреждения, %", 10, 0.1) + fieldHtml("loading", "critical_pct", "Порог критичного, %", 15, 0.1) + fieldHtml("loading", "warning_min_sec", "Слишком быстро, предупр., с", 3, 1) + fieldHtml("loading", "critical_min_sec", "Слишком быстро, критично, с", 1, 1) + fieldHtml("loading", "warning_max_sec", "Слишком долго, предупр., с", 180, 1) + fieldHtml("loading", "critical_max_sec", "Слишком долго, критично, с", 600, 1), notifyChecks("loading") ); const unloading = sectionHtml( "unloading", "Выгрузка по группам", fieldHtml("unloading", "warning_pct", "Порог предупреждения, %", 5, 0.1) + fieldHtml("unloading", "critical_pct", "Порог критичного, %", 10, 0.1), notifyChecks("unloading") ); const mix = sectionHtml( "mix_time", "Время смешивания", fieldHtml("mix_time", "warning_delta_sec", "Предупреждение, с", 30, 1) + fieldHtml("mix_time", "critical_delta_sec", "Критично, с", 60, 1), notifyChecks("mix_time") ); const mixer = sectionHtml( "left_in_mixer", "Остаток в миксере", fieldHtml("left_in_mixer", "warning_min_kg", "Предупреждение, кг", 5, 0.1) + fieldHtml("left_in_mixer", "warning_min_pct", "Предупреждение, % от партии", 2, 0.1) + fieldHtml("left_in_mixer", "critical_min_kg", "Критично, кг", 15, 0.1), notifyChecks("left_in_mixer") ); return ( `" ); } let modalEl = null; let notyfAdapter = null; let currentSettings = null; let modalStackWired = false; /** Настройки зоотехника — z-index 3000; shell-модалка Bootstrap по умолчанию ниже. */ const FQ_MODAL_Z = 3100; const FQ_BACKDROP_Z = 3099; function isSettingsModalOpen() { const settings = document.getElementById("settingsModal"); return Boolean(settings && settings.style.display === "block"); } function raiseModalAboveSettings() { if (!modalEl) return; modalEl.style.zIndex = String(FQ_MODAL_Z); const backdrops = document.querySelectorAll(".modal-backdrop"); const backdrop = backdrops[backdrops.length - 1]; if (backdrop) backdrop.style.zIndex = String(FQ_BACKDROP_Z); } function restoreBodyAfterClose() { const anyBootstrapModalOpen = document.querySelector(".modal.show"); if (!anyBootstrapModalOpen) { document.querySelectorAll(".modal-backdrop").forEach((el) => el.remove()); document.body.classList.remove("modal-open"); document.body.style.removeProperty("padding-right"); document.body.style.removeProperty("overflow"); } if (isSettingsModalOpen()) { document.body.style.overflow = "hidden"; } } function wireModalStackHandlers() { if (!modalEl || modalStackWired) return; modalStackWired = true; modalEl.addEventListener("shown.bs.modal", raiseModalAboveSettings); modalEl.addEventListener("hidden.bs.modal", restoreBodyAfterClose); } function ensureModal() { if (modalEl && modalEl.isConnected) return modalEl; modalEl = document.getElementById(MODAL_ID); if (!modalEl) { document.body.insertAdjacentHTML("beforeend", modalTemplate()); modalEl = document.getElementById(MODAL_ID); } globalThis.WespZootechModals?.wireAllShellModals?.(); wireModalStackHandlers(); modalEl.querySelector("[data-action='save-feed-quality-settings']")?.addEventListener("click", saveSettings); return modalEl; } function applySettingsToForm(settings) { const form = document.getElementById("feedQualitySettingsForm"); if (!form || !settings) return; form.querySelectorAll("[data-fq-section][data-fq-field]").forEach((el) => { const section = el.getAttribute("data-fq-section"); const field = el.getAttribute("data-fq-field"); const block = settings[section]; if (!block || field == null) return; const val = block[field]; if (el.type === "checkbox") { el.checked = Boolean(val); } else if (val != null) { el.value = String(val); } }); } function collectSettingsFromForm() { const form = document.getElementById("feedQualitySettingsForm"); const out = { loading: {}, unloading: {}, mix_time: {}, left_in_mixer: {} }; if (!form) return out; form.querySelectorAll("[data-fq-section][data-fq-field]").forEach((el) => { const section = el.getAttribute("data-fq-section"); const field = el.getAttribute("data-fq-field"); if (!out[section]) out[section] = {}; if (el.type === "checkbox") { out[section][field] = el.checked; } else { out[section][field] = el.value; } }); return out; } async function loadSettings() { const resp = await fetch("/api/feed-quality/settings"); if (!resp.ok) throw new Error("Не удалось загрузить настройки"); const data = await resp.json(); currentSettings = data.settings || data; applySettingsToForm(currentSettings); } async function openModal() { ensureModal(); try { await loadSettings(); } catch (err) { notyfAdapter?.error?.(err.message || "Ошибка загрузки"); return; } const inst = globalThis.bootstrap?.Modal?.getOrCreateInstance(modalEl); inst?.show(); requestAnimationFrame(() => { requestAnimationFrame(raiseModalAboveSettings); }); } async function saveSettings() { const payload = collectSettingsFromForm(); try { const resp = await fetch("/api/feed-quality/settings", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ settings: payload }), }); const data = await resp.json(); if (!resp.ok) throw new Error(data.message || "Ошибка сохранения"); notyfAdapter?.success?.( data.reevaluatedReports != null ? `Настройки сохранены (переоценено ${data.reevaluatedReports} рейсов)` : "Настройки сохранены" ); globalThis.WespFeedAlerts?.refreshSummary?.(); if (globalThis.WespFeedAlerts?.getCurrentView?.() === "alerts") { globalThis.WespFeedAlerts.loadAlerts(); } globalThis.bootstrap?.Modal?.getInstance(modalEl)?.hide(); } catch (err) { notyfAdapter?.error?.(err.message || "Ошибка сохранения"); } } export function initFeedQualitySettingsModal(notyf) { notyfAdapter = notyf; ensureModal(); document.addEventListener("click", (event) => { const el = event.target.closest('[data-action="open-feed-quality-settings"]'); if (!el) return; event.preventDefault(); openModal(); }); } if (typeof globalThis !== "undefined") { globalThis.WespFeedQualitySettings = { init: initFeedQualitySettingsModal, open: openModal, }; }