(() => { 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 = `
  • Moved ${formatCount(moved)} (${movedPct}%)
  • Pending ${formatCount(pending)} (${pendingPct}%)
  • `; } } function formatLastMovedTime(utcValue) { if (!utcValue) { return "—"; } const date = new Date(utcValue); if (Number.isNaN(date.getTime())) { return "—"; } return date.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", second: "2-digit", hour12: true }); } function renderDayTable(dayDetails) { const tbody = els.dayTbody(); if (!tbody) { return; } if (!dayDetails.length) { tbody.innerHTML = `No movement data for this date range.`; return; } tbody.innerHTML = ""; dayDetails.forEach(row => { const tr = document.createElement("tr"); tr.innerHTML = ` ${escapeHtml(row.dateDisplay)} ${formatCount(row.movedCartons)} ${formatCount(row.successfulScans)} ${formatCount(row.duplicateRejections)} ${formatCount(row.manualEntryRejections)} ${formatLastMovedTime(row.lastMovedAtUtc)}`; tbody.appendChild(tr); }); } function escapeHtml(value) { return (value ?? "").replace(/[&<>\"']/g, char => ({ "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" }[char])); } function exportDayDetailsCsv() { if (!lastAnalytics?.dayDetails?.length) { return; } const header = ["Date", "Moved Cartons", "Successful Scans", "Duplicate Rejections", "Manual Entry Rejections", "Last Moved At"]; const rows = lastAnalytics.dayDetails.map(row => [ row.dateDisplay, row.movedCartons, row.successfulScans, row.duplicateRejections, row.manualEntryRejections, row.lastMovedAtUtc ? new Date(row.lastMovedAtUtc).toLocaleString() : "" ]); const csv = [header, ...rows] .map(cols => cols.map(col => `"${String(col).replace(/"/g, '""')}"`).join(",")) .join("\r\n"); const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = `movement-analytics-${els.dateFrom()?.value ?? "export"}.csv`; link.click(); URL.revokeObjectURL(link.href); } function wire() { const btnAnalytics = els.btnAnalytics(); const dateFrom = els.dateFrom(); const dateTo = els.dateTo(); const exportBtn = els.exportBtn(); const footerClose = document.getElementById("scan-history-dialog-close"); const footerCloseAlt = els.footerClose(); const onDateChange = () => void refreshMovementAnalytics(); dateFrom?.addEventListener("change", onDateChange); dateTo?.addEventListener("change", onDateChange); exportBtn?.addEventListener("click", exportDayDetailsCsv); const closeDialog = () => { const dialog = els.dialog(); if (dialog && typeof dialog.close === "function") { dialog.close(); } }; footerClose?.addEventListener("click", closeDialog); footerCloseAlt?.addEventListener("click", closeDialog); btnAnalytics?.addEventListener("click", () => { const analyticsView = els.analyticsView(); const isOpen = analyticsView && !analyticsView.hidden; if (isOpen) { setAnalyticsVisible(false); window.dispatchEvent(new CustomEvent("avs-history-show-records")); return; } setAnalyticsVisible(true); }); } window.avsMovementAnalytics = { wire, setAnalyticsVisible, refreshMovementAnalytics, stopPolling }; window.avsMovementAnalytics.wire(); })();