344 lines
11 KiB
C#
344 lines
11 KiB
C#
using CommunityToolkit.Mvvm.ComponentModel;
|
||
using CommunityToolkit.Mvvm.Input;
|
||
using UtopiaCanteenSystem.Models;
|
||
using UtopiaCanteenSystem.Services;
|
||
|
||
namespace UtopiaCanteenSystem.ViewModels;
|
||
|
||
/// <summary>
|
||
/// Settings UI (frontend): persists local kiosk config; cache/production jobs call backend APIs only.
|
||
/// </summary>
|
||
public partial class SettingsViewModel : ObservableObject
|
||
{
|
||
private readonly IConfigService _configService;
|
||
private readonly INavigationService _navigation;
|
||
private readonly IAdminAuditService _adminAudit;
|
||
private readonly ICanteenBackendApiClient _backendApi;
|
||
|
||
[ObservableProperty]
|
||
private string _syncApiEndpoint = string.Empty;
|
||
|
||
[ObservableProperty]
|
||
private string _scanIntervalDays = "0";
|
||
|
||
[ObservableProperty]
|
||
private string _scanIntervalHours = "0";
|
||
|
||
[ObservableProperty]
|
||
private string _scanIntervalMinutes = "1";
|
||
|
||
[ObservableProperty]
|
||
private string _scanIntervalSeconds = "0";
|
||
|
||
[ObservableProperty]
|
||
private string _adminCardId = "ADMIN";
|
||
|
||
[ObservableProperty]
|
||
private string _saveMessage = string.Empty;
|
||
|
||
[ObservableProperty]
|
||
private bool _isError;
|
||
|
||
[ObservableProperty]
|
||
private bool _isSaving;
|
||
|
||
[ObservableProperty]
|
||
private bool _isPosting;
|
||
|
||
[ObservableProperty]
|
||
private bool _isSyncingCache;
|
||
|
||
[ObservableProperty]
|
||
private string _lastEmployeeRfidCacheSyncDisplay = "Never";
|
||
|
||
[ObservableProperty]
|
||
private string _lastMealMenuCacheSyncDisplay = "Never";
|
||
|
||
[ObservableProperty]
|
||
private string _selectedAppMode = "Server";
|
||
|
||
[ObservableProperty]
|
||
private string _backendBaseUrl = string.Empty;
|
||
|
||
[ObservableProperty]
|
||
private string _localServerListenUrls = "http://0.0.0.0:5000";
|
||
|
||
public IReadOnlyList<string> AppModeOptions { get; } = new[] { "Server", "Client" };
|
||
|
||
public bool IsCentralServerMode => string.Equals(SelectedAppMode, "Server", StringComparison.OrdinalIgnoreCase);
|
||
|
||
public string EffectiveBackendUrlDisplay =>
|
||
IsCentralServerMode
|
||
? _configService.GetBackendBaseUrl()
|
||
: (BackendBaseUrl?.Trim() ?? string.Empty);
|
||
|
||
public bool HasBackendUrl => !string.IsNullOrWhiteSpace(_configService.GetBackendBaseUrl());
|
||
|
||
public bool CanPostNow => !IsPosting && HasBackendUrl;
|
||
public string PostButtonText => IsPosting ? "Posting..." : "Post Pending Orders";
|
||
|
||
public bool CanSyncCacheNow => !IsSyncingCache && HasBackendUrl;
|
||
public string SyncCacheButtonText => IsSyncingCache ? "Syncing cache..." : "Sync Now";
|
||
|
||
partial void OnIsPostingChanged(bool value)
|
||
{
|
||
OnPropertyChanged(nameof(PostButtonText));
|
||
OnPropertyChanged(nameof(CanPostNow));
|
||
}
|
||
|
||
partial void OnIsSyncingCacheChanged(bool value)
|
||
{
|
||
OnPropertyChanged(nameof(SyncCacheButtonText));
|
||
OnPropertyChanged(nameof(CanSyncCacheNow));
|
||
}
|
||
|
||
partial void OnSelectedAppModeChanged(string value)
|
||
{
|
||
OnPropertyChanged(nameof(IsCentralServerMode));
|
||
OnPropertyChanged(nameof(EffectiveBackendUrlDisplay));
|
||
OnPropertyChanged(nameof(CanPostNow));
|
||
OnPropertyChanged(nameof(CanSyncCacheNow));
|
||
}
|
||
|
||
partial void OnBackendBaseUrlChanged(string value)
|
||
{
|
||
OnPropertyChanged(nameof(HasBackendUrl));
|
||
OnPropertyChanged(nameof(CanPostNow));
|
||
OnPropertyChanged(nameof(CanSyncCacheNow));
|
||
}
|
||
|
||
public SettingsViewModel(
|
||
IConfigService configService,
|
||
INavigationService navigation,
|
||
IAdminAuditService adminAudit,
|
||
ICanteenBackendApiClient backendApi)
|
||
{
|
||
_configService = configService;
|
||
_navigation = navigation;
|
||
_adminAudit = adminAudit;
|
||
_backendApi = backendApi;
|
||
LoadFromConfig();
|
||
_ = RefreshCacheSyncTimestampsAsync();
|
||
}
|
||
|
||
public void LoadFromConfig()
|
||
{
|
||
SyncApiEndpoint = _configService.GetSyncApiEndpoint();
|
||
ScanIntervalDays = _configService.GetScanIntervalDays().ToString();
|
||
ScanIntervalHours = _configService.GetScanIntervalHours().ToString();
|
||
ScanIntervalMinutes = _configService.GetScanIntervalMinutes().ToString();
|
||
ScanIntervalSeconds = _configService.GetScanIntervalSeconds().ToString();
|
||
|
||
AdminCardId = AdminLoginHelper.ResolveAdminCardIdForDisplay(_configService, adminAudit: _adminAudit);
|
||
|
||
SelectedAppMode = _configService.GetAppMode() == AppMode.Client ? "Client" : "Server";
|
||
BackendBaseUrl = string.IsNullOrWhiteSpace(_configService.GetCentralServerBaseUrl())
|
||
? _configService.GetBackendBaseUrl()
|
||
: _configService.GetCentralServerBaseUrl();
|
||
LocalServerListenUrls = _configService.GetLocalServerListenUrls();
|
||
OnPropertyChanged(nameof(EffectiveBackendUrlDisplay));
|
||
OnPropertyChanged(nameof(HasBackendUrl));
|
||
}
|
||
|
||
public async Task RefreshCacheSyncTimestampsAsync()
|
||
{
|
||
if (IsCentralServerMode)
|
||
{
|
||
LastEmployeeRfidCacheSyncDisplay = FormatSyncTime(_configService.GetLastEmployeeRfidCacheSyncUtc());
|
||
LastMealMenuCacheSyncDisplay = FormatSyncTime(_configService.GetLastMealMenuCacheSyncUtc());
|
||
return;
|
||
}
|
||
|
||
var status = await _backendApi.GetCacheStatusAsync().ConfigureAwait(true);
|
||
if (status != null)
|
||
{
|
||
LastEmployeeRfidCacheSyncDisplay = FormatSyncTime(status.LastEmployeeRfidCacheSyncUtc);
|
||
LastMealMenuCacheSyncDisplay = FormatSyncTime(status.LastMealMenuCacheSyncUtc);
|
||
}
|
||
}
|
||
|
||
public void RefreshCacheSyncTimestamps()
|
||
{
|
||
_ = RefreshCacheSyncTimestampsAsync();
|
||
}
|
||
|
||
[RelayCommand]
|
||
private void Save()
|
||
{
|
||
IsSaving = true;
|
||
if (!int.TryParse(ScanIntervalDays, out var days) || days < 0 || days > 365)
|
||
{
|
||
SaveMessage = "Scan interval Days must be 0–365.";
|
||
IsError = true;
|
||
IsSaving = false;
|
||
return;
|
||
}
|
||
if (!int.TryParse(ScanIntervalHours, out var hours) || hours < 0 || hours > 23)
|
||
{
|
||
SaveMessage = "Scan interval Hours must be 0–23.";
|
||
IsError = true;
|
||
IsSaving = false;
|
||
return;
|
||
}
|
||
if (!int.TryParse(ScanIntervalMinutes, out var minutes) || minutes < 0 || minutes > 59)
|
||
{
|
||
SaveMessage = "Scan interval Minutes must be 0–59.";
|
||
IsError = true;
|
||
IsSaving = false;
|
||
return;
|
||
}
|
||
if (!int.TryParse(ScanIntervalSeconds, out var seconds) || seconds < 0 || seconds > 59)
|
||
{
|
||
SaveMessage = "Scan interval Seconds must be 0–59.";
|
||
IsError = true;
|
||
IsSaving = false;
|
||
return;
|
||
}
|
||
if (days == 0 && hours == 0 && minutes == 0 && seconds == 0)
|
||
{
|
||
SaveMessage = "Scan interval cannot be zero. Use at least 1 second.";
|
||
IsError = true;
|
||
IsSaving = false;
|
||
return;
|
||
}
|
||
|
||
if (string.Equals(SelectedAppMode, "Client", StringComparison.OrdinalIgnoreCase) &&
|
||
string.IsNullOrWhiteSpace(BackendBaseUrl?.Trim()))
|
||
{
|
||
SaveMessage = "Client mode requires Backend base URL (e.g. http://192.168.1.10:5000).";
|
||
IsError = true;
|
||
IsSaving = false;
|
||
return;
|
||
}
|
||
|
||
_configService.SetAppMode(string.Equals(SelectedAppMode, "Client", StringComparison.OrdinalIgnoreCase)
|
||
? AppMode.Client
|
||
: AppMode.Server);
|
||
_configService.SetCentralServerBaseUrl(BackendBaseUrl?.Trim() ?? string.Empty);
|
||
_configService.SetLocalServerListenUrls(LocalServerListenUrls?.Trim() ?? string.Empty);
|
||
|
||
_configService.SetSyncApiEndpoint(SyncApiEndpoint?.Trim() ?? string.Empty);
|
||
_configService.SetScanIntervalDays(days);
|
||
_configService.SetScanIntervalHours(hours);
|
||
_configService.SetScanIntervalMinutes(minutes);
|
||
_configService.SetScanIntervalSeconds(seconds);
|
||
_configService.SetAdminCardId(AdminCardId?.Trim() ?? string.Empty);
|
||
SaveMessage = "Settings saved. Restart the app after changing App mode or listen URLs.";
|
||
IsError = false;
|
||
IsSaving = false;
|
||
OnPropertyChanged(nameof(EffectiveBackendUrlDisplay));
|
||
OnPropertyChanged(nameof(HasBackendUrl));
|
||
}
|
||
|
||
[RelayCommand]
|
||
private void Back()
|
||
{
|
||
_navigation.NavigateBackFromSettings(GetDashboardTimeout());
|
||
}
|
||
|
||
[RelayCommand]
|
||
private void OpenMealSchedules()
|
||
{
|
||
_navigation.NavigateToMealSchedules();
|
||
}
|
||
|
||
[RelayCommand]
|
||
private async Task SyncEmployeeAndMenuCacheNow()
|
||
{
|
||
if (IsSyncingCache)
|
||
return;
|
||
|
||
SaveMessage = "Syncing cache...";
|
||
IsError = false;
|
||
IsSyncingCache = true;
|
||
|
||
try
|
||
{
|
||
if (string.IsNullOrWhiteSpace(_configService.GetBackendBaseUrl()))
|
||
{
|
||
SaveMessage = "Backend URL is not configured.";
|
||
IsError = true;
|
||
return;
|
||
}
|
||
|
||
if (!await _backendApi.HealthCheckAsync().ConfigureAwait(true))
|
||
{
|
||
SaveMessage = "Cannot reach backend. Ensure the central server app is running and API is listening.";
|
||
IsError = true;
|
||
return;
|
||
}
|
||
|
||
var result = await _backendApi.SyncCacheNowAsync().ConfigureAwait(true);
|
||
await RefreshCacheSyncTimestampsAsync().ConfigureAwait(true);
|
||
|
||
SaveMessage = result.Message;
|
||
IsError = !result.Success;
|
||
if (!string.IsNullOrWhiteSpace(result.Details) && result.Success)
|
||
SaveMessage += " " + result.Details;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Logger.Log(ex, "SettingsViewModel.SyncEmployeeAndMenuCacheNow");
|
||
SaveMessage = "Cache sync failed: " + ex.Message;
|
||
IsError = true;
|
||
}
|
||
finally
|
||
{
|
||
IsSyncingCache = false;
|
||
}
|
||
}
|
||
|
||
[RelayCommand]
|
||
private async Task PostDataNow()
|
||
{
|
||
if (IsPosting)
|
||
return;
|
||
|
||
SaveMessage = string.Empty;
|
||
IsError = false;
|
||
IsPosting = true;
|
||
|
||
try
|
||
{
|
||
if (string.IsNullOrWhiteSpace(_configService.GetBackendBaseUrl()))
|
||
{
|
||
SaveMessage = "Backend URL is not configured.";
|
||
IsError = true;
|
||
return;
|
||
}
|
||
|
||
if (!await _backendApi.HealthCheckAsync().ConfigureAwait(true))
|
||
{
|
||
SaveMessage = "Cannot reach backend. Ensure the central server app is running.";
|
||
IsError = true;
|
||
return;
|
||
}
|
||
|
||
var result = await _backendApi.SyncOrdersNowAsync().ConfigureAwait(true);
|
||
SaveMessage = result.Message;
|
||
IsError = !result.Success;
|
||
if (!string.IsNullOrWhiteSpace(result.Details) && result.Success)
|
||
SaveMessage += " " + result.Details;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Logger.Log(ex, "SettingsViewModel.PostDataNow");
|
||
SaveMessage = "Failed to post pending orders: " + ex.Message;
|
||
IsError = true;
|
||
}
|
||
finally
|
||
{
|
||
IsPosting = false;
|
||
}
|
||
}
|
||
|
||
private static string FormatSyncTime(DateTime? utc)
|
||
{
|
||
if (utc == null)
|
||
return "Never";
|
||
return utc.Value.ToLocalTime().ToString("MM/dd/yyyy, hh:mm:ss tt");
|
||
}
|
||
|
||
private TimeSpan GetDashboardTimeout() => _configService.GetScanInterval();
|
||
}
|