Update verification API and dashboard scan behavior
Updates dashboard/API flow to return recent scan history, handle rejected scans, auto-submit scanner input, and auto-submit pasted QR values without pressing Enter.main
parent
1ffe45c76b
commit
ae9a42fa70
|
|
@ -15,7 +15,8 @@ public sealed class HomeController(IScanHistoryStore scanHistoryStore) : Control
|
|||
{
|
||||
var model = new VerificationDashboardViewModel
|
||||
{
|
||||
RecentScans = await _scanHistoryStore.GetLastFiveAsync(cancellationToken)
|
||||
RecentSuccessfulScans = await _scanHistoryStore.GetLastFiveSuccessfulScansAsync(cancellationToken),
|
||||
RecentRejectedScans = await _scanHistoryStore.GetLastFiveRejectedScansAsync(cancellationToken)
|
||||
};
|
||||
|
||||
return View(model);
|
||||
|
|
|
|||
|
|
@ -25,11 +25,16 @@ public sealed class VerificationController(
|
|||
|
||||
try
|
||||
{
|
||||
var verificationResult = await _shipmentVerificationService.VerifyQrAsync(request.QrValue, cancellationToken);
|
||||
var verificationResult = await _shipmentVerificationService.VerifyQrAsync(
|
||||
request.QrValue,
|
||||
request.EntryMethod ?? "Scanner",
|
||||
cancellationToken);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
result = verificationResult,
|
||||
recentScans = await _scanHistoryStore.GetLastFiveAsync(cancellationToken)
|
||||
recentSuccessfulScans = await _scanHistoryStore.GetLastFiveSuccessfulScansAsync(cancellationToken),
|
||||
recentRejectedScans = await _scanHistoryStore.GetLastFiveRejectedScansAsync(cancellationToken)
|
||||
});
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
|
|
@ -46,5 +51,35 @@ public sealed class VerificationController(
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all successful or all rejected scan records from SQLite (not limited to five).
|
||||
/// </summary>
|
||||
[HttpGet("history")]
|
||||
public async Task<IActionResult> GetFullHistory([FromQuery] string category = "successful", CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalized = category.Trim().ToLowerInvariant();
|
||||
try
|
||||
{
|
||||
if (normalized == "successful")
|
||||
{
|
||||
var records = await _scanHistoryStore.GetAllSuccessfulScansAsync(cancellationToken);
|
||||
return Ok(new { category = "successful", records });
|
||||
}
|
||||
|
||||
if (normalized == "rejected")
|
||||
{
|
||||
var records = await _scanHistoryStore.GetAllRejectedScansAsync(cancellationToken);
|
||||
return Ok(new { category = "rejected", records });
|
||||
}
|
||||
|
||||
return BadRequest(new { message = "Invalid category. Use 'successful' or 'rejected'." });
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "Unexpected error while loading scan history.");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Unable to load scan history." });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
namespace AVSCartonShipmentVerifier.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// User-facing rejection reasons (must match product copy).
|
||||
/// </summary>
|
||||
public static class VerificationRejectionReasons
|
||||
{
|
||||
public const string InvalidFormat = "Invalid format";
|
||||
public const string CartonDoesNotExist = "Carton does not exist";
|
||||
public const string AlreadyTransferredDuplicateScan = "Already transferred / duplicate scan";
|
||||
public const string ManualEntryNotAllowed = "Manual entry is not allowed. Please scan or paste.";
|
||||
public const string DatabaseOrSystemError = "Database error / system error";
|
||||
}
|
||||
|
|
@ -5,8 +5,17 @@
|
|||
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 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"),
|
||||
|
|
@ -19,45 +28,94 @@
|
|||
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;
|
||||
|
||||
qrInput.addEventListener("keydown", async event => {
|
||||
if (event.key !== "Enter") {
|
||||
return;
|
||||
}
|
||||
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]+$/;
|
||||
|
||||
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;
|
||||
function clearAutoSubmitTimer() {
|
||||
if (autoSubmitTimer !== null) {
|
||||
clearTimeout(autoSubmitTimer);
|
||||
autoSubmitTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitScan(qrValue) {
|
||||
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",
|
||||
|
|
@ -65,25 +123,174 @@
|
|||
"Content-Type": "application/json",
|
||||
"RequestVerificationToken": antiForgeryToken ?? ""
|
||||
},
|
||||
body: JSON.stringify({ qrValue })
|
||||
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.";
|
||||
setFailedState(payload.message ?? fallbackMessage);
|
||||
setRejectedState(payload.message ?? fallbackMessage, null);
|
||||
resetDetails();
|
||||
triggerFailureAlarm();
|
||||
return;
|
||||
}
|
||||
|
||||
const result = payload.result;
|
||||
renderDetails(result);
|
||||
renderStatus(result);
|
||||
renderHistory(payload.recentScans ?? []);
|
||||
renderSuccessfulHistory(payload.recentSuccessfulScans ?? []);
|
||||
renderRejectedHistory(payload.recentRejectedScans ?? []);
|
||||
} catch {
|
||||
setFailedState("Unexpected communication error.");
|
||||
setRejectedState("Unexpected communication error.", null);
|
||||
triggerFailureAlarm();
|
||||
resetDetails();
|
||||
} finally {
|
||||
qrInput.value = "";
|
||||
resetTypingHeuristics();
|
||||
qrInput.focus();
|
||||
isSubmitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -97,28 +304,16 @@
|
|||
return;
|
||||
}
|
||||
|
||||
setFailedState(result.message);
|
||||
if (shouldTriggerFailureAlarm(result)) {
|
||||
triggerFailureAlarm();
|
||||
}
|
||||
setRejectedState(result.message, result.processedAtUtc);
|
||||
triggerFailureAlarm();
|
||||
}
|
||||
|
||||
function shouldTriggerFailureAlarm(result) {
|
||||
const message = (result.message ?? "").toLowerCase();
|
||||
return !result.existsInSystem
|
||||
&& (
|
||||
message.includes("invalid qr format")
|
||||
|| message.includes("qr value is required")
|
||||
|| message.includes("not found")
|
||||
);
|
||||
}
|
||||
|
||||
function setFailedState(message) {
|
||||
function setRejectedState(message, processedAtUtc) {
|
||||
setStatusClasses("status-failed");
|
||||
statusMiniIcon.textContent = "!";
|
||||
statusMiniHeadline.textContent = "FAILED";
|
||||
statusMiniHeadline.textContent = "REJECTED";
|
||||
statusMiniMessage.textContent = message;
|
||||
lastUpdatedTime.textContent = "-";
|
||||
lastUpdatedTime.textContent = processedAtUtc ? formatDisplayTime(processedAtUtc) : "-";
|
||||
}
|
||||
|
||||
function setStatusClasses(statusClass) {
|
||||
|
|
@ -141,26 +336,50 @@
|
|||
details.time.textContent = "-";
|
||||
}
|
||||
|
||||
function renderHistory(records) {
|
||||
historyBody.innerHTML = "";
|
||||
function renderSuccessfulHistory(records) {
|
||||
historySuccessBody.innerHTML = "";
|
||||
|
||||
if (records.length === 0) {
|
||||
historyEmpty.classList.remove("is-hidden");
|
||||
historySuccessEmpty.classList.remove("is-hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
historyEmpty.classList.add("is-hidden");
|
||||
historySuccessEmpty.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><span class="status-pill status-true">TRUE</span></td>
|
||||
<td>${formatDisplayTime(record.processedAtUtc)}</td>
|
||||
<td>System</td>`;
|
||||
|
||||
historyBody.appendChild(row);
|
||||
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 = `
|
||||
<td>${formatDisplayTime(record.processedAtUtc)}</td>
|
||||
<td>${sanitize(record.rawInputValue)}</td>
|
||||
<td>${model}</td>
|
||||
<td>${unique}</td>
|
||||
<td>${sanitize(record.rejectionReason)}</td>`;
|
||||
|
||||
historyRejectedBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -202,4 +421,122 @@
|
|||
// 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 = `<tr><td colspan="6" class="scan-history-modal-empty">Loading…</td></tr>`;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/verification/history?category=${encodeURIComponent(normalized)}`);
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
|
||||
if (!response.ok) {
|
||||
scanHistoryModalThead.innerHTML = "";
|
||||
scanHistoryModalTbody.innerHTML = `<tr><td colspan="6" class="scan-history-modal-empty">${sanitize(payload.message ?? "Unable to load history.")}</td></tr>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const records = payload.records ?? [];
|
||||
if (normalized === "successful") {
|
||||
renderModalSuccessfulTable(records);
|
||||
} else {
|
||||
renderModalRejectedTable(records);
|
||||
}
|
||||
} catch {
|
||||
scanHistoryModalThead.innerHTML = "";
|
||||
scanHistoryModalTbody.innerHTML = `<tr><td colspan="6" class="scan-history-modal-empty">Unable to load history.</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderModalSuccessfulTable(records) {
|
||||
scanHistoryModalThead.innerHTML = `
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>MODEL NUMBER</th>
|
||||
<th>UNIQUE NUMBER</th>
|
||||
<th>STATUS</th>
|
||||
<th>MARKED AT</th>
|
||||
<th>MARKED BY</th>
|
||||
</tr>`;
|
||||
|
||||
if (!records.length) {
|
||||
scanHistoryModalTbody.innerHTML = `<tr><td colspan="6" class="scan-history-modal-empty">No successful records yet.</td></tr>`;
|
||||
return;
|
||||
}
|
||||
|
||||
scanHistoryModalTbody.innerHTML = "";
|
||||
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 status-true">TRUE</span></td>
|
||||
<td>${formatDisplayTime(record.processedAtUtc)}</td>
|
||||
<td>System</td>`;
|
||||
scanHistoryModalTbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function renderModalRejectedTable(records) {
|
||||
scanHistoryModalThead.innerHTML = `
|
||||
<tr>
|
||||
<th>TIME</th>
|
||||
<th>SCANNED / INPUT</th>
|
||||
<th>MODEL</th>
|
||||
<th>UNIQUE</th>
|
||||
<th>REASON</th>
|
||||
</tr>`;
|
||||
|
||||
if (!records.length) {
|
||||
scanHistoryModalTbody.innerHTML = `<tr><td colspan="5" class="scan-history-modal-empty">No rejected records yet.</td></tr>`;
|
||||
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 = `
|
||||
<td>${formatDisplayTime(record.processedAtUtc)}</td>
|
||||
<td>${sanitize(record.rawInputValue)}</td>
|
||||
<td>${model}</td>
|
||||
<td>${unique}</td>
|
||||
<td>${sanitize(record.rejectionReason)}</td>`;
|
||||
scanHistoryModalTbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
wireScanHistoryModal();
|
||||
|
||||
qrInput.focus();
|
||||
})();
|
||||
|
|
|
|||
Loading…
Reference in New Issue