using System.Net.Http; using System.Threading; using System.Windows; using Microsoft.EntityFrameworkCore; using UtopiaCanteenSystem.Api; using UtopiaCanteenSystem.Data; using UtopiaCanteenSystem.Models; using UtopiaCanteenSystem.Services; using UtopiaCanteenSystem.ViewModels; namespace UtopiaCanteenSystem; /// /// WPF shell: Server = backend + SQLite + API host; Client = scanner frontend calling backend HTTP API only. /// public partial class App : Application { private static Mutex _mutex = null!; private System.Timers.Timer? _syncTimer; private System.Timers.Timer? _offlineCacheSyncTimer; private int _isSyncRunning; private int _isOfflineCacheSyncRunning; private CancellationTokenSource? _apiHostCts; private Task? _apiHostTask; protected override void OnStartup(StartupEventArgs e) { bool isNewInstance; _mutex = new Mutex(true, "UtopiaCanteenSystemMutex", out isNewInstance); if (!isNewInstance) { MessageBox.Show("Another instance of the application is already running.", "Warning", MessageBoxButton.OK, MessageBoxImage.Warning); Current.Shutdown(); return; } 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(); 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); backendServices = new CanteenBackendServices(serverRfid, offlineCacheSync, syncService, configService); scannerRfidService = serverRfid; } else { 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 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, 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 mainWindow = new MainWindow { DataContext = new MainViewModel(navigationService) }; mainWindow.WindowState = WindowState.Maximized; mainWindow.Show(); if (isServer && backendServices != null) { _apiHostCts = new CancellationTokenSource(); var listenUrls = configService.GetLocalServerListenUrls(); var backend = backendServices; var cts = _apiHostCts; _apiHostTask = Task.Run(async () => { try { await Task.Delay(500, cts!.Token).ConfigureAwait(false); await CanteenBackendHost.RunAsync(backend, listenUrls, cts.Token).ConfigureAwait(false); } catch (OperationCanceledException) { // Shutdown } catch (Exception ex) { Logger.Log(ex, "App.CanteenBackendHost"); } }, CancellationToken.None); _ = RunOfflineCacheSyncViaApiAsync(backendApiClient); if (!string.IsNullOrWhiteSpace(configService.GetHrmsLookupConnectionString())) { _offlineCacheSyncTimer = new System.Timers.Timer(TimeSpan.FromMinutes(15).TotalMilliseconds) { AutoReset = true }; _offlineCacheSyncTimer.Elapsed += async (_, _) => { if (Interlocked.Exchange(ref _isOfflineCacheSyncRunning, 1) == 1) return; try { await RunOfflineCacheSyncViaApiAsync(backendApiClient).ConfigureAwait(false); } catch (Exception ex) { Logger.Log(ex, "App.OfflineCacheSyncTimer"); } finally { Interlocked.Exchange(ref _isOfflineCacheSyncRunning, 0); } }; _offlineCacheSyncTimer.Start(); } if (configService.GetSyncServiceEnabled() && syncService != null) { _syncTimer = new System.Timers.Timer(TimeSpan.FromMinutes(1).TotalMilliseconds) { AutoReset = true }; _syncTimer.Elapsed += async (_, _) => { if (Interlocked.Exchange(ref _isSyncRunning, 1) == 1) return; try { if (await backendApiClient.HealthCheckAsync().ConfigureAwait(false)) await backendApiClient.SyncOrdersNowAsync().ConfigureAwait(false); } catch (Exception ex) { Logger.Log(ex, "App.ProductionSyncTimer"); } finally { Interlocked.Exchange(ref _isSyncRunning, 0); } }; _syncTimer.Start(); } } } protected override void OnExit(ExitEventArgs e) { try { _apiHostCts?.Cancel(); _apiHostTask?.Wait(TimeSpan.FromSeconds(5)); } catch { // Best-effort } _mutex.ReleaseMutex(); _syncTimer?.Stop(); _syncTimer?.Dispose(); _offlineCacheSyncTimer?.Stop(); _offlineCacheSyncTimer?.Dispose(); _apiHostCts?.Dispose(); base.OnExit(e); } private static async Task RunOfflineCacheSyncViaApiAsync(ICanteenBackendApiClient api) { try { 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.RunOfflineCacheSyncViaApiAsync"); } } }