Persist scan history and mark verified shipments
- Add SQLite-backed scan history storage with configurable database path and retention limit - Register scan history DbContext and persist recent scan records across app restarts - Update shipment verification flow to mark matched cartons as moved to warehouse - Treat verification as successful only when the record exists and the shipment flag update succeeds - Rename verification flag configuration to use moved_to_warehouse - Make scan history APIs async and update controllers/services accordingly - Improve dashboard scan handling for pasted QR values and prevent duplicate submissions - Update UI branding to SHIPPING MARK and simplify the dashboard status layout - Display scan history timestamps in local timemain
parent
dbb9933174
commit
20482ec357
|
|
@ -9,6 +9,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,6 @@ public sealed class CartonVerificationOptions
|
|||
public string QrColumnName { get; init; } = "qr";
|
||||
|
||||
[Required]
|
||||
public string ShipmentVerificationFlagColumnName { get; init; } = "shipment_verification_flag";
|
||||
public string MovedToWarehouseColumnName { get; init; } = "moved_to_warehouse";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace AVSCartonShipmentVerifier.Configuration;
|
||||
|
||||
public sealed class ScanHistoryOptions
|
||||
{
|
||||
public const string SectionName = "ScanHistory";
|
||||
|
||||
[Required]
|
||||
public string DatabasePath { get; init; } = @"C:\Users\Public\AVSCartonShipmentVerifier\scan-history.db";
|
||||
|
||||
[Range(1, 500)]
|
||||
public int MaxRecordsToKeep { get; init; } = 500;
|
||||
}
|
||||
|
|
@ -11,11 +11,11 @@ public sealed class HomeController(IScanHistoryStore scanHistoryStore) : Control
|
|||
private readonly IScanHistoryStore _scanHistoryStore = scanHistoryStore;
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult Index()
|
||||
public async Task<IActionResult> Index(CancellationToken cancellationToken)
|
||||
{
|
||||
var model = new VerificationDashboardViewModel
|
||||
{
|
||||
RecentScans = _scanHistoryStore.GetLastFive()
|
||||
RecentScans = await _scanHistoryStore.GetLastFiveAsync(cancellationToken)
|
||||
};
|
||||
|
||||
return View(model);
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ public sealed class VerificationController(
|
|||
return Ok(new
|
||||
{
|
||||
result = verificationResult,
|
||||
recentScans = _scanHistoryStore.GetLastFive()
|
||||
recentScans = await _scanHistoryStore.GetLastFiveAsync(cancellationToken)
|
||||
});
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ public sealed class VerificationResultDto
|
|||
public string RawQrValue { get; init; } = string.Empty;
|
||||
public bool ExistsInSystem { get; init; }
|
||||
public bool ShipmentMarked { get; init; }
|
||||
public bool IsSuccessful => ExistsInSystem;
|
||||
public bool IsSuccessful => ExistsInSystem && ShipmentMarked;
|
||||
public string Message { get; init; } = string.Empty;
|
||||
public DateTime ProcessedAtUtc { get; init; }
|
||||
}
|
||||
|
|
|
|||
24
Program.cs
24
Program.cs
|
|
@ -1,7 +1,9 @@
|
|||
using AVSCartonShipmentVerifier.Configuration;
|
||||
using AVSCartonShipmentVerifier.Data;
|
||||
using AVSCartonShipmentVerifier.Repositories;
|
||||
using AVSCartonShipmentVerifier.ScanHistory;
|
||||
using AVSCartonShipmentVerifier.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
|
|
@ -11,14 +13,34 @@ builder.Services
|
|||
.ValidateDataAnnotations()
|
||||
.ValidateOnStart();
|
||||
|
||||
builder.Services
|
||||
.AddOptions<ScanHistoryOptions>()
|
||||
.Bind(builder.Configuration.GetSection(ScanHistoryOptions.SectionName))
|
||||
.ValidateDataAnnotations()
|
||||
.ValidateOnStart();
|
||||
|
||||
builder.Services.AddControllersWithViews();
|
||||
builder.Services.AddSingleton<IScanHistoryStore, InMemoryScanHistoryStore>();
|
||||
|
||||
builder.Services.AddDbContext<ScanHistoryDbContext>((serviceProvider, options) =>
|
||||
{
|
||||
var scanHistoryOptions = serviceProvider.GetRequiredService<Microsoft.Extensions.Options.IOptions<ScanHistoryOptions>>().Value;
|
||||
var connectionString = ScanHistoryPath.EnsureDirectoryAndGetConnectionString(scanHistoryOptions);
|
||||
options.UseSqlite(connectionString);
|
||||
});
|
||||
|
||||
builder.Services.AddScoped<IScanHistoryStore, SqliteScanHistoryStore>();
|
||||
builder.Services.AddScoped<IVerificationDbConnectionFactory, VerificationDbConnectionFactory>();
|
||||
builder.Services.AddScoped<ICartonVerificationRepository, CartonVerificationRepository>();
|
||||
builder.Services.AddScoped<IShipmentVerificationService, ShipmentVerificationService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<ScanHistoryDbContext>();
|
||||
db.Database.EnsureCreated();
|
||||
}
|
||||
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseExceptionHandler("/Home/Error");
|
||||
|
|
|
|||
|
|
@ -33,6 +33,23 @@ public sealed class CartonVerificationRepository(
|
|||
return result is not null;
|
||||
}
|
||||
|
||||
public async Task<bool> MarkShipmentVerifiedAsync(string uniqueNumber, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = $"""
|
||||
UPDATE {QuoteIdentifier(_options.TableName)}
|
||||
SET {QuoteIdentifier(_options.MovedToWarehouseColumnName)} = 1
|
||||
WHERE {QuoteIdentifier(_options.QrColumnName)} = @uniqueNumber;
|
||||
""";
|
||||
command.Parameters.AddWithValue("@uniqueNumber", uniqueNumber);
|
||||
|
||||
var affectedRows = await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
return affectedRows > 0;
|
||||
}
|
||||
|
||||
private static string QuoteIdentifier(string identifier)
|
||||
{
|
||||
if (!SqlIdentifierRegex.IsMatch(identifier))
|
||||
|
|
|
|||
|
|
@ -3,5 +3,6 @@ namespace AVSCartonShipmentVerifier.Repositories;
|
|||
public interface ICartonVerificationRepository
|
||||
{
|
||||
Task<bool> UniqueNumberExistsAsync(string uniqueNumber, CancellationToken cancellationToken = default);
|
||||
Task<bool> MarkShipmentVerifiedAsync(string uniqueNumber, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace AVSCartonShipmentVerifier.ScanHistory;
|
||||
|
||||
public sealed class ScanHistoryDbContext(DbContextOptions<ScanHistoryDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<ScanHistoryEntry> ScanHistoryEntries => Set<ScanHistoryEntry>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<ScanHistoryEntry>(entity =>
|
||||
{
|
||||
entity.ToTable("scan_history_entries");
|
||||
entity.HasKey(x => x.Id);
|
||||
entity.Property(x => x.ModelNumber).HasMaxLength(128);
|
||||
entity.Property(x => x.UniqueNumber).HasMaxLength(128);
|
||||
entity.Property(x => x.ProcessedAtUtc);
|
||||
entity.HasIndex(x => x.ProcessedAtUtc);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
namespace AVSCartonShipmentVerifier.ScanHistory;
|
||||
|
||||
public sealed class ScanHistoryEntry
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string ModelNumber { get; set; } = string.Empty;
|
||||
public string UniqueNumber { get; set; } = string.Empty;
|
||||
public bool ExistsInSystem { get; set; }
|
||||
public bool ShipmentMarked { get; set; }
|
||||
public DateTime ProcessedAtUtc { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
using System.IO;
|
||||
using AVSCartonShipmentVerifier.Configuration;
|
||||
|
||||
namespace AVSCartonShipmentVerifier.ScanHistory;
|
||||
|
||||
public static class ScanHistoryPath
|
||||
{
|
||||
public static string EnsureDirectoryAndGetConnectionString(ScanHistoryOptions options)
|
||||
{
|
||||
var fullPath = Path.GetFullPath(options.DatabasePath);
|
||||
var directory = Path.GetDirectoryName(fullPath);
|
||||
if (!string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
return $"Data Source={fullPath}";
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ namespace AVSCartonShipmentVerifier.Services;
|
|||
|
||||
public interface IScanHistoryStore
|
||||
{
|
||||
void Add(ScannedRecordDto record);
|
||||
IReadOnlyCollection<ScannedRecordDto> GetLastFive();
|
||||
Task AddAsync(ScannedRecordDto record, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyCollection<ScannedRecordDto>> GetLastFiveAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ public sealed class InMemoryScanHistoryStore : IScanHistoryStore
|
|||
private readonly ConcurrentQueue<ScannedRecordDto> _records = new();
|
||||
private readonly Lock _syncLock = new();
|
||||
|
||||
public void Add(ScannedRecordDto record)
|
||||
public Task AddAsync(ScannedRecordDto record, CancellationToken cancellationToken = default)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
|
|
@ -19,13 +19,15 @@ public sealed class InMemoryScanHistoryStore : IScanHistoryStore
|
|||
_records.TryDequeue(out _);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<ScannedRecordDto> GetLastFive()
|
||||
public Task<IReadOnlyCollection<ScannedRecordDto>> GetLastFiveAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
return _records.Reverse().ToArray();
|
||||
return Task.FromResult<IReadOnlyCollection<ScannedRecordDto>>(_records.Reverse().ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,22 +45,35 @@ public sealed class ShipmentVerificationService(
|
|||
ProcessedAtUtc = nowUtc
|
||||
};
|
||||
|
||||
_historyStore.Add(ToScannedRecord(missingResult));
|
||||
await _historyStore.AddAsync(ToScannedRecord(missingResult), cancellationToken);
|
||||
return missingResult;
|
||||
}
|
||||
|
||||
bool marked;
|
||||
try
|
||||
{
|
||||
marked = await _repository.MarkShipmentVerifiedAsync(parseResult.UniqueNumber, cancellationToken);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "Failed to mark shipment verified for unique number {UniqueNumber}.", parseResult.UniqueNumber);
|
||||
marked = false;
|
||||
}
|
||||
|
||||
var result = new VerificationResultDto
|
||||
{
|
||||
ModelNumber = parseResult.ModelNumber,
|
||||
UniqueNumber = parseResult.UniqueNumber,
|
||||
RawQrValue = rawQrValue,
|
||||
ExistsInSystem = true,
|
||||
ShipmentMarked = true,
|
||||
Message = "Record found in carton verification log.",
|
||||
ShipmentMarked = marked,
|
||||
Message = marked
|
||||
? "Record found and shipment marked successfully."
|
||||
: "Record found, but shipment could not be marked.",
|
||||
ProcessedAtUtc = nowUtc
|
||||
};
|
||||
|
||||
_historyStore.Add(ToScannedRecord(result));
|
||||
await _historyStore.AddAsync(ToScannedRecord(result), cancellationToken);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
using AVSCartonShipmentVerifier.DTOs;
|
||||
using AVSCartonShipmentVerifier.Configuration;
|
||||
using AVSCartonShipmentVerifier.ScanHistory;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace AVSCartonShipmentVerifier.Services;
|
||||
|
||||
public sealed class SqliteScanHistoryStore(
|
||||
ScanHistoryDbContext dbContext,
|
||||
IOptions<ScanHistoryOptions> options,
|
||||
ILogger<SqliteScanHistoryStore> logger) : IScanHistoryStore
|
||||
{
|
||||
private readonly ScanHistoryDbContext _dbContext = dbContext;
|
||||
private readonly ScanHistoryOptions _options = options.Value;
|
||||
private readonly ILogger<SqliteScanHistoryStore> _logger = logger;
|
||||
|
||||
public async Task AddAsync(ScannedRecordDto record, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
_dbContext.ScanHistoryEntries.Add(new ScanHistoryEntry
|
||||
{
|
||||
ModelNumber = record.ModelNumber,
|
||||
UniqueNumber = record.UniqueNumber,
|
||||
ExistsInSystem = record.ExistsInSystem,
|
||||
ShipmentMarked = record.ShipmentMarked,
|
||||
ProcessedAtUtc = record.ProcessedAtUtc
|
||||
});
|
||||
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
await TrimOldRecordsAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "Failed to persist scan history entry.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyCollection<ScannedRecordDto>> GetLastFiveAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entries = await _dbContext.ScanHistoryEntries
|
||||
.AsNoTracking()
|
||||
.OrderByDescending(x => x.ProcessedAtUtc)
|
||||
.Take(5)
|
||||
.ToArrayAsync(cancellationToken);
|
||||
|
||||
return entries
|
||||
.Select(x => new ScannedRecordDto
|
||||
{
|
||||
ModelNumber = x.ModelNumber,
|
||||
UniqueNumber = x.UniqueNumber,
|
||||
ExistsInSystem = x.ExistsInSystem,
|
||||
ShipmentMarked = x.ShipmentMarked,
|
||||
ProcessedAtUtc = DateTime.SpecifyKind(x.ProcessedAtUtc, DateTimeKind.Utc)
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "Failed to read scan history.");
|
||||
return Array.Empty<ScannedRecordDto>();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TrimOldRecordsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var max = _options.MaxRecordsToKeep;
|
||||
if (max <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var toDelete = await _dbContext.ScanHistoryEntries
|
||||
.OrderByDescending(x => x.ProcessedAtUtc)
|
||||
.Skip(max)
|
||||
.Select(x => x.Id)
|
||||
.ToArrayAsync(cancellationToken);
|
||||
|
||||
if (toDelete.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_dbContext.ScanHistoryEntries.RemoveRange(toDelete.Select(id => new ScanHistoryEntry { Id = id }));
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
<header class="topbar">
|
||||
<div class="topbar-brand">
|
||||
<span class="brand-icon">◈</span>
|
||||
<span>AVS CARTON SHIPMENT VERIFIER</span>
|
||||
<span>SHIPPING MARK</span>
|
||||
</div>
|
||||
<div class="topbar-siren" id="siren-indicator">
|
||||
<span>🔊</span>
|
||||
|
|
@ -47,12 +47,6 @@
|
|||
</section>
|
||||
</section>
|
||||
|
||||
<section class="panel panel-center-status status-neutral" id="status-center-card">
|
||||
<div class="status-visual" id="status-visual">-</div>
|
||||
<div class="status-headline" id="status-headline">AWAITING SCAN</div>
|
||||
<div id="status-text" class="status-message">Scan a QR code to verify carton details</div>
|
||||
</section>
|
||||
|
||||
<section class="right-column">
|
||||
<section class="panel">
|
||||
<h2 class="section-title"><span class="section-icon">ⓘ</span> SCANNED DETAILS</h2>
|
||||
|
|
@ -103,7 +97,7 @@
|
|||
@(item.ShipmentMarked ? "TRUE" : "FALSE")
|
||||
</span>
|
||||
</td>
|
||||
<td>@item.ProcessedAtUtc.ToString("dd MMM yyyy h:mm tt")</td>
|
||||
<td>@DateTime.SpecifyKind(item.ProcessedAtUtc, DateTimeKind.Utc).ToLocalTime().ToString("dd MMM yyyy h:mm tt")</td>
|
||||
<td>System</td>
|
||||
</tr>
|
||||
rowNumber++;
|
||||
|
|
@ -122,7 +116,7 @@
|
|||
<footer class="page-footer">
|
||||
<span>AVS C# .NET Application</span>
|
||||
<span>|</span>
|
||||
<span>AVS Carton Shipment Verifier</span>
|
||||
<span>SHIPPING MARK</span>
|
||||
</footer>
|
||||
|
||||
@section Scripts {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>@ViewData["Title"] - AVS Carton Shipment Verifier</title>
|
||||
<title>@ViewData["Title"] - SHIPPING MARK</title>
|
||||
<script type="importmap"></script>
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
{
|
||||
"ConnectionStrings": {
|
||||
"VerificationDatabase": "Server=utopia-industries-rr.c5qech8o9lgg.us-east-1.rds.amazonaws.com;Port=3306;Database=item_verification_system;User ID=muhammad.faique;Password=21)3lq6b!A@.;SslMode=Preferred;Allow User Variables=True;"
|
||||
"VerificationDatabase": ""
|
||||
},
|
||||
"CartonVerification": {
|
||||
"TableName": "carton_verification_log",
|
||||
"QrColumnName": "qr",
|
||||
"ShipmentVerificationFlagColumnName": "shipment_verification_flag"
|
||||
"MovedToWarehouseColumnName": "moved_to_warehouse"
|
||||
},
|
||||
"ScanHistory": {
|
||||
"DatabasePath": "C:\\Users\\Public\\AVSCartonShipmentVerifier\\scan-history.db",
|
||||
"MaxRecordsToKeep": 500
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ body {
|
|||
|
||||
.top-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.15fr 0.75fr 1.1fr;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
|
|
@ -164,56 +164,18 @@ body {
|
|||
color: #4f6687;
|
||||
}
|
||||
|
||||
.panel-center-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
min-height: 314px;
|
||||
}
|
||||
|
||||
.status-visual {
|
||||
width: 94px;
|
||||
height: 94px;
|
||||
border-radius: 50%;
|
||||
background: #8b9cb8;
|
||||
color: #fff;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 2.4rem;
|
||||
font-weight: 800;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.status-headline {
|
||||
color: #123d73;
|
||||
font-size: 2.6rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.status-message {
|
||||
margin-top: 6px;
|
||||
color: #5f7392;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.status-success .status-visual,
|
||||
.status-success .status-mini-icon {
|
||||
background: #3cae58;
|
||||
}
|
||||
|
||||
.status-failed .status-visual,
|
||||
.status-failed .status-mini-icon {
|
||||
background: #d54d56;
|
||||
}
|
||||
|
||||
.status-success .status-headline,
|
||||
.status-success .status-mini-headline {
|
||||
color: #2f9950;
|
||||
}
|
||||
|
||||
.status-failed .status-headline,
|
||||
.status-failed .status-mini-headline {
|
||||
color: #bf3340;
|
||||
}
|
||||
|
|
@ -347,10 +309,6 @@ body {
|
|||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.panel-center-status {
|
||||
min-height: 220px;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
(() => {
|
||||
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");
|
||||
|
|
@ -21,6 +17,7 @@
|
|||
};
|
||||
|
||||
const antiForgeryToken = document.querySelector('input[name="__RequestVerificationToken"]')?.value;
|
||||
let isSubmitting = false;
|
||||
|
||||
qrInput.addEventListener("keydown", async event => {
|
||||
if (event.key !== "Enter") {
|
||||
|
|
@ -28,15 +25,35 @@
|
|||
}
|
||||
|
||||
event.preventDefault();
|
||||
const qrValue = qrInput.value.trim();
|
||||
if (!qrValue) {
|
||||
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 {
|
||||
|
|
@ -72,11 +89,8 @@
|
|||
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;
|
||||
|
|
@ -88,18 +102,14 @@
|
|||
|
||||
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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue