Merge pull request 'fixes' (#2) from fixes into main

Reviewed-on: #2
pull/3/head^2
SYED MUSTUFA AHMED NAQVI 2026-03-18 08:42:20 +00:00
commit 390361ce83
7 changed files with 360 additions and 87 deletions

3
.gitignore vendored
View File

@ -26,6 +26,9 @@ appsettings.json
# Publish
publish/
# Publish Profiles
Properties/PublishProfiles/
# OS
Thumbs.db
.DS_Store

View File

@ -1,34 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project>
<PropertyGroup>
<ApplicationRevision>0</ApplicationRevision>
<ApplicationVersion>1.0.0.*</ApplicationVersion>
<BootstrapperEnabled>True</BootstrapperEnabled>
<Configuration>Release</Configuration>
<CreateWebPageOnPublish>False</CreateWebPageOnPublish>
<GenerateManifests>true</GenerateManifests>
<Install>True</Install>
<InstallFrom>Disk</InstallFrom>
<IsRevisionIncremented>True</IsRevisionIncremented>
<IsWebBootstrapper>False</IsWebBootstrapper>
<MapFileExtensions>True</MapFileExtensions>
<OpenBrowserOnPublish>False</OpenBrowserOnPublish>
<Platform>Any CPU</Platform>
<PublishDir>bin\Release\net8.0-windows\win-x86\app.publish\</PublishDir>
<PublishUrl>D:\Publish\UtopiaCanteenSystem\</PublishUrl>
<PublishProtocol>ClickOnce</PublishProtocol>
<PublishReadyToRun>False</PublishReadyToRun>
<PublishSingleFile>False</PublishSingleFile>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
<SelfContained>True</SelfContained>
<SignatureAlgorithm>(none)</SignatureAlgorithm>
<SignManifests>False</SignManifests>
<SkipPublishVerification>false</SkipPublishVerification>
<TargetFramework>net8.0-windows</TargetFramework>
<UpdateEnabled>False</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
<UpdateRequired>False</UpdateRequired>
<WebPageFileName>Publish.html</WebPageFileName>
</PropertyGroup>
</Project>

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project>
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>D:\</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<_TargetId>Folder</_TargetId>
<TargetFramework>net8.0-windows</TargetFramework>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile>
<PublishReadyToRun>false</PublishReadyToRun>
</PropertyGroup>
</Project>

View File

@ -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);
}
/// <summary>
/// Normalizes a site ID to a consistent format for comparison
/// Handles formats like "1", "01", "SITE : 1", "Site 01", etc.
/// </summary>
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)

View File

@ -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);

View File

@ -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<MealSchedule>(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<MealSchedule>(filtered);
}
private async Task LoadSchedulesAsync()
{

View File

@ -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