AVS-Carton-Shipment-Verifier/wwwroot/js/verification-dashboard.js

205 lines
6.8 KiB
JavaScript

(() => {
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 historyBody = document.querySelector("#history-table tbody");
const historyEmpty = document.getElementById("history-empty");
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;
let isSubmitting = false;
qrInput.addEventListener("keydown", async event => {
if (event.key !== "Enter") {
return;
}
event.preventDefault();
await submitCurrentInput();
});
qrInput.addEventListener("paste", async event => {
const pastedValue = event.clipboardData?.getData("text")?.trim();
if (!pastedValue) {
return;
}
event.preventDefault();
qrInput.value = pastedValue;
await submitCurrentInput();
});
async function submitCurrentInput() {
const qrValue = qrInput.value.trim();
if (!qrValue || isSubmitting) {
return;
}
isSubmitting = true;
try {
await submitScan(qrValue);
qrInput.value = "";
qrInput.focus();
} finally {
isSubmitting = false;
}
}
async function submitScan(qrValue) {
try {
const response = await fetch("/api/verification/scan", {
method: "POST",
headers: {
"Content-Type": "application/json",
"RequestVerificationToken": antiForgeryToken ?? ""
},
body: JSON.stringify({ qrValue })
});
const payload = await response.json();
if (!response.ok) {
const fallbackMessage = "Verification service is unavailable.";
setFailedState(payload.message ?? fallbackMessage);
triggerFailureAlarm();
resetDetails();
return;
}
const result = payload.result;
renderDetails(result);
renderStatus(result);
renderHistory(payload.recentScans ?? []);
} catch {
setFailedState("Unexpected communication error.");
triggerFailureAlarm();
resetDetails();
}
}
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;
}
setFailedState(result.message);
triggerFailureAlarm();
}
function setFailedState(message) {
setStatusClasses("status-failed");
statusMiniIcon.textContent = "!";
statusMiniHeadline.textContent = "FAILED";
statusMiniMessage.textContent = message;
lastUpdatedTime.textContent = "-";
}
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 renderHistory(records) {
historyBody.innerHTML = "";
if (records.length === 0) {
historyEmpty.classList.remove("is-hidden");
return;
}
historyEmpty.classList.add("is-hidden");
records.forEach((record, index) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${index + 1}</td>
<td>${sanitize(record.modelNumber)}</td>
<td>${sanitize(record.uniqueNumber)}</td>
<td><span class="status-pill ${record.shipmentMarked ? "status-true" : "status-false"}">${record.shipmentMarked ? "TRUE" : "FALSE"}</span></td>
<td>${formatDisplayTime(record.processedAtUtc)}</td>
<td>System</td>`;
historyBody.appendChild(row);
});
}
function sanitize(value) {
return (value ?? "").replace(/[&<>\"']/g, char => ({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
"\"": "&quot;",
"'": "&#39;"
}[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() {
const context = new AudioContext();
const oscillator = context.createOscillator();
const gainNode = context.createGain();
oscillator.type = "sawtooth";
oscillator.frequency.setValueAtTime(740, context.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(320, context.currentTime + 0.4);
oscillator.connect(gainNode);
gainNode.connect(context.destination);
gainNode.gain.setValueAtTime(0.0001, context.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.5, context.currentTime + 0.05);
gainNode.gain.exponentialRampToValueAtTime(0.0001, context.currentTime + 0.45);
oscillator.start(context.currentTime);
oscillator.stop(context.currentTime + 0.45);
oscillator.onended = () => context.close();
}
})();