Scanner dashboard & sync updates

- Add SiteId/DeviceId to scan pipeline (config, SQLite, sync payload, schema upgrade)
- Canteen wording: orders/place order, Today’s Orders, Total Orders (today only), scanner connectivity status
- Menu: site left, Settings/Logout right; fix focus for site input and when closing menu
- Cooldown: prominent on-screen alert to ask customer to rescan after countdown
- Sync: post to production every 15 minutes (was 3 hours)
- UI: remove “Utopia Canteen System” heading, larger logo, single menu row layout
pull/1/head
SYED MUSTUFA AHMED NAQVI 2026-02-03 18:26:39 +05:00
parent 972880fad4
commit 7bc5ffc77e
21 changed files with 513 additions and 133 deletions

View File

@ -40,7 +40,7 @@ public partial class App : Application
navigationService = new NavigationService( navigationService = new NavigationService(
session, session,
() => new AdminLoginViewModel(authService, session, navigationService, configService), () => new AdminLoginViewModel(authService, session, navigationService, configService),
() => new ScannerDashboardViewModel(rfidService, navigationService, session), () => new ScannerDashboardViewModel(rfidService, navigationService, session, configService),
() => new MainDashboardViewModel(navigationService, rfidService, configService, session), () => new MainDashboardViewModel(navigationService, rfidService, configService, session),
() => new AdminSettingsAuthViewModel(authService, session, navigationService, configService), () => new AdminSettingsAuthViewModel(authService, session, navigationService, configService),
() => new SettingsViewModel(configService, navigationService)); () => new SettingsViewModel(configService, navigationService));
@ -53,8 +53,8 @@ public partial class App : Application
}; };
mainWindow.Show(); mainWindow.Show();
// Background sync: every 3 hours, POST unsynced ScanRecords to API // Background sync: every 15 minutes, POST unsynced ScanRecords to API
_syncTimer = new System.Timers.Timer(TimeSpan.FromHours(3).TotalMilliseconds) _syncTimer = new System.Timers.Timer(TimeSpan.FromMinutes(15).TotalMilliseconds)
{ {
AutoReset = true AutoReset = true
}; };

View File

@ -0,0 +1,22 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace UtopiaCanteenSystem.Converters
{
public class StringToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return string.IsNullOrWhiteSpace(value?.ToString())
? Visibility.Collapsed
: Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -1,6 +1,7 @@
using System.Data;
using System.IO;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using UtopiaCanteenSystem.Models; using UtopiaCanteenSystem.Models;
using System.IO;
namespace UtopiaCanteenSystem.Data; namespace UtopiaCanteenSystem.Data;
@ -51,5 +52,47 @@ public class AppDbContext : DbContext
public void EnsureDatabaseCreated() public void EnsureDatabaseCreated()
{ {
Database.EnsureCreated(); Database.EnsureCreated();
UpgradeScanRecordsSchemaIfNeeded();
}
/// <summary>
/// Lightweight schema upgrade: add SiteId and DeviceId to ScanRecords if missing (no EF migrations).
/// Does not delete any data.
/// </summary>
private void UpgradeScanRecordsSchemaIfNeeded()
{
try
{
var conn = Database.GetDbConnection();
if (conn.State != ConnectionState.Open)
conn.Open();
var columns = new List<string>();
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "SELECT name FROM pragma_table_info('ScanRecords')";
using var r = cmd.ExecuteReader();
while (r.Read())
columns.Add(r.GetString(0));
}
if (!columns.Contains("SiteId", StringComparer.OrdinalIgnoreCase))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE ScanRecords ADD COLUMN SiteId TEXT DEFAULT ''";
cmd.ExecuteNonQuery();
}
if (!columns.Contains("DeviceId", StringComparer.OrdinalIgnoreCase))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE ScanRecords ADD COLUMN DeviceId TEXT DEFAULT ''";
cmd.ExecuteNonQuery();
}
}
catch
{
// Ignore; existing DB may already have columns or be incompatible
}
} }
} }

View File

@ -12,4 +12,8 @@ public class ScanRecord
public DateTime ScanTime { get; set; } public DateTime ScanTime { get; set; }
/// <summary>True after record has been successfully sent to the sync API.</summary> /// <summary>True after record has been successfully sent to the sync API.</summary>
public bool IsSynced { get; set; } public bool IsSynced { get; set; }
/// <summary>Site identifier where the scan occurred (from config).</summary>
public string SiteId { get; set; } = string.Empty;
/// <summary>Stable device identifier that generated the scan (from config).</summary>
public string DeviceId { get; set; } = string.Empty;
} }

View File

@ -51,6 +51,32 @@ public class ConfigService : IConfigService
SaveConfig(); SaveConfig();
} }
public string GetSiteId() => _config.SiteId ?? "SITE : 1";
public void SetSiteId(string siteId)
{
_config.SiteId = string.IsNullOrWhiteSpace(siteId) ? "SITE : 1" : (siteId ?? string.Empty);
SaveConfig();
}
public string GetDeviceId()
{
var id = _config.DeviceId ?? string.Empty;
if (string.IsNullOrWhiteSpace(id))
{
id = Guid.NewGuid().ToString("N");
_config.DeviceId = id;
SaveConfig();
}
return id;
}
public void SetDeviceId(string deviceId)
{
_config.DeviceId = deviceId ?? string.Empty;
SaveConfig();
}
public bool GetRememberAdminCredentials() => _config.RememberAdminCredentials; public bool GetRememberAdminCredentials() => _config.RememberAdminCredentials;
public void SetRememberAdminCredentials(bool remember) public void SetRememberAdminCredentials(bool remember)
@ -159,6 +185,8 @@ public class ConfigService : IConfigService
public bool ScannerConnected { get; set; } = false; public bool ScannerConnected { get; set; } = false;
public int ScanTimeoutSeconds { get; set; } = 60; public int ScanTimeoutSeconds { get; set; } = 60;
public string AdminCardId { get; set; } = "ADMIN"; public string AdminCardId { get; set; } = "ADMIN";
public string SiteId { get; set; } = "SITE : 1";
public string DeviceId { get; set; } = string.Empty;
// Admin credential persistence (optional). // Admin credential persistence (optional).
public bool RememberAdminCredentials { get; set; } = false; public bool RememberAdminCredentials { get; set; } = false;

View File

@ -14,6 +14,11 @@ public interface IConfigService
string GetAdminCardId(); string GetAdminCardId();
void SetAdminCardId(string cardId); void SetAdminCardId(string cardId);
string GetSiteId();
void SetSiteId(string siteId);
string GetDeviceId();
void SetDeviceId(string deviceId);
bool GetRememberAdminCredentials(); bool GetRememberAdminCredentials();
void SetRememberAdminCredentials(bool remember); void SetRememberAdminCredentials(bool remember);
string GetSavedAdminUsername(); string GetSavedAdminUsername();

