384 lines
12 KiB
C#
384 lines
12 KiB
C#
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);
|
||
FileLogger.Info(
|
||
"ClientSettings",
|
||
$"Settings saved. AdminCardId={AdminCardId?.Trim()}, SiteId={SiteId?.Trim()}, DeviceId={DeviceId?.Trim()}");
|
||
_configService.SetCentralServerBaseUrl(BackendBaseUrl.Trim());
|
||
_configService.SetDeviceId(DeviceId?.Trim() ?? string.Empty);
|
||
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<bool> 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");
|
||
}
|