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 _settingsVm; private readonly Func _mealSchedulesVm; public NavigationService( @@ -37,7 +37,7 @@ public class NavigationService : INavigationService Func scannerVm, Func dashboardVm, Func adminSettingsAuthVm, - Func settingsVm, + Func settingsVm, Func mealSchedulesVm) { _session = session; diff --git a/Services/NoOpEmployeePhotoService.cs b/Services/NoOpEmployeePhotoService.cs new file mode 100644 index 0000000..9172d71 --- /dev/null +++ b/Services/NoOpEmployeePhotoService.cs @@ -0,0 +1,10 @@ +namespace UtopiaCanteenSystem.Services; + +/// +/// Client scanners do not call HRMS for employee photos. +/// +public sealed class NoOpEmployeePhotoService : IEmployeePhotoService +{ + public Task GetPhotoBytesAsync(string parentDocumentId, CancellationToken cancellationToken = default) => + Task.FromResult(null); +} diff --git a/Services/NoOpMealScheduleService.cs b/Services/NoOpMealScheduleService.cs new file mode 100644 index 0000000..a570986 --- /dev/null +++ b/Services/NoOpMealScheduleService.cs @@ -0,0 +1,24 @@ +using UtopiaCanteenSystem.Models; + +namespace UtopiaCanteenSystem.Services; + +/// +/// Meal schedules are owned by the backend; clients do not query HRMS directly. +/// +public sealed class NoOpMealScheduleService : IMealScheduleService +{ + public IReadOnlyList GetActiveSchedulesForSite(string siteId) => + Array.Empty(); + + public Task> GetAllSchedulesAsync(CancellationToken cancellationToken = default) => + Task.FromResult>(Array.Empty()); + + public Task CreateAsync(MealSchedule schedule, CancellationToken cancellationToken = default) => + Task.FromResult(0L); + + public Task UpdateAsync(MealSchedule schedule, CancellationToken cancellationToken = default) => + Task.CompletedTask; + + public Task DeleteAsync(long id, CancellationToken cancellationToken = default) => + Task.CompletedTask; +}