using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using System.Windows.Threading; using UtopiaCanteenSystem.Services; namespace UtopiaCanteenSystem.ViewModels; /// /// ViewModel for MainDashboardView: shows last scanned card ID, date/time, scan count, and success/error message. /// Manages a configurable session timer to navigate back to login. /// public partial class MainDashboardViewModel : ObservableObject { private readonly INavigationService _navigation; private readonly IRfidService _rfidService; private readonly IConfigService _configService; private readonly AppSession _session; private CancellationTokenSource? _timerCancellation; private readonly DispatcherTimer _clockTimer; private readonly DispatcherTimer _todayCountTimer; private DateTime _lastCountDate = DateTime.Today; [ObservableProperty] private string _lastCardId = "—"; [ObservableProperty] private string _lastScanTime = "—"; [ObservableProperty] private int _scanCount; [ObservableProperty] private string _statusMessage = "Scan recorded successfully."; [ObservableProperty] private string _currentTime = string.Empty; [ObservableProperty] private bool _isSuccess = true; [ObservableProperty] private bool _canScan = true; [ObservableProperty] private bool _isAdmin; public MainDashboardViewModel( INavigationService navigation, IRfidService rfidService, IConfigService configService, AppSession session) { _navigation = navigation; _rfidService = rfidService; _configService = configService; _session = session; IsAdmin = _session.IsAdminAuthenticated; UpdateCurrentTime(); _clockTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; _clockTimer.Tick += (_, _) => UpdateCurrentTime(); _clockTimer.Start(); UpdateScanDetails(); // Refresh "today's scans" periodically so it auto-resets after midnight even without scans. _todayCountTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(30) }; _todayCountTimer.Tick += (_, _) => { if (DateTime.Today == _lastCountDate) return; _lastCountDate = DateTime.Today; _ = LoadTodayScanCountAsync(); }; _todayCountTimer.Start(); // Initial load of today's count (async). _ = LoadTodayScanCountAsync(); StartDashboardTimer(); } /// /// Updates scan details: last scan info, scan count for today, and last scan time. /// Call this after a new scan occurs. /// public void UpdateScanDetails() { var last = _rfidService.GetLastScan(); if (last != null) { LastCardId = last.CardId; LastScanTime = last.ScanTime.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"); // Default success message; may be replaced with "welcome back" if repeat detected. IsSuccess = true; StatusMessage = "Scan recorded successfully."; _ = UpdateRepeatMessageAsync(last.CardId); } else { LastCardId = "—"; LastScanTime = "—"; IsSuccess = true; StatusMessage = string.Empty; } } private async Task UpdateRepeatMessageAsync(string cardId) { try { var todayCount = await _rfidService.GetTodayScanCountForCardAsync(cardId).ConfigureAwait(false); var totalCount = await _rfidService.GetTotalScanCountForCardAsync(cardId).ConfigureAwait(false); // Only show the "came again" message when we have evidence of a prior scan. if (totalCount >= 2) { var msg = todayCount >= 2 ? $"Welcome back! You scanned again (#{todayCount} today)." : "Welcome back! You scanned again."; System.Windows.Application.Current.Dispatcher.Invoke(() => { IsSuccess = true; StatusMessage = msg; }); } } catch { // If DB query fails, keep the default status message. } } public async Task LoadTodayScanCountAsync() { try { // Counts only scans for the current LOCAL day (converted to UTC boundaries internally). var count = await _rfidService.GetTodayScanCountAsync().ConfigureAwait(false); // Ensure property changes happen on the UI thread. System.Windows.Application.Current.Dispatcher.Invoke(() => ScanCount = count); } catch { // If DB is unavailable for any reason, fail gracefully. System.Windows.Application.Current.Dispatcher.Invoke(() => ScanCount = 0); } } /// /// Starts the dashboard session timer. When the session expires, navigates back to login. /// private void StartDashboardTimer() { // Cancel any existing timer _timerCancellation?.Cancel(); _timerCancellation = new CancellationTokenSource(); // For the session, restrict scans to current employee CanScan = true; // Current employee can scan var timeout = GetDashboardTimeout(); if (_navigation.IsDashboardSessionExpired(timeout)) { _navigation.NavigateToScanner(); return; } // Use remaining time since the dashboard session started. var remaining = timeout - _navigation.GetDashboardSessionElapsed(); if (remaining <= TimeSpan.Zero) { _navigation.NavigateToScanner(); return; } // Start async timer _ = Task.Run(async () => { try { await Task.Delay(remaining, _timerCancellation.Token); // Timer completed: navigate back to login // Use Dispatcher to ensure UI thread System.Windows.Application.Current.Dispatcher.Invoke(() => { if (ReferenceEquals(_navigation.CurrentViewModel, this)) _navigation.NavigateToScanner(); }); } catch (OperationCanceledException) { // Timer was cancelled (new scan occurred, timer reset) } }); } /// /// Called when a new scan occurs. /// public void OnScanOccurred() { UpdateScanDetails(); _lastCountDate = DateTime.Today; _ = LoadTodayScanCountAsync(); } [RelayCommand] private void BackToLogin() { _timerCancellation?.Cancel(); _navigation.NavigateToScanner(); } [RelayCommand] private void GoToSettings() { // Require admin credentials at time of access (kiosk mode). _navigation.NavigateToAdminSettingsAuth(); } /// /// Cleanup: cancel timer when ViewModel is disposed or view is closed. /// ~MainDashboardViewModel() { _clockTimer.Stop(); _todayCountTimer.Stop(); _timerCancellation?.Cancel(); _timerCancellation?.Dispose(); } private TimeSpan GetDashboardTimeout() { var seconds = _configService.GetScanTimeoutSeconds(); if (seconds <= 0) seconds = 60; return TimeSpan.FromSeconds(seconds); } private void UpdateCurrentTime() { CurrentTime = DateTime.Now.ToString("MM/dd/yyyy, hh:mm:ss tt"); } }