Update settings screen for backend operations
Updates settings to work through the backend API for cache sync, order posting, backend health checks, backend URL display, and server/client mode configuration.feature/centralized-offline-canteen
parent
659c162c93
commit
e59a3f8af6
|
|
@ -6,15 +6,14 @@ using UtopiaCanteenSystem.Services;
|
||||||
namespace UtopiaCanteenSystem.ViewModels;
|
namespace UtopiaCanteenSystem.ViewModels;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ViewModel for SettingsView: scan settings, production post, and offline cache sync.
|
/// Settings UI (frontend): persists local kiosk config; cache/production jobs call backend APIs only.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class SettingsViewModel : ObservableObject
|
public partial class SettingsViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
private readonly IConfigService _configService;
|
private readonly IConfigService _configService;
|
||||||
private readonly INavigationService _navigation;
|
private readonly INavigationService _navigation;
|
||||||
private readonly IAdminAuditService _adminAudit;
|
private readonly IAdminAuditService _adminAudit;
|
||||||
private readonly ISyncService _syncService;
|
private readonly ICanteenBackendApiClient _backendApi;
|
||||||
private readonly IOfflineCacheSyncService _offlineCacheSyncService;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _syncApiEndpoint = string.Empty;
|
private string _syncApiEndpoint = string.Empty;
|
||||||
|
|
@ -59,20 +58,26 @@ public partial class SettingsViewModel : ObservableObject
|
||||||
private string _selectedAppMode = "Server";
|
private string _selectedAppMode = "Server";
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _centralServerBaseUrl = string.Empty;
|
private string _backendBaseUrl = string.Empty;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _localServerListenUrls = "http://0.0.0.0:5000";
|
private string _localServerListenUrls = "http://0.0.0.0:5000";
|
||||||
|
|
||||||
public IReadOnlyList<string> AppModeOptions { get; } = new[] { "Server", "Client" };
|
public IReadOnlyList<string> AppModeOptions { get; } = new[] { "Server", "Client" };
|
||||||
|
|
||||||
/// <summary>Central PC: runs SQLite, sync jobs, and Kestrel API.</summary>
|
|
||||||
public bool IsCentralServerMode => string.Equals(SelectedAppMode, "Server", StringComparison.OrdinalIgnoreCase);
|
public bool IsCentralServerMode => string.Equals(SelectedAppMode, "Server", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
public bool CanPostNow => !IsPosting && IsCentralServerMode;
|
public string EffectiveBackendUrlDisplay =>
|
||||||
public string PostButtonText => IsPosting ? "Posting..." : "Post Data";
|
IsCentralServerMode
|
||||||
|
? _configService.GetBackendBaseUrl()
|
||||||
|
: (BackendBaseUrl?.Trim() ?? string.Empty);
|
||||||
|
|
||||||
public bool CanSyncCacheNow => !IsSyncingCache && IsCentralServerMode;
|
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";
|
public string SyncCacheButtonText => IsSyncingCache ? "Syncing cache..." : "Sync Now";
|
||||||
|
|
||||||
partial void OnIsPostingChanged(bool value)
|
partial void OnIsPostingChanged(bool value)
|
||||||
|
|
@ -90,6 +95,14 @@ public partial class SettingsViewModel : ObservableObject
|
||||||
partial void OnSelectedAppModeChanged(string value)
|
partial void OnSelectedAppModeChanged(string value)
|
||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(IsCentralServerMode));
|
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(CanPostNow));
|
||||||
OnPropertyChanged(nameof(CanSyncCacheNow));
|
OnPropertyChanged(nameof(CanSyncCacheNow));
|
||||||
}
|
}
|
||||||
|
|
@ -98,16 +111,14 @@ public partial class SettingsViewModel : ObservableObject
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
INavigationService navigation,
|
INavigationService navigation,
|
||||||
IAdminAuditService adminAudit,
|
IAdminAuditService adminAudit,
|
||||||
ISyncService syncService,
|
ICanteenBackendApiClient backendApi)
|
||||||
IOfflineCacheSyncService offlineCacheSyncService)
|
|
||||||
{
|
{
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_navigation = navigation;
|
_navigation = navigation;
|
||||||
_adminAudit = adminAudit;
|
_adminAudit = adminAudit;
|
||||||
_syncService = syncService;
|
_backendApi = backendApi;
|
||||||
_offlineCacheSyncService = offlineCacheSyncService;
|
|
||||||
LoadFromConfig();
|
LoadFromConfig();
|
||||||
RefreshCacheSyncTimestamps();
|
_ = RefreshCacheSyncTimestampsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadFromConfig()
|
public void LoadFromConfig()
|
||||||
|
|
@ -118,21 +129,37 @@ public partial class SettingsViewModel : ObservableObject
|
||||||
ScanIntervalMinutes = _configService.GetScanIntervalMinutes().ToString();
|
ScanIntervalMinutes = _configService.GetScanIntervalMinutes().ToString();
|
||||||
ScanIntervalSeconds = _configService.GetScanIntervalSeconds().ToString();
|
ScanIntervalSeconds = _configService.GetScanIntervalSeconds().ToString();
|
||||||
|
|
||||||
var last = _adminAudit.GetLastLogin();
|
AdminCardId = AdminLoginHelper.ResolveAdminCardIdForDisplay(_configService, adminAudit: _adminAudit);
|
||||||
if (last != null && !string.IsNullOrWhiteSpace(last.EmployeeId))
|
|
||||||
AdminCardId = last.EmployeeId;
|
|
||||||
else
|
|
||||||
AdminCardId = "ADMIN";
|
|
||||||
|
|
||||||
SelectedAppMode = _configService.GetAppMode() == AppMode.Client ? "Client" : "Server";
|
SelectedAppMode = _configService.GetAppMode() == AppMode.Client ? "Client" : "Server";
|
||||||
CentralServerBaseUrl = _configService.GetCentralServerBaseUrl();
|
BackendBaseUrl = string.IsNullOrWhiteSpace(_configService.GetCentralServerBaseUrl())
|
||||||
|
? _configService.GetBackendBaseUrl()
|
||||||
|
: _configService.GetCentralServerBaseUrl();
|
||||||
LocalServerListenUrls = _configService.GetLocalServerListenUrls();
|
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()
|
public void RefreshCacheSyncTimestamps()
|
||||||
{
|
{
|
||||||
LastEmployeeRfidCacheSyncDisplay = FormatSyncTime(_configService.GetLastEmployeeRfidCacheSyncUtc());
|
_ = RefreshCacheSyncTimestampsAsync();
|
||||||
LastMealMenuCacheSyncDisplay = FormatSyncTime(_configService.GetLastMealMenuCacheSyncUtc());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
|
|
@ -176,9 +203,9 @@ public partial class SettingsViewModel : ObservableObject
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(SelectedAppMode, "Client", StringComparison.OrdinalIgnoreCase) &&
|
if (string.Equals(SelectedAppMode, "Client", StringComparison.OrdinalIgnoreCase) &&
|
||||||
string.IsNullOrWhiteSpace(CentralServerBaseUrl?.Trim()))
|
string.IsNullOrWhiteSpace(BackendBaseUrl?.Trim()))
|
||||||
{
|
{
|
||||||
SaveMessage = "Client mode requires Central server base URL (e.g. http://192.168.1.10:5000).";
|
SaveMessage = "Client mode requires Backend base URL (e.g. http://192.168.1.10:5000).";
|
||||||
IsError = true;
|
IsError = true;
|
||||||
IsSaving = false;
|
IsSaving = false;
|
||||||
return;
|
return;
|
||||||
|
|
@ -187,7 +214,7 @@ public partial class SettingsViewModel : ObservableObject
|
||||||
_configService.SetAppMode(string.Equals(SelectedAppMode, "Client", StringComparison.OrdinalIgnoreCase)
|
_configService.SetAppMode(string.Equals(SelectedAppMode, "Client", StringComparison.OrdinalIgnoreCase)
|
||||||
? AppMode.Client
|
? AppMode.Client
|
||||||
: AppMode.Server);
|
: AppMode.Server);
|
||||||
_configService.SetCentralServerBaseUrl(CentralServerBaseUrl?.Trim() ?? string.Empty);
|
_configService.SetCentralServerBaseUrl(BackendBaseUrl?.Trim() ?? string.Empty);
|
||||||
_configService.SetLocalServerListenUrls(LocalServerListenUrls?.Trim() ?? string.Empty);
|
_configService.SetLocalServerListenUrls(LocalServerListenUrls?.Trim() ?? string.Empty);
|
||||||
|
|
||||||
_configService.SetSyncApiEndpoint(SyncApiEndpoint?.Trim() ?? string.Empty);
|
_configService.SetSyncApiEndpoint(SyncApiEndpoint?.Trim() ?? string.Empty);
|
||||||
|
|
@ -196,9 +223,11 @@ public partial class SettingsViewModel : ObservableObject
|
||||||
_configService.SetScanIntervalMinutes(minutes);
|
_configService.SetScanIntervalMinutes(minutes);
|
||||||
_configService.SetScanIntervalSeconds(seconds);
|
_configService.SetScanIntervalSeconds(seconds);
|
||||||
_configService.SetAdminCardId(AdminCardId?.Trim() ?? string.Empty);
|
_configService.SetAdminCardId(AdminCardId?.Trim() ?? string.Empty);
|
||||||
SaveMessage = "Settings saved. Restart the app for App mode / server URL changes to take full effect.";
|
SaveMessage = "Settings saved. Restart the app after changing App mode or listen URLs.";
|
||||||
IsError = false;
|
IsError = false;
|
||||||
IsSaving = false;
|
IsSaving = false;
|
||||||
|
OnPropertyChanged(nameof(EffectiveBackendUrlDisplay));
|
||||||
|
OnPropertyChanged(nameof(HasBackendUrl));
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
|
|
@ -225,33 +254,27 @@ public partial class SettingsViewModel : ObservableObject
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!IsCentralServerMode)
|
if (string.IsNullOrWhiteSpace(_configService.GetBackendBaseUrl()))
|
||||||
{
|
{
|
||||||
SaveMessage = "Cache sync runs only on the central server PC.";
|
SaveMessage = "Backend URL is not configured.";
|
||||||
IsError = true;
|
IsError = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(_configService.GetHrmsLookupConnectionString()))
|
if (!await _backendApi.HealthCheckAsync().ConfigureAwait(true))
|
||||||
{
|
{
|
||||||
SaveMessage = "HRMS connection is not configured. Cannot sync offline cache.";
|
SaveMessage = "Cannot reach backend. Ensure the central server app is running and API is listening.";
|
||||||
IsError = true;
|
IsError = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await _offlineCacheSyncService.SyncEmployeeAndMenuCacheAsync().ConfigureAwait(true);
|
var result = await _backendApi.SyncCacheNowAsync().ConfigureAwait(true);
|
||||||
RefreshCacheSyncTimestamps();
|
await RefreshCacheSyncTimestampsAsync().ConfigureAwait(true);
|
||||||
|
|
||||||
if (result.Success)
|
SaveMessage = result.Message;
|
||||||
{
|
IsError = !result.Success;
|
||||||
SaveMessage = "Employee and menu cache synced successfully.";
|
if (!string.IsNullOrWhiteSpace(result.Details) && result.Success)
|
||||||
IsError = false;
|
SaveMessage += " " + result.Details;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
SaveMessage = result.ErrorMessage ?? "Cache sync failed.";
|
|
||||||
IsError = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
@ -277,28 +300,30 @@ public partial class SettingsViewModel : ObservableObject
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!IsCentralServerMode)
|
if (string.IsNullOrWhiteSpace(_configService.GetBackendBaseUrl()))
|
||||||
{
|
{
|
||||||
SaveMessage = "Posting to production runs only on the central server PC.";
|
SaveMessage = "Backend URL is not configured.";
|
||||||
IsError = true;
|
IsError = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(_configService.GetMySqlConnectionString()))
|
if (!await _backendApi.HealthCheckAsync().ConfigureAwait(true))
|
||||||
{
|
{
|
||||||
SaveMessage = "MySQL connection string not configured.";
|
SaveMessage = "Cannot reach backend. Ensure the central server app is running.";
|
||||||
IsError = true;
|
IsError = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _syncService.SyncNowAsync().ConfigureAwait(false);
|
var result = await _backendApi.SyncOrdersNowAsync().ConfigureAwait(true);
|
||||||
SaveMessage = "Posted data to production.";
|
SaveMessage = result.Message;
|
||||||
IsError = false;
|
IsError = !result.Success;
|
||||||
|
if (!string.IsNullOrWhiteSpace(result.Details) && result.Success)
|
||||||
|
SaveMessage += " " + result.Details;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.Log(ex, "SettingsViewModel.PostDataNow");
|
Logger.Log(ex, "SettingsViewModel.PostDataNow");
|
||||||
SaveMessage = "Failed to post data to production.";
|
SaveMessage = "Failed to post pending orders: " + ex.Message;
|
||||||
IsError = true;
|
IsError = true;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
|
||||||
|
|
@ -286,10 +286,14 @@
|
||||||
Margin="0,0,0,12"
|
Margin="0,0,0,12"
|
||||||
Padding="12,8"/>
|
Padding="12,8"/>
|
||||||
|
|
||||||
<TextBlock Text="Central server base URL (client PCs)" FontSize="16" Foreground="{StaticResource PrimaryText}" FontWeight="SemiBold" />
|
<TextBlock Text="Backend base URL" FontSize="16" Foreground="{StaticResource PrimaryText}" FontWeight="SemiBold" />
|
||||||
<TextBox Text="{Binding CentralServerBaseUrl, UpdateSourceTrigger=PropertyChanged}"
|
<TextBox Text="{Binding BackendBaseUrl, UpdateSourceTrigger=PropertyChanged}"
|
||||||
Style="{StaticResource ModernTextBoxStyle}" Margin="0,8,0,4" />
|
Style="{StaticResource ModernTextBoxStyle}" Margin="0,8,0,4" />
|
||||||
<TextBlock Text="Example: http://192.168.1.10:5000 — used when this PC is in Client mode."
|
<TextBlock Text="Client: central server IP (e.g. http://192.168.1.10:5000). Server: leave blank to use http://localhost:5000 from listen URL below."
|
||||||
|
FontSize="14"
|
||||||
|
Foreground="{StaticResource MutedText}"
|
||||||
|
Margin="0,0,0,4" />
|
||||||
|
<TextBlock Text="{Binding EffectiveBackendUrlDisplay, StringFormat=API calls use: {0}}"
|
||||||
FontSize="14"
|
FontSize="14"
|
||||||
Foreground="{StaticResource MutedText}"
|
Foreground="{StaticResource MutedText}"
|
||||||
Margin="0,0,0,12" />
|
Margin="0,0,0,12" />
|
||||||
|
|
@ -302,43 +306,9 @@
|
||||||
Foreground="{StaticResource MutedText}"
|
Foreground="{StaticResource MutedText}"
|
||||||
Margin="0,0,0,20" />
|
Margin="0,0,0,20" />
|
||||||
|
|
||||||
<StackPanel Visibility="{Binding IsCentralServerMode, Converter={StaticResource BoolToVis}}">
|
<TextBlock Text="Backend jobs (calls central API — same on server and client)" FontSize="16" Foreground="{StaticResource PrimaryText}" FontWeight="SemiBold" Margin="0,0,0,8" />
|
||||||
<!--<TextBlock Text="Sync API Endpoint (UIND)" FontSize="16" Foreground="{StaticResource PrimaryText}" FontWeight="SemiBold" />
|
|
||||||
<TextBox Text="{Binding SyncApiEndpoint, UpdateSourceTrigger=PropertyChanged}"
|
|
||||||
Style="{StaticResource ModernTextBoxStyle}" Margin="0,8,0,8" />
|
|
||||||
<TextBlock Text="Endpoint used to sync scan records."
|
|
||||||
FontSize="14"
|
|
||||||
Foreground="{StaticResource MutedText}"
|
|
||||||
Margin="0,0,0,20" />-->
|
|
||||||
<TextBlock Text="Sync Data To (UIND)"
|
|
||||||
FontSize="16"
|
|
||||||
Foreground="{StaticResource PrimaryText}"
|
|
||||||
FontWeight="SemiBold" />
|
|
||||||
|
|
||||||
<Grid Margin="0,8,0,8">
|
<TextBlock Text="Download employee RFID tags, meal schedules, and menu data to central SQLite (HRMS → backend)."
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="16" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<TextBlock Grid.Column="0"
|
|
||||||
Text="Post data to production"
|
|
||||||
FontSize="18"
|
|
||||||
Foreground="{StaticResource PrimaryText}"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Margin="4,0,0,0" />
|
|
||||||
|
|
||||||
<!-- Post button right next to textbox -->
|
|
||||||
<Button Grid.Column="2"
|
|
||||||
Content="{Binding PostButtonText}"
|
|
||||||
Command="{Binding PostDataNowCommand}"
|
|
||||||
Style="{StaticResource PrimaryButtonStyle}"
|
|
||||||
MinWidth="150"
|
|
||||||
IsEnabled="{Binding CanPostNow}" />
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<TextBlock Text="Download employee RFID tags, meal schedules, and menu data for offline scanning."
|
|
||||||
FontSize="14"
|
FontSize="14"
|
||||||
Foreground="{StaticResource MutedText}"
|
Foreground="{StaticResource MutedText}"
|
||||||
Margin="0,0,0,8" />
|
Margin="0,0,0,8" />
|
||||||
|
|
@ -358,9 +328,27 @@
|
||||||
<TextBlock Text="{Binding LastMealMenuCacheSyncDisplay, StringFormat=Last meal/menu cache sync: {0}}"
|
<TextBlock Text="{Binding LastMealMenuCacheSyncDisplay, StringFormat=Last meal/menu cache sync: {0}}"
|
||||||
FontSize="14"
|
FontSize="14"
|
||||||
Foreground="{StaticResource MutedText}"
|
Foreground="{StaticResource MutedText}"
|
||||||
Margin="0,0,0,20" />
|
Margin="0,0,0,12" />
|
||||||
|
|
||||||
</StackPanel>
|
<Grid Margin="0,0,0,8">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="16" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Grid.Column="0"
|
||||||
|
Text="Post pending orders to production (UIND)"
|
||||||
|
FontSize="18"
|
||||||
|
Foreground="{StaticResource PrimaryText}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="4,0,0,0" />
|
||||||
|
<Button Grid.Column="2"
|
||||||
|
Content="{Binding PostButtonText}"
|
||||||
|
Command="{Binding PostDataNowCommand}"
|
||||||
|
Style="{StaticResource PrimaryButtonStyle}"
|
||||||
|
MinWidth="200"
|
||||||
|
IsEnabled="{Binding CanPostNow}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<!-- Message area -->
|
<!-- Message area -->
|
||||||
<Border Margin="0,0,0,20" Padding="16" CornerRadius="8">
|
<Border Margin="0,0,0,20" Padding="16" CornerRadius="8">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue