using Microsoft.EntityFrameworkCore;
using MySqlConnector;
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;
///
/// 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.
///
public class RfidService : IRfidService
{
private readonly IDbContextFactory _dbFactory;
private readonly IConfigService _configService;
private readonly IEmployeeLookupService _employeeLookup;
private readonly IMealSessionResolver _mealSessionResolver;
private readonly IMenuLookupService _menuLookup;
private readonly ISyncService _syncService;
public RfidService(
IDbContextFactory dbFactory,
IConfigService configService,
IEmployeeLookupService employeeLookup,
IMealSessionResolver mealSessionResolver,
IMenuLookupService menuLookup,
ISyncService syncService)
{
_dbFactory = dbFactory;
_configService = configService;
_employeeLookup = employeeLookup;
_mealSessionResolver = mealSessionResolver;
_menuLookup = menuLookup;
_syncService = syncService;
}
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;
var mealLabel = resolvedSession.MealName;
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);
}
// Production duplicate check by employee serial + date + meal + site.
// Different meal on same day is allowed.
if (int.TryParse(new string(siteIdForSession.Where(char.IsDigit).ToArray()), out var siteNumericForDup) &&
siteNumericForDup > 0 &&
HasProductionMealAlreadyTaken(employee.EmployeeId, siteNumericForDup, mealLabel, nowLocal.Date))
{
return new ScanResult(false, "Meal already taken", 0, 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 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,
Designation = employee.PositionTitle ?? string.Empty
};
db.LunchOrderTransactions.Add(record);
db.SaveChanges();
// If we can reach production now, try posting immediately.
// On success (or duplicate already in production), SyncService will mark IsSynced=1.
if (CanReachProduction())
{
try
{
_syncService.SyncNowAsync().GetAwaiter().GetResult();
}
catch (Exception ex)
{
Logger.Log(ex, "RfidService.ProcessScanDetailed immediate sync");
}
}
return new ScanResult(true, "Order recorded successfully.", 0, employee, session, normalizedEmployeeSite, normalizedCurrentSite);
}
private bool HasProductionMealAlreadyTaken(string? employeeSerialNumber, int siteIdNumeric, string mealName, DateTime orderDateLocal)
{
var employeeSerial = employeeSerialNumber?.Trim() ?? string.Empty;
var meal = mealName?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(employeeSerial) || string.IsNullOrWhiteSpace(meal) || siteIdNumeric <= 0)
return false;
var hrmsConnStr = _configService.GetHrmsLookupConnectionString();
if (string.IsNullOrWhiteSpace(hrmsConnStr))
return false;
const string sql = @"
SELECT 1
FROM lunch_order
WHERE employee_serial_number = @EmployeeSerialNumber
AND order_date = @OrderDate
AND meal_name = @MealName
AND location_site_id = @LocationSiteId
LIMIT 1";
try
{
using var conn = new MySqlConnection(hrmsConnStr);
conn.Open();
using var cmd = new MySqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@EmployeeSerialNumber", employeeSerial);
cmd.Parameters.AddWithValue("@OrderDate", orderDateLocal.Date);
cmd.Parameters.AddWithValue("@MealName", meal);
cmd.Parameters.AddWithValue("@LocationSiteId", siteIdNumeric);
var exists = cmd.ExecuteScalar();
return exists != null && exists != DBNull.Value;
}
catch (Exception ex)
{
Logger.Log(ex, "RfidService.HasProductionMealAlreadyTaken");
return false;
}
}
private bool CanReachProduction()
{
var connStr = _configService.GetMySqlConnectionString();
if (string.IsNullOrWhiteSpace(connStr))
return false;
try
{
using var conn = new MySqlConnection(connStr);
conn.Open();
return true;
}
catch
{
return false;
}
}
//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);
//}
///
/// 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)
//{
// 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 GetLastScans(int count)
{
if (count <= 0) return Array.Empty();
using var db = _dbFactory.CreateDbContext();
return db.LunchOrderTransactions
.OrderByDescending(r => r.ScanTime)
.Take(count)
.ToList();
}
public IReadOnlyList 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 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 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 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 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;
}
}
}