View File

@ -31,6 +31,9 @@ public interface IRfidService
/// </summary> /// </summary>
Task<int> GetTodayScanCountAsync(CancellationToken cancellationToken = default); Task<int> GetTodayScanCountAsync(CancellationToken cancellationToken = default);
/// <summary>Returns total number of scans/orders recorded (all time).</summary>
Task<int> GetTotalScanCountAsync(CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// Returns total number of scans recorded for a given card ID (all time). /// Returns total number of scans recorded for a given card ID (all time).
/// Used to detect repeat scans by the same user. /// Used to detect repeat scans by the same user.

View File

@ -56,7 +56,7 @@ public class RfidService : IRfidService
var remaining = GetCooldownRemainingSeconds(nowUtc, lastInWindow.ScanTime, timeoutSeconds); var remaining = GetCooldownRemainingSeconds(nowUtc, lastInWindow.ScanTime, timeoutSeconds);
return new ScanResult( return new ScanResult(
false, false,
$"Multiple scans within {FormatTimeout(timeoutSeconds)} are not allowed. Please wait.", $"One order per customer within {FormatTimeout(timeoutSeconds)}. Ask this customer to rescan after countdown.",
remaining); remaining);
} }
@ -71,7 +71,7 @@ public class RfidService : IRfidService
var remaining = GetCooldownRemainingSeconds(nowUtc, lastAnyScanInWindow.ScanTime, timeoutSeconds); var remaining = GetCooldownRemainingSeconds(nowUtc, lastAnyScanInWindow.ScanTime, timeoutSeconds);
return new ScanResult( return new ScanResult(
false, false,
$"Only one scan within {FormatTimeout(timeoutSeconds)} is allowed. Please wait.", $"Only one order at a time within {FormatTimeout(timeoutSeconds)}. Ask this customer to rescan after countdown.",
remaining); remaining);
} }
@ -79,12 +79,14 @@ public class RfidService : IRfidService
{ {
CardId = cardId, CardId = cardId,
ScanTime = nowUtc, ScanTime = nowUtc,
IsSynced = false IsSynced = false,
SiteId = _configService.GetSiteId(),
DeviceId = _configService.GetDeviceId()
}; };
db.ScanRecords.Add(record); db.ScanRecords.Add(record);
db.SaveChanges(); db.SaveChanges();
return new ScanResult(true, "Scan recorded successfully.", 0); return new ScanResult(true, "Order recorded successfully.", 0);
} }
public ScanRecord? GetLastScan() public ScanRecord? GetLastScan()
@ -124,6 +126,12 @@ public class RfidService : IRfidService
.ConfigureAwait(false); .ConfigureAwait(false);
} }
public async Task<int> GetTotalScanCountAsync(CancellationToken cancellationToken = default)
{
await using var db = await _dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
return await db.ScanRecords.CountAsync(cancellationToken).ConfigureAwait(false);
}
public async Task<int> GetTotalScanCountForCardAsync(string cardId, CancellationToken cancellationToken = default) public async Task<int> GetTotalScanCountForCardAsync(string cardId, CancellationToken cancellationToken = default)
{ {
if (string.IsNullOrWhiteSpace(cardId)) if (string.IsNullOrWhiteSpace(cardId))

View File

@ -44,10 +44,11 @@ public class SyncService : ISyncService
var payload = toSync.Select(r => new var payload = toSync.Select(r => new
{ {
r.Id, DeviceLocalRowId = r.Id,
r.CardId, ScanTimeUtc = r.ScanTime,
ScanTime = r.ScanTime, SiteId = r.SiteId ?? string.Empty,
r.IsSynced DeviceId = r.DeviceId ?? string.Empty,
r.CardId
}).ToList(); }).ToList();
try try

View File

@ -20,6 +20,7 @@ public partial class ScannerDashboardViewModel : ObservableObject
private readonly IRfidService _rfidService; private readonly IRfidService _rfidService;
private readonly INavigationService _navigation; private readonly INavigationService _navigation;
private readonly AppSession _session; private readonly AppSession _session;
private readonly IConfigService _configService;
private readonly Dispatcher _uiDispatcher; private readonly Dispatcher _uiDispatcher;
private readonly DebounceTimer _debounceTimer; private readonly DebounceTimer _debounceTimer;
@ -55,8 +56,9 @@ public partial class ScannerDashboardViewModel : ObservableObject
[NotifyPropertyChangedFor(nameof(ScannerStatusDisplay))] [NotifyPropertyChangedFor(nameof(ScannerStatusDisplay))]
private string _scannerStatus = "Disconnected"; private string _scannerStatus = "Disconnected";
/// <summary>Shows scanner device connectivity: connected = ready; disconnected = check device.</summary>
public string ScannerStatusDisplay => public string ScannerStatusDisplay =>
string.Equals(ScannerStatus, "Connected", StringComparison.Ordinal) ? "Connected" : "waiting for scan..."; string.Equals(ScannerStatus, "Connected", StringComparison.Ordinal) ? "Scanner connected" : "Scanner not connected";
[ObservableProperty] [ObservableProperty]
private string _currentTime = DateTime.Now.ToString("MM/dd/yyyy, hh:mm:ss tt"); private string _currentTime = DateTime.Now.ToString("MM/dd/yyyy, hh:mm:ss tt");
@ -65,12 +67,22 @@ public partial class ScannerDashboardViewModel : ObservableObject
[ObservableProperty] [ObservableProperty]
private int _todaysScans; private int _todaysScans;
[ObservableProperty]
private int _totalOrders;
[ObservableProperty] [ObservableProperty]
private string _lastCardId = "—"; private string _lastCardId = "—";
[ObservableProperty] [ObservableProperty]
private string _lastScanTimeDisplay = "—"; private string _lastScanTimeDisplay = "—";
/// <summary>Prominent alert when customer scanned during cooldown: ask them to rescan after countdown.</summary>
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowCooldownAlert))]
private string _cooldownAlertMessage = string.Empty;
public bool ShowCooldownAlert => !string.IsNullOrWhiteSpace(CooldownAlertMessage);
// Message area (green/red) // Message area (green/red)
[ObservableProperty] [ObservableProperty]
private string _message = string.Empty; private string _message = string.Empty;
@ -80,11 +92,23 @@ public partial class ScannerDashboardViewModel : ObservableObject
public bool IsAdminAuthenticated => _session.IsAdminAuthenticated; public bool IsAdminAuthenticated => _session.IsAdminAuthenticated;
public ScannerDashboardViewModel(IRfidService rfidService, INavigationService navigation, AppSession session) // --- Menu + site selection ---
[ObservableProperty]
private bool _isMenuOpen;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CurrentSiteDisplay))]
private string _siteNumber = "1";
/// <summary>Display string for current site (e.g. "SITE : 1").</summary>
public string CurrentSiteDisplay => string.IsNullOrWhiteSpace(SiteNumber) ? "SITE : 1" : $"SITE : {SiteNumber.Trim()}";
public ScannerDashboardViewModel(IRfidService rfidService, INavigationService navigation, AppSession session, IConfigService configService)
{ {
_rfidService = rfidService; _rfidService = rfidService;
_navigation = navigation; _navigation = navigation;
_session = session; _session = session;
_configService = configService;
_uiDispatcher = Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher; _uiDispatcher = Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher;
// Debounce auto-submit (keyboard wedge scanners). // Debounce auto-submit (keyboard wedge scanners).
@ -121,10 +145,33 @@ public partial class ScannerDashboardViewModel : ObservableObject
_clockTimer.Tick += (_, _) => CurrentTime = DateTime.Now.ToString("MM/dd/yyyy, hh:mm:ss tt"); _clockTimer.Tick += (_, _) => CurrentTime = DateTime.Now.ToString("MM/dd/yyyy, hh:mm:ss tt");
_clockTimer.Start(); _clockTimer.Start();
// Load initial site from config (e.g. "SITE : 1" -> "1").
var siteId = _configService.GetSiteId();
if (!string.IsNullOrWhiteSpace(siteId) && siteId.StartsWith("SITE : ", StringComparison.OrdinalIgnoreCase))
{
var num = siteId.Substring("SITE : ".Length).Trim();
if (num.Length > 0 && num.All(char.IsDigit))
SiteNumber = num;
}
RefreshScannerStatus(); RefreshScannerStatus();
_ = RefreshDashboardAsync(); _ = RefreshDashboardAsync();
} }
partial void OnSiteNumberChanged(string value)
{
// Numeric only: filter to digits so display stays valid.
var digits = value == null ? string.Empty : new string(value.Where(char.IsDigit).ToArray());
if (digits != value)
{
SiteNumber = digits;
return;
}
// Persist to config when valid (non-empty numeric).
if (!string.IsNullOrWhiteSpace(digits))
_configService.SetSiteId("SITE : " + digits);
}
partial void OnCardIdInputChanged(string value) partial void OnCardIdInputChanged(string value)
{ {
// If the field is cleared, also clear any remembered blocked-id prefix. // If the field is cleared, also clear any remembered blocked-id prefix.
@ -285,7 +332,7 @@ public partial class ScannerDashboardViewModel : ObservableObject
_uiDispatcher.Invoke(() => _uiDispatcher.Invoke(() =>
{ {
IsSuccess = true; IsSuccess = true;
Message = $"Welcome back! You scanned again (#{todayCountForCard} today)."; Message = $"Welcome back! Another order (#{todayCountForCard} today).";
}); });
} }
} }
@ -305,6 +352,7 @@ public partial class ScannerDashboardViewModel : ObservableObject
_uiDispatcher.Invoke(() => _uiDispatcher.Invoke(() =>
{ {
TodaysScans = todayCount; TodaysScans = todayCount;
TotalOrders = todayCount; // Total orders = today's total only (not previous days)
ApplyLastScan(last); ApplyLastScan(last);
}); });
} }
@ -338,6 +386,7 @@ public partial class ScannerDashboardViewModel : ObservableObject
_cooldownBlockedCardId = CardIdInput?.Trim() ?? string.Empty; _cooldownBlockedCardId = CardIdInput?.Trim() ?? string.Empty;
_cooldownEndsUtc = DateTime.UtcNow.AddSeconds(seconds); _cooldownEndsUtc = DateTime.UtcNow.AddSeconds(seconds);
_lastDisplayedCooldownSeconds = -1; _lastDisplayedCooldownSeconds = -1;
CooldownAlertMessage = $"Ask this customer to rescan their card after the countdown ({seconds} seconds remaining)";
UpdateCooldownMessage(); UpdateCooldownMessage();
_cooldownTimer.Start(); _cooldownTimer.Start();
} }
@ -350,18 +399,23 @@ public partial class ScannerDashboardViewModel : ObservableObject
_isCooldownActive = false; _isCooldownActive = false;
IsCooldownActiveUi = false; IsCooldownActiveUi = false;
IsProcessing = false; IsProcessing = false;
CooldownAlertMessage = string.Empty;
} }
private void UpdateCooldownMessage() private void UpdateCooldownMessage()
{ {
if (_cooldownEndsUtc is null) if (_cooldownEndsUtc is null)
{
CooldownAlertMessage = string.Empty;
return; return;
}
var remaining = (int)Math.Ceiling((_cooldownEndsUtc.Value - DateTime.UtcNow).TotalSeconds); var remaining = (int)Math.Ceiling((_cooldownEndsUtc.Value - DateTime.UtcNow).TotalSeconds);
if (remaining <= 0) if (remaining <= 0)
{ {
StopCooldownCountdown(); StopCooldownCountdown();
Message = "You can scan now."; CooldownAlertMessage = string.Empty;
Message = "Ready for next order.";
IsSuccess = true; IsSuccess = true;
return; return;
} }
@ -372,6 +426,7 @@ public partial class ScannerDashboardViewModel : ObservableObject
var unit = remaining == 1 ? "second" : "seconds"; var unit = remaining == 1 ? "second" : "seconds";
Message = $"Please wait {remaining} {unit}…"; Message = $"Please wait {remaining} {unit}…";
CooldownAlertMessage = $"Ask this customer to rescan their card after the countdown ({remaining} {unit} remaining)";
IsSuccess = false; IsSuccess = false;
} }

