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>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
|
||||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
|
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,6 @@ public sealed class CartonVerificationOptions
|
||||||
public string QrColumnName { get; init; } = "qr";
|
public string QrColumnName { get; init; } = "qr";
|
||||||
|
|
||||||
[Required]
|
[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;
|
private readonly IScanHistoryStore _scanHistoryStore = scanHistoryStore;
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public IActionResult Index()
|
public async Task<IActionResult> Index(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var model = new VerificationDashboardViewModel
|
var model = new VerificationDashboardViewModel
|
||||||
{
|
{
|
||||||
RecentScans = _scanHistoryStore.GetLastFive()
|
RecentScans = await _scanHistoryStore.GetLastFiveAsync(cancellationToken)
|
||||||
};
|
};
|
||||||
|
|
||||||
return View(model);
|
return View(model);
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ public sealed class VerificationController(
|
||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
result = verificationResult,
|
result = verificationResult,
|
||||||
recentScans = _scanHistoryStore.GetLastFive()
|
recentScans = await _scanHistoryStore.GetLastFiveAsync(cancellationToken)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ public sealed class VerificationResultDto
|
||||||
public string RawQrValue { get; init; } = string.Empty;
|
public string RawQrValue { get; init; } = string.Empty;
|
||||||
public bool ExistsInSystem { get; init; }
|
public bool ExistsInSystem { get; init; }
|
||||||
public bool ShipmentMarked { get; init; }
|
public bool ShipmentMarked { get; init; }
|
||||||
public bool IsSuccessful => ExistsInSystem;
|
public bool IsSuccessful => ExistsInSystem && ShipmentMarked;
|
||||||
public string Message { get; init; } = string.Empty;
|
public string Message { get; init; } = string.Empty;
|
||||||
public DateTime ProcessedAtUtc { get; init; }
|
public DateTime ProcessedAtUtc { get; init; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
24
Program.cs
24
Program.cs
|
|
@ -1,7 +1,9 @@
|
||||||
using AVSCartonShipmentVerifier.Configuration;
|
using AVSCartonShipmentVerifier.Configuration;
|
||||||
using AVSCartonShipmentVerifier.Data;
|
using AVSCartonShipmentVerifier.Data;
|
||||||
using AVSCartonShipmentVerifier.Repositories;
|
using AVSCartonShipmentVerifier.Repositories;
|
||||||
|
using AVSCartonShipmentVerifier.ScanHistory;
|
||||||
using AVSCartonShipmentVerifier.Services;
|
using AVSCartonShipmentVerifier.Services;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
|
@ -11,14 +13,34 @@ builder.Services
|
||||||
.ValidateDataAnnotations()
|
.ValidateDataAnnotations()
|
||||||
.ValidateOnStart();
|
.ValidateOnStart();
|
||||||
|
|
||||||
|
builder.Services
|
||||||
|
.AddOptions<ScanHistoryOptions>()
|
||||||
|
.Bind(builder.Configuration.GetSection(ScanHistoryOptions.SectionName))
|
||||||
|
.ValidateDataAnnotations()
|
||||||
|
.ValidateOnStart();
|
||||||
|
|
||||||
builder.Services.AddControllersWithViews();
|
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<IVerificationDbConnectionFactory, VerificationDbConnectionFactory>();
|
||||||
builder.Services.AddScoped<ICartonVerificationRepository, CartonVerificationRepository>();
|
builder.Services.AddScoped<ICartonVerificationRepository, CartonVerificationRepository>();
|
||||||
builder.Services.AddScoped<IShipmentVerificationService, ShipmentVerificationService>();
|
builder.Services.AddScoped<IShipmentVerificationService, ShipmentVerificationService>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
using (var scope = app.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<ScanHistoryDbContext>();
|
||||||
|
db.Database.EnsureCreated();
|
||||||
|
}
|
||||||
|
|
||||||
if (!app.Environment.IsDevelopment())
|
if (!app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseExceptionHandler("/Home/Error");
|
app.UseExceptionHandler("/Home/Error");
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,23 @@ public sealed class CartonVerificationRepository(
|
||||||
return result is not null;
|
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)
|
private static string QuoteIdentifier(string identifier)
|
||||||
{
|
{
|
||||||
if (!SqlIdentifierRegex.IsMatch(identifier))
|
if (!SqlIdentifierRegex.IsMatch(identifier))
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,6 @@ namespace AVSCartonShipmentVerifier.Repositories;
|
||||||
public interface ICartonVerificationRepository
|
public interface ICartonVerificationRepository
|
||||||
{
|
{
|
||||||
Task<bool> UniqueNumberExistsAsync(string uniqueNumber, CancellationToken cancellationToken = default);
|
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
|
public interface IScanHistoryStore
|
||||||
{
|
{
|
||||||
void Add(ScannedRecordDto record);
|
Task AddAsync(ScannedRecordDto record, CancellationToken cancellationToken = default);
|
||||||
IReadOnlyCollection<ScannedRecordDto> GetLastFive();
|
Task<IReadOnlyCollection<ScannedRecordDto>> GetLastFiveAsync(CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ public sealed class InMemoryScanHistoryStore : IScanHistoryStore
|
||||||
private readonly ConcurrentQueue<ScannedRecordDto> _records = new();
|
private readonly ConcurrentQueue<ScannedRecordDto> _records = new();
|
||||||
private readonly Lock _syncLock = new();
|
private readonly Lock _syncLock = new();
|
||||||
|
|
||||||
public void Add(ScannedRecordDto record)
|
public Task AddAsync(ScannedRecordDto record, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
lock (_syncLock)
|
lock (_syncLock)
|
||||||
{
|
{
|
||||||
|
|
@ -19,13 +19,15 @@ public sealed class InMemoryScanHistoryStore : IScanHistoryStore
|
||||||
_records.TryDequeue(out _);
|
_records.TryDequeue(out _);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IReadOnlyCollection<ScannedRecordDto> GetLastFive()
|
public Task<IReadOnlyCollection<ScannedRecordDto>> GetLastFiveAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
lock (_syncLock)
|
lock (_syncLock)
|
||||||
{
|
{
|
||||||
return _records.Reverse().ToArray();
|
return Task.FromResult<IReadOnlyCollection<ScannedRecordDto>>(_records.Reverse().ToArray());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,22 +45,35 @@ public sealed class ShipmentVerificationService(
|
||||||
ProcessedAtUtc = nowUtc
|
ProcessedAtUtc = nowUtc
|
||||||
};
|
};
|
||||||
|
|
||||||
_historyStore.Add(ToScannedRecord(missingResult));
|
await _historyStore.AddAsync(ToScannedRecord(missingResult), cancellationToken);
|
||||||
return missingResult;
|
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
|
var result = new VerificationResultDto
|
||||||
{
|
{
|
||||||
ModelNumber = parseResult.ModelNumber,
|
ModelNumber = parseResult.ModelNumber,
|
||||||
UniqueNumber = parseResult.UniqueNumber,
|
UniqueNumber = parseResult.UniqueNumber,
|
||||||
RawQrValue = rawQrValue,
|
RawQrValue = rawQrValue,
|
||||||
ExistsInSystem = true,
|
ExistsInSystem = true,
|
||||||
ShipmentMarked = true,
|
ShipmentMarked = marked,
|
||||||
Message = "Record found in carton verification log.",
|
Message = marked
|
||||||
|
? "Record found and shipment marked successfully."
|
||||||
|
: "Record found, but shipment could not be marked.",
|
||||||
ProcessedAtUtc = nowUtc
|
ProcessedAtUtc = nowUtc
|
||||||
};
|
};
|
||||||
|
|
||||||
_historyStore.Add(ToScannedRecord(result));
|
await _historyStore.AddAsync(ToScannedRecord(result), cancellationToken);
|
||||||
return result;
|
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">
|
<header class="topbar">
|
||||||
<div class="topbar-brand">
|
<div class="topbar-brand">
|
||||||
<span class="brand-icon">◈</span>
|
<span class="brand-icon">◈</span>
|
||||||
<span>AVS CARTON SHIPMENT VERIFIER</span>
|
<span>SHIPPING MARK</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="topbar-siren" id="siren-indicator">
|
<div class="topbar-siren" id="siren-indicator">
|
||||||
<span>🔊</span>
|
<span>🔊</span>
|
||||||
|
|
@ -47,12 +47,6 @@
|
||||||
</section>
|
</section>
|
||||||
</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="right-column">
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<h2 class="section-title"><span class="section-icon">ⓘ</span> SCANNED DETAILS</h2>
|
<h2 class="section-title"><span class="section-icon">ⓘ</span> SCANNED DETAILS</h2>
|
||||||
|
|
@ -103,7 +97,7 @@
|
||||||
@(item.ShipmentMarked ? "TRUE" : "FALSE")
|
@(item.ShipmentMarked ? "TRUE" : "FALSE")
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</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>
|
<td>System</td>
|
||||||
</tr>
|
</tr>
|
||||||
rowNumber++;
|
rowNumber++;
|
||||||
|
|
@ -122,7 +116,7 @@
|
||||||
<footer class="page-footer">
|
<footer class="page-footer">
|
||||||
<span>AVS C# .NET Application</span>
|
<span>AVS C# .NET Application</span>
|
||||||
<span>|</span>
|
<span>|</span>
|
||||||
<span>AVS Carton Shipment Verifier</span>
|
<span>SHIPPING MARK</span>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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>
|
<script type="importmap"></script>
|
||||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
|
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
|
||||||
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
|
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
{
|
{
|
||||||
"ConnectionStrings": {
|
"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": {
|
"CartonVerification": {
|
||||||
"TableName": "carton_verification_log",
|
"TableName": "carton_verification_log",
|
||||||
"QrColumnName": "qr",
|
"QrColumnName": "qr",
|
||||||
"ShipmentVerificationFlagColumnName": "shipment_verification_flag"
|
"MovedToWarehouseColumnName": "moved_to_warehouse"
|
||||||
|
},
|
||||||
|
"ScanHistory": {
|
||||||
|
"DatabasePath": "C:\\Users\\Public\\AVSCartonShipmentVerifier\\scan-history.db",
|
||||||
|
"MaxRecordsToKeep": 500
|
||||||
},
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ body {
|
||||||
|
|
||||||
.top-grid {
|
.top-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1.15fr 0.75fr 1.1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,56 +164,18 @@ body {
|
||||||
color: #4f6687;
|
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 {
|
.status-success .status-mini-icon {
|
||||||
background: #3cae58;
|
background: #3cae58;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-failed .status-visual,
|
|
||||||
.status-failed .status-mini-icon {
|
.status-failed .status-mini-icon {
|
||||||
background: #d54d56;
|
background: #d54d56;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-success .status-headline,
|
|
||||||
.status-success .status-mini-headline {
|
.status-success .status-mini-headline {
|
||||||
color: #2f9950;
|
color: #2f9950;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-failed .status-headline,
|
|
||||||
.status-failed .status-mini-headline {
|
.status-failed .status-mini-headline {
|
||||||
color: #bf3340;
|
color: #bf3340;
|
||||||
}
|
}
|
||||||
|
|
@ -347,10 +309,6 @@ body {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-center-status {
|
|
||||||
min-height: 220px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-grid {
|
.info-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,8 @@
|
||||||
(() => {
|
(() => {
|
||||||
const qrInput = document.getElementById("qr-input");
|
const qrInput = document.getElementById("qr-input");
|
||||||
const statusCard = document.getElementById("status-card");
|
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 statusMiniIcon = document.getElementById("status-mini-icon");
|
||||||
const statusHeadline = document.getElementById("status-headline");
|
|
||||||
const statusMiniHeadline = document.getElementById("status-mini-headline");
|
const statusMiniHeadline = document.getElementById("status-mini-headline");
|
||||||
const statusText = document.getElementById("status-text");
|
|
||||||
const statusMiniMessage = document.getElementById("status-mini-message");
|
const statusMiniMessage = document.getElementById("status-mini-message");
|
||||||
const lastUpdatedTime = document.getElementById("last-updated-time");
|
const lastUpdatedTime = document.getElementById("last-updated-time");
|
||||||
const historyBody = document.querySelector("#history-table tbody");
|
const historyBody = document.querySelector("#history-table tbody");
|
||||||
|
|
@ -21,6 +17,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const antiForgeryToken = document.querySelector('input[name="__RequestVerificationToken"]')?.value;
|
const antiForgeryToken = document.querySelector('input[name="__RequestVerificationToken"]')?.value;
|
||||||
|
let isSubmitting = false;
|
||||||
|
|
||||||
qrInput.addEventListener("keydown", async event => {
|
qrInput.addEventListener("keydown", async event => {
|
||||||
if (event.key !== "Enter") {
|
if (event.key !== "Enter") {
|
||||||
|
|
@ -28,16 +25,36 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const qrValue = qrInput.value.trim();
|
await submitCurrentInput();
|
||||||
if (!qrValue) {
|
});
|
||||||
|
|
||||||
|
qrInput.addEventListener("paste", async event => {
|
||||||
|
const pastedValue = event.clipboardData?.getData("text")?.trim();
|
||||||
|
if (!pastedValue) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await submitScan(qrValue);
|
event.preventDefault();
|
||||||
qrInput.value = "";
|
qrInput.value = pastedValue;
|
||||||
qrInput.focus();
|
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) {
|
async function submitScan(qrValue) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/verification/scan", {
|
const response = await fetch("/api/verification/scan", {
|
||||||
|
|
@ -72,11 +89,8 @@
|
||||||
function renderStatus(result) {
|
function renderStatus(result) {
|
||||||
if (result.isSuccessful) {
|
if (result.isSuccessful) {
|
||||||
setStatusClasses("status-success");
|
setStatusClasses("status-success");
|
||||||
statusVisual.textContent = "\u2713";
|
|
||||||
statusMiniIcon.textContent = "\u2713";
|
statusMiniIcon.textContent = "\u2713";
|
||||||
statusHeadline.textContent = "VERIFIED";
|
|
||||||
statusMiniHeadline.textContent = "VERIFIED";
|
statusMiniHeadline.textContent = "VERIFIED";
|
||||||
statusText.textContent = result.message;
|
|
||||||
statusMiniMessage.textContent = result.message;
|
statusMiniMessage.textContent = result.message;
|
||||||
lastUpdatedTime.textContent = formatDisplayTime(result.processedAtUtc);
|
lastUpdatedTime.textContent = formatDisplayTime(result.processedAtUtc);
|
||||||
return;
|
return;
|
||||||
|
|
@ -88,18 +102,14 @@
|
||||||
|
|
||||||
function setFailedState(message) {
|
function setFailedState(message) {
|
||||||
setStatusClasses("status-failed");
|
setStatusClasses("status-failed");
|
||||||
statusVisual.textContent = "!";
|
|
||||||
statusMiniIcon.textContent = "!";
|
statusMiniIcon.textContent = "!";
|
||||||
statusHeadline.textContent = "FAILED";
|
|
||||||
statusMiniHeadline.textContent = "FAILED";
|
statusMiniHeadline.textContent = "FAILED";
|
||||||
statusText.textContent = message;
|
|
||||||
statusMiniMessage.textContent = message;
|
statusMiniMessage.textContent = message;
|
||||||
lastUpdatedTime.textContent = "-";
|
lastUpdatedTime.textContent = "-";
|
||||||
}
|
}
|
||||||
|
|
||||||
function setStatusClasses(statusClass) {
|
function setStatusClasses(statusClass) {
|
||||||
statusCard.className = `panel panel-status ${statusClass}`;
|
statusCard.className = `panel panel-status ${statusClass}`;
|
||||||
statusCenterCard.className = `panel panel-center-status ${statusClass}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDetails(result) {
|
function renderDetails(result) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue