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
SYED MUSTUFA AHMED NAQVI 2026-05-25 16:23:45 +05:00
parent 659c162c93
commit e59a3f8af6
2 changed files with 103 additions and 90 deletions

View File

@ -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

View File

@ -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">