diff --git a/App.xaml.cs b/App.xaml.cs
index 180c5b1..39c3e97 100644
--- a/App.xaml.cs
+++ b/App.xaml.cs
@@ -31,6 +31,7 @@ public partial class App : Application
var configService = new ConfigService();
var rfidService = new RfidService(dbFactory, configService);
var syncService = new SyncService(dbFactory, configService);
+ var adminAuditService = new AdminAuditService(dbFactory);
var session = new AppSession();
var authenticationUrl = "https://portal.utopiaindustries.pk/uind/rest/auth/user/";
var authService = new AuthService(authenticationUrl);
@@ -39,11 +40,11 @@ public partial class App : Application
NavigationService navigationService = null!;
navigationService = new NavigationService(
session,
- () => new AdminLoginViewModel(authService, session, navigationService, configService),
+ () => new AdminLoginViewModel(authService, session, navigationService, configService, adminAuditService),
() => new ScannerDashboardViewModel(rfidService, navigationService, session, configService),
() => new MainDashboardViewModel(navigationService, rfidService, configService, session),
() => new AdminSettingsAuthViewModel(authService, session, navigationService, configService),
- () => new SettingsViewModel(configService, navigationService));
+ () => new SettingsViewModel(configService, navigationService, adminAuditService));
var mainViewModel = new MainViewModel(navigationService);
diff --git a/Converters/StringToImageSourceConverter.cs b/Converters/StringToImageSourceConverter.cs
new file mode 100644
index 0000000..5229b74
--- /dev/null
+++ b/Converters/StringToImageSourceConverter.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+using System.Windows.Media.Imaging;
+
+namespace UtopiaCanteenSystem.Converters
+{
+ ///
+ /// Converts a string URI (e.g. pack:// or file path) to an ImageSource for Image controls.
+ ///
+ public class StringToImageSourceConverter : IValueConverter
+ {
+ public object? Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ var path = value?.ToString();
+ if (string.IsNullOrWhiteSpace(path))
+ return null;
+
+ try
+ {
+ var uri = new Uri(path, UriKind.RelativeOrAbsolute);
+ return new BitmapImage(uri);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/Converters/StringToVisibilityConverter.cs b/Converters/StringToVisibilityConverter.cs
index 79751c2..6885a3b 100644
--- a/Converters/StringToVisibilityConverter.cs
+++ b/Converters/StringToVisibilityConverter.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
@@ -9,9 +9,10 @@ namespace UtopiaCanteenSystem.Converters
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
- return string.IsNullOrWhiteSpace(value?.ToString())
- ? Visibility.Collapsed
- : Visibility.Visible;
+ var visible = !string.IsNullOrWhiteSpace(value?.ToString());
+ if (string.Equals(parameter?.ToString(), "Invert", StringComparison.OrdinalIgnoreCase))
+ visible = !visible;
+ return visible ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
diff --git a/Data/AppDbContext.cs b/Data/AppDbContext.cs
index bbfb880..a912085 100644
--- a/Data/AppDbContext.cs
+++ b/Data/AppDbContext.cs
@@ -17,6 +17,7 @@ public class AppDbContext : DbContext
public DbSet Labour { get; set; }
public DbSet ScanRecords { get; set; }
+ public DbSet AdminLoginRecords { get; set; }
public AppDbContext() { }
@@ -44,6 +45,12 @@ public class AppDbContext : DbContext
e.HasIndex(x => x.IsSynced);
e.HasIndex(x => x.ScanTime);
});
+
+ modelBuilder.Entity(e =>
+ {
+ e.HasKey(x => x.Id);
+ e.HasIndex(x => x.LoginTimeUtc);
+ });
}
///
@@ -53,6 +60,7 @@ public class AppDbContext : DbContext
{
Database.EnsureCreated();
UpgradeScanRecordsSchemaIfNeeded();
+ EnsureAdminLoginTableExists();
}
///
@@ -95,4 +103,30 @@ public class AppDbContext : DbContext
// Ignore; existing DB may already have columns or be incompatible
}
}
+
+ ///
+ /// Lightweight creation of AdminLoginRecords table for existing databases.
+ ///
+ private void EnsureAdminLoginTableExists()
+ {
+ try
+ {
+ var conn = Database.GetDbConnection();
+ if (conn.State != ConnectionState.Open)
+ conn.Open();
+
+ using var cmd = conn.CreateCommand();
+ cmd.CommandText =
+ "CREATE TABLE IF NOT EXISTS AdminLoginRecords (" +
+ "Id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+ "Username TEXT NOT NULL, " +
+ "EmployeeId TEXT NOT NULL, " +
+ "LoginTimeUtc TEXT NOT NULL)";
+ cmd.ExecuteNonQuery();
+ }
+ catch
+ {
+ // Ignore; table may already exist or DB may be read-only.
+ }
+ }
}
diff --git a/MainWindow.xaml b/MainWindow.xaml
index 20dbf98..c9ba628 100644
--- a/MainWindow.xaml
+++ b/MainWindow.xaml
@@ -3,10 +3,12 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Utopia Canteen System"
Icon="pack://siteoforigin:,,,/assets/favicon.ico"
- MinHeight="600" MinWidth="800"
- WindowStartupLocation="CenterScreen"
- SizeToContent="WidthAndHeight">
+ MinHeight="400" MinWidth="480"
+ Width="900" Height="700"
+ WindowStartupLocation="CenterScreen">
-
+
diff --git a/Models/AdminLoginRecord.cs b/Models/AdminLoginRecord.cs
new file mode 100644
index 0000000..9505edc
--- /dev/null
+++ b/Models/AdminLoginRecord.cs
@@ -0,0 +1,14 @@
+namespace UtopiaCanteenSystem.Models;
+
+///
+/// Audit record of an admin login on this kiosk/device.
+/// Passwords are NEVER stored here – only identity and timestamps.
+///
+public class AdminLoginRecord
+{
+ public int Id { get; set; }
+ public string Username { get; set; } = string.Empty;
+ public string EmployeeId { get; set; } = string.Empty;
+ public DateTime LoginTimeUtc { get; set; }
+}
+
diff --git a/Services/AdminAuditService.cs b/Services/AdminAuditService.cs
new file mode 100644
index 0000000..b3fa9f5
--- /dev/null
+++ b/Services/AdminAuditService.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using UtopiaCanteenSystem.Data;
+using UtopiaCanteenSystem.Models;
+
+namespace UtopiaCanteenSystem.Services;
+
+///
+/// Persists simple admin login audit records to the local SQLite database.
+///
+public class AdminAuditService : IAdminAuditService
+{
+ private readonly DbContextFactory _dbFactory;
+
+ public AdminAuditService(DbContextFactory dbFactory)
+ {
+ _dbFactory = dbFactory;
+ }
+
+ public async Task RecordLoginAsync(string username, string employeeId)
+ {
+ try
+ {
+ await using var db = _dbFactory.CreateDbContext();
+ var record = new AdminLoginRecord
+ {
+ Username = username ?? string.Empty,
+ EmployeeId = employeeId ?? string.Empty,
+ LoginTimeUtc = DateTime.UtcNow
+ };
+ db.AdminLoginRecords.Add(record);
+ await db.SaveChangesAsync().ConfigureAwait(false);
+ }
+ catch
+ {
+ // Audit failures should never block login; ignore errors.
+ }
+ }
+
+ ///
+ /// Returns the most recent admin login record for this kiosk, or null if none exist.
+ ///
+ public AdminLoginRecord? GetLastLogin()
+ {
+ try
+ {
+ using var db = _dbFactory.CreateDbContext();
+ return db.AdminLoginRecords
+ .OrderByDescending(x => x.LoginTimeUtc)
+ .FirstOrDefault();
+ }
+ catch
+ {
+ return null;
+ }
+ }
+}
+
diff --git a/Services/IAdminAuditService.cs b/Services/IAdminAuditService.cs
new file mode 100644
index 0000000..8ff409b
--- /dev/null
+++ b/Services/IAdminAuditService.cs
@@ -0,0 +1,11 @@
+using System.Threading.Tasks;
+using UtopiaCanteenSystem.Models;
+
+namespace UtopiaCanteenSystem.Services;
+
+public interface IAdminAuditService
+{
+ Task RecordLoginAsync(string username, string employeeId);
+ AdminLoginRecord? GetLastLogin();
+}
+
diff --git a/ViewModels/AdminLoginViewModel.cs b/ViewModels/AdminLoginViewModel.cs
index be78ee7..1817e66 100644
--- a/ViewModels/AdminLoginViewModel.cs
+++ b/ViewModels/AdminLoginViewModel.cs
@@ -10,6 +10,7 @@ public partial class AdminLoginViewModel : ObservableObject
private readonly AppSession _session;
private readonly INavigationService _navigation;
private readonly IConfigService _config;
+ private readonly IAdminAuditService _adminAudit;
[ObservableProperty]
private string _username = string.Empty;
@@ -27,12 +28,18 @@ public partial class AdminLoginViewModel : ObservableObject
[ObservableProperty]
private bool _rememberCredentials;
- public AdminLoginViewModel(IAuthService authService, AppSession session, INavigationService navigation, IConfigService config)
+ public AdminLoginViewModel(
+ IAuthService authService,
+ AppSession session,
+ INavigationService navigation,
+ IConfigService config,
+ IAdminAuditService adminAudit)
{
_authService = authService;
_session = session;
_navigation = navigation;
_config = config;
+ _adminAudit = adminAudit;
RememberCredentials = _config.GetRememberAdminCredentials();
if (RememberCredentials)
@@ -76,6 +83,9 @@ public partial class AdminLoginViewModel : ObservableObject
_session.SetAdminAuthenticated(user, result.EmployeeId);
+ // Persist who logged in (for audit / reporting).
+ await _adminAudit.RecordLoginAsync(user, result.EmployeeId).ConfigureAwait(false);
+
// Persist credentials only if user opted in.
_config.SetRememberAdminCredentials(RememberCredentials);
if (RememberCredentials)
diff --git a/ViewModels/ScannerDashboardViewModel.cs b/ViewModels/ScannerDashboardViewModel.cs
index 8a561fc..cfea386 100644
--- a/ViewModels/ScannerDashboardViewModel.cs
+++ b/ViewModels/ScannerDashboardViewModel.cs
@@ -21,6 +21,8 @@ public partial class ScannerDashboardViewModel : ObservableObject
private readonly INavigationService _navigation;
private readonly AppSession _session;
private readonly IConfigService _configService;
+ private readonly DispatcherTimer _scannerStatusTimer;
+ private DateTime? _lastScanActivityUtc;
private readonly Dispatcher _uiDispatcher;
private readonly DebounceTimer _debounceTimer;
@@ -31,9 +33,6 @@ public partial class ScannerDashboardViewModel : ObservableObject
private string _cooldownBlockedCardId = string.Empty;
private bool _suppressCooldownRewrite;
- private readonly DispatcherTimer _scannerStatusTimer;
- private DateTime? _lastScanActivityUtc;
-
private readonly DispatcherTimer _clockTimer;
private readonly object _submitLock = new();
@@ -103,7 +102,29 @@ public partial class ScannerDashboardViewModel : ObservableObject
/// Display string for current site (e.g. "SITE : 1").
public string CurrentSiteDisplay => string.IsNullOrWhiteSpace(SiteNumber) ? "SITE : 1" : $"SITE : {SiteNumber.Trim()}";
- public ScannerDashboardViewModel(IRfidService rfidService, INavigationService navigation, AppSession session, IConfigService configService)
+ // --- Left panel: Employee information (sample data for display; replace with real lookup when available) ---
+ [ObservableProperty]
+ private string _employeeName = "Syed Mustufa Ahmed Naqvi";
+
+ [ObservableProperty]
+ private string _employeeDepartment = "Technology";
+
+ [ObservableProperty]
+ private string _employeeId = "15399";
+
+ /// Item selected by the employee for the order (e.g. Chicken Biryani).
+ [ObservableProperty]
+ private string _employeeOrderItem = "Chicken Biryani";
+
+ /// Optional profile image path; null = show placeholder.
+ [ObservableProperty]
+ private string? _employeeProfileImagePath = "pack://siteoforigin:,,,/assets/emp-pic/emppic.jpeg";
+
+ public ScannerDashboardViewModel(
+ IRfidService rfidService,
+ INavigationService navigation,
+ AppSession session,
+ IConfigService configService)
{
_rfidService = rfidService;
_navigation = navigation;
@@ -135,7 +156,7 @@ public partial class ScannerDashboardViewModel : ObservableObject
_cooldownTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(CooldownTickSeconds) };
_cooldownTimer.Tick += (_, _) => UpdateCooldownMessage();
- // Activity-based scanner status.
+ // Activity-based scanner status (keyboard-wedge style scanners).
_scannerStatusTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
_scannerStatusTimer.Tick += (_, _) => UpdateScannerStatusFromActivity();
_scannerStatusTimer.Start();
@@ -217,17 +238,17 @@ public partial class ScannerDashboardViewModel : ObservableObject
}
}
- // Clear any previous message once a new scan starts (outside cooldown).
- if (!string.IsNullOrWhiteSpace(value) && !string.IsNullOrEmpty(Message))
- Message = string.Empty;
-
- // Any incoming characters means activity.
+ // Any incoming characters means activity from the scanner keyboard wedge.
if (!string.IsNullOrWhiteSpace(value))
{
_lastScanActivityUtc = DateTime.UtcNow;
ScannerStatus = "Connected";
}
+ // Clear any previous message once a new scan starts (outside cooldown).
+ if (!string.IsNullOrWhiteSpace(value) && !string.IsNullOrEmpty(Message))
+ Message = string.Empty;
+
// Don't auto-submit while cooldown is active or while we're processing.
if (!_isCooldownActive && !IsProcessing)
RestartDebounceTimer();
@@ -246,6 +267,23 @@ public partial class ScannerDashboardViewModel : ObservableObject
ScannerStatus = "Disconnected";
}
+ private void UpdateScannerStatusFromActivity()
+ {
+ if (_lastScanActivityUtc is null)
+ {
+ if (!string.Equals(ScannerStatus, "Disconnected", StringComparison.Ordinal))
+ ScannerStatus = "Disconnected";
+ return;
+ }
+
+ var inactiveFor = DateTime.UtcNow - _lastScanActivityUtc.Value;
+ if (inactiveFor > ScannerInactivityTimeout &&
+ !string.Equals(ScannerStatus, "Disconnected", StringComparison.Ordinal))
+ {
+ ScannerStatus = "Disconnected";
+ }
+ }
+
[RelayCommand]
private void Logout()
{
@@ -430,22 +468,6 @@ public partial class ScannerDashboardViewModel : ObservableObject
IsSuccess = false;
}
- private void UpdateScannerStatusFromActivity()
- {
- if (_lastScanActivityUtc is null)
- {
- if (!string.Equals(ScannerStatus, "Disconnected", StringComparison.Ordinal))
- ScannerStatus = "Disconnected";
- return;
- }
-
- if (DateTime.UtcNow - _lastScanActivityUtc.Value > ScannerInactivityTimeout)
- {
- if (!string.Equals(ScannerStatus, "Disconnected", StringComparison.Ordinal))
- ScannerStatus = "Disconnected";
- }
- }
-
~ScannerDashboardViewModel()
{
_scannerStatusTimer.Stop();
diff --git a/ViewModels/SettingsViewModel.cs b/ViewModels/SettingsViewModel.cs
index 6b39771..be2ed97 100644
--- a/ViewModels/SettingsViewModel.cs
+++ b/ViewModels/SettingsViewModel.cs
@@ -11,6 +11,7 @@ public partial class SettingsViewModel : ObservableObject
{
private readonly IConfigService _configService;
private readonly INavigationService _navigation;
+ private readonly IAdminAuditService _adminAudit;
[ObservableProperty]
private string _syncApiEndpoint = string.Empty;
@@ -30,10 +31,11 @@ public partial class SettingsViewModel : ObservableObject
[ObservableProperty]
private bool _isSaving;
- public SettingsViewModel(IConfigService configService, INavigationService navigation)
+ public SettingsViewModel(IConfigService configService, INavigationService navigation, IAdminAuditService adminAudit)
{
_configService = configService;
_navigation = navigation;
+ _adminAudit = adminAudit;
LoadFromConfig();
}
@@ -42,7 +44,13 @@ public partial class SettingsViewModel : ObservableObject
{
SyncApiEndpoint = _configService.GetSyncApiEndpoint();
ScanTimeoutSeconds = _configService.GetScanTimeoutSeconds().ToString();
- AdminCardId = _configService.GetAdminCardId();
+
+ // Always prefer the latest admin login from the database.
+ var last = _adminAudit.GetLastLogin();
+ if (last != null && !string.IsNullOrWhiteSpace(last.EmployeeId))
+ AdminCardId = last.EmployeeId;
+ else
+ AdminCardId = "ADMIN";
}
[RelayCommand]
diff --git a/Views/AdminLoginView.xaml b/Views/AdminLoginView.xaml
index d2d86a6..b1802d1 100644
--- a/Views/AdminLoginView.xaml
+++ b/Views/AdminLoginView.xaml
@@ -51,27 +51,30 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
@@ -81,8 +84,8 @@
@@ -171,8 +174,9 @@
-
-
+
+
+
diff --git a/Views/AdminSettingsAuthView.xaml b/Views/AdminSettingsAuthView.xaml
index 623fec9..25ebb89 100644
--- a/Views/AdminSettingsAuthView.xaml
+++ b/Views/AdminSettingsAuthView.xaml
@@ -76,27 +76,30 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
@@ -147,9 +150,9 @@
-
+
-
+
-
-
+
+
+
diff --git a/Views/ScannerDashboardView.xaml b/Views/ScannerDashboardView.xaml
index feb0a2e..74af39d 100644
--- a/Views/ScannerDashboardView.xaml
+++ b/Views/ScannerDashboardView.xaml
@@ -4,7 +4,9 @@
xmlns:local="clr-namespace:UtopiaCanteenSystem.Converters">
+
+
@@ -18,10 +20,10 @@
-
+
-
-
+
+
@@ -144,7 +146,7 @@
-
+
-
-
-
-
+ HorizontalScrollBarVisibility="Auto"
+ Padding="16"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch">
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
- ⋮
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+ ⋮
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
diff --git a/Views/SettingsView.xaml b/Views/SettingsView.xaml
index 9b303c9..3a1356d 100644
--- a/Views/SettingsView.xaml
+++ b/Views/SettingsView.xaml
@@ -1,4 +1,4 @@
-
@@ -167,13 +167,19 @@
-
+
-
+ Padding="16"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch">
+
+
@@ -209,7 +215,7 @@
-
+
-
+
@@ -230,7 +236,7 @@
Foreground="{StaticResource MutedText}"
Margin="0,0,0,20" />
-
+
-
+
-
+
@@ -339,7 +345,8 @@
-
+
+
\ No newline at end of file
diff --git a/assets/emp-pic/emppic.jpeg b/assets/emp-pic/emppic.jpeg
new file mode 100644
index 0000000..14a67ac
Binary files /dev/null and b/assets/emp-pic/emppic.jpeg differ