275 lines
8.3 KiB
C#
275 lines
8.3 KiB
C#
using DebounceTimer = System.Timers.Timer;
|
|
using System.Windows;
|
|
using System.Windows.Threading;
|
|
using System.Windows.Input;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using UtopiaCanteenSystem.Services;
|
|
|
|
namespace UtopiaCanteenSystem.ViewModels;
|
|
|
|
/// <summary>
|
|
/// ViewModel for ScannerView: RFID input, scan command, scanner status, current scan time.
|
|
/// Supports both Enter-to-submit and debounce-to-submit for keyboard-wedge scanners.
|
|
/// </summary>
|
|
public partial class ScannerViewModel : ObservableObject
|
|
{
|
|
private const int DebounceMs = 200;
|
|
private const int CooldownTickSeconds = 1;
|
|
private static readonly TimeSpan ScannerInactivityTimeout = TimeSpan.FromSeconds(10);
|
|
|
|
private readonly IRfidService _rfidService;
|
|
private readonly INavigationService _navigation;
|
|
private readonly AppSession _session;
|
|
private readonly Dispatcher _uiDispatcher;
|
|
private readonly DebounceTimer _debounceTimer;
|
|
private readonly DispatcherTimer _cooldownTimer;
|
|
private DateTime? _cooldownEndsUtc;
|
|
private int _lastDisplayedCooldownSeconds = -1;
|
|
private readonly DispatcherTimer _scannerStatusTimer;
|
|
private DateTime? _lastScanActivityUtc;
|
|
private readonly object _submitLock = new();
|
|
private bool _isSubmitting;
|
|
|
|
[ObservableProperty]
|
|
[NotifyPropertyChangedFor(nameof(IsWatermarkVisible))]
|
|
private string _cardIdInput = string.Empty;
|
|
|
|
/// <summary>True when CardIdInput is empty; used to show/hide the "Scan Card ID" watermark.</summary>
|
|
public bool IsWatermarkVisible => string.IsNullOrWhiteSpace(CardIdInput);
|
|
|
|
[ObservableProperty]
|
|
[NotifyPropertyChangedFor(nameof(ScannerStatusDisplay))]
|
|
private string _scannerStatus = "Disconnected";
|
|
|
|
public string ScannerStatusDisplay =>
|
|
string.Equals(ScannerStatus, "Connected", StringComparison.Ordinal) ? "Connected" : "waiting for scan...";
|
|
|
|
[ObservableProperty]
|
|
private string _currentScanTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
|
|
|
[ObservableProperty]
|
|
private string _message = string.Empty;
|
|
|
|
[ObservableProperty]
|
|
private bool _isSuccess;
|
|
|
|
public ScannerViewModel(IRfidService rfidService, INavigationService navigation, AppSession session)
|
|
{
|
|
_rfidService = rfidService;
|
|
_navigation = navigation;
|
|
_session = session;
|
|
|
|
_uiDispatcher = Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher;
|
|
_debounceTimer = new DebounceTimer(DebounceMs)
|
|
{
|
|
AutoReset = false
|
|
};
|
|
_debounceTimer.Elapsed += (_, _) =>
|
|
{
|
|
// Timer runs on a background thread; marshal back to UI thread.
|
|
_uiDispatcher.BeginInvoke(() =>
|
|
{
|
|
lock (_submitLock)
|
|
{
|
|
if (_isSubmitting)
|
|
return;
|
|
}
|
|
|
|
// If something already cleared the input (e.g., Enter submit), do nothing.
|
|
if (string.IsNullOrWhiteSpace(CardIdInput))
|
|
return;
|
|
|
|
if (ScanCommand.CanExecute(null))
|
|
ScanCommand.Execute(null);
|
|
}, DispatcherPriority.Background);
|
|
};
|
|
|
|
_cooldownTimer = new DispatcherTimer
|
|
{
|
|
Interval = TimeSpan.FromSeconds(CooldownTickSeconds)
|
|
};
|
|
_cooldownTimer.Tick += (_, _) => UpdateCooldownMessage();
|
|
|
|
_scannerStatusTimer = new DispatcherTimer
|
|
{
|
|
Interval = TimeSpan.FromSeconds(1)
|
|
};
|
|
_scannerStatusTimer.Tick += (_, _) => UpdateScannerStatusFromActivity();
|
|
_scannerStatusTimer.Start();
|
|
|
|
RefreshScannerStatus();
|
|
RefreshTime();
|
|
}
|
|
|
|
partial void OnCardIdInputChanged(string value)
|
|
{
|
|
// Clear any previous message once a new scan starts.
|
|
if (!string.IsNullOrWhiteSpace(value) && !string.IsNullOrEmpty(Message))
|
|
Message = string.Empty;
|
|
|
|
// If the user starts scanning again, don't keep overwriting their feedback with an old countdown.
|
|
StopCooldownCountdown();
|
|
|
|
// Keyboard-wedge scanners "type" into the TextBox. Any incoming characters means activity/connected.
|
|
if (!string.IsNullOrWhiteSpace(value))
|
|
{
|
|
_lastScanActivityUtc = DateTime.UtcNow;
|
|
ScannerStatus = "Connected";
|
|
}
|
|
|
|
RestartDebounceTimer();
|
|
}
|
|
|
|
private void RestartDebounceTimer()
|
|
{
|
|
_debounceTimer.Stop();
|
|
|
|
// Only auto-submit when we have a non-empty value.
|
|
if (string.IsNullOrWhiteSpace(CardIdInput))
|
|
return;
|
|
|
|
_debounceTimer.Start();
|
|
}
|
|
|
|
public void RefreshScannerStatus()
|
|
{
|
|
// Default: waiting for activity.
|
|
ScannerStatus = "Disconnected";
|
|
}
|
|
|
|
public void RefreshTime()
|
|
{
|
|
CurrentScanTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void Logout()
|
|
{
|
|
_session.Logout();
|
|
_navigation.NavigateToAdminLogin();
|
|
}
|
|
|
|
/// <summary>Processes the current CardIdInput as a scan and navigates to dashboard on success.</summary>
|
|
[RelayCommand]
|
|
private void Scan()
|
|
{
|
|
lock (_submitLock)
|
|
{
|
|
if (_isSubmitting)
|
|
return;
|
|
_isSubmitting = true;
|
|
}
|
|
|
|
try
|
|
{
|
|
// Ensure any pending debounce doesn't fire after an explicit submit.
|
|
_debounceTimer.Stop();
|
|
StopCooldownCountdown();
|
|
|
|
Message = string.Empty;
|
|
IsSuccess = false;
|
|
|
|
// Keep CardId as string (preserves leading zeros).
|
|
var cardId = CardIdInput?.Trim() ?? string.Empty;
|
|
|
|
// Avoid showing "empty" errors if something triggered submit after we already cleared input.
|
|
if (string.IsNullOrWhiteSpace(cardId))
|
|
return;
|
|
|
|
var result = _rfidService.ProcessScanDetailed(cardId);
|
|
IsSuccess = result.Success;
|
|
Message = result.Message;
|
|
|
|
// Always clear input after attempt so the next scan starts cleanly.
|
|
CardIdInput = string.Empty;
|
|
|
|
if (!result.Success && result.CooldownSecondsRemaining > 0)
|
|
{
|
|
StartCooldownCountdown(result.CooldownSecondsRemaining);
|
|
return;
|
|
}
|
|
|
|
if (result.Success)
|
|
{
|
|
_navigation.StartDashboardSession();
|
|
_navigation.NavigateToDashboard();
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
lock (_submitLock)
|
|
{
|
|
_isSubmitting = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void StartCooldownCountdown(int seconds)
|
|
{
|
|
if (seconds <= 0)
|
|
return;
|
|
|
|
_cooldownEndsUtc = DateTime.UtcNow.AddSeconds(seconds);
|
|
_lastDisplayedCooldownSeconds = -1;
|
|
UpdateCooldownMessage(); // immediate update
|
|
_cooldownTimer.Start();
|
|
}
|
|
|
|
private void StopCooldownCountdown()
|
|
{
|
|
_cooldownTimer.Stop();
|
|
_cooldownEndsUtc = null;
|
|
_lastDisplayedCooldownSeconds = -1;
|
|
}
|
|
|
|
private void UpdateCooldownMessage()
|
|
{
|
|
if (_cooldownEndsUtc is null)
|
|
return;
|
|
|
|
var remaining = (int)Math.Ceiling((_cooldownEndsUtc.Value - DateTime.UtcNow).TotalSeconds);
|
|
if (remaining <= 0)
|
|
{
|
|
StopCooldownCountdown();
|
|
Message = "You can scan now.";
|
|
IsSuccess = true;
|
|
return;
|
|
}
|
|
|
|
// Avoid redundant PropertyChanged churn.
|
|
if (remaining == _lastDisplayedCooldownSeconds)
|
|
return;
|
|
_lastDisplayedCooldownSeconds = remaining;
|
|
|
|
var unit = remaining == 1 ? "second" : "seconds";
|
|
Message = $"Please wait {remaining} {unit}…";
|
|
IsSuccess = false;
|
|
}
|
|
|
|
private void UpdateScannerStatusFromActivity()
|
|
{
|
|
if (_lastScanActivityUtc is null)
|
|
{
|
|
if (!string.Equals(ScannerStatus, "Disconnected", StringComparison.Ordinal))
|
|
ScannerStatus = "Disconnected";
|
|
return;
|
|
}
|
|
|
|
var inactiveFor = DateTime.UtcNow - _lastScanActivityUtc.Value;
|
|
if (inactiveFor > ScannerInactivityTimeout)
|
|
{
|
|
if (!string.Equals(ScannerStatus, "Disconnected", StringComparison.Ordinal))
|
|
ScannerStatus = "Disconnected";
|
|
}
|
|
}
|
|
|
|
~ScannerViewModel()
|
|
{
|
|
_scannerStatusTimer.Stop();
|
|
_cooldownTimer.Stop();
|
|
_debounceTimer.Stop();
|
|
}
|
|
}
|
|
|