257 lines
9.8 KiB
JavaScript
257 lines
9.8 KiB
JavaScript
/**
|
||
* Модалка настроек контроля отклонений (глобальные пороги и уведомления).
|
||
*/
|
||
const MODAL_ID = "feedQualitySettingsModal";
|
||
|
||
function escapeHtml(value) {
|
||
return String(value ?? "")
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """);
|
||
}
|
||
|
||
function sectionHtml(id, title, fieldsHtml, checksHtml) {
|
||
return (
|
||
`<section class="fq-settings-section" data-fq-section="${escapeHtml(id)}">` +
|
||
'<div class="fq-settings-section__head">' +
|
||
`<h4 class="fq-settings-section__title">${escapeHtml(title)}</h4>` +
|
||
'<label class="fq-settings-check">' +
|
||
`<input type="checkbox" data-fq-field="enabled" data-fq-section="${escapeHtml(id)}" checked>` +
|
||
" Учитывать</label></div>" +
|
||
`<div class="fq-settings-grid">${fieldsHtml}</div>` +
|
||
`<div class="fq-settings-checks">${checksHtml}</div></section>`
|
||
);
|
||
}
|
||
|
||
function fieldHtml(section, key, label, value, step) {
|
||
return (
|
||
'<div class="fq-settings-field">' +
|
||
`<label>${escapeHtml(label)}</label>` +
|
||
`<input type="number" class="form-control form-control-sm" data-fq-field="${escapeHtml(key)}" ` +
|
||
`data-fq-section="${escapeHtml(section)}" value="${escapeHtml(value)}" step="${escapeHtml(step)}" min="0">` +
|
||
"</div>"
|
||
);
|
||
}
|
||
|
||
function notifyChecks(section) {
|
||
return (
|
||
'<label class="fq-settings-check">' +
|
||
`<input type="checkbox" data-fq-field="notify_warning" data-fq-section="${escapeHtml(section)}" checked>` +
|
||
" Уведомление: предупреждение</label>" +
|
||
'<label class="fq-settings-check">' +
|
||
`<input type="checkbox" data-fq-field="notify_critical" data-fq-section="${escapeHtml(section)}" checked>` +
|
||
" Уведомление: критичное</label>"
|
||
);
|
||
}
|
||
|
||
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 (
|
||
`<div class="modal wesp-shell-modal fq-settings-modal" id="${MODAL_ID}" tabindex="-1" aria-hidden="true">` +
|
||
'<div class="modal-dialog modal-dialog--shell modal-dialog--shell-wide modal-dialog--shell-fq">' +
|
||
'<div class="modal-content wesp-shell-bsmodal border-0 shadow-none fq-settings-modal__content">' +
|
||
'<div class="wesp-shell-bsmodal-title-row">' +
|
||
'<h2 class="wesp-shell-bsmodal-title">Контроль отклонений</h2>' +
|
||
'<button type="button" class="wesp-shell-bsmodal-close" data-bs-dismiss="modal" aria-label="Закрыть">×</button>' +
|
||
"</div>" +
|
||
'<div class="wesp-shell-bsmodal-body">' +
|
||
`<form id="feedQualitySettingsForm">${loading}${unloading}${mix}${mixer}</form>` +
|
||
"</div>" +
|
||
'<div class="wesp-shell-bsmodal-actions">' +
|
||
'<button type="button" class="wesp-shell-bsmodal-btn" data-bs-dismiss="modal">Отмена</button>' +
|
||
'<button type="button" class="wesp-shell-bsmodal-btn wesp-shell-bsmodal-btn-primary" data-action="save-feed-quality-settings">' +
|
||
"Сохранить</button>" +
|
||
"</div></div></div></div>"
|
||
);
|
||
}
|
||
|
||
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 settingsUrl = "/api/feed-quality/settings";
|
||
const resp = await fetch(settingsUrl);
|
||
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,
|
||
};
|
||
}
|