792 lines
32 KiB
C#
792 lines
32 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using UtopiaCanteenSystem.Data;
|
|
using UtopiaCanteenSystem.Helpers;
|
|
using UtopiaCanteenSystem.Models;
|
|
|
|
namespace UtopiaCanteenSystem.Services;
|
|
|
|
/// <summary>
|
|
/// Handles RFID scan logic: validates input, enforces configurable timeout,
|
|
/// and saves scan events to SQLite (lunch_order_transactions table).
|
|
/// Validates card against local HRMS (employee lookup) before accepting scan.
|
|
/// </summary>
|
|
public class RfidService : IRfidService
|
|
{
|
|
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
|
private readonly IConfigService _configService;
|
|
private readonly IEmployeeLookupService _employeeLookup;
|
|
private readonly IMealSessionResolver _mealSessionResolver;
|
|
private readonly IMenuLookupService _menuLookup;
|
|
|
|
public RfidService(
|
|
IDbContextFactory<AppDbContext> dbFactory,
|
|
IConfigService configService,
|
|
IEmployeeLookupService employeeLookup,
|
|
IMealSessionResolver mealSessionResolver,
|
|
IMenuLookupService menuLookup)
|
|
{
|
|
_dbFactory = dbFactory;
|
|
_configService = configService;
|
|
_employeeLookup = employeeLookup;
|
|
_mealSessionResolver = mealSessionResolver;
|
|
_menuLookup = menuLookup;
|
|
}
|
|
|
|
public (bool Success, string Message) ProcessScan(string cardId)
|
|
{
|
|
var result = ProcessScanDetailed(cardId);
|
|
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))
|
|
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;
|
|
|
|
// 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 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, employee);
|
|
|
|
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 = resolvedSession.MealName;
|
|
return new ScanResult(false, $"This card is already scanned for {sessionName} today.", 0, employee, session);
|
|
}
|
|
|
|
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,
|
|
employee,
|
|
session);
|
|
}
|
|
|
|
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
|
|
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)
|
|
{
|
|
// Pass mealLabel as the mealName parameter
|
|
var menuItems = _menuLookup
|
|
.GetMenuItemsForSiteAndDateAsync(siteNumeric, nowLocal.Date, employee.GradeType, mealLabel)
|
|
.GetAwaiter()
|
|
.GetResult();
|
|
|
|
var authorized = menuItems
|
|
.Where(i => i.MenuItemId > 0)
|
|
.Where(i =>
|
|
{
|
|
try
|
|
{
|
|
return _employeeLookup
|
|
.IsMenuItemAuthorizedForRfidAsync(cardId, i.MenuItemId)
|
|
.GetAwaiter()
|
|
.GetResult();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.Log(ex, $"RfidService.EmployeeMenuValidation card={cardId}, menuItemId={i.MenuItemId}");
|
|
return false;
|
|
}
|
|
})
|
|
.ToList();
|
|
|
|
// Fallback: if employee_menu_item_tag has no matches for this employee/session,
|
|
// still allow showing the menu items fetched from HRMS menu_item.
|
|
var matching = authorized.Count > 0
|
|
? authorized
|
|
: menuItems.ToList();
|
|
|
|
if (authorized.Count == 0)
|
|
{
|
|
Logger.Log(
|
|
new Exception($"No employee_menu_item_tag match; using fallback menu. card={cardId}, site={siteNumeric}, date={nowLocal:yyyy-MM-dd}, meal={mealLabel}, grade={employee.GradeType}"),
|
|
"RfidService.ProcessScanDetailed authorization");
|
|
}
|
|
|
|
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,
|
|
grade_type = employee.GradeType ?? string.Empty
|
|
};
|
|
db.LunchOrderTransactions.Add(record);
|
|
db.SaveChanges();
|
|
|
|
return new ScanResult(true, "Order recorded successfully.", 0, employee, session, normalizedEmployeeSite, normalizedCurrentSite);
|
|
}
|
|
|
|
|
|
//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;
|
|
|
|
// // 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 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, employee);
|
|
|
|
// 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 = resolvedSession.MealName;
|
|
// return new ScanResult(false, $"This card is already scanned for {sessionName} today.", 0, employee, session);
|
|
// }
|
|
|
|
// 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,
|
|
// employee,
|
|
// session);
|
|
// }
|
|
|
|
// 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
|
|
// 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, employee.GradeType)
|
|
// .GetAwaiter()
|
|
// .GetResult();
|
|
|
|
// 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,
|
|
// grade_type = employee.GradeType ?? string.Empty
|
|
// };
|
|
// db.LunchOrderTransactions.Add(record);
|
|
// db.SaveChanges();
|
|
|
|
// 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)
|
|
//{
|
|
// return session switch
|
|
// {
|
|
// MealSession.Breakfast => "Sehri",
|
|
// MealSession.Lunch => "Iftari",
|
|
// MealSession.Tea => "Tea",
|
|
// MealSession.Dinner => "Dinner",
|
|
// _ => "this meal"
|
|
// };
|
|
//}
|
|
|
|
public ScanRecord? GetLastScan()
|
|
{
|
|
using var db = _dbFactory.CreateDbContext();
|
|
return db.LunchOrderTransactions
|
|
.OrderByDescending(r => r.ScanTime)
|
|
.FirstOrDefault();
|
|
}
|
|
|
|
public IReadOnlyList<ScanRecord> GetLastScans(int count)
|
|
{
|
|
if (count <= 0) return Array.Empty<ScanRecord>();
|
|
using var db = _dbFactory.CreateDbContext();
|
|
return db.LunchOrderTransactions
|
|
.OrderByDescending(r => r.ScanTime)
|
|
.Take(count)
|
|
.ToList();
|
|
}
|
|
|
|
public IReadOnlyList<ScanRecord> GetScansForToday()
|
|
{
|
|
var startOfTodayLocal = DateTime.Today;
|
|
var startOfTomorrowLocal = startOfTodayLocal.AddDays(1);
|
|
var startUtc = startOfTodayLocal.ToUniversalTime();
|
|
var endUtc = startOfTomorrowLocal.ToUniversalTime();
|
|
using var db = _dbFactory.CreateDbContext();
|
|
return db.LunchOrderTransactions
|
|
.Where(r => r.ScanTime >= startUtc && r.ScanTime < endUtc)
|
|
.OrderByDescending(r => r.ScanTime)
|
|
.ToList();
|
|
}
|
|
|
|
public int GetTodayScanCount()
|
|
{
|
|
// Define "today" by the local calendar day, but ScanTime is stored as UTC.
|
|
// Convert local day boundaries to UTC for correct comparisons.
|
|
var startOfTodayLocal = DateTime.Today;
|
|
var startOfTomorrowLocal = startOfTodayLocal.AddDays(1);
|
|
var startUtc = startOfTodayLocal.ToUniversalTime();
|
|
var endUtc = startOfTomorrowLocal.ToUniversalTime();
|
|
|
|
using var db = _dbFactory.CreateDbContext();
|
|
return db.LunchOrderTransactions
|
|
.Count(r => r.ScanTime >= startUtc && r.ScanTime < endUtc);
|
|
}
|
|
|
|
public async Task<int> GetTodayScanCountAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
// Define "today" by the local calendar day, but ScanTime is stored as UTC.
|
|
// Convert local day boundaries to UTC for correct comparisons.
|
|
var startOfTodayLocal = DateTime.Today;
|
|
var startOfTomorrowLocal = startOfTodayLocal.AddDays(1);
|
|
var startUtc = startOfTodayLocal.ToUniversalTime();
|
|
var endUtc = startOfTomorrowLocal.ToUniversalTime();
|
|
|
|
await using var db = await _dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
|
return await db.LunchOrderTransactions
|
|
.CountAsync(r => r.ScanTime >= startUtc && r.ScanTime < endUtc, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
public async Task<int> GetTotalScanCountAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
await using var db = await _dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
|
return await db.LunchOrderTransactions.CountAsync(cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
public async Task<int> GetTotalScanCountForCardAsync(string cardId, CancellationToken cancellationToken = default)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(cardId))
|
|
return 0;
|
|
|
|
cardId = cardId.Trim();
|
|
|
|
await using var db = await _dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
|
return await db.LunchOrderTransactions
|
|
.CountAsync(r => r.CardId == cardId, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
public async Task<int> GetTodayScanCountForCardAsync(string cardId, CancellationToken cancellationToken = default)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(cardId))
|
|
return 0;
|
|
|
|
cardId = cardId.Trim();
|
|
|
|
// Define "today" by the local calendar day, but ScanTime is stored as UTC.
|
|
// Convert local day boundaries to UTC for correct comparisons.
|
|
var startOfTodayLocal = DateTime.Today;
|
|
var startOfTomorrowLocal = startOfTodayLocal.AddDays(1);
|
|
var startUtc = startOfTodayLocal.ToUniversalTime();
|
|
var endUtc = startOfTomorrowLocal.ToUniversalTime();
|
|
|
|
await using var db = await _dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
|
return await db.LunchOrderTransactions
|
|
.CountAsync(r => r.CardId == cardId && r.ScanTime >= startUtc && r.ScanTime < endUtc, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
public void UpdateLastScanMealInfo(string mealLabel, string mealItems, double totalPrice)
|
|
{
|
|
using var db = _dbFactory.CreateDbContext();
|
|
var last = db.LunchOrderTransactions
|
|
.OrderByDescending(r => r.ScanTime)
|
|
.FirstOrDefault();
|
|
if (last == null)
|
|
return;
|
|
|
|
last.MealLabel = mealLabel ?? string.Empty;
|
|
last.MealItems = mealItems ?? string.Empty;
|
|
last.TotalPrice = totalPrice;
|
|
db.SaveChanges();
|
|
}
|
|
|
|
private static string FormatTimeout(int seconds)
|
|
{
|
|
if (seconds <= 0)
|
|
return "the timeout window";
|
|
|
|
var minutes = seconds / 60;
|
|
var secs = seconds % 60;
|
|
|
|
if (minutes == 0)
|
|
return secs == 1 ? "1 second" : $"{secs} seconds";
|
|
|
|
if (secs == 0)
|
|
return minutes == 1 ? "1 minute" : $"{minutes} minutes";
|
|
|
|
var minutePart = minutes == 1 ? "1 minute" : $"{minutes} minutes";
|
|
var secondPart = secs == 1 ? "1 second" : $"{secs} seconds";
|
|
return $"{minutePart} {secondPart}";
|
|
}
|
|
|
|
private static int GetCooldownRemainingSeconds(DateTime nowUtc, DateTime lastScanUtc, int timeoutSeconds)
|
|
{
|
|
// Remaining = (lastScan + timeout) - now. Use ceiling so UI shows a whole-second countdown.
|
|
var endUtc = lastScanUtc.AddSeconds(timeoutSeconds);
|
|
var remaining = (int)Math.Ceiling((endUtc - nowUtc).TotalSeconds);
|
|
return Math.Max(0, remaining);
|
|
}
|
|
|
|
private static string GetLocalIpAddress()
|
|
{
|
|
try
|
|
{
|
|
var host = Dns.GetHostEntry(Dns.GetHostName());
|
|
var ip = host.AddressList.FirstOrDefault(a => a.AddressFamily == AddressFamily.InterNetwork);
|
|
return ip?.ToString() ?? string.Empty;
|
|
}
|
|
catch
|
|
{
|
|
return string.Empty;
|
|
}
|
|
}
|
|
}
|