(() => { const qrInput = document.getElementById("qr-input"); const statusCard = document.getElementById("status-card"); const statusMiniIcon = document.getElementById("status-mini-icon"); const statusMiniHeadline = document.getElementById("status-mini-headline"); const statusMiniMessage = document.getElementById("status-mini-message"); const lastUpdatedTime = document.getElementById("last-updated-time"); const historySuccessBody = document.getElementById("history-success-body"); const historySuccessEmpty = document.getElementById("history-success-empty"); const historyRejectedBody = document.getElementById("history-rejected-body"); const historyRejectedEmpty = document.getElementById("history-rejected-empty"); const scanHistoryDialog = document.getElementById("scan-history-dialog"); const btnOpenScanHistory = document.getElementById("btn-open-scan-history"); const btnCloseScanHistory = document.getElementById("scan-history-dialog-close"); const scanHistoryFilter = document.getElementById("scan-history-filter"); const scanHistoryModalThead = document.getElementById("scan-history-modal-thead"); const scanHistoryModalTbody = document.getElementById("scan-history-modal-tbody"); const details = { model: document.getElementById("detail-model"), unique: document.getElementById("detail-unique"), exists: document.getElementById("detail-exists"), marked: document.getElementById("detail-marked"), time: document.getElementById("detail-time") }; const antiForgeryToken = document.querySelector('input[name="__RequestVerificationToken"]')?.value; const failureAlarm = new Audio("/resources/Sound/alert.wav"); failureAlarm.preload = "auto"; let isSubmitting = false; let autoSubmitTimer = null; let incompleteIdleTimer = null; /** Last time a non-paste text insertion was allowed (beforeinput insertText path). */ let lastBurstCharAt = 0; /** True while paste handler is applying clipboard text (avoids timing false positives). */ let fromPasteHandler = false; const maxInterKeyMs = 120; const autoSubmitIdleMs = 72; const incompleteNoSemicolonIdleMs = 420; const manualEntryMessage = "Manual entry is not allowed. Please scan or paste."; const completeQrPattern = /^[^;\r\n]+;[^;\r\n]+$/; function clearAutoSubmitTimer() { if (autoSubmitTimer !== null) { clearTimeout(autoSubmitTimer); autoSubmitTimer = null; } } function clearIncompleteIdleTimer() { if (incompleteIdleTimer !== null) { clearTimeout(incompleteIdleTimer); incompleteIdleTimer = null; } } function resetBurstTimer() { lastBurstCharAt = 0; } function resetTypingHeuristics() { resetBurstTimer(); clearIncompleteIdleTimer(); } function normalizeQrValue(value) { return (value ?? "").trim().replace(/\r?\n/g, ""); } function scheduleAutoSubmit() { clearAutoSubmitTimer(); autoSubmitTimer = window.setTimeout(() => { autoSubmitTimer = null; const v = normalizeQrValue(qrInput.value); if (!completeQrPattern.test(v) || isSubmitting || fromPasteHandler) { return; } void submitScan(v, "Scanner"); }, autoSubmitIdleMs); } function scheduleIncompleteIdleCheck() { clearIncompleteIdleTimer(); incompleteIdleTimer = window.setTimeout(() => { incompleteIdleTimer = null; if (isSubmitting || fromPasteHandler) { return; } const v = normalizeQrValue(qrInput.value); if (!v || completeQrPattern.test(v)) { return; } // Typing without a ';' (or abandoned partial) — not a scanner/paste flow. if (!v.includes(";")) { rejectManualTypingImmediate(v); } }, incompleteNoSemicolonIdleMs); } function rejectManualTypingImmediate(rawForServer) { clearAutoSubmitTimer(); clearIncompleteIdleTimer(); const toLog = normalizeQrValue(rawForServer ?? qrInput.value); qrInput.value = ""; resetBurstTimer(); setRejectedState(manualEntryMessage, null); resetDetails(); triggerFailureAlarm(); void persistManualRejection(toLog); } async function persistManualRejection(qrValue) { try { const response = await fetch("/api/verification/scan", { method: "POST", headers: { "Content-Type": "application/json", "RequestVerificationToken": antiForgeryToken ?? "" }, body: JSON.stringify({ qrValue, entryMethod: "Manual" }) }); if (!response.ok) { return; } const payload = await response.json(); renderSuccessfulHistory(payload.recentSuccessfulScans ?? []); renderRejectedHistory(payload.recentRejectedScans ?? []); } catch { // Ignore — UI already shows manual rejection. } } function isInsertFromKeyboard(beforeInputEvent) { const t = beforeInputEvent.inputType ?? ""; return t === "insertText" || t === "insertCompositionText"; } qrInput.addEventListener("beforeinput", event => { if (fromPasteHandler) { return; } if (event.inputType === "insertFromPaste") { return; } if (event.inputType?.startsWith("delete")) { return; } if (!isInsertFromKeyboard(event)) { return; } const now = Date.now(); if (lastBurstCharAt > 0 && now - lastBurstCharAt > maxInterKeyMs) { event.preventDefault(); const bufferBeforeBlockedChar = normalizeQrValue(qrInput.value); rejectManualTypingImmediate(bufferBeforeBlockedChar); return; } lastBurstCharAt = now; }); qrInput.addEventListener("keydown", event => { if (event.key !== "Enter") { return; } event.preventDefault(); clearAutoSubmitTimer(); clearIncompleteIdleTimer(); const v = normalizeQrValue(qrInput.value); if (!v || isSubmitting) { return; } void submitScan(v, "Scanner"); }); qrInput.addEventListener("paste", event => { const pastedValue = event.clipboardData?.getData("text"); if (!pastedValue) { return; } event.preventDefault(); fromPasteHandler = true; clearAutoSubmitTimer(); clearIncompleteIdleTimer(); resetBurstTimer(); const text = normalizeQrValue(pastedValue); qrInput.value = text; if (!text || isSubmitting) { fromPasteHandler = false; return; } void submitScan(text, "Paste").finally(() => { fromPasteHandler = false; }); }); qrInput.addEventListener("input", () => { if (isSubmitting || fromPasteHandler) { return; } const v = normalizeQrValue(qrInput.value); if (!v) { resetBurstTimer(); clearIncompleteIdleTimer(); clearAutoSubmitTimer(); return; } scheduleAutoSubmit(); scheduleIncompleteIdleCheck(); }); qrInput.addEventListener("keyup", event => { if (isSubmitting || fromPasteHandler) { return; } if (event.key === "Enter") { return; } const v = normalizeQrValue(qrInput.value); if (completeQrPattern.test(v)) { scheduleAutoSubmit(); } }); qrInput.addEventListener("focus", () => { if (!qrInput.value) { resetTypingHeuristics(); } }); async function submitScan(qrValue, entryMethod) { if (!qrValue || isSubmitting) { return; } isSubmitting = true; clearAutoSubmitTimer(); clearIncompleteIdleTimer(); resetBurstTimer(); try { const response = await fetch("/api/verification/scan", { method: "POST", headers: { "Content-Type": "application/json", "RequestVerificationToken": antiForgeryToken ?? "" }, body: JSON.stringify({ qrValue, entryMethod }) }); const payload = await response.json(); if (!response.ok) { const fallbackMessage = "Verification service is unavailable."; setRejectedState(payload.message ?? fallbackMessage, null); resetDetails(); triggerFailureAlarm(); return; } const result = payload.result; renderDetails(result); renderStatus(result); renderSuccessfulHistory(payload.recentSuccessfulScans ?? []); renderRejectedHistory(payload.recentRejectedScans ?? []); } catch { setRejectedState("Unexpected communication error.", null); triggerFailureAlarm(); resetDetails(); } finally { qrInput.value = ""; resetTypingHeuristics(); qrInput.focus(); isSubmitting = false; } } function renderStatus(result) { if (result.isSuccessful) { setStatusClasses("status-success"); statusMiniIcon.textContent = "\u2713"; statusMiniHeadline.textContent = "VERIFIED"; statusMiniMessage.textContent = result.message; lastUpdatedTime.textContent = formatDisplayTime(result.processedAtUtc); return; } setRejectedState(result.message, result.processedAtUtc); triggerFailureAlarm(); } function setRejectedState(message, processedAtUtc) { setStatusClasses("status-failed"); statusMiniIcon.textContent = "!"; statusMiniHeadline.textContent = "REJECTED"; statusMiniMessage.textContent = message; lastUpdatedTime.textContent = processedAtUtc ? formatDisplayTime(processedAtUtc) : "-"; } function setStatusClasses(statusClass) { statusCard.className = `panel panel-status ${statusClass}`; } function renderDetails(result) { details.model.textContent = result.modelNumber || "-"; details.unique.textContent = result.uniqueNumber || "-"; details.exists.textContent = result.existsInSystem ? "YES" : "NO"; details.marked.textContent = result.shipmentMarked ? "TRUE" : "FALSE"; details.time.textContent = formatDisplayTime(result.processedAtUtc); } function resetDetails() { details.model.textContent = "-"; details.unique.textContent = "-"; details.exists.textContent = "-"; details.marked.textContent = "-"; details.time.textContent = "-"; } function renderSuccessfulHistory(records) { historySuccessBody.innerHTML = ""; if (records.length === 0) { historySuccessEmpty.classList.remove("is-hidden"); return; } historySuccessEmpty.classList.add("is-hidden"); records.forEach((record, index) => { const row = document.createElement("tr"); row.innerHTML = ` ${index + 1} ${sanitize(record.modelNumber)} ${sanitize(record.uniqueNumber)} TRUE ${formatDisplayTime(record.processedAtUtc)} System`; historySuccessBody.appendChild(row); }); } function renderRejectedHistory(records) { historyRejectedBody.innerHTML = ""; if (records.length === 0) { historyRejectedEmpty.classList.remove("is-hidden"); return; } historyRejectedEmpty.classList.add("is-hidden"); records.forEach(record => { const row = document.createElement("tr"); const model = record.modelNumber ? sanitize(record.modelNumber) : "—"; const unique = record.uniqueNumber ? sanitize(record.uniqueNumber) : "—"; row.innerHTML = ` ${formatDisplayTime(record.processedAtUtc)} ${sanitize(record.rawInputValue)} ${model} ${unique} ${sanitize(record.rejectionReason)}`; historyRejectedBody.appendChild(row); }); } function sanitize(value) { return (value ?? "").replace(/[&<>\"']/g, char => ({ "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" }[char])); } function formatDisplayTime(utcValue) { if (!utcValue) { return "-"; } const date = new Date(utcValue); if (Number.isNaN(date.getTime())) { return "-"; } return date.toLocaleString("en-US", { day: "2-digit", month: "short", year: "numeric", hour: "numeric", minute: "2-digit", second: "2-digit", hour12: true }); } function triggerFailureAlarm() { failureAlarm.pause(); failureAlarm.currentTime = 0; failureAlarm.play().catch(() => { // Some browsers block audio until the scanner input is treated as a user gesture. }); } function wireScanHistoryModal() { if (!scanHistoryDialog || !btnOpenScanHistory || !btnCloseScanHistory || !scanHistoryFilter || !scanHistoryModalThead || !scanHistoryModalTbody) { return; } btnOpenScanHistory.addEventListener("click", async () => { scanHistoryFilter.value = "successful"; await loadFullScanHistory("successful"); if (typeof scanHistoryDialog.showModal === "function") { scanHistoryDialog.showModal(); scanHistoryFilter.focus(); } }); btnCloseScanHistory.addEventListener("click", () => { scanHistoryDialog.close(); }); scanHistoryFilter.addEventListener("change", async () => { await loadFullScanHistory(scanHistoryFilter.value); }); scanHistoryDialog.addEventListener("close", () => { qrInput.focus(); }); } async function loadFullScanHistory(category) { const normalized = category === "rejected" ? "rejected" : "successful"; scanHistoryModalThead.innerHTML = ""; scanHistoryModalTbody.innerHTML = `Loading…`; try { const response = await fetch(`/api/verification/history?category=${encodeURIComponent(normalized)}`); const payload = await response.json().catch(() => ({})); if (!response.ok) { scanHistoryModalThead.innerHTML = ""; scanHistoryModalTbody.innerHTML = `${sanitize(payload.message ?? "Unable to load history.")}`; return; } const records = payload.records ?? []; if (normalized === "successful") { renderModalSuccessfulTable(records); } else { renderModalRejectedTable(records); } } catch { scanHistoryModalThead.innerHTML = ""; scanHistoryModalTbody.innerHTML = `Unable to load history.`; } } function renderModalSuccessfulTable(records) { scanHistoryModalThead.innerHTML = ` # MODEL NUMBER UNIQUE NUMBER STATUS MARKED AT MARKED BY `; if (!records.length) { scanHistoryModalTbody.innerHTML = `No successful records yet.`; return; } scanHistoryModalTbody.innerHTML = ""; records.forEach((record, index) => { const row = document.createElement("tr"); row.innerHTML = ` ${index + 1} ${sanitize(record.modelNumber)} ${sanitize(record.uniqueNumber)} TRUE ${formatDisplayTime(record.processedAtUtc)} System`; scanHistoryModalTbody.appendChild(row); }); } function renderModalRejectedTable(records) { scanHistoryModalThead.innerHTML = ` TIME SCANNED / INPUT MODEL UNIQUE REASON `; if (!records.length) { scanHistoryModalTbody.innerHTML = `No rejected records yet.`; return; } scanHistoryModalTbody.innerHTML = ""; records.forEach(record => { const row = document.createElement("tr"); const model = record.modelNumber ? sanitize(record.modelNumber) : "—"; const unique = record.uniqueNumber ? sanitize(record.uniqueNumber) : "—"; row.innerHTML = ` ${formatDisplayTime(record.processedAtUtc)} ${sanitize(record.rawInputValue)} ${model} ${unique} ${sanitize(record.rejectionReason)}`; scanHistoryModalTbody.appendChild(row); }); } wireScanHistoryModal(); qrInput.focus(); })();