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; /// /// 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. /// 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; /// True when CardIdInput is empty; used to show/hide the "Scan Card ID" watermark. 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(); } /// Processes the current CardIdInput as a scan and navigates to dashboard on success. [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(); } }