195 lines
6.9 KiB
JavaScript
195 lines
6.9 KiB
JavaScript
(() => {
|
|
const qrInput = document.getElementById("qr-input");
|
|
const statusCard = document.getElementById("status-card");
|
|
const statusCenterCard = document.getElementById("status-center-card");
|
|
const statusVisual = document.getElementById("status-visual");
|
|
const statusMiniIcon = document.getElementById("status-mini-icon");
|
|
const statusHeadline = document.getElementById("status-headline");
|
|
const statusMiniHeadline = document.getElementById("status-mini-headline");
|
|
const statusText = document.getElementById("status-text");
|
|
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;
|
|
|
|
qrInput.addEventListener("keydown", async event => {
|
|
if (event.key !== "Enter") {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
const qrValue = qrInput.value.trim();
|
|
if (!qrValue) {
|
|
return;
|
|
}
|
|
|
|
await submitScan(qrValue);
|
|
qrInput.value = "";
|
|
qrInput.focus();
|
|
});
|
|
|
|
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");
|
|
statusVisual.textContent = "\u2713";
|
|
statusMiniIcon.textContent = "\u2713";
|
|
statusHeadline.textContent = "VERIFIED";
|
|
statusMiniHeadline.textContent = "VERIFIED";
|
|
statusText.textContent = result.message;
|
|
statusMiniMessage.textContent = result.message;
|
|
lastUpdatedTime.textContent = formatDisplayTime(result.processedAtUtc);
|
|
return;
|
|
}
|
|
|
|
setFailedState(result.message);
|
|
triggerFailureAlarm();
|
|
}
|
|
|
|
function setFailedState(message) {
|
|
setStatusClasses("status-failed");
|
|
statusVisual.textContent = "!";
|
|
statusMiniIcon.textContent = "!";
|
|
statusHeadline.textContent = "FAILED";
|
|
statusMiniHeadline.textContent = "FAILED";
|
|
statusText.textContent = message;
|
|
statusMiniMessage.textContent = message;
|
|
lastUpdatedTime.textContent = "-";
|
|
}
|
|
|
|
function setStatusClasses(statusClass) {
|
|
statusCard.className = `panel panel-status ${statusClass}`;
|
|
statusCenterCard.className = `panel panel-center-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 => ({
|
|
"&": "&",
|
|
"<": "<",
|
|
">": ">",
|
|
"\"": """,
|
|
"'": "'"
|
|
}[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();
|
|
}
|
|
})();
|