diff --git a/.gitignore b/.gitignore
index 551f5be..99de91a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,6 +26,9 @@ appsettings.json
# Publish
publish/
+# Publish Profiles
+Properties/PublishProfiles/
+
# OS
Thumbs.db
.DS_Store
diff --git a/Properties/PublishProfiles/ClickOnceProfile.pubxml b/Properties/PublishProfiles/ClickOnceProfile.pubxml
deleted file mode 100644
index 2f0f0d5..0000000
--- a/Properties/PublishProfiles/ClickOnceProfile.pubxml
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
-
-
- 0
- 1.0.0.*
- True
- Release
- False
- true
- True
- Disk
- True
- False
- True
- False
- Any CPU
- bin\Release\net8.0-windows\win-x86\app.publish\
- D:\Publish\UtopiaCanteenSystem\
- ClickOnce
- False
- False
- win-x86
- True
- (none)
- False
- false
- net8.0-windows
- False
- Foreground
- False
- Publish.html
-
-
\ No newline at end of file
diff --git a/Properties/PublishProfiles/FolderProfile.pubxml b/Properties/PublishProfiles/FolderProfile.pubxml
deleted file mode 100644
index 4fe46ef..0000000
--- a/Properties/PublishProfiles/FolderProfile.pubxml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
- Release
- Any CPU
- D:\
- FileSystem
- <_TargetId>Folder
- net8.0-windows
- win-x86
- true
- false
- false
-
-
\ No newline at end of file
diff --git a/Services/RfidService.cs b/Services/RfidService.cs
index c3394d0..805fda3 100644
--- a/Services/RfidService.cs
+++ b/Services/RfidService.cs
@@ -42,6 +42,159 @@ public class RfidService : IRfidService
return (result.Success, result.Message);
}
+ //public ScanResult ProcessScanDetailed(string cardId)
+ //{
+ // if (string.IsNullOrWhiteSpace(cardId))
+ // return new ScanResult(false, "Card ID cannot be empty.", 0);
+
+ // cardId = cardId.Trim();
+
+ // // HRMS lookup: reject if card not registered
+ // var employee = _employeeLookup.GetEmployeeByRfidAsync(cardId).GetAwaiter().GetResult();
+ // if (employee == null)
+ // return new ScanResult(false, "Card not registered in HRMS.", 0);
+
+ // var nowUtc = DateTime.UtcNow;
+ // var nowLocal = DateTime.Now;
+
+ // // Phase 2.5: DB-driven meal windows (MealSchedules table). Reject if no matching session.
+ // var siteIdForSession = !string.IsNullOrWhiteSpace(employee.LocationSiteId)
+ // ? employee.LocationSiteId.Trim()
+ // : _configService.GetSiteId();
+ // //var session = _mealSessionResolver.GetCurrentSession(nowLocal, siteIdForSession);
+ // //if (session == MealSession.None)
+ // // return new ScanResult(false, "This scan is outside of valid meal timings.", 0);
+
+ // //var sessionCode = (int)session;
+
+ // var resolvedSession = _mealSessionResolver.GetCurrentSession(nowLocal, siteIdForSession);
+ // if (resolvedSession == null || resolvedSession.Session == MealSession.None)
+ // return new ScanResult(false, "This scan is outside of valid meal timings.", 0);
+
+ // var session = resolvedSession.Session;
+ // var sessionCode = (int)session;
+
+ // using var db = _dbFactory.CreateDbContext();
+
+ // // Once-per-session-per-day rule: same card, same session, same local day is not allowed.
+ // var startOfTodayLocal = DateTime.Today;
+ // var startOfTomorrowLocal = startOfTodayLocal.AddDays(1);
+ // var startUtc = startOfTodayLocal.ToUniversalTime();
+ // var endUtc = startOfTomorrowLocal.ToUniversalTime();
+
+ // var alreadyScannedThisSessionToday = db.LunchOrderTransactions
+ // .Where(r => r.CardId == cardId)
+ // .Where(r => r.MealSessionCode == sessionCode)
+ // .Where(r => r.ScanTime >= startUtc && r.ScanTime < endUtc)
+ // .OrderByDescending(r => r.ScanTime)
+ // .FirstOrDefault();
+
+ // //if (alreadyScannedThisSessionToday != null)
+ // //{
+ // // var sessionName = GetMealSessionDisplayName(session);
+ // // return new ScanResult(false, $"This card is already scanned for {sessionName} today.", 0);
+ // //}
+ // if (alreadyScannedThisSessionToday != null)
+ // {
+ // //var sessionName = session.ToString();
+ // var sessionName = resolvedSession.MealName;
+ // return new ScanResult(false, $"This card is already scanned for {sessionName} today.", 0);
+ // }
+
+ // var interval = _configService.GetScanInterval();
+ // var timeoutSeconds = (int)Math.Max(1, interval.TotalSeconds);
+
+ // var windowStart = nowUtc.AddSeconds(-timeoutSeconds);
+
+ // // Safety debounce: short cooldown per card to prevent accidental double-tap.
+ // var lastInWindow = db.LunchOrderTransactions
+ // .Where(r => r.CardId == cardId)
+ // .Where(r => r.ScanTime >= windowStart)
+ // .OrderByDescending(r => r.ScanTime)
+ // .FirstOrDefault();
+
+ // if (lastInWindow != null)
+ // {
+ // var remaining = GetCooldownRemainingSeconds(nowUtc, lastInWindow.ScanTime, timeoutSeconds);
+ // return new ScanResult(
+ // false,
+ // $"One order per customer within {FormatTimeout(timeoutSeconds)}. Ask this customer to rescan after countdown.",
+ // remaining);
+ // }
+
+ // var fullName = string.Join(" ", new[] { employee.FirstName, employee.MiddleName }.Where(s => !string.IsNullOrWhiteSpace(s))).Trim();
+ // var siteId = !string.IsNullOrWhiteSpace(employee.LocationSiteId)
+ // ? employee.LocationSiteId.Trim()
+ // : _configService.GetSiteId();
+
+ // // Resolve menu items for this scan (store on record so order history shows actual items)
+ // //var mealLabel = GetMealSessionDisplayName(session);
+ // //var mealLabel = session.ToString();
+ // var mealLabel = resolvedSession.MealName;
+ // var mealItemsDisplay = string.Empty;
+ // double totalPrice = 0;
+ // try
+ // {
+ // if (int.TryParse(new string(siteIdForSession.Where(char.IsDigit).ToArray()), out var siteNumeric) && siteNumeric > 0)
+ // {
+ // var menuItems = _menuLookup
+ // .GetMenuItemsForSiteAndDateAsync(siteNumeric, nowLocal.Date)
+ // .GetAwaiter()
+ // .GetResult();
+
+ // //var matching = menuItems
+ // // .Where(i => string.Equals(i.MealName, mealLabel, StringComparison.OrdinalIgnoreCase))
+ // // .ToList();
+ // var matching = menuItems.ToList();
+
+ // var names = matching
+ // .Select(i => i.ItemName)
+ // .Where(n => !string.IsNullOrWhiteSpace(n))
+ // .Distinct()
+ // .ToList();
+
+ // if (names.Count > 0)
+ // {
+ // mealItemsDisplay = string.Join(" + ", names);
+ // totalPrice = matching.Sum(i => (double)i.Price);
+ // }
+ // }
+ // }
+ // catch (Exception ex)
+ // {
+ // Logger.Log(ex, "RfidService.ProcessScanDetailed menu lookup");
+ // }
+
+ // var record = new ScanRecord
+ // {
+ // CardId = cardId,
+ // ScanTime = nowUtc,
+ // IsSynced = false,
+ // SiteId = siteId,
+ // DeviceId = _configService.GetDeviceId(),
+ // IpAddress = GetLocalIpAddress(),
+ // MealSessionCode = sessionCode,
+ // ParentDocumentId = employee.ParentDocumentId ?? string.Empty,
+ // EmployeeId = employee.EmployeeId ?? string.Empty,
+ // UindSerial = employee.UindSerial ?? string.Empty,
+ // FunctionId = employee.FunctionId,
+ // DepartmentId = employee.DepartmentId,
+ // TagCreatedAtUtc = employee.TagCreatedAtUtc,
+ // TagCreatedBy = employee.TagCreatedBy ?? string.Empty,
+ // EmployeeName = string.IsNullOrEmpty(fullName) ? string.Empty : fullName,
+ // Department = employee.DepartmentTitle ?? string.Empty,
+ // DepartmentType = employee.DepartmentType ?? string.Empty,
+ // MealLabel = mealLabel,
+ // MealItems = mealItemsDisplay,
+ // TotalPrice = totalPrice
+ // };
+ // db.LunchOrderTransactions.Add(record);
+ // db.SaveChanges();
+
+ // return new ScanResult(true, "Order recorded successfully.", 0, employee, session);
+ //}
+
+
public ScanResult ProcessScanDetailed(string cardId)
{
if (string.IsNullOrWhiteSpace(cardId))
@@ -57,19 +210,61 @@ public class RfidService : IRfidService
var nowUtc = DateTime.UtcNow;
var nowLocal = DateTime.Now;
- // Phase 2.5: DB-driven meal windows (MealSchedules table). Reject if no matching session.
+ // SITE VERIFICATION: Check if employee is assigned to this site
+ var currentSiteId = _configService.GetSiteId();
+ var normalizedCurrentSite = NormalizeSiteId(currentSiteId);
+
+ var employeeSiteId = !string.IsNullOrWhiteSpace(employee.LocationSiteId)
+ ? employee.LocationSiteId.Trim()
+ : string.Empty;
+ var normalizedEmployeeSite = NormalizeSiteId(employeeSiteId);
+
+ // If employee has a site assigned, verify it matches the current system
+ if (!string.IsNullOrWhiteSpace(normalizedEmployeeSite))
+ {
+ if (!string.Equals(normalizedEmployeeSite, normalizedCurrentSite, StringComparison.OrdinalIgnoreCase))
+ {
+ var message = $"Employee from site {normalizedEmployeeSite} is not allowed to scan here. " +
+ $"Only employees from site {normalizedCurrentSite} can scan.";
+ return new ScanResult(
+ false,
+ message,
+ 0,
+ employee, // Include employee info so UI can show who tried to scan
+ MealSession.None,
+ normalizedEmployeeSite,
+ normalizedCurrentSite);
+ }
+ }
+ else
+ {
+ // Employee has no site assigned - log warning but allow scan
+ // You can change this to block if required by commenting out the next line
+ Logger.Log(
+ new Exception($"Site Validation - Card: {cardId}, Employee Site: '{employeeSiteId}', Config Site: '{currentSiteId}'"),
+ "RfidService");
+
+ // If you want to BLOCK employees with no site, uncomment the following:
+ /*
+ return new ScanResult(
+ false,
+ "Employee has no site assignment. Please contact administrator.",
+ 0,
+ employee,
+ MealSession.None,
+ "UNASSIGNED",
+ normalizedCurrentSite);
+ */
+ }
+
+ // Continue with meal session validation
var siteIdForSession = !string.IsNullOrWhiteSpace(employee.LocationSiteId)
? employee.LocationSiteId.Trim()
: _configService.GetSiteId();
- //var session = _mealSessionResolver.GetCurrentSession(nowLocal, siteIdForSession);
- //if (session == MealSession.None)
- // return new ScanResult(false, "This scan is outside of valid meal timings.", 0);
-
- //var sessionCode = (int)session;
var resolvedSession = _mealSessionResolver.GetCurrentSession(nowLocal, siteIdForSession);
if (resolvedSession == null || resolvedSession.Session == MealSession.None)
- return new ScanResult(false, "This scan is outside of valid meal timings.", 0);
+ return new ScanResult(false, "This scan is outside of valid meal timings.", 0, employee);
var session = resolvedSession.Session;
var sessionCode = (int)session;
@@ -89,16 +284,10 @@ public class RfidService : IRfidService
.OrderByDescending(r => r.ScanTime)
.FirstOrDefault();
- //if (alreadyScannedThisSessionToday != null)
- //{
- // var sessionName = GetMealSessionDisplayName(session);
- // return new ScanResult(false, $"This card is already scanned for {sessionName} today.", 0);
- //}
if (alreadyScannedThisSessionToday != null)
{
- //var sessionName = session.ToString();
var sessionName = resolvedSession.MealName;
- return new ScanResult(false, $"This card is already scanned for {sessionName} today.", 0);
+ return new ScanResult(false, $"This card is already scanned for {sessionName} today.", 0, employee, session);
}
var interval = _configService.GetScanInterval();
@@ -119,7 +308,9 @@ public class RfidService : IRfidService
return new ScanResult(
false,
$"One order per customer within {FormatTimeout(timeoutSeconds)}. Ask this customer to rescan after countdown.",
- remaining);
+ remaining,
+ employee,
+ session);
}
var fullName = string.Join(" ", new[] { employee.FirstName, employee.MiddleName }.Where(s => !string.IsNullOrWhiteSpace(s))).Trim();
@@ -127,9 +318,7 @@ public class RfidService : IRfidService
? employee.LocationSiteId.Trim()
: _configService.GetSiteId();
- // Resolve menu items for this scan (store on record so order history shows actual items)
- //var mealLabel = GetMealSessionDisplayName(session);
- //var mealLabel = session.ToString();
+ // Resolve menu items for this scan
var mealLabel = resolvedSession.MealName;
var mealItemsDisplay = string.Empty;
double totalPrice = 0;
@@ -142,9 +331,6 @@ public class RfidService : IRfidService
.GetAwaiter()
.GetResult();
- //var matching = menuItems
- // .Where(i => string.Equals(i.MealName, mealLabel, StringComparison.OrdinalIgnoreCase))
- // .ToList();
var matching = menuItems.ToList();
var names = matching
@@ -191,7 +377,29 @@ public class RfidService : IRfidService
db.LunchOrderTransactions.Add(record);
db.SaveChanges();
- return new ScanResult(true, "Order recorded successfully.", 0, employee, session);
+ return new ScanResult(true, "Order recorded successfully.", 0, employee, session, normalizedEmployeeSite, normalizedCurrentSite);
+ }
+
+ ///
+ /// Normalizes a site ID to a consistent format for comparison
+ /// Handles formats like "1", "01", "SITE : 1", "Site 01", etc.
+ ///
+ private static string NormalizeSiteId(string? siteId)
+ {
+ if (string.IsNullOrWhiteSpace(siteId))
+ return string.Empty;
+
+ // Extract digits only
+ var digits = new string(siteId.Where(char.IsDigit).ToArray());
+
+ if (string.IsNullOrEmpty(digits))
+ return siteId.Trim(); // Return original if no digits
+
+ // Pad to 2 digits if it's a single digit
+ if (digits.Length == 1)
+ return digits.PadLeft(2, '0');
+
+ return digits;
}
//private static string GetMealSessionDisplayName(MealSession session)
diff --git a/Services/ScanResult.cs b/Services/ScanResult.cs
index 809d9d9..38e949f 100644
--- a/Services/ScanResult.cs
+++ b/Services/ScanResult.cs
@@ -12,5 +12,7 @@ public readonly record struct ScanResult(
string Message,
int CooldownSecondsRemaining,
HrmsEmployeeInfo? EmployeeInfo = null,
- MealSession MealSession = MealSession.None);
+ MealSession MealSession = MealSession.None,
+ string? EmployeeSiteId = null,
+ string? CurrentSiteId = null);
diff --git a/ViewModels/MealSchedulesViewModel.cs b/ViewModels/MealSchedulesViewModel.cs
index e867893..e3b6692 100644
--- a/ViewModels/MealSchedulesViewModel.cs
+++ b/ViewModels/MealSchedulesViewModel.cs
@@ -296,15 +296,32 @@ public partial class MealSchedulesViewModel : ObservableObject
{
ApplyFilter();
}
+
+ private void ApplyFilter()
+{
+ // Remove "SITE : " from the config SiteId and normalize it (pad with leading zeros if necessary)
+ var site = string.IsNullOrWhiteSpace(_currentSiteId)
+ ? "01"
+ : _currentSiteId.Replace("SITE : ", "").PadLeft(2, '0'); // Normalize to two digits if empty or contains "SITE : "
- private void ApplyFilter()
- {
- var site = string.IsNullOrWhiteSpace(_currentSiteId) ? "01" : _currentSiteId;
- var filtered = _allSchedules
- .Where(s => string.Equals(s.LocationSiteId?.Trim(), site, StringComparison.OrdinalIgnoreCase))
- .ToList();
- Schedules = new ObservableCollection(filtered);
- }
+ // Log the site ID we're filtering by (for debugging purposes).
+ Console.WriteLine($"Filtering for site: {site}");
+
+ // Filter the meal schedules by the normalized site ID.
+ var filtered = _allSchedules
+ .Where(s =>
+ {
+ // Normalize the LocationSiteId from the schedule to ensure consistency.
+ var normalizedLocationSiteId = s.LocationSiteId?.Trim().PadLeft(2, '0') ?? "00";
+ Console.WriteLine($"Checking: {normalizedLocationSiteId} vs {site}");
+
+ return string.Equals(normalizedLocationSiteId, site, StringComparison.OrdinalIgnoreCase);
+ })
+ .ToList();
+
+ // Update the UI with the filtered results.
+ Schedules = new ObservableCollection(filtered);
+}
private async Task LoadSchedulesAsync()
{
diff --git a/ViewModels/ScannerDashboardViewModel.cs b/ViewModels/ScannerDashboardViewModel.cs
index cc4cccd..d92c067 100644
--- a/ViewModels/ScannerDashboardViewModel.cs
+++ b/ViewModels/ScannerDashboardViewModel.cs
@@ -485,6 +485,77 @@ public partial class ScannerDashboardViewModel : ObservableObject
_navigation.NavigateToAdminSettingsAuth();
}
+ //[RelayCommand]
+ //private void Scan()
+ //{
+ // lock (_submitLock)
+ // {
+ // if (_isSubmitting || IsProcessing || _isCooldownActive)
+ // return;
+ // _isSubmitting = true;
+ // IsProcessing = true;
+ // }
+
+ // try
+ // {
+ // _debounceTimer.Stop();
+
+ // Message = string.Empty;
+ // IsSuccess = false;
+
+ // var cardId = CardIdInput?.Trim() ?? string.Empty;
+ // if (string.IsNullOrWhiteSpace(cardId))
+ // return;
+
+ // var result = _rfidService.ProcessScanDetailed(cardId);
+ // IsSuccess = result.Success;
+ // Message = result.Message;
+
+ // if (!result.Success && result.CooldownSecondsRemaining > 0)
+ // {
+ // // Requirement: when blocked due to cooldown, keep the scanned ID visible
+ // // so users understand which card was blocked.
+ // CardIdInput = cardId;
+ // StartCooldownCountdown(result.CooldownSecondsRemaining);
+ // return;
+ // }
+
+ // // Clear input for normal success/failure so the next scan starts cleanly.
+ // CardIdInput = string.Empty;
+
+ // if (result.Success)
+ // {
+ // // Update left panel with HRMS employee data from this scan
+ // if (result.EmployeeInfo is { } info)
+ // {
+ // EmployeeId = !string.IsNullOrWhiteSpace(info.EmployeeId) ? info.EmployeeId : "—";
+ // var fullName = string.Join(" ", new[] { info.FirstName, info.MiddleName }.Where(s => !string.IsNullOrWhiteSpace(s))).Trim();
+ // EmployeeName = string.IsNullOrEmpty(fullName) ? "—" : fullName;
+ // EmployeeDepartment = !string.IsNullOrWhiteSpace(info.DepartmentTitle) ? info.DepartmentTitle : "—";
+ // EmployeeDepartmentType = !string.IsNullOrWhiteSpace(info.DepartmentType) ? info.DepartmentType : "—";
+ // // Load menu using site from employee_rfid_tag.location_site_id (not config)
+ // if (int.TryParse(info.LocationSiteId?.Trim(), out var siteFromRfid))
+ // //_ = LoadMenuForSiteAsync(siteFromRfid, result.MealSession);
+ // // Load employee photo from hrms.employee_photo by parent_document_id (employee document id)
+ // _ = LoadEmployeePhotoAsync(info.ParentDocumentId);
+ // }
+ // // Refresh dashboard stats and order history
+ // _ = RefreshDashboardAfterSuccessfulScanAsync(cardId);
+ // }
+ // }
+ // finally
+ // {
+ // lock (_submitLock)
+ // {
+ // _isSubmitting = false;
+ // // Cooldown keeps the UI locked; otherwise unlock after scan completes.
+ // if (!_isCooldownActive)
+ // IsProcessing = false;
+ // }
+ // }
+ //}
+
+
[RelayCommand]
private void Scan()
{
@@ -511,10 +582,34 @@ public partial class ScannerDashboardViewModel : ObservableObject
IsSuccess = result.Success;
Message = result.Message;
+ // Check if this is a site mismatch error
+ if (!result.Success && result.Message.Contains("not allowed to scan here"))
+ {
+ // Clear input for site mismatch so the next scan can start fresh
+ CardIdInput = string.Empty;
+
+ // The employee info is included so we can show who tried to scan
+ if (result.EmployeeInfo != null)
+ {
+ var fullName = string.Join(" ", new[] { result.EmployeeInfo.FirstName, result.EmployeeInfo.MiddleName }
+ .Where(s => !string.IsNullOrWhiteSpace(s))).Trim();
+
+ // Optionally update the UI with the employee info even though scan failed
+ EmployeeName = string.IsNullOrEmpty(fullName) ? "—" : fullName;
+ EmployeeId = !string.IsNullOrWhiteSpace(result.EmployeeInfo.EmployeeId) ? result.EmployeeInfo.EmployeeId : "—";
+ EmployeeDepartment = !string.IsNullOrWhiteSpace(result.EmployeeInfo.DepartmentTitle) ? result.EmployeeInfo.DepartmentTitle : "—";
+ EmployeeDepartmentType = !string.IsNullOrWhiteSpace(result.EmployeeInfo.DepartmentType) ? result.EmployeeInfo.DepartmentType : "—";
+
+ // Load employee photo
+ _ = LoadEmployeePhotoAsync(result.EmployeeInfo.ParentDocumentId);
+ }
+
+ return;
+ }
+
if (!result.Success && result.CooldownSecondsRemaining > 0)
{
- // Requirement: when blocked due to cooldown, keep the scanned ID visible
- // so users understand which card was blocked.
+ // Keep the scanned ID visible during cooldown
CardIdInput = cardId;
StartCooldownCountdown(result.CooldownSecondsRemaining);
return;
@@ -533,10 +628,8 @@ public partial class ScannerDashboardViewModel : ObservableObject
EmployeeName = string.IsNullOrEmpty(fullName) ? "—" : fullName;
EmployeeDepartment = !string.IsNullOrWhiteSpace(info.DepartmentTitle) ? info.DepartmentTitle : "—";
EmployeeDepartmentType = !string.IsNullOrWhiteSpace(info.DepartmentType) ? info.DepartmentType : "—";
- // Load menu using site from employee_rfid_tag.location_site_id (not config)
- if (int.TryParse(info.LocationSiteId?.Trim(), out var siteFromRfid))
- //_ = LoadMenuForSiteAsync(siteFromRfid, result.MealSession);
- // Load employee photo from hrms.employee_photo by parent_document_id (employee document id)
+
+ // Load employee photo
_ = LoadEmployeePhotoAsync(info.ParentDocumentId);
}
// Refresh dashboard stats and order history