View File

@ -80,7 +80,7 @@
<StackPanel Grid.Row="0"> <StackPanel Grid.Row="0">
<Grid HorizontalAlignment="Center" Margin="0,0,0,12"> <Grid HorizontalAlignment="Center" Margin="0,0,0,12">
<Image x:Name="LogoImage" <Image x:Name="LogoImage"
Source="pack://siteoforigin:,,,/assets/logo.png" Source="pack://siteoforigin:,,,/assets/logo4.png"
Height="190" Height="190"
MaxWidth="520" MaxWidth="520"
Stretch="Uniform" Stretch="Uniform"

View File

@ -78,7 +78,7 @@ public partial class AdminLoginView : UserControl
{ {
try try
{ {
var uri = new Uri("pack://siteoforigin:,,,/assets/logo.png", UriKind.Absolute); var uri = new Uri("pack://siteoforigin:,,,/assets/logo4.png", UriKind.Absolute);
var bmp = new BitmapImage(); var bmp = new BitmapImage();
bmp.BeginInit(); bmp.BeginInit();
bmp.UriSource = uri; bmp.UriSource = uri;

View File

@ -1,8 +1,11 @@
<UserControl x:Class="UtopiaCanteenSystem.Views.ScannerDashboardView" <UserControl x:Class="UtopiaCanteenSystem.Views.ScannerDashboardView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:UtopiaCanteenSystem.Converters">
<UserControl.Resources> <UserControl.Resources>
<local:StringToVisibilityConverter x:Key="StringToVisibility"/>
<SolidColorBrush x:Key="AccentBrush" Color="#5BA3A0"/> <SolidColorBrush x:Key="AccentBrush" Color="#5BA3A0"/>
<SolidColorBrush x:Key="AccentBorderBrush" Color="#995BA3A0"/> <SolidColorBrush x:Key="AccentBorderBrush" Color="#995BA3A0"/>
<SolidColorBrush x:Key="MutedTextBrush" Color="#718096"/> <SolidColorBrush x:Key="MutedTextBrush" Color="#718096"/>
@ -10,7 +13,7 @@
<SolidColorBrush x:Key="ErrorTextBrush" Color="#E53E3E"/> <SolidColorBrush x:Key="ErrorTextBrush" Color="#E53E3E"/>
<SolidColorBrush x:Key="ConnectedBrush" Color="#38A169"/> <SolidColorBrush x:Key="ConnectedBrush" Color="#38A169"/>
<SolidColorBrush x:Key="SuccessTextBrush" Color="#38A169"/> <SolidColorBrush x:Key="SuccessTextBrush" Color="#38A169"/>
<SolidColorBrush x:Key="SettingsIconBrush" Color="#5BA3A0"/>
<Style x:Key="RoundedButtonStyle" TargetType="Button"> <Style x:Key="RoundedButtonStyle" TargetType="Button">
<Setter Property="Foreground" Value="White"/> <Setter Property="Foreground" Value="White"/>
<Setter Property="Background" Value="{StaticResource AccentBrush}"/> <Setter Property="Background" Value="{StaticResource AccentBrush}"/>
@ -35,13 +38,15 @@
<Style x:Key="SecondaryButtonStyle" TargetType="Button"> <Style x:Key="SecondaryButtonStyle" TargetType="Button">
<Setter Property="Foreground" Value="{StaticResource TitleTextBrush}"/> <Setter Property="Foreground" Value="{StaticResource TitleTextBrush}"/>
<Setter Property="Background" Value="#f4f7f7"/> <Setter Property="Background" Value="#FFFFFF"/>
<Setter Property="BorderBrush" Value="#e2e8f0"/> <Setter Property="BorderBrush" Value="#e2e8f0"/>
<Setter Property="BorderThickness" Value="1"/> <Setter Property="BorderThickness" Value="1"/>
<Setter Property="FontSize" Value="16"/> <Setter Property="FontSize" Value="16"/>
<Setter Property="FontWeight" Value="SemiBold"/> <Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Padding" Value="20,12"/> <Setter Property="Padding" Value="20,12"/>
<Setter Property="MinHeight" Value="50"/> <Setter Property="MinHeight" Value="52"/>
<Setter Property="MinWidth" Value="120"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="Button"> <ControlTemplate TargetType="Button">
@ -49,16 +54,35 @@
Background="{TemplateBinding Background}" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="8"> CornerRadius="10">
<ContentPresenter HorizontalAlignment="Center" <Grid>
VerticalAlignment="Center"/> <StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="8,0">
<TextBlock Text="⚙"
FontSize="16"
Foreground="{StaticResource SettingsIconBrush}"
Margin="0,0,8,0"
VerticalAlignment="Center"/>
<TextBlock Text="{TemplateBinding Content}"
Foreground="{TemplateBinding Foreground}"
FontSize="{TemplateBinding FontSize}"
FontWeight="{TemplateBinding FontWeight}"
VerticalAlignment="Center"/>
</StackPanel>
</Grid>
</Border> </Border>
<ControlTemplate.Triggers> <ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True"> <Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="ButtonBorder" Property="Background" Value="#eef2f2"/> <Setter TargetName="ButtonBorder" Property="Background" Value="#f8fafc"/>
<Setter TargetName="ButtonBorder" Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
<Setter Property="Foreground" Value="{StaticResource AccentBrush}"/>
</Trigger> </Trigger>
<Trigger Property="IsPressed" Value="True"> <Trigger Property="IsPressed" Value="True">
<Setter TargetName="ButtonBorder" Property="Background" Value="#e6eded"/> <Setter TargetName="ButtonBorder" Property="Background" Value="#f1f5f9"/>
<Setter TargetName="ButtonBorder" Property="BorderBrush" Value="#4a918e"/>
<Setter Property="Foreground" Value="#4a918e"/>
</Trigger> </Trigger>
</ControlTemplate.Triggers> </ControlTemplate.Triggers>
</ControlTemplate> </ControlTemplate>
@ -70,21 +94,47 @@
<Setter Property="Background" Value="Transparent"/> <Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/> <Setter Property="BorderThickness" Value="0"/>
<Setter Property="Foreground" Value="{StaticResource MutedTextBrush}"/> <Setter Property="Foreground" Value="{StaticResource MutedTextBrush}"/>
<Setter Property="FontSize" Value="14"/> <Setter Property="FontSize" Value="15"/>
<Setter Property="FontWeight" Value="SemiBold"/> <Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Padding" Value="0"/> <Setter Property="Padding" Value="16,12"/>
<Setter Property="MinHeight" Value="52"/>
<Setter Property="MinWidth" Value="120"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="Button"> <ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"> <Border x:Name="ButtonBorder"
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/> Background="{TemplateBinding Background}"
CornerRadius="10"
BorderBrush="#fee2e2"
BorderThickness="1">
<Grid>
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock Text="⎋"
FontSize="16"
Foreground="#e53e3e"
Margin="0,0,8,0"
VerticalAlignment="Center"/>
<TextBlock Text="Logout"
Foreground="#e53e3e"
FontSize="{TemplateBinding FontSize}"
FontWeight="{TemplateBinding FontWeight}"
VerticalAlignment="Center"/>
</StackPanel>
</Grid>
</Border> </Border>
<ControlTemplate.Triggers> <ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True"> <Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value="{StaticResource TitleTextBrush}"/> <Setter TargetName="ButtonBorder" Property="Background" Value="#fef2f2"/>
<Setter TargetName="ButtonBorder" Property="BorderBrush" Value="#fca5a5"/>
<Setter Property="Foreground" Value="#dc2626"/>
</Trigger> </Trigger>
<Trigger Property="IsPressed" Value="True"> <Trigger Property="IsPressed" Value="True">
<Setter Property="Opacity" Value="0.75"/> <Setter TargetName="ButtonBorder" Property="Background" Value="#fee2e2"/>
<Setter TargetName="ButtonBorder" Property="BorderBrush" Value="#f87171"/>
<Setter Property="Foreground" Value="#b91c1c"/>
</Trigger> </Trigger>
</ControlTemplate.Triggers> </ControlTemplate.Triggers>
</ControlTemplate> </ControlTemplate>
@ -158,29 +208,119 @@
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- Menu button (top-right, prominent) -->
<ToggleButton x:Name="MenuToggleButton"
Grid.Row="0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Margin="0,0,0,8"
IsChecked="{Binding IsMenuOpen, Mode=TwoWay}"
Unchecked="MenuToggleButton_Unchecked"
Background="#f4f7f7"
BorderBrush="#e2e8f0"
BorderThickness="1"
Padding="14,10"
MinWidth="48"
MinHeight="44"
FontSize="22"
FontWeight="SemiBold"
Foreground="{StaticResource TitleTextBrush}"
Focusable="False"
IsTabStop="False"
ToolTip="Menu"
Cursor="Hand">
<ToggleButton.Content>⋮</ToggleButton.Content>
<ToggleButton.Style>
<Style TargetType="ToggleButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Border x:Name="MenuBtnBorder" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="8" Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="MenuBtnBorder" Property="Background" Value="#e8eeed"/>
<Setter TargetName="MenuBtnBorder" Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="MenuBtnBorder" Property="Background" Value="#e0e7e6"/>
<Setter TargetName="MenuBtnBorder" Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ToggleButton.Style>
</ToggleButton>
<!-- Inline menu panel (opens inside dashboard) -->
<Border x:Name="MenuPanel"
Grid.Row="1"
Background="#f8fafc"
BorderBrush="#e2e8f0"
BorderThickness="1"
CornerRadius="8"
Padding="16"
Margin="0,0,0,12"
Visibility="{Binding IsMenuOpen, Converter={StaticResource BoolToVisibility}}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Left">
<TextBlock Text="SITE :" VerticalAlignment="Center" FontSize="14" FontWeight="SemiBold" Foreground="{StaticResource TitleTextBrush}" Margin="0,0,8,0"/>
<TextBox x:Name="SiteNumberTextBox"
Text="{Binding SiteNumber, UpdateSourceTrigger=PropertyChanged}"
Width="80"
FontSize="14"
Padding="8,6"
VerticalContentAlignment="Center"
PreviewTextInput="SiteNumberTextBox_PreviewTextInput"/>
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right">
<Button Content="Settings"
Command="{Binding OpenSettingsCommand}"
Style="{StaticResource SecondaryButtonStyle}"
Margin="0,0,12,0"
Focusable="False"
IsTabStop="False"/>
<Button Command="{Binding LogoutCommand}"
Style="{StaticResource LogoutLinkButtonStyle}"
Focusable="False"
IsTabStop="False">
<StackPanel Orientation="Horizontal">
<TextBlock Text="⎋" FontSize="14" Margin="0,0,8,0" VerticalAlignment="Center"/>
<TextBlock Text="Logout" FontSize="14" VerticalAlignment="Center"/>
</StackPanel>
</Button>
</StackPanel>
</Grid>
</Border>
<!-- Top section --> <!-- Top section -->
<StackPanel Grid.Row="0" HorizontalAlignment="Center"> <StackPanel Grid.Row="2" HorizontalAlignment="Center">
<Image Source="pack://siteoforigin:,,,/assets/scanner.png" <Image Source="pack://siteoforigin:,,,/assets/logo4.png"
Height="120" Height="180"
MaxWidth="320" MaxWidth="480"
Stretch="Uniform" Stretch="Uniform"
Margin="0,0,0,10"/> Margin="0,0,0,16"/>
<TextBlock Text="Utopia Canteen System" <TextBlock Text="Tap card to place order"
FontSize="32"
FontWeight="Bold"
Foreground="{StaticResource TitleTextBrush}"
HorizontalAlignment="Center"/>
<TextBlock Text="Scan your card to record"
FontSize="16" FontSize="16"
Foreground="{StaticResource MutedTextBrush}" Foreground="{StaticResource MutedTextBrush}"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Margin="0,8,0,0"/> Margin="0,8,0,0"/>
</StackPanel> </StackPanel>
<!-- Scanner input --> <!-- Order input -->
<StackPanel Grid.Row="1" Margin="0,24,0,0"> <StackPanel Grid.Row="3" Margin="0,24,0,0">
<Border MinHeight="50" <Border MinHeight="50"
CornerRadius="8" CornerRadius="8"
BorderBrush="{StaticResource AccentBorderBrush}" BorderBrush="{StaticResource AccentBorderBrush}"
@ -197,7 +337,7 @@
Focusable="True" Focusable="True"
KeyDown="RfidInputTextBox_OnKeyDown" KeyDown="RfidInputTextBox_OnKeyDown"
LostKeyboardFocus="RfidInputTextBox_OnLostKeyboardFocus"/> LostKeyboardFocus="RfidInputTextBox_OnLostKeyboardFocus"/>
<TextBlock Text="Scan Card ID" <TextBlock Text="Scan..."
Foreground="Gray" Foreground="Gray"
FontSize="18" FontSize="18"
VerticalAlignment="Center" VerticalAlignment="Center"
@ -208,7 +348,7 @@
</Grid> </Grid>
</Border> </Border>
<Button Content="SCAN" <Button Content="PLACE ORDER"
Command="{Binding ScanCommand}" Command="{Binding ScanCommand}"
Margin="0,16,0,0" Margin="0,16,0,0"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
@ -216,7 +356,7 @@
</StackPanel> </StackPanel>
<!-- Status row + timestamp --> <!-- Status row + timestamp -->
<StackPanel Grid.Row="2" Margin="0,18,0,0" HorizontalAlignment="Center"> <StackPanel Grid.Row="4" Margin="0,18,0,0" HorizontalAlignment="Center">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<TextBlock Style="{StaticResource StatusIconStyle}" Margin="0,0,8,0"/> <TextBlock Style="{StaticResource StatusIconStyle}" Margin="0,0,8,0"/>
<TextBlock Text="{Binding ScannerStatusDisplay}" Style="{StaticResource StatusTextStyle}"/> <TextBlock Text="{Binding ScannerStatusDisplay}" Style="{StaticResource StatusTextStyle}"/>
@ -229,33 +369,72 @@
</StackPanel> </StackPanel>
<!-- Divider --> <!-- Divider -->
<Border Grid.Row="3" <Border Grid.Row="5"
Height="1" Height="1"
Background="#e2e8f0" Background="#e2e8f0"
Margin="0,24,0,24"/> Margin="0,24,0,24"/>
<!-- Cooldown alert: ask customer to rescan after countdown -->
<Border Grid.Row="6"
Background="#fef2f2"
BorderBrush="#fca5a5"
BorderThickness="2"
CornerRadius="8"
Padding="20"
Margin="0,0,0,16"
Visibility="{Binding ShowCooldownAlert, Converter={StaticResource BoolToVisibility}}">
<TextBlock Text="{Binding CooldownAlertMessage}"
FontSize="20"
FontWeight="Bold"
Foreground="{StaticResource ErrorTextBrush}"
TextWrapping="Wrap"
TextAlignment="Center"
HorizontalAlignment="Center"/>
</Border>
<!-- Dashboard section --> <!-- Dashboard section -->
<Grid Grid.Row="4"> <Grid Grid.Row="7">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Border Grid.Row="0" Background="#f4f7f7" CornerRadius="8" Padding="24" Margin="0,0,0,16"> <Grid Grid.Row="0" Margin="0,0,0,16">
<StackPanel HorizontalAlignment="Center"> <Grid.ColumnDefinitions>
<TextBlock Text="{Binding TodaysScans}" <ColumnDefinition Width="*"/>
FontSize="56" <ColumnDefinition Width="*"/>
FontWeight="Bold" </Grid.ColumnDefinitions>
Foreground="{StaticResource TitleTextBrush}" <Border Grid.Column="0" Background="#f4f7f7" CornerRadius="8" Padding="24" Margin="0,0,8,0">
HorizontalAlignment="Center"/> <StackPanel HorizontalAlignment="Center">
<TextBlock Text="Today's Scans" <TextBlock Text="{Binding TodaysScans}"
FontSize="16" FontSize="56"
Foreground="{StaticResource MutedTextBrush}" FontWeight="Bold"
HorizontalAlignment="Center" Foreground="{StaticResource TitleTextBrush}"
Margin="0,6,0,0"/> HorizontalAlignment="Center"/>
</StackPanel> <TextBlock Text="Today's Customer"
</Border> FontSize="16"
Foreground="{StaticResource MutedTextBrush}"
HorizontalAlignment="Center"
Margin="0,6,0,0"/>
</StackPanel>
</Border>
<Border Grid.Column="1" Background="#f4f7f7" CornerRadius="8" Padding="24" Margin="8,0,0,0">
<StackPanel HorizontalAlignment="Center">
<TextBlock Text="{Binding TotalOrders}"
FontSize="56"
FontWeight="Bold"
Foreground="{StaticResource TitleTextBrush}"
HorizontalAlignment="Center"/>
<TextBlock Text="Total Orders"
FontSize="16"
Foreground="{StaticResource MutedTextBrush}"
HorizontalAlignment="Center"
Margin="0,6,0,0"/>
</StackPanel>
</Border>
</Grid>
<Border Grid.Row="1" BorderBrush="#e2e8f0" BorderThickness="1" CornerRadius="8" Padding="16" Margin="0,0,0,16"> <Border Grid.Row="1" BorderBrush="#e2e8f0" BorderThickness="1" CornerRadius="8" Padding="16" Margin="0,0,0,16">
<Grid> <Grid>
@ -269,18 +448,21 @@
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<StackPanel Grid.Row="0" Grid.Column="0" Margin="0,0,12,12"> <StackPanel Grid.Row="0" Grid.Column="0" Margin="0,0,12,12">
<TextBlock Text="LAST CARD ID" FontSize="12" Foreground="{StaticResource MutedTextBrush}" FontWeight="SemiBold"/> <TextBlock Text="LAST CARD" FontSize="12" Foreground="{StaticResource MutedTextBrush}" FontWeight="SemiBold"/>
<TextBlock Text="{Binding LastCardId}" FontSize="20" FontWeight="SemiBold" Foreground="{StaticResource TitleTextBrush}" /> <TextBlock Text="{Binding LastCardId}" FontSize="20" FontWeight="SemiBold" Foreground="{StaticResource TitleTextBrush}" />
</StackPanel> </StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1" Margin="12,0,0,12"> <StackPanel Grid.Row="0" Grid.Column="1" Margin="12,0,0,12">
<TextBlock Text="LAST SCAN TIME" FontSize="12" Foreground="{StaticResource MutedTextBrush}" FontWeight="SemiBold"/> <TextBlock Text="LAST ORDER TIME" FontSize="12" Foreground="{StaticResource MutedTextBrush}" FontWeight="SemiBold"/>
<TextBlock Text="{Binding LastScanTimeDisplay}" FontSize="20" FontWeight="SemiBold" Foreground="{StaticResource TitleTextBrush}" /> <TextBlock Text="{Binding LastScanTimeDisplay}" FontSize="20" FontWeight="SemiBold" Foreground="{StaticResource TitleTextBrush}" />
</StackPanel> </StackPanel>
</Grid> </Grid>
</Border> </Border>
<Border Grid.Row="2" Style="{StaticResource StatusBorderStyle}"> <Border Grid.Row="2"
Style="{StaticResource StatusBorderStyle}"
Visibility="{Binding Message, Converter={StaticResource StringToVisibility}}">
<TextBlock Text="{Binding Message}" <TextBlock Text="{Binding Message}"
FontSize="16" FontSize="16"
FontWeight="SemiBold" FontWeight="SemiBold"
@ -298,34 +480,6 @@
</TextBlock> </TextBlock>
</Border> </Border>
</Grid> </Grid>
<!-- Bottom row -->
<Grid Grid.Row="5" Margin="0,24,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="0"
Content="Settings"
Command="{Binding OpenSettingsCommand}"
Style="{StaticResource SecondaryButtonStyle}"
Visibility="{Binding IsAdminAuthenticated, Converter={StaticResource BoolToVisibility}}"
Focusable="False"
IsTabStop="False"/>
<Button Grid.Column="2"
Command="{Binding LogoutCommand}"
Style="{StaticResource LogoutLinkButtonStyle}"
Focusable="False"
IsTabStop="False">
<StackPanel Orientation="Horizontal">
<TextBlock Text="⎋" FontSize="14" Margin="0,0,8,0" VerticalAlignment="Center"/>
<TextBlock Text="Logout" FontSize="14" VerticalAlignment="Center"/>
</StackPanel>
</Button>
</Grid>
</Grid> </Grid>
</Border> </Border>
</ScrollViewer> </ScrollViewer>

View File

@ -1,6 +1,8 @@
using System.Text.RegularExpressions;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading; using System.Windows.Threading;
using UtopiaCanteenSystem.ViewModels; using UtopiaCanteenSystem.ViewModels;
@ -51,10 +53,24 @@ public partial class ScannerDashboardView : UserControl
private void RfidInputTextBox_OnLostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) private void RfidInputTextBox_OnLostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{ {
// Keep view scan-ready while allowing clicks (Settings/Logout) to complete first. // If focus moved into the menu panel (e.g. Site number field), don't steal it back.
if (IsDescendantOf(MenuPanel, e.NewFocus as DependencyObject))
return;
// Otherwise keep view scan-ready (e.g. after closing menu or clicking elsewhere).
FocusRfidInput(selectAll: false, DispatcherPriority.ApplicationIdle); FocusRfidInput(selectAll: false, DispatcherPriority.ApplicationIdle);
} }
private static bool IsDescendantOf(DependencyObject? ancestor, DependencyObject? element)
{
if (ancestor == null || element == null) return false;
while (element != null)
{
if (element == ancestor) return true;
element = VisualTreeHelper.GetParent(element);
}
return false;
}
private void RfidInputTextBox_OnKeyDown(object sender, KeyEventArgs e) private void RfidInputTextBox_OnKeyDown(object sender, KeyEventArgs e)
{ {
if (e.Key != Key.Enter) if (e.Key != Key.Enter)
@ -66,5 +82,16 @@ public partial class ScannerDashboardView : UserControl
e.Handled = true; e.Handled = true;
} }
} }
private void SiteNumberTextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
e.Handled = !Regex.IsMatch(e.Text, @"^\d+$");
}
private void MenuToggleButton_Unchecked(object sender, RoutedEventArgs e)
{
// When user closes the menu, focus scanner input so they can scan without clicking it.
FocusRfidInput(selectAll: false, DispatcherPriority.ApplicationIdle);
}
} }

View File

@ -155,7 +155,7 @@
<!-- Row 0: main scanner content --> <!-- Row 0: main scanner content -->
<StackPanel Grid.Row="0" HorizontalAlignment="Stretch" VerticalAlignment="Center"> <StackPanel Grid.Row="0" HorizontalAlignment="Stretch" VerticalAlignment="Center">
<Image Source="pack://siteoforigin:,,,/assets/scanner.png" <Image Source="pack://siteoforigin:,,,/assets/logo4.png"
Height="120" Height="120"
MaxWidth="320" MaxWidth="320"
Stretch="Uniform" Stretch="Uniform"

View File

@ -1,17 +1,17 @@
<UserControl x:Class="UtopiaCanteenSystem.Views.SettingsView" <UserControl x:Class="UtopiaCanteenSystem.Views.SettingsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<UserControl.Resources> <UserControl.Resources>
<SolidColorBrush x:Key="AppBackground" Color="#F5F7FA"/> <SolidColorBrush x:Key="AppBackground" Color="#F0F2F5"/>
<SolidColorBrush x:Key="CardBackground" Color="#FFFFFF"/> <SolidColorBrush x:Key="CardBackground" Color="#FFFFFF"/>
<SolidColorBrush x:Key="PrimaryText" Color="#0A1628"/> <SolidColorBrush x:Key="PrimaryText" Color="#2D3748"/>
<SolidColorBrush x:Key="MutedText" Color="#64748B"/> <SolidColorBrush x:Key="MutedText" Color="#718096"/>
<SolidColorBrush x:Key="PrimaryAccent" Color="#3B82F6"/> <SolidColorBrush x:Key="PrimaryAccent" Color="#5BA3A0"/>
<SolidColorBrush x:Key="PrimaryHover" Color="#2563EB"/> <SolidColorBrush x:Key="PrimaryHover" Color="#4a918e"/>
<SolidColorBrush x:Key="SuccessBrush" Color="#16A34A"/> <SolidColorBrush x:Key="SuccessBrush" Color="#38A169"/>
<SolidColorBrush x:Key="ErrorBrush" Color="#EF4444"/> <SolidColorBrush x:Key="ErrorBrush" Color="#E53E3E"/>
<SolidColorBrush x:Key="BorderBrush" Color="#E2E8F0"/> <SolidColorBrush x:Key="BorderBrush" Color="#E2E8F0"/>
<SolidColorBrush x:Key="FocusRingBrush" Color="#1F3B82F6"/> <SolidColorBrush x:Key="FocusRingBrush" Color="#335BA3A0"/>
<Style x:Key="ModernTextBoxStyle" TargetType="TextBox"> <Style x:Key="ModernTextBoxStyle" TargetType="TextBox">
<Setter Property="MinHeight" Value="50"/> <Setter Property="MinHeight" Value="50"/>
@ -48,7 +48,7 @@
<Storyboard> <Storyboard>
<ColorAnimation Storyboard.TargetName="InputBorderBrush" <ColorAnimation Storyboard.TargetName="InputBorderBrush"
Storyboard.TargetProperty="Color" Storyboard.TargetProperty="Color"
To="#7F3B82F6" To="#995BA3A0"
Duration="0:0:0.2" /> Duration="0:0:0.2" />
</Storyboard> </Storyboard>
</BeginStoryboard> </BeginStoryboard>
@ -71,7 +71,7 @@
<Storyboard> <Storyboard>
<ColorAnimation Storyboard.TargetName="InputBorderBrush" <ColorAnimation Storyboard.TargetName="InputBorderBrush"
Storyboard.TargetProperty="Color" Storyboard.TargetProperty="Color"
To="#3B82F6" To="#5BA3A0"
Duration="0:0:0.2" /> Duration="0:0:0.2" />
</Storyboard> </Storyboard>
</BeginStoryboard> </BeginStoryboard>
@ -87,6 +87,7 @@
</Setter> </Setter>
</Style> </Style>
<!-- Save Button Style - Green/Teal Theme -->
<Style x:Key="PrimaryButtonStyle" TargetType="Button"> <Style x:Key="PrimaryButtonStyle" TargetType="Button">
<Setter Property="Background" Value="{StaticResource PrimaryAccent}"/> <Setter Property="Background" Value="{StaticResource PrimaryAccent}"/>
<Setter Property="Foreground" Value="White"/> <Setter Property="Foreground" Value="White"/>
@ -95,6 +96,7 @@
<Setter Property="FontWeight" Value="SemiBold"/> <Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Padding" Value="24,14"/> <Setter Property="Padding" Value="24,14"/>
<Setter Property="MinHeight" Value="50"/> <Setter Property="MinHeight" Value="50"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="Button"> <ControlTemplate TargetType="Button">
@ -114,6 +116,7 @@
<ScaleTransform ScaleX="0.98" ScaleY="0.98" /> <ScaleTransform ScaleX="0.98" ScaleY="0.98" />
</Setter.Value> </Setter.Value>
</Setter> </Setter>
<Setter TargetName="ButtonBorder" Property="Background" Value="#3f7f7c"/>
</Trigger> </Trigger>
</ControlTemplate.Triggers> </ControlTemplate.Triggers>
</ControlTemplate> </ControlTemplate>
@ -121,6 +124,7 @@
</Setter> </Setter>
</Style> </Style>
<!-- Cancel Button Style - Gray Outline Theme -->
<Style x:Key="OutlineButtonStyle" TargetType="Button"> <Style x:Key="OutlineButtonStyle" TargetType="Button">
<Setter Property="Background" Value="Transparent"/> <Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{StaticResource PrimaryText}"/> <Setter Property="Foreground" Value="{StaticResource PrimaryText}"/>
@ -130,6 +134,7 @@
<Setter Property="FontWeight" Value="SemiBold"/> <Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Padding" Value="24,14"/> <Setter Property="Padding" Value="24,14"/>
<Setter Property="MinHeight" Value="50"/> <Setter Property="MinHeight" Value="50"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="Button"> <ControlTemplate TargetType="Button">
@ -143,9 +148,12 @@
</Border> </Border>
<ControlTemplate.Triggers> <ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True"> <Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="ButtonBorder" Property="Background" Value="#F5F7FA"/> <Setter TargetName="ButtonBorder" Property="Background" Value="#f4f7f7"/>
<Setter TargetName="ButtonBorder" Property="BorderBrush" Value="#5BA3A0"/>
<Setter Property="Foreground" Value="#5BA3A0"/>
</Trigger> </Trigger>
<Trigger Property="IsPressed" Value="True"> <Trigger Property="IsPressed" Value="True">
<Setter TargetName="ButtonBorder" Property="Background" Value="#eef2f2"/>
<Setter TargetName="ButtonBorder" Property="RenderTransform"> <Setter TargetName="ButtonBorder" Property="RenderTransform">
<Setter.Value> <Setter.Value>
<ScaleTransform ScaleX="0.98" ScaleY="0.98" /> <ScaleTransform ScaleX="0.98" ScaleY="0.98" />
@ -168,7 +176,7 @@
MaxWidth="1200"> MaxWidth="1200">
<!-- Title section --> <!-- Title section -->
<StackPanel Orientation="Horizontal" Margin="0,0,0,24"> <StackPanel Orientation="Horizontal" Margin="0,0,0,24">
<Border Width="48" Height="48" Background="#E7F0FE" CornerRadius="24" Margin="0,0,16,0"> <Border Width="48" Height="48" Background="#eef2f2" CornerRadius="24" Margin="0,0,16,0">
<Viewbox Margin="10"> <Viewbox Margin="10">
<Canvas Width="28" Height="28"> <Canvas Width="28" Height="28">
<Ellipse Width="28" Height="28" Stroke="{StaticResource PrimaryAccent}" StrokeThickness="2"/> <Ellipse Width="28" Height="28" Stroke="{StaticResource PrimaryAccent}" StrokeThickness="2"/>
@ -190,7 +198,7 @@
BorderBrush="{StaticResource BorderBrush}" BorderBrush="{StaticResource BorderBrush}"
BorderThickness="1"> BorderThickness="1">
<Border.Effect> <Border.Effect>
<DropShadowEffect BlurRadius="24" ShadowDepth="4" Opacity="0.08" Color="#000000" /> <DropShadowEffect BlurRadius="24" ShadowDepth="0" Opacity="0.08" Color="#000000" />
</Border.Effect> </Border.Effect>
<Grid> <Grid>
@ -242,8 +250,8 @@
<Border Margin="0,0,0,20" Padding="16" CornerRadius="8"> <Border Margin="0,0,0,20" Padding="16" CornerRadius="8">
<Border.Style> <Border.Style>
<Style TargetType="Border"> <Style TargetType="Border">
<Setter Property="Background" Value="#DCFCE7" /> <Setter Property="Background" Value="#dcfce7" />
<Setter Property="BorderBrush" Value="#86EFAC" /> <Setter Property="BorderBrush" Value="#86efac" />
<Setter Property="BorderThickness" Value="1" /> <Setter Property="BorderThickness" Value="1" />
<Setter Property="Visibility" Value="Visible" /> <Setter Property="Visibility" Value="Visible" />
<Style.Triggers> <Style.Triggers>
@ -254,8 +262,8 @@
<Setter Property="Visibility" Value="Collapsed" /> <Setter Property="Visibility" Value="Collapsed" />
</DataTrigger> </DataTrigger>
<DataTrigger Binding="{Binding IsError}" Value="True"> <DataTrigger Binding="{Binding IsError}" Value="True">
<Setter Property="Background" Value="#FEE2E2" /> <Setter Property="Background" Value="#fee2e2" />
<Setter Property="BorderBrush" Value="#FCA5A5" /> <Setter Property="BorderBrush" Value="#fca5a5" />
</DataTrigger> </DataTrigger>
<Trigger Property="IsVisible" Value="True"> <Trigger Property="IsVisible" Value="True">
<Trigger.EnterActions> <Trigger.EnterActions>
@ -270,25 +278,47 @@
</Style.Triggers> </Style.Triggers>
</Style> </Style>
</Border.Style> </Border.Style>
<TextBlock Text="{Binding SaveMessage}" <StackPanel Orientation="Horizontal">
FontSize="16" <TextBlock Text="{Binding SaveIcon}"
Foreground="{StaticResource SuccessBrush}"> FontFamily="Segoe MDL2 Assets"
<TextBlock.Style> FontSize="16"
<Style TargetType="TextBlock"> FontWeight="SemiBold"
<Setter Property="Foreground" Value="{StaticResource SuccessBrush}" /> VerticalAlignment="Center"
<Style.Triggers> Margin="0,0,12,0">
<DataTrigger Binding="{Binding IsError}" Value="True"> <TextBlock.Style>
<Setter Property="Foreground" Value="{StaticResource ErrorBrush}" /> <Style TargetType="TextBlock">
</DataTrigger> <Setter Property="Text" Value=""/>
</Style.Triggers> <Setter Property="Foreground" Value="{StaticResource SuccessBrush}" />
</Style> <Style.Triggers>
</TextBlock.Style> <DataTrigger Binding="{Binding IsError}" Value="True">
</TextBlock> <Setter Property="Text" Value=""/>
<Setter Property="Foreground" Value="{StaticResource ErrorBrush}" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<TextBlock Text="{Binding SaveMessage}"
FontSize="16"
FontWeight="SemiBold"
VerticalAlignment="Center">
<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>
</StackPanel>
</Border> </Border>
</StackPanel> </StackPanel>
<!-- Button row --> <!-- Button row -->
<Border Grid.Row="2" BorderBrush="{StaticResource BorderBrush}" BorderThickness="0,1,0,0" Padding="32,16"> <Border Grid.Row="2" BorderBrush="{StaticResource BorderBrush}" BorderThickness="0,1,0,0" Padding="32,24" Background="#f9fbfb">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
@ -300,7 +330,7 @@
Style="{StaticResource OutlineButtonStyle}" Style="{StaticResource OutlineButtonStyle}"
MinWidth="120" MinWidth="120"
Margin="0,0,16,0" /> Margin="0,0,16,0" />
<Button Content="Save" <Button Content="Save Changes"
Command="{Binding SaveCommand}" Command="{Binding SaveCommand}"
Style="{StaticResource PrimaryButtonStyle}" Style="{StaticResource PrimaryButtonStyle}"
MinWidth="120" /> MinWidth="120" />
@ -312,4 +342,4 @@
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
</Grid> </Grid>
</UserControl> </UserControl>

BIN
assets/logo-final.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
assets/logo1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

BIN
assets/logo2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
assets/logo3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

BIN
assets/logo4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB