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(
|
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
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
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