diff --git a/UtopiaCanteen.Client/App.xaml b/UtopiaCanteen.Client/App.xaml
new file mode 100644
index 0000000..461d862
--- /dev/null
+++ b/UtopiaCanteen.Client/App.xaml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/UtopiaCanteen.Client/App.xaml.cs b/UtopiaCanteen.Client/App.xaml.cs
new file mode 100644
index 0000000..a6e0826
--- /dev/null
+++ b/UtopiaCanteen.Client/App.xaml.cs
@@ -0,0 +1,65 @@
+using System.Net.Http;
+using System.Windows;
+using Microsoft.EntityFrameworkCore;
+using UtopiaCanteenSystem;
+using UtopiaCanteenSystem.Data;
+using UtopiaCanteenSystem.Services;
+using UtopiaCanteen.Client.ViewModels;
+using UtopiaCanteenSystem.ViewModels;
+
+namespace UtopiaCanteen.Client;
+
+public partial class App : Application
+{
+ private static Mutex _mutex = null!;
+
+ protected override void OnStartup(StartupEventArgs e)
+ {
+ base.OnStartup(e);
+ bool isNewInstance;
+ _mutex = new Mutex(true, "UtopiaCanteenClientMutex", out isNewInstance);
+
+ if (!isNewInstance)
+ {
+ MessageBox.Show("Another instance of the scanner client is already running.", "Warning",
+ MessageBoxButton.OK, MessageBoxImage.Warning);
+ Current.Shutdown();
+ return;
+ }
+
+ var configService = new ClientConfigService();
+ var httpClient = new HttpClient { Timeout = TimeSpan.FromMinutes(5) };
+ var backendApiClient = new CanteenBackendApiClient(httpClient, configService);
+
+ var dbFactory = new DbContextFactory();
+ using (var db = dbFactory.CreateDbContext())
+ db.EnsureDatabaseCreated();
+
+ var employeeLookupService = new EmployeeLookupService(dbFactory, configService);
+ var menuLookupService = new EmptyMenuLookupService();
+ var employeePhotoService = new NoOpEmployeePhotoService();
+ 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(backendApiClient, navigationService, session, configService, menuLookupService, employeePhotoService),
+ () => new MainDashboardViewModel(navigationService, backendApiClient, configService, session),
+ () => new AdminSettingsAuthViewModel(authService, session, navigationService, configService, employeeLookupService, backendApiClient),
+ () => new ClientSettingsViewModel(configService, navigationService, backendApiClient, session),
+ () => new MealSchedulesViewModel(new BackendApiMealScheduleService(backendApiClient), navigationService, configService));
+
+ var mainWindow = new MainWindow { DataContext = new MainViewModel(navigationService) };
+ mainWindow.WindowState = WindowState.Maximized;
+ mainWindow.Show();
+ }
+
+ protected override void OnExit(ExitEventArgs e)
+ {
+ _mutex?.ReleaseMutex();
+ base.OnExit(e);
+ }
+}
diff --git a/UtopiaCanteen.Client/UtopiaCanteen.Client.csproj b/UtopiaCanteen.Client/UtopiaCanteen.Client.csproj
new file mode 100644
index 0000000..7b7b848
--- /dev/null
+++ b/UtopiaCanteen.Client/UtopiaCanteen.Client.csproj
@@ -0,0 +1,88 @@
+
+
+ WinExe
+ net8.0-windows
+ enable
+ enable
+ true
+ UtopiaCanteen.Client
+ UtopiaCanteen.Client
+ ..\assets\favicon.ico
+
+
+
+
+
+
+
+
+
+
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ assets\%(RecursiveDir)%(Filename)%(Extension)
+ PreserveNewest
+
+
+
diff --git a/UtopiaCanteen.Client/ViewModels/ClientSettingsViewModel.cs b/UtopiaCanteen.Client/ViewModels/ClientSettingsViewModel.cs
new file mode 100644
index 0000000..4a5b1b3
--- /dev/null
+++ b/UtopiaCanteen.Client/ViewModels/ClientSettingsViewModel.cs
@@ -0,0 +1,340 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using UtopiaCanteenSystem.Services;
+
+namespace UtopiaCanteen.Client.ViewModels;
+
+public partial class ClientSettingsViewModel : ObservableObject
+{
+ public const string BackendUnavailableMessage =
+ "Backend server unavailable. Please check Backend Server Base URL or server service.";
+
+ private readonly IConfigService _configService;
+ private readonly INavigationService _navigation;
+ private readonly ICanteenBackendApiClient _backendApi;
+ private readonly AppSession _session;
+
+ [ObservableProperty]
+ private string _scanIntervalDays = "0";
+
+ [ObservableProperty]
+ private string _scanIntervalHours = "0";
+
+ [ObservableProperty]
+ private string _scanIntervalMinutes = "0";
+
+ [ObservableProperty]
+ private string _scanIntervalSeconds = "5";
+
+ [ObservableProperty]
+ private string _adminCardId = "ADMIN";
+
+ [ObservableProperty]
+ private string _applicationModeDisplay = "Client";
+
+ [ObservableProperty]
+ private string _backendBaseUrl = string.Empty;
+
+ [ObservableProperty]
+ private string _deviceId = string.Empty;
+
+ [ObservableProperty]
+ private string _siteId = string.Empty;
+
+ [ObservableProperty]
+ private string _backendHealthDisplay = "Checking backend...";
+
+ [ObservableProperty]
+ private bool _isBackendOnline;
+
+ [ObservableProperty]
+ private string _saveMessage = string.Empty;
+
+ [ObservableProperty]
+ private bool _isError;
+
+ [ObservableProperty]
+ private bool _isSaving;
+
+ [ObservableProperty]
+ private bool _isPosting;
+
+ [ObservableProperty]
+ private bool _isSyncingCache;
+
+ [ObservableProperty]
+ private string _lastEmployeeRfidCacheSyncDisplay = "Never";
+
+ [ObservableProperty]
+ private string _lastMealMenuCacheSyncDisplay = "Never";
+
+ public bool HasBackendUrl => !string.IsNullOrWhiteSpace(BackendBaseUrl?.Trim());
+
+ public bool CanPostNow => !IsPosting && HasBackendUrl;
+ public string PostButtonText => IsPosting ? "Posting..." : "Post Data";
+
+ public bool CanSyncCacheNow => !IsSyncingCache && HasBackendUrl;
+ public string SyncCacheButtonText => IsSyncingCache ? "Syncing..." : "Sync Now";
+
+ partial void OnIsPostingChanged(bool value)
+ {
+ OnPropertyChanged(nameof(PostButtonText));
+ OnPropertyChanged(nameof(CanPostNow));
+ }
+
+ partial void OnIsSyncingCacheChanged(bool value)
+ {
+ OnPropertyChanged(nameof(SyncCacheButtonText));
+ OnPropertyChanged(nameof(CanSyncCacheNow));
+ }
+
+ partial void OnBackendBaseUrlChanged(string value)
+ {
+ OnPropertyChanged(nameof(HasBackendUrl));
+ OnPropertyChanged(nameof(CanPostNow));
+ OnPropertyChanged(nameof(CanSyncCacheNow));
+ }
+
+ public ClientSettingsViewModel(
+ IConfigService configService,
+ INavigationService navigation,
+ ICanteenBackendApiClient backendApi,
+ AppSession session)
+ {
+ _configService = configService;
+ _navigation = navigation;
+ _backendApi = backendApi;
+ _session = session;
+ LoadFromConfig();
+ _ = InitializeAsync();
+ }
+
+ public void LoadFromConfig()
+ {
+ ScanIntervalDays = _configService.GetScanIntervalDays().ToString();
+ ScanIntervalHours = _configService.GetScanIntervalHours().ToString();
+ ScanIntervalMinutes = _configService.GetScanIntervalMinutes().ToString();
+ ScanIntervalSeconds = _configService.GetScanIntervalSeconds().ToString();
+ AdminCardId = AdminLoginHelper.ResolveAdminCardIdForDisplay(_configService, _session);
+ ApplicationModeDisplay = "Client";
+ BackendBaseUrl = _configService.GetBackendBaseUrl();
+ DeviceId = _configService.GetDeviceId();
+ SiteId = AdminLoginHelper.FormatSiteIdForDisplay(_configService.GetSiteId());
+ OnPropertyChanged(nameof(HasBackendUrl));
+ }
+
+ public async Task InitializeAsync()
+ {
+ await RefreshBackendStatusAsync().ConfigureAwait(true);
+ await RefreshCacheSyncTimestampsAsync().ConfigureAwait(true);
+ }
+
+ public async Task RefreshBackendStatusAsync()
+ {
+ if (!HasBackendUrl)
+ {
+ IsBackendOnline = false;
+ BackendHealthDisplay = "Backend URL not configured.";
+ return;
+ }
+
+ try
+ {
+ var health = await _backendApi.GetHealthAsync().ConfigureAwait(true);
+ if (health != null && string.Equals(health.Status, "ok", StringComparison.OrdinalIgnoreCase))
+ {
+ IsBackendOnline = true;
+ BackendHealthDisplay = $"Backend online ({health.Mode}) — {health.Utc.ToLocalTime():MM/dd/yyyy hh:mm:ss tt}";
+ }
+ else
+ {
+ IsBackendOnline = false;
+ BackendHealthDisplay = BackendUnavailableMessage;
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.Log(ex, "ClientSettingsViewModel.RefreshBackendStatusAsync");
+ IsBackendOnline = false;
+ BackendHealthDisplay = BackendUnavailableMessage;
+ }
+ }
+
+ public async Task RefreshCacheSyncTimestampsAsync()
+ {
+ if (!HasBackendUrl || !IsBackendOnline)
+ return;
+
+ var status = await _backendApi.GetCacheStatusAsync().ConfigureAwait(true);
+ if (status == null)
+ return;
+
+ LastEmployeeRfidCacheSyncDisplay = FormatSyncTime(status.LastEmployeeRfidCacheSyncUtc);
+ LastMealMenuCacheSyncDisplay = FormatSyncTime(status.LastMealMenuCacheSyncUtc);
+ }
+
+ [RelayCommand]
+ private async Task Save()
+ {
+ IsSaving = true;
+ SaveMessage = string.Empty;
+ IsError = false;
+
+ try
+ {
+ if (!int.TryParse(ScanIntervalDays, out var days) || days < 0 || days > 365)
+ {
+ SetSaveError("Scan interval Days must be 0–365.");
+ return;
+ }
+ if (!int.TryParse(ScanIntervalHours, out var hours) || hours < 0 || hours > 23)
+ {
+ SetSaveError("Scan interval Hours must be 0–23.");
+ return;
+ }
+ if (!int.TryParse(ScanIntervalMinutes, out var minutes) || minutes < 0 || minutes > 59)
+ {
+ SetSaveError("Scan interval Minutes must be 0–59.");
+ return;
+ }
+ if (!int.TryParse(ScanIntervalSeconds, out var seconds) || seconds < 0 || seconds > 59)
+ {
+ SetSaveError("Scan interval Seconds must be 0–59.");
+ return;
+ }
+ if (days == 0 && hours == 0 && minutes == 0 && seconds == 0)
+ {
+ SetSaveError("Scan interval cannot be zero. At least 1 second required.");
+ return;
+ }
+
+ if (string.IsNullOrWhiteSpace(BackendBaseUrl?.Trim()))
+ {
+ SetSaveError("Backend Server Base URL is required.");
+ return;
+ }
+
+ _configService.SetScanIntervalDays(days);
+ _configService.SetScanIntervalHours(hours);
+ _configService.SetScanIntervalMinutes(minutes);
+ _configService.SetScanIntervalSeconds(seconds);
+ _configService.SetAdminCardId(AdminCardId?.Trim() ?? string.Empty);
+ _configService.SetCentralServerBaseUrl(BackendBaseUrl.Trim());
+ _configService.SetDeviceId(DeviceId?.Trim() ?? string.Empty);
+ var site = SiteId?.Trim() ?? string.Empty;
+ _configService.SetSiteId(
+ string.IsNullOrEmpty(site)
+ ? string.Empty
+ : AdminLoginHelper.FormatSiteIdForDisplay(site));
+
+ SaveMessage = "Settings saved.";
+ IsError = false;
+ await RefreshBackendStatusAsync().ConfigureAwait(true);
+ }
+ finally
+ {
+ IsSaving = false;
+ }
+ }
+
+ [RelayCommand]
+ private void Back() => _navigation.NavigateBackFromSettings(_configService.GetScanInterval());
+
+ [RelayCommand]
+ private void OpenMealSchedules() => _navigation.NavigateToMealSchedules();
+
+ [RelayCommand]
+ private async Task SyncEmployeeAndMenuCacheNow()
+ {
+ if (IsSyncingCache)
+ return;
+
+ SaveMessage = "Syncing...";
+ IsError = false;
+ IsSyncingCache = true;
+
+ try
+ {
+ if (!await EnsureBackendAvailableAsync().ConfigureAwait(true))
+ return;
+
+ var result = await _backendApi.SyncCacheNowAsync().ConfigureAwait(true);
+ await RefreshCacheSyncTimestampsAsync().ConfigureAwait(true);
+
+ SaveMessage = result.Message;
+ IsError = !result.Success;
+ if (!string.IsNullOrWhiteSpace(result.Details) && result.Success)
+ SaveMessage += " " + result.Details;
+ }
+ catch (Exception ex)
+ {
+ Logger.Log(ex, "ClientSettingsViewModel.SyncEmployeeAndMenuCacheNow");
+ SaveMessage = "Cache sync failed: " + ex.Message;
+ IsError = true;
+ }
+ finally
+ {
+ IsSyncingCache = false;
+ }
+ }
+
+ [RelayCommand]
+ private async Task PostDataNow()
+ {
+ if (IsPosting)
+ return;
+
+ SaveMessage = string.Empty;
+ IsError = false;
+ IsPosting = true;
+
+ try
+ {
+ if (!await EnsureBackendAvailableAsync().ConfigureAwait(true))
+ return;
+
+ var result = await _backendApi.SyncOrdersNowAsync().ConfigureAwait(true);
+ SaveMessage = result.Message;
+ IsError = !result.Success;
+ if (!string.IsNullOrWhiteSpace(result.Details) && result.Success)
+ SaveMessage += " " + result.Details;
+ }
+ catch (Exception ex)
+ {
+ Logger.Log(ex, "ClientSettingsViewModel.PostDataNow");
+ SaveMessage = "Failed to post data: " + ex.Message;
+ IsError = true;
+ }
+ finally
+ {
+ IsPosting = false;
+ }
+ }
+
+ private async Task EnsureBackendAvailableAsync()
+ {
+ if (!HasBackendUrl)
+ {
+ SaveMessage = "Backend URL is not configured.";
+ IsError = true;
+ return false;
+ }
+
+ await RefreshBackendStatusAsync().ConfigureAwait(true);
+ if (IsBackendOnline)
+ return true;
+
+ SaveMessage = BackendUnavailableMessage;
+ IsError = true;
+ return false;
+ }
+
+ private void SetSaveError(string message)
+ {
+ SaveMessage = message;
+ IsError = true;
+ }
+
+ private static string FormatSyncTime(DateTime? utc) =>
+ utc == null ? "Never" : utc.Value.ToLocalTime().ToString("MM/dd/yyyy, hh:mm:ss tt");
+}
diff --git a/UtopiaCanteen.Client/Views/ClientSettingsView.xaml b/UtopiaCanteen.Client/Views/ClientSettingsView.xaml
new file mode 100644
index 0000000..f61e07f
--- /dev/null
+++ b/UtopiaCanteen.Client/Views/ClientSettingsView.xaml
@@ -0,0 +1,363 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/UtopiaCanteen.Client/Views/ClientSettingsView.xaml.cs b/UtopiaCanteen.Client/Views/ClientSettingsView.xaml.cs
new file mode 100644
index 0000000..57af2c0
--- /dev/null
+++ b/UtopiaCanteen.Client/Views/ClientSettingsView.xaml.cs
@@ -0,0 +1,19 @@
+using System.Windows.Controls;
+using UtopiaCanteen.Client.ViewModels;
+
+namespace UtopiaCanteen.Client.Views;
+
+public partial class ClientSettingsView : UserControl
+{
+ public ClientSettingsView()
+ {
+ InitializeComponent();
+ }
+
+ public ClientSettingsView(ClientSettingsViewModel viewModel)
+ {
+ InitializeComponent();
+ DataContext = viewModel;
+ Loaded += (_, _) => _ = viewModel.InitializeAsync();
+ }
+}