(() => { const pollIntervalMs = 5000; let barChart = null; let donutChart = null; let pollTimer = null; let lastAnalytics = null; const els = { dialog: () => document.getElementById("scan-history-dialog"), recordsView: () => document.getElementById("scan-history-records-view"), analyticsView: () => document.getElementById("scan-history-analytics-view"), filterRecords: () => document.getElementById("scan-history-filter"), btnAnalytics: () => document.getElementById("btn-movement-analytics"), dateFrom: () => document.getElementById("analytics-date-from"), dateTo: () => document.getElementById("analytics-date-to"), exportBtn: () => document.getElementById("btn-analytics-export"), footerClose: () => document.getElementById("btn-scan-history-dialog-close"), summaryToday: () => document.getElementById("summary-moved-today"), summaryWeek: () => document.getElementById("summary-moved-week"), summaryMonth: () => document.getElementById("summary-moved-month"), summaryTotal: () => document.getElementById("summary-total-moved"), dayTbody: () => document.getElementById("analytics-day-tbody"), legend: () => document.getElementById("analytics-status-legend") }; function formatCount(value) { if (value === null || value === undefined || Number.isNaN(Number(value))) { return "0"; } return Number(value).toLocaleString("en-US"); } function toInputDate(date) { const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, "0"); const d = String(date.getDate()).padStart(2, "0"); return `${y}-${m}-${d}`; } function setDefaultDateRange() { const dateFrom = els.dateFrom(); const dateTo = els.dateTo(); if (!dateFrom || !dateTo) { return; } const to = new Date(); const from = new Date(); from.setDate(from.getDate() - 6); dateTo.value = toInputDate(to); dateFrom.value = toInputDate(from); } function setModeButtonActive(active) { const btn = els.btnAnalytics(); if (!btn) { return; } btn.classList.toggle("is-active", active); btn.setAttribute("aria-pressed", active ? "true" : "false"); } function setAnalyticsVisible(visible) { const recordsView = els.recordsView(); const analyticsView = els.analyticsView(); const dialog = els.dialog(); if (!recordsView || !analyticsView) { return; } if (visible) { recordsView.classList.add("is-hidden"); recordsView.hidden = true; analyticsView.classList.remove("is-hidden"); analyticsView.hidden = false; dialog?.classList.add("scan-history-dialog--wide"); setModeButtonActive(true); setDefaultDateRange(); void refreshMovementAnalytics(); startPolling(); return; } analyticsView.classList.add("is-hidden"); analyticsView.hidden = true; recordsView.classList.remove("is-hidden"); recordsView.hidden = false; dialog?.classList.remove("scan-history-dialog--wide"); setModeButtonActive(false); stopPolling(); } function startPolling() { stopPolling(); pollTimer = window.setInterval(() => { if (els.analyticsView()?.hidden) { return; } void refreshMovementAnalytics(); }, pollIntervalMs); } function stopPolling() { if (pollTimer !== null) { window.clearInterval(pollTimer); pollTimer = null; } } async function refreshMovementAnalytics() { const dateFrom = els.dateFrom(); const dateTo = els.dateTo(); if (!dateFrom?.value || !dateTo?.value) { return; } const url = `/api/verification/movement-analytics?from=${encodeURIComponent(dateFrom.value)}&to=${encodeURIComponent(dateTo.value)}`; try { const response = await fetch(url); if (!response.ok) { return; } const data = await response.json(); lastAnalytics = data; renderMovementAnalytics(data); } catch { // Keep last rendered values on failure. } } function renderMovementAnalytics(data) { const summary = data.summary ?? {}; const statusShare = data.statusShare ?? {}; if (els.summaryToday()) { els.summaryToday().textContent = formatCount(summary.movedToday); } if (els.summaryWeek()) { els.summaryWeek().textContent = formatCount(summary.movedThisWeek); } if (els.summaryMonth()) { els.summaryMonth().textContent = formatCount(summary.movedThisMonth); } if (els.summaryTotal()) { els.summaryTotal().textContent = formatCount(summary.totalMoved); } renderBarChart(data.chartByDay ?? []); renderDonutChart(statusShare); renderDayTable(data.dayDetails ?? []); } function renderBarChart(chartByDay) { const canvas = document.getElementById("analytics-bar-chart"); if (!canvas || typeof Chart === "undefined") { return; } const labels = chartByDay.map(x => x.dayLabel); const values = chartByDay.map(x => x.movedCartons); if (barChart) { barChart.destroy(); } barChart = new Chart(canvas, { type: "bar", data: { labels, datasets: [{ label: "Moved cartons", data: values, backgroundColor: "#1a5fb4", borderRadius: 4, maxBarThickness: 48 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true, position: "bottom", labels: { boxWidth: 12, color: "#4f6687" } } }, scales: { x: { grid: { display: false }, ticks: { color: "#4f6687" } }, y: { beginAtZero: true, title: { display: true, text: "Cartons", color: "#4f6687" }, ticks: { color: "#4f6687" } } } } }); } function renderDonutChart(statusShare) { const canvas = document.getElementById("analytics-donut-chart"); const legend = els.legend(); if (!canvas || typeof Chart === "undefined") { return; } const moved = statusShare.moved ?? 0; const pending = statusShare.pending ?? 0; const total = statusShare.total ?? moved + pending; const movedPct = statusShare.movedPercent ?? (total > 0 ? Math.round(moved * 1000 / total) / 10 : 0); const pendingPct = statusShare.pendingPercent ?? (total > 0 ? Math.round(pending * 1000 / total) / 10 : 0); if (donutChart) { donutChart.destroy(); } donutChart = new Chart(canvas, { type: "doughnut", data: { labels: ["Moved", "Pending"], datasets: [{ data: [moved, pending], backgroundColor: ["#3cae58", "#d5dde8"], borderWidth: 0 }] }, options: { responsive: true, maintainAspectRatio: false, cutout: "62%", plugins: { legend: { display: false }, tooltip: { enabled: true } } }, plugins: [{ id: "centerText", beforeDraw(chart) { const { ctx, chartArea } = chart; if (!chartArea) { return; } ctx.save(); ctx.font = "bold 14px Segoe UI, sans-serif"; ctx.fillStyle = "#0d3b7a"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText(`Total ${formatCount(total)}`, (chartArea.left + chartArea.right) / 2, (chartArea.top + chartArea.bottom) / 2); ctx.restore(); } }] }); if (legend) { legend.innerHTML = `