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(); }