using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using UtopiaCanteenSystem.Models;
using UtopiaCanteenSystem.Services;
namespace UtopiaCanteenSystem.ViewModels;
///
/// Settings UI (frontend): persists local kiosk config; cache/production jobs call backend APIs only.
///
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 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();
}