257 lines
7.5 KiB
C#
257 lines
7.5 KiB
C#
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using System.Windows.Threading;
|
|
using UtopiaCanteenSystem.Services;
|
|
|
|
namespace UtopiaCanteenSystem.ViewModels;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates scan details: last scan info, scan count for today, and last scan time.
|
|
/// Call this after a new scan occurs.
|
|
/// </summary>
|
|
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);
|
|
|
|
// Show "scanned again" ONLY when the same card has 2+ scans today.
|
|
if (todayCount >= 2)
|
|
{
|
|
var msg = $"Welcome back! You scanned again (#{todayCount} today).";
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts the dashboard session timer. When the session expires, navigates back to login.
|
|
/// </summary>
|
|
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)
|
|
}
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when a new scan occurs.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cleanup: cancel timer when ViewModel is disposed or view is closed.
|
|
/// </summary>
|
|
~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");
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|