Files
site/apps/web/public/wesp/js/pages/feed-alerts.js
T

378 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Контроль отклонений — лента событий (не дубль отчётов).
*/
(function (global) {
const EVENT_LABELS = {
OVERLOAD: "Перегруз",
UNDERLOAD: "Недогруз",
LOADING_TIME: "Долгая загрузка",
LOADING_FAST: "Быстрая загрузка",
MIX_TIME: "Смешивание",
LEFT_IN_MIXER: "Остаток в миксере",
};
const EVENT_ICONS = {
OVERLOAD: "fa-arrow-trend-up",
UNDERLOAD: "fa-arrow-trend-down",
LOADING_TIME: "fa-hourglass-half",
LOADING_FAST: "fa-forward",
MIX_TIME: "fa-clock",
LEFT_IN_MIXER: "fa-triangle-exclamation",
};
let currentView = "summary";
let journalSubView = "reports";
let alertsSeverityFilter = "";
function escapeHtml(value) {
return String(value ?? "")
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function pluralDeviations(count) {
const n = Math.abs(Number(count) || 0);
const mod10 = n % 10;
const mod100 = n % 100;
if (mod10 === 1 && mod100 !== 11) return `${n} отклонение`;
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return `${n} отклонения`;
return `${n} отклонений`;
}
function getDateParams() {
return global.WespReportsFilters?.getDateParams?.() || {
date_from: document.getElementById("date-from")?.value || "",
date_to: document.getElementById("date-to")?.value || "",
};
}
function getDispenserId() {
return global.WespReportsFilters?.getDispenserId?.() || "";
}
function buildAlertsUrl() {
const params = global.WespReportsFilters?.buildAlertsParams?.() || new URLSearchParams(getDateParams());
if (alertsSeverityFilter) params.set("severity", alertsSeverityFilter);
return `/api/feed-quality/alerts?${params.toString()}`;
}
function buildSummaryUrl() {
const params = global.WespReportsFilters?.buildAlertsParams?.() || new URLSearchParams(getDateParams());
return `/api/feed-quality/alerts/summary?${params.toString()}`;
}
function deviationClass(devKg, eventType) {
if (eventType === "MIX_TIME" || eventType === "LOADING_TIME" || eventType === "LOADING_FAST") {
return "deviation-significant";
}
if (devKg == null || Number.isNaN(Number(devKg))) return "";
const v = Number(devKg);
if (v > 0) return "deviation-positive";
if (v < 0) return "deviation-negative";
return "";
}
function formatDeltaPct(item) {
if (
item.eventType === "MIX_TIME" ||
item.eventType === "LOADING_TIME" ||
item.eventType === "LOADING_FAST"
) {
if (item.deviationKg == null) return "—";
const sec = Number(item.deviationKg);
return `${sec > 0 ? "+" : ""}${Math.round(sec)} с`;
}
if (item.deviationPct == null) return "—";
const pct = Number(item.deviationPct);
return `${pct > 0 ? "+" : ""}${pct.toFixed(1)}%`;
}
function formatMixingClock(sec) {
const total = Math.max(0, Math.round(Number(sec) || 0));
const m = Math.floor(total / 60);
const s = total % 60;
return `${m}:${String(s).padStart(2, "0")}`;
}
function objectLabel(item) {
if (item.componentName) return item.componentName;
if (item.groupName) return `группа «${item.groupName}»`;
if (item.eventType === "MIX_TIME") return "смешивание";
if (item.eventType === "LEFT_IN_MIXER") return "миксер";
return "рейс";
}
function buildContextLine(item) {
const parts = [];
const target = item.targetKg;
const actual = item.actualKg;
if (item.eventType === "MIX_TIME") {
if (actual != null && target != null) {
parts.push(`факт ${formatMixingClock(actual)} при плане ${formatMixingClock(target)}`);
}
} else if (item.eventType === "LEFT_IN_MIXER") {
if (actual != null) parts.push(`осталось ${Number(actual).toFixed(1)} кг`);
} else if (item.groupName) {
if (actual != null && target != null) {
parts.push(`выгружено ${Number(actual).toFixed(1)} из ${Number(target).toFixed(1)} кг`);
} else if (actual != null) {
parts.push(`выгружено ${Number(actual).toFixed(1)} кг`);
}
if (item.durationSec != null && Number(item.durationSec) > 0) {
const sec = Number(item.durationSec);
parts.push(sec >= 60 ? `выгрузка ${(sec / 60).toFixed(1)} мин` : `выгрузка ${sec.toFixed(1)} с`);
}
} else if (item.componentName) {
if (actual != null && target != null) {
parts.push(`загружено ${Number(actual).toFixed(1)} из ${Number(target).toFixed(1)} кг`);
} else if (actual != null) {
parts.push(`загружено ${Number(actual).toFixed(1)} кг`);
}
if (item.durationSec != null && Number(item.durationSec) > 0) {
parts.push(`загрузка ${Number(item.durationSec).toFixed(1)} с`);
}
}
if (item.costDeviationRub != null && item.costDeviationRub > 0) {
parts.push(`+${Number(item.costDeviationRub).toFixed(0)} ₽`);
}
return parts.length ? parts.join(" · ") : (item.detail || "").split(":").pop()?.trim() || "";
}
function sortItems(items) {
return [...items].sort((a, b) => {
const ta = a.createdAt ? new Date(a.createdAt).getTime() : 0;
const tb = b.createdAt ? new Date(b.createdAt).getTime() : 0;
return tb - ta;
});
}
async function refreshSummary() {
const chip = document.getElementById("feedAlertsSummaryChip");
if (!chip) return;
try {
const resp = await fetch(buildSummaryUrl());
if (!resp.ok) return;
const data = await resp.json();
const total = Number(data.total) || 0;
if (total <= 0) {
chip.hidden = true;
chip.textContent = "";
return;
}
chip.hidden = false;
const err = Number(data.error) || 0;
chip.textContent =
err > 0
? `${pluralDeviations(total)} (${err} критич.)`
: pluralDeviations(total) + " за период";
} catch {
chip.hidden = true;
}
}
function renderAlerts(items) {
const listEl = document.getElementById("alertsList");
if (!listEl) return;
if (!items || !items.length) {
listEl.innerHTML =
'<div class="empty-state wesp-content-reveal zt-empty">' +
'<div class="empty-icon"><i class="fas fa-check-circle"></i></div>' +
'<h3 class="zt-empty__title">Отклонений нет</h3>' +
'<p class="zt-empty__hint">За выбранный период отклонений не зафиксировано</p></div>';
global.WespZootechModals?.revealContent(listEl.querySelector(".empty-state"));
return;
}
const sorted = sortItems(items);
const html = sorted
.map((item) => {
const sev = item.severity || "warning";
const typeLabel = EVENT_LABELS[item.eventType] || "Отклонение";
const icon = EVENT_ICONS[item.eventType] || "fa-circle-exclamation";
const devClass = deviationClass(item.deviationKg, item.eventType);
const delta = formatDeltaPct(item);
const context = buildContextLine(item);
const when = item.createdAt
? new Date(item.createdAt).toLocaleString("ru-RU", {
day: "2-digit",
month: "2-digit",
hour: "2-digit",
minute: "2-digit",
})
: "";
const trip = item.recipeName || "Рейс";
return (
`<button type="button" class="deviation-alert-card list-item zt-card zt-card--interactive deviation-feed-item deviation-feed-item--${escapeHtml(sev)}" ` +
`data-loading-report-id="${escapeHtml(item.loadingReportId)}">` +
'<div class="deviation-alert-card__top">' +
`<span class="deviation-feed-item__dot deviation-feed-item__dot--${escapeHtml(sev)}" aria-hidden="true"></span>` +
'<div class="deviation-alert-card__head">' +
`<h3 class="deviation-alert-card__title">` +
`<i class="fas ${icon}" aria-hidden="true"></i>` +
`${escapeHtml(typeLabel)} · ${escapeHtml(objectLabel(item))}` +
"</h3>" +
`<span class="deviation-type-badge deviation-type-badge--${escapeHtml(sev)}">${escapeHtml(sev === "error" ? "Критично" : "Предупр.")}</span>` +
"</div>" +
`<span class="deviation-feed-item__delta ${escapeHtml(devClass)}">${escapeHtml(delta)}</span>` +
"</div>" +
(context ? `<div class="deviation-alert-card__info"><span class="deviation-alert-card__context">${escapeHtml(context)}</span></div>` : "") +
`<div class="deviation-alert-card__meta">${escapeHtml(trip)}${when ? ` · ${escapeHtml(when)}` : ""}</div>` +
'<span class="deviation-feed-item__chevron" aria-hidden="true"><i class="fas fa-chevron-right"></i></span>' +
"</button>"
);
})
.join("");
listEl.innerHTML = `<div class="deviation-feed">${html}</div>`;
listEl.querySelectorAll("[data-loading-report-id]").forEach((btn) => {
btn.addEventListener("click", () => {
const id = btn.getAttribute("data-loading-report-id");
if (id) openReportFromAlert(id);
});
});
global.WespZootechModals?.revealContent(listEl.querySelector(".deviation-feed"));
}
async function loadAlerts() {
const listEl = document.getElementById("alertsList");
if (!listEl) return;
listEl.innerHTML =
'<div class="loading-state wesp-content-reveal"><div class="zt-spinner" role="status"></div>' +
'<p class="zt-loading-caption">Загрузка отклонений…</p></div>';
try {
const resp = await fetch(buildAlertsUrl());
if (!resp.ok) throw new Error("Не удалось загрузить отклонения");
const data = await resp.json();
renderAlerts(data.items || []);
await refreshSummary();
} catch (err) {
listEl.innerHTML =
`<div class="empty-state zt-empty"><p class="zt-empty__hint">${escapeHtml(err.message)}</p></div>`;
}
}
function openReportFromAlert(loadingReportId) {
switchView("journal");
setJournalSubView("reports");
global.__wespHighlightReportId = loadingReportId;
if (typeof global.filterAndDisplayReports === "function") {
global.filterAndDisplayReports();
} else if (typeof global.loadReports === "function") {
global.loadReports();
}
}
function openJournalAlerts() {
switchView("journal");
setJournalSubView("alerts");
}
function setJournalSubView(sub) {
journalSubView = sub === "alerts" ? "alerts" : "reports";
document.querySelectorAll("[data-journal-view-tab]").forEach((btn) => {
btn.classList.toggle("active", btn.getAttribute("data-journal-view-tab") === journalSubView);
});
const alertsContainer = document.querySelector(".reports-container--alerts");
const reportsContainer = document.querySelector(".reports-container--reports");
const severityBlock = document.getElementById("alertsSeverityBlock");
if (reportsContainer) reportsContainer.hidden = journalSubView !== "reports";
if (alertsContainer) alertsContainer.hidden = journalSubView !== "alerts";
if (severityBlock) severityBlock.hidden = journalSubView !== "alerts";
if (journalSubView === "alerts") loadAlerts();
else if (typeof global.loadReports === "function") global.loadReports();
}
function updateToolbarVisibility() {
const journalBlock = document.getElementById("journalSubTabsBlock");
if (journalBlock) journalBlock.hidden = currentView !== "journal";
const severityBlock = document.getElementById("alertsSeverityBlock");
if (severityBlock) {
severityBlock.hidden = currentView !== "journal" || journalSubView !== "alerts";
}
}
function switchView(view) {
const allowed = ["summary", "comparison", "journal"];
currentView = allowed.includes(view) ? view : "summary";
document.querySelectorAll("[data-reports-view-tab]").forEach((btn) => {
btn.classList.toggle("active", btn.getAttribute("data-reports-view-tab") === currentView);
});
const summaryEl = document.querySelector(".reports-container--summary");
const comparisonEl = document.querySelector(".reports-container--comparison");
const reportsContainer = document.querySelector(".reports-container--reports");
const alertsContainer = document.querySelector(".reports-container--alerts");
if (summaryEl) summaryEl.hidden = currentView !== "summary";
if (comparisonEl) comparisonEl.hidden = currentView !== "comparison";
if (reportsContainer) reportsContainer.hidden = currentView !== "journal" || journalSubView !== "reports";
if (alertsContainer) alertsContainer.hidden = currentView !== "journal" || journalSubView !== "alerts";
updateToolbarVisibility();
if (currentView === "summary") {
global.WespAnalyticsSummary?.load?.();
} else if (currentView === "comparison") {
global.WespAnalyticsPlanFact?.load?.();
} else if (currentView === "journal") {
setJournalSubView(journalSubView);
} else {
refreshSummary();
}
}
function setSeverityFilter(value) {
alertsSeverityFilter = value || "";
document.querySelectorAll("[data-alerts-severity]").forEach((btn) => {
btn.classList.toggle("active", btn.getAttribute("data-alerts-severity") === alertsSeverityFilter);
});
if (currentView === "journal" && journalSubView === "alerts") loadAlerts();
}
function init() {
document.querySelectorAll("[data-reports-view-tab]").forEach((btn) => {
btn.addEventListener("click", () => {
switchView(btn.getAttribute("data-reports-view-tab"));
});
});
document.querySelectorAll("[data-journal-view-tab]").forEach((btn) => {
btn.addEventListener("click", () => {
if (currentView !== "journal") switchView("journal");
setJournalSubView(btn.getAttribute("data-journal-view-tab"));
});
});
document.querySelectorAll("[data-alerts-severity]").forEach((btn) => {
btn.addEventListener("click", () => {
setSeverityFilter(btn.getAttribute("data-alerts-severity") || "");
});
});
global.WespAnalyticsSummary?.init?.();
switchView("summary");
refreshSummary();
}
function onFiltersApplied() {
refreshSummary();
global.WespAnalyticsSummary?.onFiltersApplied?.();
global.WespAnalyticsPlanFact?.onFiltersApplied?.();
if (currentView === "journal" && journalSubView === "alerts") loadAlerts();
}
global.WespFeedAlerts = {
init,
loadAlerts,
refreshSummary,
switchView,
openReportFromAlert,
openJournalAlerts,
onFiltersApplied,
getCurrentView: () => currentView,
};
})(typeof window !== "undefined" ? window : globalThis);