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 layoutpull/1/head
parent
972880fad4
commit
7bc5ffc77e
|
|
@ -40,7 +40,7 @@ public partial class App : Application
|
|||
navigationService = new NavigationService(
|
||||
session,
|
||||
() => new AdminLoginViewModel(authService, session, navigationService, configService),
|
||||
() => new ScannerDashboardViewModel(rfidService, navigationService, session),
|
||||
() => new ScannerDashboardViewModel(rfidService, navigationService, session, configService),
|
||||
() => new MainDashboardViewModel(navigationService, rfidService, configService, session),
|
||||
() => new AdminSettingsAuthViewModel(authService, session, navigationService, configService),
|
||||
() => new SettingsViewModel(configService, navigationService));
|
||||
|
|
@ -53,8 +53,8 @@ public partial class App : Application
|
|||
};
|
||||
mainWindow.Show();
|
||||
|
||||
// Background sync: every 3 hours, POST unsynced ScanRecords to API
|
||||
_syncTimer = new System.Timers.Timer(TimeSpan.FromHours(3).TotalMilliseconds)
|
||||
// Background sync: every 15 minutes, POST unsynced ScanRecords to API
|
||||
_syncTimer = new System.Timers.Timer(TimeSpan.FromMinutes(15).TotalMilliseconds)
|
||||
{
|
||||
AutoReset = true
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Data;
|
||||
using System.IO;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using UtopiaCanteenSystem.Models;
|
||||
using System.IO;
|
||||
|
||||
namespace UtopiaCanteenSystem.Data;
|
||||
|
||||
|
|
@ -51,5 +52,47 @@ public class AppDbContext : DbContext
|
|||
public void EnsureDatabaseCreated()
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,4 +12,8 @@ public class ScanRecord
|
|||
public DateTime ScanTime { get; set; }
|
||||
/// <summary>True after record has been successfully sent to the sync API.</summary>
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,32 @@ public class ConfigService : IConfigService
|
|||
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 void SetRememberAdminCredentials(bool remember)
|
||||
|
|
@ -159,6 +185,8 @@ public class ConfigService : IConfigService
|
|||
public bool ScannerConnected { get; set; } = false;
|
||||
public int ScanTimeoutSeconds { get; set; } = 60;
|
||||
public string AdminCardId { get; set; } = "ADMIN";
|
||||
public string SiteId { get; set; } = "SITE : 1";
|
||||
public string DeviceId { get; set; } = string.Empty;
|
||||
|
||||
// Admin credential persistence (optional).
|
||||
public bool RememberAdminCredentials { get; set; } = false;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@ public interface IConfigService
|
|||
string GetAdminCardId();
|
||||
void SetAdminCardId(string cardId);
|
||||
|
||||
string GetSiteId();
|
||||
void SetSiteId(string siteId);
|
||||
string GetDeviceId();
|
||||
void SetDeviceId(string deviceId);
|
||||
|
||||
bool GetRememberAdminCredentials();
|
||||
void SetRememberAdminCredentials(bool remember);
|
||||
string GetSavedAdminUsername();
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ public interface IRfidService
|
|||
/// </summary>
|
||||
Task<int> GetTodayScanCountAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Returns total number of scans/orders recorded (all time).</summary>
|
||||
Task<int> GetTotalScanCountAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Returns total number of scans recorded for a given card ID (all time).
|
||||
/// Used to detect repeat scans by the same user.
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ public class RfidService : IRfidService
|
|||
var remaining = GetCooldownRemainingSeconds(nowUtc, lastInWindow.ScanTime, timeoutSeconds);
|
||||
return new ScanResult(
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -71,7 +71,7 @@ public class RfidService : IRfidService
|
|||
var remaining = GetCooldownRemainingSeconds(nowUtc, lastAnyScanInWindow.ScanTime, timeoutSeconds);
|
||||
return new ScanResult(
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -79,12 +79,14 @@ public class RfidService : IRfidService
|
|||
{
|
||||
CardId = cardId,
|
||||
ScanTime = nowUtc,
|
||||
IsSynced = false
|
||||
IsSynced = false,
|
||||
SiteId = _configService.GetSiteId(),
|
||||
DeviceId = _configService.GetDeviceId()
|
||||
};
|
||||
db.ScanRecords.Add(record);
|
||||
db.SaveChanges();
|
||||
|
||||
return new ScanResult(true, "Scan recorded successfully.", 0);
|
||||
return new ScanResult(true, "Order recorded successfully.", 0);
|
||||
}
|
||||
|
||||
public ScanRecord? GetLastScan()
|
||||
|
|
@ -124,6 +126,12 @@ public class RfidService : IRfidService
|
|||
.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)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(cardId))
|
||||
|
|
|
|||
|
|
@ -44,10 +44,11 @@ public class SyncService : ISyncService
|
|||
|
||||
var payload = toSync.Select(r => new
|
||||
{
|
||||
r.Id,
|
||||
r.CardId,
|
||||
ScanTime = r.ScanTime,
|
||||
r.IsSynced
|
||||
DeviceLocalRowId = r.Id,
|
||||
ScanTimeUtc = r.ScanTime,
|
||||
SiteId = r.SiteId ?? string.Empty,
|
||||
DeviceId = r.DeviceId ?? string.Empty,
|
||||
r.CardId
|
||||
}).ToList();
|
||||
|
||||
try
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ public partial class ScannerDashboardViewModel : ObservableObject
|
|||
private readonly IRfidService _rfidService;
|
||||
private readonly INavigationService _navigation;
|
||||
private readonly AppSession _session;
|
||||
private readonly IConfigService _configService;
|
||||
private readonly Dispatcher _uiDispatcher;
|
||||
|
||||
private readonly DebounceTimer _debounceTimer;
|
||||
|
|
@ -55,8 +56,9 @@ public partial class ScannerDashboardViewModel : ObservableObject
|
|||
[NotifyPropertyChangedFor(nameof(ScannerStatusDisplay))]
|
||||
private string _scannerStatus = "Disconnected";
|
||||
|
||||
/// <summary>Shows scanner device connectivity: connected = ready; disconnected = check device.</summary>
|
||||
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]
|
||||
private string _currentTime = DateTime.Now.ToString("MM/dd/yyyy, hh:mm:ss tt");
|
||||
|
|
@ -65,12 +67,22 @@ public partial class ScannerDashboardViewModel : ObservableObject
|
|||
[ObservableProperty]
|
||||
private int _todaysScans;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _totalOrders;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _lastCardId = "—";
|
||||
|
||||
[ObservableProperty]
|
||||
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)
|
||||
[ObservableProperty]
|
||||
private string _message = string.Empty;
|
||||
|
|
@ -80,11 +92,23 @@ public partial class ScannerDashboardViewModel : ObservableObject
|
|||
|
||||
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;
|
||||
_navigation = navigation;
|
||||
_session = session;
|
||||
_configService = configService;
|
||||
_uiDispatcher = Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher;
|
||||
|
||||
// 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.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();
|
||||
_ = 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)
|
||||
{
|
||||
// If the field is cleared, also clear any remembered blocked-id prefix.
|
||||
|
|
@ -285,7 +332,7 @@ public partial class ScannerDashboardViewModel : ObservableObject
|
|||
_uiDispatcher.Invoke(() =>
|
||||
{
|
||||
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(() =>
|
||||
{
|
||||
TodaysScans = todayCount;
|
||||
TotalOrders = todayCount; // Total orders = today's total only (not previous days)
|
||||
ApplyLastScan(last);
|
||||
});
|
||||
}
|
||||
|
|
@ -338,6 +386,7 @@ public partial class ScannerDashboardViewModel : ObservableObject
|
|||
_cooldownBlockedCardId = CardIdInput?.Trim() ?? string.Empty;
|
||||
_cooldownEndsUtc = DateTime.UtcNow.AddSeconds(seconds);
|
||||
_lastDisplayedCooldownSeconds = -1;
|
||||
CooldownAlertMessage = $"Ask this customer to rescan their card after the countdown ({seconds} seconds remaining)";
|
||||
UpdateCooldownMessage();
|
||||
_cooldownTimer.Start();
|
||||
}
|
||||
|
|
@ -350,18 +399,23 @@ public partial class ScannerDashboardViewModel : ObservableObject
|
|||
_isCooldownActive = false;
|
||||
IsCooldownActiveUi = false;
|
||||
IsProcessing = false;
|
||||
CooldownAlertMessage = string.Empty;
|
||||
}
|
||||
|
||||
private void UpdateCooldownMessage()
|
||||
{
|
||||
if (_cooldownEndsUtc is null)
|
||||
{
|
||||
CooldownAlertMessage = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
var remaining = (int)Math.Ceiling((_cooldownEndsUtc.Value - DateTime.UtcNow).TotalSeconds);
|
||||
if (remaining <= 0)
|
||||
{
|
||||
StopCooldownCountdown();
|
||||
Message = "You can scan now.";
|
||||
CooldownAlertMessage = string.Empty;
|
||||
Message = "Ready for next order.";
|
||||
IsSuccess = true;
|
||||
return;
|
||||
}
|
||||
|
|
@ -372,6 +426,7 @@ public partial class ScannerDashboardViewModel : ObservableObject
|
|||
|
||||
var unit = remaining == 1 ? "second" : "seconds";
|
||||
Message = $"Please wait {remaining} {unit}…";
|
||||
CooldownAlertMessage = $"Ask this customer to rescan their card after the countdown ({remaining} {unit} remaining)";
|
||||
IsSuccess = false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@
|
|||
<StackPanel Grid.Row="0">
|
||||
<Grid HorizontalAlignment="Center" Margin="0,0,0,12">
|
||||
<Image x:Name="LogoImage"
|
||||
Source="pack://siteoforigin:,,,/assets/logo.png"
|
||||
Source="pack://siteoforigin:,,,/assets/logo4.png"
|
||||
Height="190"
|
||||
MaxWidth="520"
|
||||
Stretch="Uniform"
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ public partial class AdminLoginView : UserControl
|
|||
{
|
||||
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();
|
||||
bmp.BeginInit();
|
||||
bmp.UriSource = uri;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
<UserControl x:Class="UtopiaCanteenSystem.Views.ScannerDashboardView"
|
||||
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>
|
||||
<local:StringToVisibilityConverter x:Key="StringToVisibility"/>
|
||||
|
||||
<SolidColorBrush x:Key="AccentBrush" Color="#5BA3A0"/>
|
||||
<SolidColorBrush x:Key="AccentBorderBrush" Color="#995BA3A0"/>
|
||||
<SolidColorBrush x:Key="MutedTextBrush" Color="#718096"/>
|
||||
|
|
@ -10,7 +13,7 @@
|
|||
<SolidColorBrush x:Key="ErrorTextBrush" Color="#E53E3E"/>
|
||||
<SolidColorBrush x:Key="ConnectedBrush" Color="#38A169"/>
|
||||
<SolidColorBrush x:Key="SuccessTextBrush" Color="#38A169"/>
|
||||
|
||||
<SolidColorBrush x:Key="SettingsIconBrush" Color="#5BA3A0"/>
|
||||
<Style x:Key="RoundedButtonStyle" TargetType="Button">
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="Background" Value="{StaticResource AccentBrush}"/>
|
||||
|
|
@ -35,13 +38,15 @@
|
|||
|
||||
<Style x:Key="SecondaryButtonStyle" TargetType="Button">
|
||||
<Setter Property="Foreground" Value="{StaticResource TitleTextBrush}"/>
|
||||
<Setter Property="Background" Value="#f4f7f7"/>
|
||||
<Setter Property="Background" Value="#FFFFFF"/>
|
||||
<Setter Property="BorderBrush" Value="#e2e8f0"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="FontSize" Value="16"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<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.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
|
|
@ -49,16 +54,35 @@
|
|||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="8">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
CornerRadius="10">
|
||||
<Grid>
|
||||
<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>
|
||||
<ControlTemplate.Triggers>
|
||||
<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 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>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
|
|
@ -70,21 +94,47 @@
|
|||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource MutedTextBrush}"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="FontSize" Value="15"/>
|
||||
<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.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Border x:Name="ButtonBorder"
|
||||
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>
|
||||
<ControlTemplate.Triggers>
|
||||
<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 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>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
|
|
@ -158,29 +208,119 @@
|
|||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Top section -->
|
||||
<StackPanel Grid.Row="0" HorizontalAlignment="Center">
|
||||
<Image Source="pack://siteoforigin:,,,/assets/scanner.png"
|
||||
Height="120"
|
||||
MaxWidth="320"
|
||||
Stretch="Uniform"
|
||||
Margin="0,0,0,10"/>
|
||||
<TextBlock Text="Utopia Canteen System"
|
||||
FontSize="32"
|
||||
FontWeight="Bold"
|
||||
<!-- 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}"
|
||||
HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="Scan your card to record"
|
||||
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 -->
|
||||
<StackPanel Grid.Row="2" HorizontalAlignment="Center">
|
||||
<Image Source="pack://siteoforigin:,,,/assets/logo4.png"
|
||||
Height="180"
|
||||
MaxWidth="480"
|
||||
Stretch="Uniform"
|
||||
Margin="0,0,0,16"/>
|
||||
<TextBlock Text="Tap card to place order"
|
||||
FontSize="16"
|
||||
Foreground="{StaticResource MutedTextBrush}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,8,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Scanner input -->
|
||||
<StackPanel Grid.Row="1" Margin="0,24,0,0">
|
||||
<!-- Order input -->
|
||||
<StackPanel Grid.Row="3" Margin="0,24,0,0">
|
||||
<Border MinHeight="50"
|
||||
CornerRadius="8"
|
||||
BorderBrush="{StaticResource AccentBorderBrush}"
|
||||
|
|
@ -197,7 +337,7 @@
|
|||
Focusable="True"
|
||||
KeyDown="RfidInputTextBox_OnKeyDown"
|
||||
LostKeyboardFocus="RfidInputTextBox_OnLostKeyboardFocus"/>
|
||||
<TextBlock Text="Scan Card ID"
|
||||
<TextBlock Text="Scan..."
|
||||
Foreground="Gray"
|
||||
FontSize="18"
|
||||
VerticalAlignment="Center"
|
||||
|
|
@ -208,7 +348,7 @@
|
|||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Button Content="SCAN"
|
||||
<Button Content="PLACE ORDER"
|
||||
Command="{Binding ScanCommand}"
|
||||
Margin="0,16,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
|
|
@ -216,7 +356,7 @@
|
|||
</StackPanel>
|
||||
|
||||
<!-- 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">
|
||||
<TextBlock Style="{StaticResource StatusIconStyle}" Margin="0,0,8,0"/>
|
||||
<TextBlock Text="{Binding ScannerStatusDisplay}" Style="{StaticResource StatusTextStyle}"/>
|
||||
|
|
@ -229,33 +369,72 @@
|
|||
</StackPanel>
|
||||
|
||||
<!-- Divider -->
|
||||
<Border Grid.Row="3"
|
||||
<Border Grid.Row="5"
|
||||
Height="1"
|
||||
Background="#e2e8f0"
|
||||
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 -->
|
||||
<Grid Grid.Row="4">
|
||||
<Grid Grid.Row="7">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</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">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border Grid.Column="0" Background="#f4f7f7" CornerRadius="8" Padding="24" Margin="0,0,8,0">
|
||||
<StackPanel HorizontalAlignment="Center">
|
||||
<TextBlock Text="{Binding TodaysScans}"
|
||||
FontSize="56"
|
||||
FontWeight="Bold"
|
||||
Foreground="{StaticResource TitleTextBrush}"
|
||||
HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="Today's Scans"
|
||||
<TextBlock Text="Today's Customer"
|
||||
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">
|
||||
<Grid>
|
||||
|
|
@ -269,18 +448,21 @@
|
|||
</Grid.ColumnDefinitions>
|
||||
|
||||
<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}" />
|
||||
</StackPanel>
|
||||
|
||||
<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}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</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}"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
|
|
@ -298,34 +480,6 @@
|
|||
</TextBlock>
|
||||
</Border>
|
||||
</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>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
using System.Text.RegularExpressions;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using UtopiaCanteenSystem.ViewModels;
|
||||
|
||||
|
|
@ -51,10 +53,24 @@ public partial class ScannerDashboardView : UserControl
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (e.Key != Key.Enter)
|
||||
|
|
@ -66,5 +82,16 @@ public partial class ScannerDashboardView : UserControl
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@
|
|||
|
||||
<!-- Row 0: main scanner content -->
|
||||
<StackPanel Grid.Row="0" HorizontalAlignment="Stretch" VerticalAlignment="Center">
|
||||
<Image Source="pack://siteoforigin:,,,/assets/scanner.png"
|
||||
<Image Source="pack://siteoforigin:,,,/assets/logo4.png"
|
||||
Height="120"
|
||||
MaxWidth="320"
|
||||
Stretch="Uniform"
|
||||
|
|
|
|||
|
|
@ -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:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<UserControl.Resources>
|
||||
<SolidColorBrush x:Key="AppBackground" Color="#F5F7FA"/>
|
||||
<SolidColorBrush x:Key="AppBackground" Color="#F0F2F5"/>
|
||||
<SolidColorBrush x:Key="CardBackground" Color="#FFFFFF"/>
|
||||
<SolidColorBrush x:Key="PrimaryText" Color="#0A1628"/>
|
||||
<SolidColorBrush x:Key="MutedText" Color="#64748B"/>
|
||||
<SolidColorBrush x:Key="PrimaryAccent" Color="#3B82F6"/>
|
||||
<SolidColorBrush x:Key="PrimaryHover" Color="#2563EB"/>
|
||||
<SolidColorBrush x:Key="SuccessBrush" Color="#16A34A"/>
|
||||
<SolidColorBrush x:Key="ErrorBrush" Color="#EF4444"/>
|
||||
<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="FocusRingBrush" Color="#1F3B82F6"/>
|
||||
<SolidColorBrush x:Key="FocusRingBrush" Color="#335BA3A0"/>
|
||||
|
||||
<Style x:Key="ModernTextBoxStyle" TargetType="TextBox">
|
||||
<Setter Property="MinHeight" Value="50"/>
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
<Storyboard>
|
||||
<ColorAnimation Storyboard.TargetName="InputBorderBrush"
|
||||
Storyboard.TargetProperty="Color"
|
||||
To="#7F3B82F6"
|
||||
To="#995BA3A0"
|
||||
Duration="0:0:0.2" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
<Storyboard>
|
||||
<ColorAnimation Storyboard.TargetName="InputBorderBrush"
|
||||
Storyboard.TargetProperty="Color"
|
||||
To="#3B82F6"
|
||||
To="#5BA3A0"
|
||||
Duration="0:0:0.2" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
|
|
@ -87,6 +87,7 @@
|
|||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Save Button Style - Green/Teal Theme -->
|
||||
<Style x:Key="PrimaryButtonStyle" TargetType="Button">
|
||||
<Setter Property="Background" Value="{StaticResource PrimaryAccent}"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
|
|
@ -95,6 +96,7 @@
|
|||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Padding" Value="24,14"/>
|
||||
<Setter Property="MinHeight" Value="50"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
|
|
@ -114,6 +116,7 @@
|
|||
<ScaleTransform ScaleX="0.98" ScaleY="0.98" />
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter TargetName="ButtonBorder" Property="Background" Value="#3f7f7c"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
|
|
@ -121,6 +124,7 @@
|
|||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Cancel Button Style - Gray Outline Theme -->
|
||||
<Style x:Key="OutlineButtonStyle" TargetType="Button">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource PrimaryText}"/>
|
||||
|
|
@ -130,6 +134,7 @@
|
|||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Padding" Value="24,14"/>
|
||||
<Setter Property="MinHeight" Value="50"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
|
|
@ -143,9 +148,12 @@
|
|||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<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 Property="IsPressed" Value="True">
|
||||
<Setter TargetName="ButtonBorder" Property="Background" Value="#eef2f2"/>
|
||||
<Setter TargetName="ButtonBorder" Property="RenderTransform">
|
||||
<Setter.Value>
|
||||
<ScaleTransform ScaleX="0.98" ScaleY="0.98" />
|
||||
|
|
@ -168,7 +176,7 @@
|
|||
MaxWidth="1200">
|
||||
<!-- Title section -->
|
||||
<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">
|
||||
<Canvas Width="28" Height="28">
|
||||
<Ellipse Width="28" Height="28" Stroke="{StaticResource PrimaryAccent}" StrokeThickness="2"/>
|
||||
|
|
@ -190,7 +198,7 @@
|
|||
BorderBrush="{StaticResource BorderBrush}"
|
||||
BorderThickness="1">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="24" ShadowDepth="4" Opacity="0.08" Color="#000000" />
|
||||
<DropShadowEffect BlurRadius="24" ShadowDepth="0" Opacity="0.08" Color="#000000" />
|
||||
</Border.Effect>
|
||||
|
||||
<Grid>
|
||||
|
|
@ -242,8 +250,8 @@
|
|||
<Border Margin="0,0,0,20" Padding="16" CornerRadius="8">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#DCFCE7" />
|
||||
<Setter Property="BorderBrush" Value="#86EFAC" />
|
||||
<Setter Property="Background" Value="#dcfce7" />
|
||||
<Setter Property="BorderBrush" Value="#86efac" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
<Style.Triggers>
|
||||
|
|
@ -254,8 +262,8 @@
|
|||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding IsError}" Value="True">
|
||||
<Setter Property="Background" Value="#FEE2E2" />
|
||||
<Setter Property="BorderBrush" Value="#FCA5A5" />
|
||||
<Setter Property="Background" Value="#fee2e2" />
|
||||
<Setter Property="BorderBrush" Value="#fca5a5" />
|
||||
</DataTrigger>
|
||||
<Trigger Property="IsVisible" Value="True">
|
||||
<Trigger.EnterActions>
|
||||
|
|
@ -270,9 +278,30 @@
|
|||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding SaveIcon}"
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,12,0">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Text" Value=""/>
|
||||
<Setter Property="Foreground" Value="{StaticResource SuccessBrush}" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsError}" Value="True">
|
||||
<Setter Property="Text" Value=""/>
|
||||
<Setter Property="Foreground" Value="{StaticResource ErrorBrush}" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
<TextBlock Text="{Binding SaveMessage}"
|
||||
FontSize="16"
|
||||
Foreground="{StaticResource SuccessBrush}">
|
||||
FontWeight="SemiBold"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{StaticResource SuccessBrush}" />
|
||||
|
|
@ -284,11 +313,12 @@
|
|||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 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.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
|
|
@ -300,7 +330,7 @@
|
|||
Style="{StaticResource OutlineButtonStyle}"
|
||||
MinWidth="120"
|
||||
Margin="0,0,16,0" />
|
||||
<Button Content="Save"
|
||||
<Button Content="Save Changes"
|
||||
Command="{Binding SaveCommand}"
|
||||
Style="{StaticResource PrimaryButtonStyle}"
|
||||
MinWidth="120" />
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 9.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 142 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
Loading…
Reference in New Issue