Add standalone scanner client application
Adds a separate WPF scanner client app with its own startup, project file, client settings view model, and client settings UI.feature/centralized-offline-canteen
parent
eff36f0e9f
commit
e85d465bf1
|
|
@ -0,0 +1,31 @@
|
||||||
|
<Application x:Class="UtopiaCanteen.Client.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:UtopiaCanteenSystem.ViewModels"
|
||||||
|
xmlns:clientVm="clr-namespace:UtopiaCanteen.Client.ViewModels"
|
||||||
|
xmlns:views="clr-namespace:UtopiaCanteenSystem.Views"
|
||||||
|
xmlns:clientViews="clr-namespace:UtopiaCanteen.Client.Views">
|
||||||
|
<Application.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<BooleanToVisibilityConverter x:Key="BoolToVisibility" />
|
||||||
|
<DataTemplate DataType="{x:Type vm:AdminLoginViewModel}">
|
||||||
|
<views:AdminLoginView />
|
||||||
|
</DataTemplate>
|
||||||
|
<DataTemplate DataType="{x:Type vm:AdminSettingsAuthViewModel}">
|
||||||
|
<views:AdminSettingsAuthView />
|
||||||
|
</DataTemplate>
|
||||||
|
<DataTemplate DataType="{x:Type vm:ScannerDashboardViewModel}">
|
||||||
|
<views:ScannerDashboardView />
|
||||||
|
</DataTemplate>
|
||||||
|
<DataTemplate DataType="{x:Type vm:MainDashboardViewModel}">
|
||||||
|
<views:MainDashboardView />
|
||||||
|
</DataTemplate>
|
||||||
|
<DataTemplate DataType="{x:Type clientVm:ClientSettingsViewModel}">
|
||||||
|
<clientViews:ClientSettingsView />
|
||||||
|
</DataTemplate>
|
||||||
|
<DataTemplate DataType="{x:Type vm:MealSchedulesViewModel}">
|
||||||
|
<views:MealSchedulesView />
|
||||||
|
</DataTemplate>
|
||||||
|
</ResourceDictionary>
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<RootNamespace>UtopiaCanteen.Client</RootNamespace>
|
||||||
|
<AssemblyName>UtopiaCanteen.Client</AssemblyName>
|
||||||
|
<ApplicationIcon Condition="Exists('..\assets\favicon.ico')">..\assets\favicon.ico</ApplicationIcon>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\UtopiaCanteen.Shared\UtopiaCanteen.Shared.csproj" />
|
||||||
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.11" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.11" />
|
||||||
|
<PackageReference Include="MySqlConnector" Version="2.3.5" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Page Include="..\MainWindow.xaml" Link="MainWindow.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</Page>
|
||||||
|
<Page Include="..\Views\**\*.xaml" LinkBase="Views" Exclude="..\Views\SettingsView.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</Page>
|
||||||
|
<Compile Include="..\MainWindow.xaml.cs" Link="MainWindow.xaml.cs" />
|
||||||
|
<Compile Include="..\Views\**\*.xaml.cs" LinkBase="Views" Exclude="..\Views\SettingsView.xaml.cs" />
|
||||||
|
<Compile Include="..\ViewModels\**\*.cs" LinkBase="ViewModels" Exclude="..\ViewModels\SettingsViewModel.cs" />
|
||||||
|
<Compile Include="..\Converters\**\*.cs" LinkBase="Converters" />
|
||||||
|
<Compile Include="..\Helpers\**\*.cs" LinkBase="Helpers" />
|
||||||
|
<Compile Include="..\Models\ScanRecord.cs" Link="Models\ScanRecord.cs" />
|
||||||
|
<Compile Include="..\Models\HrmsEmployeeInfo.cs" Link="Models\HrmsEmployeeInfo.cs" />
|
||||||
|
<Compile Include="..\Models\MealSession.cs" Link="Models\MealSession.cs" />
|
||||||
|
<Compile Include="..\Models\ResolvedMealSession.cs" Link="Models\ResolvedMealSession.cs" />
|
||||||
|
<Compile Include="..\Models\OrderHistoryItem.cs" Link="Models\OrderHistoryItem.cs" />
|
||||||
|
<Compile Include="..\Models\AdminLoginRecord.cs" Link="Models\AdminLoginRecord.cs" />
|
||||||
|
<Compile Include="..\Models\HrmsMenuItem.cs" Link="Models\HrmsMenuItem.cs" />
|
||||||
|
<Compile Include="..\Models\MealSchedule.cs" Link="Models\MealSchedule.cs" />
|
||||||
|
<Compile Include="..\Models\Labour.cs" Link="Models\Labour.cs" />
|
||||||
|
<Compile Include="..\Models\EmployeeRfidTagCache.cs" Link="Models\EmployeeRfidTagCache.cs" />
|
||||||
|
<Compile Include="..\Models\MealScheduleCache.cs" Link="Models\MealScheduleCache.cs" />
|
||||||
|
<Compile Include="..\Models\LunchMenuWeekCache.cs" Link="Models\LunchMenuWeekCache.cs" />
|
||||||
|
<Compile Include="..\Models\LunchMenuItemCache.cs" Link="Models\LunchMenuItemCache.cs" />
|
||||||
|
<Compile Include="..\Models\MenuItemCache.cs" Link="Models\MenuItemCache.cs" />
|
||||||
|
<Compile Include="..\Models\AppMode.cs" Link="Models\AppMode.cs" />
|
||||||
|
<Compile Include="..\Data\AppDbContext.cs" Link="Data\AppDbContext.cs" />
|
||||||
|
<Compile Include="..\Data\DbContextFactory.cs" Link="Data\DbContextFactory.cs" />
|
||||||
|
<Compile Include="..\Data\DatabasePath.cs" Link="Data\DatabasePath.cs" />
|
||||||
|
<Compile Include="..\Services\CanteenBackendApiClient.cs" Link="Services\CanteenBackendApiClient.cs" />
|
||||||
|
<Compile Include="..\Services\ICanteenBackendApiClient.cs" Link="Services\ICanteenBackendApiClient.cs" />
|
||||||
|
<Compile Include="..\Services\ClientConfigService.cs" Link="Services\ClientConfigService.cs" />
|
||||||
|
<Compile Include="..\Services\IConfigService.cs" Link="Services\IConfigService.cs" />
|
||||||
|
<Compile Include="..\Services\EmptyMenuLookupService.cs" Link="Services\EmptyMenuLookupService.cs" />
|
||||||
|
<Compile Include="..\Services\IMenuLookupService.cs" Link="Services\IMenuLookupService.cs" />
|
||||||
|
<Compile Include="..\Services\IRfidService.cs" Link="Services\IRfidService.cs" />
|
||||||
|
<Compile Include="..\Services\ScanResult.cs" Link="Services\ScanResult.cs" />
|
||||||
|
<Compile Include="..\Services\Logger.cs" Link="Services\Logger.cs" />
|
||||||
|
<Compile Include="..\Services\NavigationService.cs" Link="Services\NavigationService.cs" />
|
||||||
|
<Compile Include="..\Services\INavigationService.cs" Link="Services\INavigationService.cs" />
|
||||||
|
<Compile Include="..\Services\AppSession.cs" Link="Services\AppSession.cs" />
|
||||||
|
<Compile Include="..\Services\AuthService.cs" Link="Services\AuthService.cs" />
|
||||||
|
<Compile Include="..\Services\IAuthService.cs" Link="Services\IAuthService.cs" />
|
||||||
|
<Compile Include="..\Services\AuthResult.cs" Link="Services\AuthResult.cs" />
|
||||||
|
<Compile Include="..\Services\AdminAuditService.cs" Link="Services\AdminAuditService.cs" />
|
||||||
|
<Compile Include="..\Services\IAdminAuditService.cs" Link="Services\IAdminAuditService.cs" />
|
||||||
|
<Compile Include="..\Services\IEmployeeLookupService.cs" Link="Services\IEmployeeLookupService.cs" />
|
||||||
|
<Compile Include="..\Services\EmployeeLookupService.cs" Link="Services\EmployeeLookupService.cs" />
|
||||||
|
<Compile Include="..\Services\NoOpEmployeePhotoService.cs" Link="Services\NoOpEmployeePhotoService.cs" />
|
||||||
|
<Compile Include="..\Services\IEmployeePhotoService.cs" Link="Services\IEmployeePhotoService.cs" />
|
||||||
|
<Compile Include="..\Services\IMealScheduleService.cs" Link="Services\IMealScheduleService.cs" />
|
||||||
|
<Compile Include="..\Services\BackendApiMealScheduleService.cs" Link="Services\BackendApiMealScheduleService.cs" />
|
||||||
|
<Compile Include="..\Services\NoOpMealScheduleService.cs" Link="Services\NoOpMealScheduleService.cs" />
|
||||||
|
<Compile Include="..\Services\SiteIdHelper.cs" Link="Services\SiteIdHelper.cs" />
|
||||||
|
<Compile Include="..\Services\AdminLoginHelper.cs" Link="Services\AdminLoginHelper.cs" />
|
||||||
|
<Compile Include="..\Api\ApiDtoMapper.cs" Link="Api\ApiDtoMapper.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="..\assets\**\*">
|
||||||
|
<Link>assets\%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
|
|
@ -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<bool> 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");
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,363 @@
|
||||||
|
<UserControl x:Class="UtopiaCanteen.Client.Views.ClientSettingsView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Stretch"
|
||||||
|
VerticalContentAlignment="Stretch">
|
||||||
|
<UserControl.Resources>
|
||||||
|
<BooleanToVisibilityConverter x:Key="BoolToVis"/>
|
||||||
|
<SolidColorBrush x:Key="AppBackground" Color="#F0F2F5"/>
|
||||||
|
<SolidColorBrush x:Key="CardBackground" Color="#FFFFFF"/>
|
||||||
|
<SolidColorBrush x:Key="PrimaryText" Color="#2D3748"/>
|
||||||
|
<SolidColorBrush x:Key="MutedText" Color="#718096"/>
|
||||||
|
<SolidColorBrush x:Key="PrimaryAccent" Color="#5BA3A0"/>
|
||||||
|
<SolidColorBrush x:Key="PrimaryHover" Color="#4a918e"/>
|
||||||
|
<SolidColorBrush x:Key="SuccessBrush" Color="#38A169"/>
|
||||||
|
<SolidColorBrush x:Key="ErrorBrush" Color="#E53E3E"/>
|
||||||
|
<SolidColorBrush x:Key="BorderBrush" Color="#E2E8F0"/>
|
||||||
|
<SolidColorBrush x:Key="OrangeAccent" Color="#ED8936"/>
|
||||||
|
<SolidColorBrush x:Key="CardHeaderBg" Color="#F7FAFC"/>
|
||||||
|
|
||||||
|
<Style x:Key="SectionCardStyle" TargetType="Border">
|
||||||
|
<Setter Property="Background" Value="{StaticResource CardBackground}"/>
|
||||||
|
<Setter Property="CornerRadius" Value="12"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Padding" Value="24"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,0,16"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ModernTextBoxStyle" TargetType="TextBox">
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||||
|
<Setter Property="MinHeight" Value="48"/>
|
||||||
|
<Setter Property="Background" Value="White"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#E2E8F0"/>
|
||||||
|
<Setter Property="Foreground" Value="#0A1628"/>
|
||||||
|
<Setter Property="FontSize" Value="16"/>
|
||||||
|
<Setter Property="Padding" Value="14,0"/>
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="PrimaryButtonStyle" TargetType="Button">
|
||||||
|
<Setter Property="Background" Value="{StaticResource PrimaryAccent}"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="FontSize" Value="15"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Padding" Value="20,10"/>
|
||||||
|
<Setter Property="MinHeight" Value="42"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="OutlineButtonStyle" TargetType="Button">
|
||||||
|
<Setter Property="Background" Value="White"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource PrimaryText}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="FontSize" Value="15"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Padding" Value="20,10"/>
|
||||||
|
<Setter Property="MinHeight" Value="42"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="SyncSubCardStyle" TargetType="Border">
|
||||||
|
<Setter Property="Background" Value="{StaticResource CardHeaderBg}"/>
|
||||||
|
<Setter Property="CornerRadius" Value="10"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Padding" Value="20"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||||
|
</Style>
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<Grid Background="{StaticResource AppBackground}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Row="0"
|
||||||
|
VerticalScrollBarVisibility="Auto"
|
||||||
|
HorizontalScrollBarVisibility="Disabled"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Padding="32,24,32,16">
|
||||||
|
<Grid HorizontalAlignment="Stretch"
|
||||||
|
Width="{Binding ViewportWidth, RelativeSource={RelativeSource AncestorType=ScrollViewer}}"
|
||||||
|
MinWidth="320">
|
||||||
|
<StackPanel HorizontalAlignment="Stretch">
|
||||||
|
<!-- Page header -->
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
|
||||||
|
<Border Width="48" Height="48" Background="#E6F4F3" CornerRadius="24" Margin="0,0,16,0">
|
||||||
|
<Viewbox Margin="10">
|
||||||
|
<Canvas Width="24" Height="24">
|
||||||
|
<Path Data="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11.03L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.26C16.04,5.86 15.48,5.51 14.87,5.23L14.5,2.58C14.46,2.34 14.25,2.17 14,2.17H10C9.75,2.17 9.54,2.34 9.5,2.58L9.13,5.23C8.52,5.51 7.96,5.86 7.44,6.26L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.22,8.95 2.27,9.22 2.46,9.37L4.57,11.03C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.22,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.74C7.96,18.14 8.52,18.49 9.13,18.77L9.5,21.42C9.54,21.66 9.75,21.83 10,21.83H14C14.25,21.83 14.46,21.66 14.5,21.42L14.87,18.77C15.48,18.49 16.04,18.14 16.56,17.74L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z"
|
||||||
|
Fill="{StaticResource PrimaryAccent}" Stretch="Uniform"/>
|
||||||
|
</Canvas>
|
||||||
|
</Viewbox>
|
||||||
|
</Border>
|
||||||
|
<StackPanel VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="Settings" FontSize="32" FontWeight="Bold" Foreground="{StaticResource PrimaryText}"/>
|
||||||
|
<TextBlock Text="Configure scanning, access, connectivity, and sync settings."
|
||||||
|
FontSize="15" Foreground="{StaticResource MutedText}" Margin="0,4,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 1. Scan Interval -->
|
||||||
|
<Border Style="{StaticResource SectionCardStyle}">
|
||||||
|
<StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,12">
|
||||||
|
<Border Width="36" Height="36" Background="#E6F4F3" CornerRadius="18" Margin="0,0,12,0">
|
||||||
|
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="16"
|
||||||
|
Foreground="{StaticResource PrimaryAccent}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<StackPanel VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="Scan Interval" FontSize="20" FontWeight="Bold" Foreground="{StaticResource PrimaryText}"/>
|
||||||
|
<TextBlock Text="Set the minimum time between scans to prevent duplicates."
|
||||||
|
FontSize="14" Foreground="{StaticResource MutedText}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
<Grid Margin="0,8,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/><ColumnDefinition Width="12"/>
|
||||||
|
<ColumnDefinition Width="*"/><ColumnDefinition Width="12"/>
|
||||||
|
<ColumnDefinition Width="*"/><ColumnDefinition Width="12"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel Grid.Column="0">
|
||||||
|
<TextBlock Text="Days (0–365)" FontSize="13" Foreground="{StaticResource MutedText}" Margin="0,0,0,4"/>
|
||||||
|
<TextBox Text="{Binding ScanIntervalDays, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource ModernTextBoxStyle}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="2">
|
||||||
|
<TextBlock Text="Hours (0–23)" FontSize="13" Foreground="{StaticResource MutedText}" Margin="0,0,0,4"/>
|
||||||
|
<TextBox Text="{Binding ScanIntervalHours, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource ModernTextBoxStyle}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="4">
|
||||||
|
<TextBlock Text="Minutes (0–59)" FontSize="13" Foreground="{StaticResource MutedText}" Margin="0,0,0,4"/>
|
||||||
|
<TextBox Text="{Binding ScanIntervalMinutes, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource ModernTextBoxStyle}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="6">
|
||||||
|
<TextBlock Text="Seconds (0–59)" FontSize="13" Foreground="{StaticResource MutedText}" Margin="0,0,0,4"/>
|
||||||
|
<TextBox Text="{Binding ScanIntervalSeconds, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource ModernTextBoxStyle}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,12,0,0">
|
||||||
|
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="14"
|
||||||
|
Foreground="{StaticResource MutedText}" Margin="0,2,8,0"/>
|
||||||
|
<TextBlock Text="The minimum time between scans to prevent duplicates. At least 1 second required."
|
||||||
|
FontSize="13" Foreground="{StaticResource MutedText}" TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 2. Admin Access -->
|
||||||
|
<Border Style="{StaticResource SectionCardStyle}">
|
||||||
|
<StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,12">
|
||||||
|
<Border Width="36" Height="36" Background="#E6F4F3" CornerRadius="18" Margin="0,0,12,0">
|
||||||
|
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="16"
|
||||||
|
Foreground="{StaticResource PrimaryAccent}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<TextBlock Text="Admin Access" FontSize="20" FontWeight="Bold" Foreground="{StaticResource PrimaryText}" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Text="Admin Employee ID" FontSize="14" FontWeight="SemiBold" Foreground="{StaticResource PrimaryText}"/>
|
||||||
|
<TextBox Text="{Binding AdminCardId, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Style="{StaticResource ModernTextBoxStyle}" Margin="0,8,0,4"/>
|
||||||
|
<TextBlock Text="Card ID with admin access to settings." FontSize="13" Foreground="{StaticResource MutedText}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 3. Application Mode & Connectivity -->
|
||||||
|
<Border Style="{StaticResource SectionCardStyle}">
|
||||||
|
<StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,12">
|
||||||
|
<Border Width="36" Height="36" Background="#E6F4F3" CornerRadius="18" Margin="0,0,12,0">
|
||||||
|
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="16"
|
||||||
|
Foreground="{StaticResource PrimaryAccent}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<StackPanel VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="Application Mode & Connectivity" FontSize="20" FontWeight="Bold" Foreground="{StaticResource PrimaryText}"/>
|
||||||
|
<TextBlock Text="Configure how this application connects to servers."
|
||||||
|
FontSize="14" Foreground="{StaticResource MutedText}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="24"/>
|
||||||
|
<ColumnDefinition Width="2*"/>
|
||||||
|
<ColumnDefinition Width="24"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel Grid.Row="0" Grid.Column="0">
|
||||||
|
<TextBlock Text="Application Mode" FontSize="14" FontWeight="SemiBold" Foreground="{StaticResource PrimaryText}"/>
|
||||||
|
<TextBox Text="{Binding ApplicationModeDisplay}" IsReadOnly="True" IsTabStop="False"
|
||||||
|
Style="{StaticResource ModernTextBoxStyle}" Margin="0,8,0,4" Background="#F8FAFC"/>
|
||||||
|
<TextBlock Text="Client: this PC sends scans to the central backend server."
|
||||||
|
FontSize="12" Foreground="{StaticResource MutedText}" TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Row="0" Grid.Column="2">
|
||||||
|
<TextBlock Text="Backend Server Base URL" FontSize="14" FontWeight="SemiBold" Foreground="{StaticResource PrimaryText}"/>
|
||||||
|
<TextBox Text="{Binding BackendBaseUrl, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Style="{StaticResource ModernTextBoxStyle}" Margin="0,8,0,4"/>
|
||||||
|
<TextBlock Text="Example: http://192.168.1.10:5000" FontSize="12" Foreground="{StaticResource MutedText}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Row="0" Grid.Column="4">
|
||||||
|
<TextBlock Text="Device ID" FontSize="14" FontWeight="SemiBold" Foreground="{StaticResource PrimaryText}"/>
|
||||||
|
<TextBox Text="{Binding DeviceId, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Style="{StaticResource ModernTextBoxStyle}" Margin="0,8,0,4"/>
|
||||||
|
<TextBlock Text="Identifies this scanner PC in scan records." FontSize="12" Foreground="{StaticResource MutedText}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Margin="0,16,0,0">
|
||||||
|
<TextBlock Text="Site ID" FontSize="14" FontWeight="SemiBold" Foreground="{StaticResource PrimaryText}"/>
|
||||||
|
<TextBox Text="{Binding SiteId, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Style="{StaticResource ModernTextBoxStyle}" Margin="0,8,0,4" MaxWidth="480" HorizontalAlignment="Left"/>
|
||||||
|
<TextBlock Text="Canteen location site code sent with each scan." FontSize="12" Foreground="{StaticResource MutedText}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
<Border Margin="0,16,0,0" Padding="12" CornerRadius="8" Background="#EDF2F7">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Ellipse Width="10" Height="10" Margin="0,4,10,0" VerticalAlignment="Top">
|
||||||
|
<Ellipse.Style>
|
||||||
|
<Style TargetType="Ellipse">
|
||||||
|
<Setter Property="Fill" Value="{StaticResource ErrorBrush}"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsBackendOnline}" Value="True">
|
||||||
|
<Setter Property="Fill" Value="{StaticResource SuccessBrush}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Ellipse.Style>
|
||||||
|
</Ellipse>
|
||||||
|
<TextBlock Text="{Binding BackendHealthDisplay}" FontSize="13" Foreground="{StaticResource PrimaryText}" TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 4. Data Sync -->
|
||||||
|
<Border Style="{StaticResource SectionCardStyle}" Margin="0,0,0,8">
|
||||||
|
<StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,16">
|
||||||
|
<Border Width="36" Height="36" Background="#E6F4F3" CornerRadius="18" Margin="0,0,12,0">
|
||||||
|
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="16"
|
||||||
|
Foreground="{StaticResource PrimaryAccent}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<StackPanel VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="Data Sync" FontSize="20" FontWeight="Bold" Foreground="{StaticResource PrimaryText}"/>
|
||||||
|
<TextBlock Text="Keep your offline data up to date and send transactions to production."
|
||||||
|
FontSize="14" Foreground="{StaticResource MutedText}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/><ColumnDefinition Width="16"/><ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<!-- Download Offline Data -->
|
||||||
|
<Border Grid.Column="0" Style="{StaticResource SyncSubCardStyle}">
|
||||||
|
<StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
|
||||||
|
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="18"
|
||||||
|
Foreground="{StaticResource PrimaryAccent}" Margin="0,0,8,0"/>
|
||||||
|
<TextBlock Text="Download Offline Data" FontSize="17" FontWeight="SemiBold" Foreground="{StaticResource PrimaryText}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Text="Download employee RFID tags, meal schedules, and menu data for offline scanning."
|
||||||
|
FontSize="13" Foreground="{StaticResource MutedText}" TextWrapping="Wrap" Margin="0,0,0,16"/>
|
||||||
|
<Button Content="{Binding SyncCacheButtonText}"
|
||||||
|
Command="{Binding SyncEmployeeAndMenuCacheNowCommand}"
|
||||||
|
Style="{StaticResource PrimaryButtonStyle}"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
IsEnabled="{Binding CanSyncCacheNow}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<!-- Post Data to Production -->
|
||||||
|
<Border Grid.Column="2" Style="{StaticResource SyncSubCardStyle}">
|
||||||
|
<StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
|
||||||
|
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="18"
|
||||||
|
Foreground="{StaticResource OrangeAccent}" Margin="0,0,8,0"/>
|
||||||
|
<TextBlock Text="Post Data to Production" FontSize="17" FontWeight="SemiBold" Foreground="{StaticResource PrimaryText}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Text="Post captured scan and transaction data to the production server."
|
||||||
|
FontSize="13" Foreground="{StaticResource MutedText}" TextWrapping="Wrap" Margin="0,0,0,16"/>
|
||||||
|
<Button Content="{Binding PostButtonText}"
|
||||||
|
Command="{Binding PostDataNowCommand}"
|
||||||
|
Style="{StaticResource PrimaryButtonStyle}"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
IsEnabled="{Binding CanPostNow}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
<TextBlock Text="{Binding LastEmployeeRfidCacheSyncDisplay, StringFormat=Last employee RFID cache sync: {0}}"
|
||||||
|
FontSize="13" Foreground="{StaticResource MutedText}" Margin="0,16,0,4"/>
|
||||||
|
<TextBlock Text="{Binding LastMealMenuCacheSyncDisplay, StringFormat=Last meal/menu cache sync: {0}}"
|
||||||
|
FontSize="13" Foreground="{StaticResource MutedText}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Status message -->
|
||||||
|
<Border Padding="14" CornerRadius="8" Margin="0,0,0,8">
|
||||||
|
<Border.Style>
|
||||||
|
<Style TargetType="Border">
|
||||||
|
<Setter Property="Background" Value="#dcfce7"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#86efac"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Visibility" Value="Visible"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding SaveMessage}" Value="">
|
||||||
|
<Setter Property="Visibility" Value="Collapsed"/>
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding IsError}" Value="True">
|
||||||
|
<Setter Property="Background" Value="#fee2e2"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#fca5a5"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Border.Style>
|
||||||
|
<TextBlock Text="{Binding SaveMessage}" FontSize="15" FontWeight="SemiBold" TextWrapping="Wrap">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource SuccessBrush}"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsError}" Value="True">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource ErrorBrush}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<!-- Sticky bottom action bar (full width) -->
|
||||||
|
<Border Grid.Row="1" Background="#F9FBFB" BorderBrush="{StaticResource BorderBrush}" BorderThickness="0,1,0,0" Padding="32,16">
|
||||||
|
<Grid HorizontalAlignment="Stretch">
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
|
<Button Command="{Binding OpenMealSchedulesCommand}" Style="{StaticResource OutlineButtonStyle}" MinWidth="160" Margin="0,0,12,0">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="14" Margin="0,0,8,0" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="Meal Schedules" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
<Button Content="Back" Command="{Binding BackCommand}" Style="{StaticResource OutlineButtonStyle}" MinWidth="100" Margin="0,0,12,0"/>
|
||||||
|
<Button Command="{Binding SaveCommand}" Style="{StaticResource PrimaryButtonStyle}" MinWidth="100">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="14" Margin="0,0,8,0" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="Save" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue