diff --git a/App.xaml.cs b/App.xaml.cs
index 07dca65..7278f53 100644
--- a/App.xaml.cs
+++ b/App.xaml.cs
@@ -11,7 +11,7 @@ using UtopiaCanteenSystem.ViewModels;
namespace UtopiaCanteenSystem;
///
-/// Application entry point. Initializes database, builds service graph, optional Kestrel API (server), HTTP client scans (client).
+/// WPF shell: Server = backend + SQLite + API host; Client = scanner frontend calling backend HTTP API only.
///
public partial class App : Application
{
@@ -37,73 +37,80 @@ public partial class App : Application
base.OnStartup(e);
+ var configService = new ConfigService();
+ var isServer = configService.GetAppMode() == AppMode.Server;
+
+ var httpClient = new HttpClient { Timeout = TimeSpan.FromMinutes(5) };
+ var backendApiClient = new CanteenBackendApiClient(httpClient, configService);
+
var dbFactory = new DbContextFactory();
+ CanteenBackendServices? backendServices = null;
+ RfidService? serverRfid = null;
+ IEmployeeLookupService employeeLookupService;
+ IMenuLookupService menuLookupService;
+ IRfidService scannerRfidService;
+ ISyncService? syncService = null;
+ ProductionMealScheduleService? mealScheduleService = null;
using (var db = dbFactory.CreateDbContext())
db.EnsureDatabaseCreated();
- var configService = new ConfigService();
- var isServer = configService.GetAppMode() != AppMode.Client;
-
- var employeeRfidTagSyncService = new EmployeeRfidTagSyncService(dbFactory, configService);
- var mealMenuCacheSyncService = new MealMenuCacheSyncService(dbFactory, configService);
- var offlineCacheSyncService = new OfflineCacheSyncService(employeeRfidTagSyncService, mealMenuCacheSyncService);
- var employeeLookupService = new EmployeeLookupService(dbFactory, configService);
-
- var httpClient = new HttpClient { Timeout = TimeSpan.FromMinutes(2) };
- var employeePhotoService = new EmployeePhotoService(configService, httpClient);
- var menuLookupService = new MenuLookupService(dbFactory);
- var mealScheduleService = new ProductionMealScheduleService(configService);
- var mealSessionResolver = new DbMealSessionResolver(dbFactory);
- var syncService = new SyncService(dbFactory, configService);
-
- RfidService? serverRfid = null;
- IRfidService rfidService;
if (isServer)
{
+
+ var employeeRfidTagSync = new EmployeeRfidTagSyncService(dbFactory, configService);
+ var mealMenuCacheSync = new MealMenuCacheSyncService(dbFactory, configService);
+ var offlineCacheSync = new OfflineCacheSyncService(employeeRfidTagSync, mealMenuCacheSync);
+ employeeLookupService = new EmployeeLookupService(dbFactory, configService);
+ menuLookupService = new MenuLookupService(dbFactory);
+ mealScheduleService = new ProductionMealScheduleService(configService);
+ var mealSessionResolver = new DbMealSessionResolver(dbFactory);
+ syncService = new SyncService(dbFactory, configService);
serverRfid = new RfidService(dbFactory, configService, employeeLookupService, mealSessionResolver, menuLookupService, syncService);
- rfidService = serverRfid;
+ backendServices = new CanteenBackendServices(serverRfid, offlineCacheSync, syncService, configService);
+ scannerRfidService = serverRfid;
}
else
{
- rfidService = new ClientRfidService(httpClient, configService);
+ employeeLookupService = new EmployeeLookupService(dbFactory, configService);
+ menuLookupService = new EmptyMenuLookupService();
+ scannerRfidService = backendApiClient;
}
+ var employeePhotoService = new EmployeePhotoService(configService, httpClient);
var adminAuditService = new AdminAuditService(dbFactory);
var session = new AppSession();
- var authenticationUrl = "https://portal.utopiaindustries.pk/uind/rest/auth/user/";
- var authService = new AuthService(authenticationUrl);
+ var authService = new AuthService("https://portal.utopiaindustries.pk/uind/rest/auth/user/");
NavigationService navigationService = null!;
navigationService = new NavigationService(
session,
- () => new AdminLoginViewModel(authService, session, navigationService, configService, adminAuditService, employeeLookupService),
- () => new ScannerDashboardViewModel(rfidService, navigationService, session, configService, menuLookupService, employeePhotoService),
- () => new MainDashboardViewModel(navigationService, rfidService, configService, session),
- () => new AdminSettingsAuthViewModel(authService, session, navigationService, configService, employeeLookupService),
- () => new SettingsViewModel(configService, navigationService, adminAuditService, syncService, offlineCacheSyncService),
- () => new MealSchedulesViewModel(mealScheduleService, navigationService, configService));
+ () => new AdminLoginViewModel(authService, session, navigationService, configService, adminAuditService, employeeLookupService, backendApiClient),
+ () => new ScannerDashboardViewModel(scannerRfidService, navigationService, session, configService, menuLookupService, employeePhotoService),
+ () => new MainDashboardViewModel(navigationService, scannerRfidService, configService, session),
+ () => new AdminSettingsAuthViewModel(authService, session, navigationService, configService, employeeLookupService, backendApiClient),
+ () => new SettingsViewModel(configService, navigationService, adminAuditService, backendApiClient),
+ () => new MealSchedulesViewModel(
+ mealScheduleService ?? new ProductionMealScheduleService(configService),
+ navigationService,
+ configService));
- var mainViewModel = new MainViewModel(navigationService);
-
- var mainWindow = new MainWindow
- {
- DataContext = mainViewModel
- };
+ var mainWindow = new MainWindow { DataContext = new MainViewModel(navigationService) };
mainWindow.WindowState = WindowState.Maximized;
mainWindow.Show();
- if (isServer && serverRfid != null)
+ if (isServer && backendServices != null)
{
_apiHostCts = new CancellationTokenSource();
var listenUrls = configService.GetLocalServerListenUrls();
- var rfid = serverRfid;
+ var backend = backendServices;
var cts = _apiHostCts;
_apiHostTask = Task.Run(async () =>
{
try
{
- await CanteenLocalApiHost.RunAsync(rfid, listenUrls, cts!.Token).ConfigureAwait(false);
+ await Task.Delay(500, cts!.Token).ConfigureAwait(false);
+ await CanteenBackendHost.RunAsync(backend, listenUrls, cts.Token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
@@ -111,28 +118,22 @@ public partial class App : Application
}
catch (Exception ex)
{
- Logger.Log(ex, "App.CanteenLocalApiHost");
+ Logger.Log(ex, "App.CanteenBackendHost");
}
}, CancellationToken.None);
- }
- if (isServer)
- {
- _ = RunOfflineCacheSyncAsync(offlineCacheSyncService);
+ _ = RunOfflineCacheSyncViaApiAsync(backendApiClient);
if (!string.IsNullOrWhiteSpace(configService.GetHrmsLookupConnectionString()))
{
- _offlineCacheSyncTimer = new System.Timers.Timer(TimeSpan.FromMinutes(15).TotalMilliseconds)
- {
- AutoReset = true
- };
+ _offlineCacheSyncTimer = new System.Timers.Timer(TimeSpan.FromMinutes(15).TotalMilliseconds) { AutoReset = true };
_offlineCacheSyncTimer.Elapsed += async (_, _) =>
{
if (Interlocked.Exchange(ref _isOfflineCacheSyncRunning, 1) == 1)
return;
try
{
- await RunOfflineCacheSyncAsync(offlineCacheSyncService).ConfigureAwait(false);
+ await RunOfflineCacheSyncViaApiAsync(backendApiClient).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -146,19 +147,17 @@ public partial class App : Application
_offlineCacheSyncTimer.Start();
}
- if (configService.GetSyncServiceEnabled())
+ if (configService.GetSyncServiceEnabled() && syncService != null)
{
- _syncTimer = new System.Timers.Timer(TimeSpan.FromMinutes(1).TotalMilliseconds)
- {
- AutoReset = true
- };
+ _syncTimer = new System.Timers.Timer(TimeSpan.FromMinutes(1).TotalMilliseconds) { AutoReset = true };
_syncTimer.Elapsed += async (_, _) =>
{
if (Interlocked.Exchange(ref _isSyncRunning, 1) == 1)
return;
try
{
- await syncService.SyncNowAsync().ConfigureAwait(false);
+ if (await backendApiClient.HealthCheckAsync().ConfigureAwait(false))
+ await backendApiClient.SyncOrdersNowAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -183,7 +182,7 @@ public partial class App : Application
}
catch
{
- // Best-effort shutdown
+ // Best-effort
}
_mutex.ReleaseMutex();
@@ -195,15 +194,23 @@ public partial class App : Application
base.OnExit(e);
}
- private static async Task RunOfflineCacheSyncAsync(IOfflineCacheSyncService offlineCacheSyncService)
+ private static async Task RunOfflineCacheSyncViaApiAsync(ICanteenBackendApiClient api)
{
try
{
- await offlineCacheSyncService.SyncEmployeeAndMenuCacheAsync().ConfigureAwait(false);
+ for (var i = 0; i < 30; i++)
+ {
+ if (await api.HealthCheckAsync().ConfigureAwait(false))
+ {
+ await api.SyncCacheNowAsync().ConfigureAwait(false);
+ return;
+ }
+ await Task.Delay(1000).ConfigureAwait(false);
+ }
}
catch (Exception ex)
{
- Logger.Log(ex, "App.RunOfflineCacheSyncAsync");
+ Logger.Log(ex, "App.RunOfflineCacheSyncViaApiAsync");
}
}
}
diff --git a/Services/EmptyMenuLookupService.cs b/Services/EmptyMenuLookupService.cs
new file mode 100644
index 0000000..a178802
--- /dev/null
+++ b/Services/EmptyMenuLookupService.cs
@@ -0,0 +1,20 @@
+using UtopiaCanteenSystem.Models;
+
+namespace UtopiaCanteenSystem.Services;
+
+///
+/// Client scanner UI: menu data comes from scan API response, not local SQLite/HRMS.
+///
+public sealed class EmptyMenuLookupService : IMenuLookupService
+{
+ public Task> GetMenuItemsForSiteAsync(int siteIdNumeric, CancellationToken cancellationToken = default) =>
+ Task.FromResult>(Array.Empty());
+
+ public Task> GetMenuItemsForSiteAndDateAsync(
+ int siteIdNumeric,
+ DateTime menuDateLocal,
+ string gradeType,
+ string mealName,
+ CancellationToken cancellationToken = default) =>
+ Task.FromResult>(Array.Empty());
+}
diff --git a/Services/NavigationService.cs b/Services/NavigationService.cs
index 3f126fc..bdf1db1 100644
--- a/Services/NavigationService.cs
+++ b/Services/NavigationService.cs
@@ -28,7 +28,7 @@ public class NavigationService : INavigationService
private readonly Func _scannerVm;
private readonly Func _dashboardVm;
private readonly Func _adminSettingsAuthVm;
- private readonly Func _settingsVm;
+ private readonly Func