Интегрирован 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,255 @@
/**
* Модалка настроек контроля отклонений (глобальные пороги и уведомления).
*/
const MODAL_ID = "feedQualitySettingsModal";
function escapeHtml(value) {
return String(value ?? "")
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
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 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,
};
}