using System.Diagnostics; using System.IO; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using UtopiaCanteenSystem.Services; using UtopiaCanteenSystem.Services.Logging; namespace UtopiaCanteen.Client.ViewModels; public partial class ClientSettingsViewModel : ObservableObject { public const string BackendUnavailableMessage = "Backend server unavailable. Please check Backend Server Base URL or server service."; private readonly IConfigService _configService; private readonly INavigationService _navigation; private readonly ICanteenBackendApiClient _backendApi; private readonly AppSession _session; [ObservableProperty] private string _scanIntervalDays = "0"; [ObservableProperty] private string _scanIntervalHours = "0"; [ObservableProperty] private string _scanIntervalMinutes = "0"; [ObservableProperty] private string _scanIntervalSeconds = "5"; [ObservableProperty] private string _adminCardId = "ADMIN"; [ObservableProperty] private string _applicationModeDisplay = "Client"; [ObservableProperty] private string _backendBaseUrl = string.Empty; [ObservableProperty] private string _deviceId = string.Empty; [ObservableProperty] private string _siteId = string.Empty; [ObservableProperty] private string _backendHealthDisplay = "Checking backend..."; [ObservableProperty] private bool _isBackendOnline; [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"; public bool HasBackendUrl => !string.IsNullOrWhiteSpace(BackendBaseUrl?.Trim()); public bool CanPostNow => !IsPosting && HasBackendUrl; public string PostButtonText => IsPosting ? "Posting..." : "Post Data"; public bool CanSyncCacheNow => !IsSyncingCache && HasBackendUrl; public string SyncCacheButtonText => IsSyncingCache ? "Syncing..." : "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 OnBackendBaseUrlChanged(string value) { OnPropertyChanged(nameof(HasBackendUrl)); OnPropertyChanged(nameof(CanPostNow)); OnPropertyChanged(nameof(CanSyncCacheNow)); } public ClientSettingsViewModel( IConfigService configService, INavigationService navigation, ICanteenBackendApiClient backendApi, AppSession session) { _configService = configService; _navigation = navigation; _backendApi = backendApi; _session = session; LoadFromConfig(); _ = InitializeAsync(); } public void LoadFromConfig() { ScanIntervalDays = _configService.GetScanIntervalDays().ToString(); ScanIntervalHours = _configService.GetScanIntervalHours().ToString(); ScanIntervalMinutes = _configService.GetScanIntervalMinutes().ToString(); ScanIntervalSeconds = _configService.GetScanIntervalSeconds().ToString(); AdminCardId = AdminLoginHelper.ResolveAdminCardIdForDisplay(_configService, _session); ApplicationModeDisplay = "Client"; BackendBaseUrl = _configService.GetBackendBaseUrl(); DeviceId = _configService.GetDeviceId(); SiteId = AdminLoginHelper.FormatSiteIdForDisplay(_configService.GetSiteId()); OnPropertyChanged(nameof(HasBackendUrl)); } public async Task InitializeAsync() { await RefreshBackendStatusAsync().ConfigureAwait(true); await RefreshCacheSyncTimestampsAsync().ConfigureAwait(true); } public async Task RefreshBackendStatusAsync() { if (!HasBackendUrl) { IsBackendOnline = false; BackendHealthDisplay = "Backend URL not configured."; return; } try { var health = await _backendApi.GetHealthAsync().ConfigureAwait(true); if (health != null && string.Equals(health.Status, "ok", StringComparison.OrdinalIgnoreCase)) { IsBackendOnline = true; BackendHealthDisplay = $"Backend online ({health.Mode}) — {health.Utc.ToLocalTime():MM/dd/yyyy hh:mm:ss tt}"; } else { IsBackendOnline = false; BackendHealthDisplay = BackendUnavailableMessage; } } catch (Exception ex) { Logger.Log(ex, "ClientSettingsViewModel.RefreshBackendStatusAsync"); IsBackendOnline = false; BackendHealthDisplay = BackendUnavailableMessage; } } public async Task RefreshCacheSyncTimestampsAsync() { if (!HasBackendUrl || !IsBackendOnline) return; var status = await _backendApi.GetCacheStatusAsync().ConfigureAwait(true); if (status == null) return; LastEmployeeRfidCacheSyncDisplay = FormatSyncTime(status.LastEmployeeRfidCacheSyncUtc); LastMealMenuCacheSyncDisplay = FormatSyncTime(status.LastMealMenuCacheSyncUtc); } [RelayCommand] private async Task Save() { IsSaving = true; SaveMessage = string.Empty; IsError = false; try { if (!int.TryParse(ScanIntervalDays, out var days) || days < 0 || days > 365) { SetSaveError("Scan interval Days must be 0–365."); return; } if (!int.TryParse(ScanIntervalHours, out var hours) || hours < 0 || hours > 23) { SetSaveError("Scan interval Hours must be 0–23."); return; } if (!int.TryParse(ScanIntervalMinutes, out var minutes) || minutes < 0 || minutes > 59) { SetSaveError("Scan interval Minutes must be 0–59."); return; } if (!int.TryParse(ScanIntervalSeconds, out var seconds) || seconds < 0 || seconds > 59) { SetSaveError("Scan interval Seconds must be 0–59."); return; } if (days == 0 && hours == 0 && minutes == 0 && seconds == 0) { SetSaveError("Scan interval cannot be zero. At least 1 second required."); return; } if (string.IsNullOrWhiteSpace(BackendBaseUrl?.Trim())) { SetSaveError("Backend Server Base URL is required."); return; } _configService.SetScanIntervalDays(days); _configService.SetScanIntervalHours(hours); _configService.SetScanIntervalMinutes(minutes); _configService.SetScanIntervalSeconds(seconds); _configService.SetAdminCardId(AdminCardId?.Trim() ?? string.Empty); _configService.SetCentralServerBaseUrl(BackendBaseUrl.Trim()); _configService.SetDeviceId(DeviceId?.Trim() ?? string.Empty); DeviceId = _configService.GetDeviceId(); FileLogger.Info( "ClientSettings", $"Settings saved. AdminCardId={AdminCardId?.Trim()}, SiteId={SiteId?.Trim()}, DeviceId={DeviceId}"); var site = SiteId?.Trim() ?? string.Empty; _configService.SetSiteId( string.IsNullOrEmpty(site) ? string.Empty : AdminLoginHelper.FormatSiteIdForDisplay(site)); SaveMessage = "Settings saved."; IsError = false; await RefreshBackendStatusAsync().ConfigureAwait(true); } finally { IsSaving = false; } } [RelayCommand] private void Back() => _navigation.NavigateBackFromSettings(_configService.GetScanInterval()); [RelayCommand] private void OpenMealSchedules() => _navigation.NavigateToMealSchedules(); [RelayCommand] private void OpenLogsFolder() { try { var dir = LogPaths.ClientLogsDirectory; Directory.CreateDirectory(dir); Process.Start(new ProcessStartInfo { FileName = dir, UseShellExecute = true }); FileLogger.Info("ClientSettings", $"Opened logs folder: {dir}"); } catch (Exception ex) { FileLogger.Error("ClientSettings", "Failed to open logs folder.", ex); SaveMessage = "Could not open logs folder: " + ex.Message; IsError = true; } } [RelayCommand] private async Task SyncEmployeeAndMenuCacheNow() { if (IsSyncingCache) return; SaveMessage = "Syncing..."; IsError = false; IsSyncingCache = true; try { FileLogger.Info("ClientSettings", "Sync Now clicked."); if (!await EnsureBackendAvailableAsync().ConfigureAwait(true)) return; var url = $"{_configService.GetBackendBaseUrl().TrimEnd('/')}/api/cache/sync-now"; FileLogger.Info("ClientSettings", $"Calling POST {url}"); var result = await _backendApi.SyncCacheNowAsync().ConfigureAwait(true); await RefreshCacheSyncTimestampsAsync().ConfigureAwait(true); SaveMessage = result.Message; IsError = !result.Success; FileLogger.Info( "ClientSettings", $"Sync Now response. Success={result.Success}, Message={result.Message}, Details={result.Details}"); if (!string.IsNullOrWhiteSpace(result.Details) && result.Success) SaveMessage += " " + result.Details; } catch (Exception ex) { Logger.Log(ex, "ClientSettingsViewModel.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 { FileLogger.Info("ClientSettings", "Post Data clicked."); if (!await EnsureBackendAvailableAsync().ConfigureAwait(true)) return; var url = $"{_configService.GetBackendBaseUrl().TrimEnd('/')}/api/orders/sync-now"; FileLogger.Info("ClientSettings", $"Calling POST {url}"); var result = await _backendApi.SyncOrdersNowAsync().ConfigureAwait(true); SaveMessage = result.Message; IsError = !result.Success; FileLogger.Info( "ClientSettings", $"Post Data response. Success={result.Success}, Message={result.Message}, Details={result.Details}"); if (!string.IsNullOrWhiteSpace(result.Details) && result.Success) SaveMessage += " " + result.Details; } catch (Exception ex) { Logger.Log(ex, "ClientSettingsViewModel.PostDataNow"); SaveMessage = "Failed to post data: " + ex.Message; IsError = true; } finally { IsPosting = false; } } private async Task EnsureBackendAvailableAsync() { if (!HasBackendUrl) { SaveMessage = "Backend URL is not configured."; IsError = true; return false; } await RefreshBackendStatusAsync().ConfigureAwait(true); if (IsBackendOnline) return true; SaveMessage = BackendUnavailableMessage; IsError = true; FileLogger.Warn("ClientSettings", $"Backend unavailable. Url={_configService.GetBackendBaseUrl()}"); return false; } private void SetSaveError(string message) { SaveMessage = message; IsError = true; } private static string FormatSyncTime(DateTime? utc) => utc == null ? "Never" : utc.Value.ToLocalTime().ToString("MM/dd/yyyy, hh:mm:ss tt"); }