Wire WPF app for server and client modes
Updates application startup to compose server-mode backend services or client-mode API services, and adds empty/no-op services for frontend-only client behavior.feature/centralized-offline-canteen
parent
bf1f8accd1
commit
8123d67121
121
App.xaml.cs
121
App.xaml.cs
|
|
@ -11,7 +11,7 @@ using UtopiaCanteenSystem.ViewModels;
|
|||
namespace UtopiaCanteenSystem;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
using UtopiaCanteenSystem.Models;
|
||||
|
||||
namespace UtopiaCanteenSystem.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Client scanner UI: menu data comes from scan API response, not local SQLite/HRMS.
|
||||
/// </summary>
|
||||
public sealed class EmptyMenuLookupService : IMenuLookupService
|
||||
{
|
||||
public Task<IReadOnlyList<HrmsMenuItem>> GetMenuItemsForSiteAsync(int siteIdNumeric, CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult<IReadOnlyList<HrmsMenuItem>>(Array.Empty<HrmsMenuItem>());
|
||||
|
||||
public Task<IReadOnlyList<HrmsMenuItem>> GetMenuItemsForSiteAndDateAsync(
|
||||
int siteIdNumeric,
|
||||
DateTime menuDateLocal,
|
||||
string gradeType,
|
||||
string mealName,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult<IReadOnlyList<HrmsMenuItem>>(Array.Empty<HrmsMenuItem>());
|
||||
}
|
||||
|
|
@ -28,7 +28,7 @@ public class NavigationService : INavigationService
|
|||
private readonly Func<ScannerDashboardViewModel> _scannerVm;
|
||||
private readonly Func<MainDashboardViewModel> _dashboardVm;
|
||||
private readonly Func<AdminSettingsAuthViewModel> _adminSettingsAuthVm;
|
||||
private readonly Func<SettingsViewModel> _settingsVm;
|
||||
private readonly Func<object> _settingsVm;
|
||||
private readonly Func<MealSchedulesViewModel> _mealSchedulesVm;
|
||||
|
||||
public NavigationService(
|
||||
|
|
@ -37,7 +37,7 @@ public class NavigationService : INavigationService
|
|||
Func<ScannerDashboardViewModel> scannerVm,
|
||||
Func<MainDashboardViewModel> dashboardVm,
|
||||
Func<AdminSettingsAuthViewModel> adminSettingsAuthVm,
|
||||
Func<SettingsViewModel> settingsVm,
|
||||
Func<object> settingsVm,
|
||||
Func<MealSchedulesViewModel> mealSchedulesVm)
|
||||
{
|
||||
_session = session;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
namespace UtopiaCanteenSystem.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Client scanners do not call HRMS for employee photos.
|
||||
/// </summary>
|
||||
public sealed class NoOpEmployeePhotoService : IEmployeePhotoService
|
||||
{
|
||||
public Task<byte[]?> GetPhotoBytesAsync(string parentDocumentId, CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult<byte[]?>(null);
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
using UtopiaCanteenSystem.Models;
|
||||
|
||||
namespace UtopiaCanteenSystem.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Meal schedules are owned by the backend; clients do not query HRMS directly.
|
||||
/// </summary>
|
||||
public sealed class NoOpMealScheduleService : IMealScheduleService
|
||||
{
|
||||
public IReadOnlyList<MealSchedule> GetActiveSchedulesForSite(string siteId) =>
|
||||
Array.Empty<MealSchedule>();
|
||||
|
||||
public Task<IReadOnlyList<MealSchedule>> GetAllSchedulesAsync(CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult<IReadOnlyList<MealSchedule>>(Array.Empty<MealSchedule>());
|
||||
|
||||
public Task<long> 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;
|
||||
}
|
||||
Loading…
Reference in New Issue