Use offline cache for RFID employee, session, and menu lookup

Switches RFID employee lookup, meal session resolution, and menu lookup from live HRMS queries to the local SQLite cache.

Adds cache availability checks and shared site ID normalization for scan validation.
feature/centralized-offline-canteen
SYED MUSTUFA AHMED NAQVI 2026-05-21 09:31:10 +05:00
parent 1d427b6e18
commit 37f52bc3c6
8 changed files with 256 additions and 451 deletions

View File

@ -1,7 +1,7 @@
namespace UtopiaCanteenSystem.Models;
/// <summary>
/// Employee info loaded from local HRMS by RFID (employee_rfid_tag → employee → department).
/// Employee info from offline RFID cache (synced from employee_rfid_tag → employee → department).
/// </summary>
public class HrmsEmployeeInfo
{

View File

@ -1,24 +1,41 @@
using Microsoft.EntityFrameworkCore;
using UtopiaCanteenSystem.Data;
using UtopiaCanteenSystem.Models;
namespace UtopiaCanteenSystem.Services;
/// <summary>
/// Resolves current meal session from production (MySQL meal_schedule). Caches schedules per site for 60 seconds.
/// No SQLite; scan validation uses production data only.
/// Resolves current meal session from local SQLite <c>meal_schedule_cache</c>.
/// </summary>
public class DbMealSessionResolver : IMealSessionResolver
{
private readonly IMealScheduleService _mealScheduleService;
public const string OfflineCacheMissingMessage =
"Meal schedule/menu cache not found. Please sync once while online.";
private readonly IDbContextFactory<AppDbContext> _dbFactory;
private readonly TimeSpan _cacheTtl = TimeSpan.FromSeconds(60);
private readonly Dictionary<string, (List<MealSchedule> Schedules, DateTime ExpiryUtc)> _cache = new(StringComparer.OrdinalIgnoreCase);
private readonly object _cacheLock = new();
public DbMealSessionResolver(IMealScheduleService mealScheduleService)
public DbMealSessionResolver(IDbContextFactory<AppDbContext> dbFactory)
{
_mealScheduleService = mealScheduleService;
_dbFactory = dbFactory;
}
public bool IsScheduleCacheAvailable()
{
using var db = _dbFactory.CreateDbContext();
return db.MealScheduleCache.Any();
}
public bool IsMenuCacheAvailable()
{
using var db = _dbFactory.CreateDbContext();
return db.MenuItemCache.Any() && db.LunchMenuWeekCache.Any();
}
public bool IsOfflineMealDataAvailable() => IsScheduleCacheAvailable() && IsMenuCacheAvailable();
private static MealSession MapMealNameToSession(string? mealName)
{
@ -39,49 +56,9 @@ public class DbMealSessionResolver : IMealSessionResolver
};
}
/// <inheritdoc />
//public MealSession GetCurrentSession(DateTime nowLocal, string siteId)
//{
// var normalizedSite = (siteId ?? string.Empty).Trim();
// if (string.IsNullOrEmpty(normalizedSite))
// normalizedSite = "01";
// if (normalizedSite.Length == 1 && char.IsDigit(normalizedSite[0]))
// normalizedSite = normalizedSite.PadLeft(2, '0');
// var schedules = GetSchedulesForSiteCached(normalizedSite);
// var t = nowLocal.TimeOfDay;
// //foreach (var s in schedules)
// //{
// // if (!TryParseTime(s.StartTime, out var start) || !TryParseTime(s.EndTime, out var end))
// // continue;
// // if (t >= start && t < end)
// // return (MealSession)s.MealSession;
// //}
// foreach (var s in schedules)
// {
// if (!TryParseTime(s.StartTime, out var start) || !TryParseTime(s.EndTime, out var end))
// continue;
// if (t >= start && t < end)
// return MapMealNameToSession(s.MealName);
// }
// return MealSession.None;
//}
public ResolvedMealSession? GetCurrentSession(DateTime nowLocal, string siteId)
{
var normalizedSite = (siteId ?? string.Empty).Trim();
if (string.IsNullOrEmpty(normalizedSite))
normalizedSite = "01";
if (normalizedSite.Length == 1 && char.IsDigit(normalizedSite[0]))
normalizedSite = normalizedSite.PadLeft(2, '0');
var normalizedSite = SiteIdHelper.NormalizeSiteId(siteId);
var schedules = GetSchedulesForSiteCached(normalizedSite);
var t = nowLocal.TimeOfDay;
@ -106,7 +83,6 @@ public class DbMealSessionResolver : IMealSessionResolver
return null;
}
private List<MealSchedule> GetSchedulesForSiteCached(string siteId)
{
lock (_cacheLock)
@ -115,7 +91,7 @@ public class DbMealSessionResolver : IMealSessionResolver
return entry.Schedules;
}
var list = _mealScheduleService.GetActiveSchedulesForSite(siteId).ToList();
var list = LoadSchedulesForSiteFromCache(siteId);
lock (_cacheLock)
{
@ -125,6 +101,35 @@ public class DbMealSessionResolver : IMealSessionResolver
return list;
}
private List<MealSchedule> LoadSchedulesForSiteFromCache(string siteId)
{
var siteIdInt = SiteIdHelper.ToInt(siteId);
using var db = _dbFactory.CreateDbContext();
var rows = db.MealScheduleCache
.AsNoTracking()
.Where(x => x.LocationSiteId == siteIdInt)
.OrderBy(x => x.StartTime)
.ToList();
return rows.Select(MapToMealSchedule).ToList();
}
private static MealSchedule MapToMealSchedule(MealScheduleCache row)
{
return new MealSchedule
{
Id = row.HrmsId,
MealName = row.MealName,
LocationSiteId = SiteIdHelper.ToDisplayString(row.LocationSiteId),
StartTime = row.StartTime,
EndTime = row.EndTime,
CreatedAt = row.CreatedAt ?? DateTime.MinValue,
UpdatedAt = row.UpdatedAt ?? DateTime.MinValue,
IsActive = true
};
}
private static bool TryParseTime(string value, out TimeSpan time)
{
time = TimeSpan.Zero;

View File

@ -1,124 +1,83 @@
using Microsoft.EntityFrameworkCore;
using MySqlConnector;
using UtopiaCanteenSystem.Data;
using UtopiaCanteenSystem.Models;
namespace UtopiaCanteenSystem.Services;
/// <summary>
/// Looks up employee from local HRMS MySQL: employee_rfid_tag.manufacturer_serial → employee (parent_document_id = serial_number) → department.
/// Uses IConfigService.GetHrmsLookupConnectionString(); separate from production sync connection.
/// Looks up employee by RFID from local SQLite <c>employee_rfid_tag_cache</c> (synced from production HRMS).
/// Menu authorization and admin site lookup still use HRMS when configured.
/// </summary>
public class EmployeeLookupService : IEmployeeLookupService
{
private readonly IConfigService _configService;
private readonly IDbContextFactory<AppDbContext> _dbFactory;
private readonly IConfigService _configService;
public EmployeeLookupService(IConfigService configService)
{
_configService = configService;
}
public EmployeeLookupService(IDbContextFactory<AppDbContext> dbFactory, IConfigService configService)
{
_dbFactory = dbFactory;
_configService = configService;
}
/// <inheritdoc />
public async Task<HrmsEmployeeInfo?> GetEmployeeByRfidAsync(string rfid, CancellationToken cancellationToken = default)
{
var connectionString = _configService.GetHrmsLookupConnectionString();
if (string.IsNullOrWhiteSpace(connectionString))
return null;
/// <inheritdoc />
public async Task<HrmsEmployeeInfo?> GetEmployeeByRfidAsync(string rfid, CancellationToken cancellationToken = default)
{
var cardId = rfid?.Trim() ?? string.Empty;
if (string.IsNullOrEmpty(cardId))
return null;
const string sql = @"
SELECT
e.id AS parent_document_id,
e.serial_number AS employee_id,
e.concatenated_name AS first_name,
'' AS middle_name,
r.uind_serial AS uind_serial,
r.function_id AS function_id,
r.department_id AS tag_department_id,
r.date_time_created AS tag_date_time_created,
r.created_by AS tag_created_by,
d.title AS department_title,
d.department_type,
r.location_site_id AS location_site_id,
r.grade_type AS grade_type
FROM employee_rfid_tag r
JOIN employee e ON e.id = r.parent_document_id
LEFT JOIN department d ON d.id = e.department_id
WHERE r.manufacturer_serial = @rfid
AND r.parent_document_type = 'Employee'
LIMIT 1";
await using var db = await _dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using var conn = new MySqlConnection(connectionString);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var cmd = new MySqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@rfid", rfid?.Trim() ?? string.Empty);
var tag = await db.EmployeeRfidTagCache
.AsNoTracking()
.Where(x => x.ManufacturerSerial == cardId)
.Where(x => x.ParentDocumentType == "Employee")
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
return null;
if (tag == null)
return null;
var parentDocumentId = GetString(reader, 0);
return new HrmsEmployeeInfo
{
ParentDocumentId = parentDocumentId,
EmployeeId = GetString(reader, 1),
FirstName = GetString(reader, 2),
MiddleName = GetString(reader, 3),
UindSerial = GetString(reader, 4),
FunctionId = GetInt(reader, 5),
DepartmentId = GetInt(reader, 6),
TagCreatedAtUtc = GetDateTimeNullable(reader, 7),
TagCreatedBy = GetString(reader, 8),
DepartmentTitle = GetString(reader, 9),
DepartmentType = GetString(reader, 10),
LocationSiteId = GetString(reader, 11),
GradeType = GetString(reader, 12)
};
}
return MapToHrmsEmployeeInfo(tag);
}
/// <inheritdoc />
public async Task<string?> GetLocationSiteIdByEmployeeSerialAsync(string employeeSerial, CancellationToken cancellationToken = default)
{
var serial = employeeSerial?.Trim() ?? string.Empty;
if (string.IsNullOrEmpty(serial))
return null;
/// <inheritdoc />
public async Task<string?> GetLocationSiteIdByEmployeeSerialAsync(string employeeSerial, CancellationToken cancellationToken = default)
{
var serial = employeeSerial?.Trim() ?? string.Empty;
if (string.IsNullOrEmpty(serial))
return null;
var connectionString = _configService.GetHrmsLookupConnectionString();
if (string.IsNullOrWhiteSpace(connectionString))
return null;
await using var db = await _dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
const string sql = @"
SELECT location_site_id
FROM employee
WHERE serial_number = @serial
LIMIT 1";
var siteId = await db.EmployeeRfidTagCache
.AsNoTracking()
.Where(x => x.ParentDocumentType == "Employee")
.Where(x => x.EmployeeSerialNumber == serial)
.Where(x => !string.IsNullOrEmpty(x.LocationSiteId))
.Select(x => x.LocationSiteId)
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
await using var conn = new MySqlConnection(connectionString);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var cmd = new MySqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@serial", serial);
if (!string.IsNullOrWhiteSpace(siteId))
return siteId.Trim();
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
return null;
return await GetLocationSiteIdFromHrmsAsync(serial, cancellationToken).ConfigureAwait(false);
}
if (reader.IsDBNull(0))
return null;
/// <inheritdoc />
public async Task<bool> IsMenuItemAuthorizedForRfidAsync(string rfid, int menuItemId, CancellationToken cancellationToken = default)
{
var card = rfid?.Trim() ?? string.Empty;
if (string.IsNullOrEmpty(card) || menuItemId <= 0)
return false;
var raw = reader.GetValue(0)?.ToString()?.Trim();
return string.IsNullOrEmpty(raw) ? null : raw;
}
var connectionString = _configService.GetHrmsLookupConnectionString();
if (string.IsNullOrWhiteSpace(connectionString))
return false;
/// <inheritdoc />
public async Task<bool> IsMenuItemAuthorizedForRfidAsync(string rfid, int menuItemId, CancellationToken cancellationToken = default)
{
var card = rfid?.Trim() ?? string.Empty;
if (string.IsNullOrEmpty(card) || menuItemId <= 0)
return false;
var connectionString = _configService.GetHrmsLookupConnectionString();
if (string.IsNullOrWhiteSpace(connectionString))
return false;
const string sql = @"
const string sql = @"
SELECT 1
FROM employee_rfid_tag r
JOIN employee_menu_item_tag em ON em.employee_rfid_tag_id = r.id
@ -126,52 +85,61 @@ public class EmployeeLookupService : IEmployeeLookupService
AND em.item_id = @itemId
LIMIT 1";
await using var conn = new MySqlConnection(connectionString);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var cmd = new MySqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@rfid", card);
cmd.Parameters.AddWithValue("@itemId", menuItemId);
await using var conn = new MySqlConnection(connectionString);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var cmd = new MySqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@rfid", card);
cmd.Parameters.AddWithValue("@itemId", menuItemId);
var exists = await cmd.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
return exists != null && exists != DBNull.Value;
}
var exists = await cmd.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
return exists != null && exists != DBNull.Value;
}
private static string GetString(MySqlDataReader reader, int ordinal)
private static HrmsEmployeeInfo MapToHrmsEmployeeInfo(EmployeeRfidTagCache tag)
{
return new HrmsEmployeeInfo
{
if (reader.IsDBNull(ordinal)) return string.Empty;
var v = reader.GetValue(ordinal);
return v?.ToString() ?? string.Empty;
}
ParentDocumentId = tag.ParentDocumentId,
EmployeeId = tag.EmployeeSerialNumber,
FirstName = tag.EmployeeConcatenatedName,
MiddleName = string.Empty,
UindSerial = tag.UindSerial,
FunctionId = tag.FunctionId,
DepartmentId = tag.DepartmentId,
TagCreatedAtUtc = tag.DateTimeCreated,
TagCreatedBy = tag.CreatedBy,
DepartmentTitle = tag.DepartmentTitle,
DepartmentType = tag.DepartmentType,
LocationSiteId = tag.LocationSiteId,
GradeType = tag.GradeType
};
}
private static int GetInt(MySqlDataReader reader, int ordinal)
{
try
{
if (reader.IsDBNull(ordinal)) return 0;
var v = reader.GetValue(ordinal);
if (v is int i) return i;
if (v is long l) return (int)l;
return int.TryParse(v?.ToString(), out var parsed) ? parsed : 0;
}
catch
{
return 0;
}
}
private async Task<string?> GetLocationSiteIdFromHrmsAsync(string serial, CancellationToken cancellationToken)
{
var connectionString = _configService.GetHrmsLookupConnectionString();
if (string.IsNullOrWhiteSpace(connectionString))
return null;
private static DateTime? GetDateTimeNullable(MySqlDataReader reader, int ordinal)
{
try
{
if (reader.IsDBNull(ordinal)) return null;
var v = reader.GetValue(ordinal);
if (v is DateTime dt) return dt;
if (DateTime.TryParse(v?.ToString(), out var parsed)) return parsed;
return null;
}
catch
{
return null;
}
}
const string sql = @"
SELECT location_site_id
FROM employee
WHERE serial_number = @serial
LIMIT 1";
await using var conn = new MySqlConnection(connectionString);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var cmd = new MySqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@serial", serial);
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
return null;
if (reader.IsDBNull(0))
return null;
var raw = reader.GetValue(0)?.ToString()?.Trim();
return string.IsNullOrEmpty(raw) ? null : raw;
}
}

View File

@ -3,13 +3,13 @@ using UtopiaCanteenSystem.Models;
namespace UtopiaCanteenSystem.Services;
/// <summary>
/// Looks up employee info from local HRMS MySQL by RFID (manufacturer_serial).
/// Uses a separate connection from production sync.
/// Employee lookup by RFID from local SQLite cache; menu authorization may still use HRMS.
/// </summary>
public interface IEmployeeLookupService
{
/// <summary>
/// Finds employee by RFID. Returns null if card is not registered in HRMS.
/// Finds employee by RFID (<c>manufacturer_serial</c>) in local <c>employee_rfid_tag_cache</c>.
/// Returns null if the card is not in the cache (sync from HRMS first).
/// </summary>
Task<HrmsEmployeeInfo?> GetEmployeeByRfidAsync(string rfid, CancellationToken cancellationToken = default);

View File

@ -7,9 +7,17 @@ namespace UtopiaCanteenSystem.Services;
/// </summary>
public interface IMealSessionResolver
{
/// <summary>True when local <c>meal_schedule_cache</c> has at least one row.</summary>
bool IsScheduleCacheAvailable();
/// <summary>True when local menu cache tables have data.</summary>
bool IsMenuCacheAvailable();
/// <summary>True when both schedule and menu caches are populated.</summary>
bool IsOfflineMealDataAvailable();
/// <summary>
/// Returns the active meal session for the given local time and site, or MealSession.None if outside all windows.
/// Returns the active meal session for the given local time and site, or null if outside all windows.
/// </summary>
//MealSession GetCurrentSession(DateTime nowLocal, string siteId);
ResolvedMealSession? GetCurrentSession(DateTime nowLocal, string siteId);
}

View File

@ -3,7 +3,7 @@ using UtopiaCanteenSystem.Models;
namespace UtopiaCanteenSystem.Services;
/// <summary>
/// Fetches lunch menu items from local HRMS by site (lunch_menu_week.location_site_id).
/// Fetches lunch menu items from local SQLite cache by site (synced from HRMS).
/// Uses same HRMS connection as employee lookup.
/// </summary>
public interface IMenuLookupService

View File

@ -1,281 +1,75 @@
using MySqlConnector;
using Microsoft.EntityFrameworkCore;
using UtopiaCanteenSystem.Data;
using UtopiaCanteenSystem.Models;
namespace UtopiaCanteenSystem.Services;
/// <summary>
/// Fetches lunch menu from HRMS: lunch_menu_week (by location_site_id) → lunch_menu_item → menu_item.
/// Uses IConfigService.GetHrmsLookupConnectionString(). Only current week is considered.
/// Fetches lunch menu from local SQLite cache (lunch_menu_week → lunch_menu_item → menu_item).
/// </summary>
//public class MenuLookupService : IMenuLookupService
//{
// private readonly IConfigService _configService;
// public MenuLookupService(IConfigService configService)
// {
// _configService = configService;
// }
// /// <inheritdoc />
// public async Task<IReadOnlyList<HrmsMenuItem>> GetMenuItemsForSiteAsync(int siteIdNumeric, CancellationToken cancellationToken = default)
// {
// // Backwards-compatible: use today's local date.
// return await GetMenuItemsForSiteAndDateAsync(siteIdNumeric, DateTime.Today, cancellationToken).ConfigureAwait(false);
// }
// /// <inheritdoc />
// public async Task<IReadOnlyList<HrmsMenuItem>> GetMenuItemsForSiteAndDateAsync(int siteIdNumeric, DateTime menuDateLocal, CancellationToken cancellationToken = default)
// {
// var connectionString = _configService.GetHrmsLookupConnectionString();
// if (string.IsNullOrWhiteSpace(connectionString))
// return Array.Empty<HrmsMenuItem>();
// //const string sql = @"
// // SELECT DISTINCT mi.id, mi.item_name, mi.item_type, mi.price
// // FROM lunch_menu_week w
// // JOIN lunch_menu_item li ON li.lunch_menu_week_id = w.id
// // JOIN menu_item mi ON mi.id = li.menu_item_id
// // WHERE w.location_site_id = @siteId
// // AND CURDATE() BETWEEN w.week_start_date AND w.week_end_date
// // ORDER BY mi.item_type, mi.item_name";
// const string sql = @"
// SELECT
// mi.id,
// mi.item_name,
// mi.item_type,
// mi.price,
// li.menu_date,
// li.day_of_week
// FROM lunch_menu_week w
// JOIN lunch_menu_item li ON li.lunch_menu_week_id = w.id
// JOIN menu_item mi ON mi.id = li.menu_item_id
// WHERE w.location_site_id = @siteId
// AND @menuDate BETWEEN w.week_start_date AND w.week_end_date
// AND li.menu_date = @menuDate
// ORDER BY mi.item_name;";
// //const string sql = @"
// // SELECT
// // mi.id,
// // mi.item_name,
// // mi.item_type,
// // mi.price,
// // li.meal_name,
// // li.menu_date,
// // li.day_of_week
// // FROM lunch_menu_week w
// // JOIN lunch_menu_item li ON li.lunch_menu_week_id = w.id
// // JOIN menu_item mi ON mi.id = li.menu_item_id
// // WHERE w.location_site_id = @siteId
// // AND @menuDate BETWEEN w.week_start_date AND w.week_end_date
// // AND li.menu_date = @menuDate
// // ORDER BY li.meal_name, mi.item_name;";
// await using var conn = new MySqlConnection(connectionString);
// await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
// await using var cmd = new MySqlCommand(sql, conn);
// cmd.Parameters.AddWithValue("@siteId", siteIdNumeric);
// cmd.Parameters.AddWithValue("@menuDate", menuDateLocal.Date);
// var list = new List<HrmsMenuItem>();
// await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
// while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
// {
// //list.Add(new HrmsMenuItem
// //{
// // MenuItemId = reader.IsDBNull(0) ? 0 : reader.GetInt32(0),
// // ItemName = GetString(reader, 1),
// // ItemType = GetString(reader, 2),
// // Price = GetDecimal(reader, 3)
// //});
// list.Add(new HrmsMenuItem
// {
// MenuItemId = reader.IsDBNull(0) ? 0 : reader.GetInt32(0),
// ItemName = GetString(reader, 1),
// ItemType = GetString(reader, 2),
// Price = GetDecimal(reader, 3),
// MenuDate = GetString(reader, 4),
// DayOfWeek = GetString(reader, 5),
// MealName = string.Empty
// });
// //list.Add(new HrmsMenuItem
// //{
// // MenuItemId = reader.IsDBNull(0) ? 0 : reader.GetInt32(0),
// // ItemName = GetString(reader, 1),
// // ItemType = GetString(reader, 2),
// // Price = GetDecimal(reader, 3),
// // MealName = GetString(reader, 4),
// // MenuDate = GetString(reader, 5),
// // DayOfWeek = GetString(reader, 6),
// //});
// }
// return list;
// }
// private static string GetString(MySqlDataReader reader, int ordinal)
// {
// if (reader.IsDBNull(ordinal)) return string.Empty;
// var v = reader.GetValue(ordinal);
// return v?.ToString() ?? string.Empty;
// }
// private static decimal GetDecimal(MySqlDataReader reader, int ordinal)
// {
// if (reader.IsDBNull(ordinal)) return 0m;
// var v = reader.GetValue(ordinal);
// return v is decimal d ? d : Convert.ToDecimal(v);
// }
//}
public class MenuLookupService : IMenuLookupService
{
private readonly IConfigService _configService;
private readonly IDbContextFactory<AppDbContext> _dbFactory;
public MenuLookupService(IConfigService configService)
public MenuLookupService(IDbContextFactory<AppDbContext> dbFactory)
{
_configService = configService;
_dbFactory = dbFactory;
}
// Implementing the method for fetching menu items by site
public async Task<IReadOnlyList<HrmsMenuItem>> GetMenuItemsForSiteAsync(int siteIdNumeric, CancellationToken cancellationToken = default)
{
// Backwards-compatible: use today's local date.
return await GetMenuItemsForSiteAndDateAsync(siteIdNumeric, DateTime.Today, "NonManagement","no-active-meal-session", cancellationToken).ConfigureAwait(false);
return await GetMenuItemsForSiteAndDateAsync(
siteIdNumeric,
DateTime.Today,
"NonManagement",
"no-active-meal-session",
cancellationToken).ConfigureAwait(false);
}
// Implementing the method for fetching menu items by site, date, and gradeType
//public async Task<IReadOnlyList<HrmsMenuItem>> GetMenuItemsForSiteAndDateAsync(int siteIdNumeric, DateTime menuDateLocal, string gradeType, CancellationToken cancellationToken = default)
//{
// var connectionString = _configService.GetHrmsLookupConnectionString();
// if (string.IsNullOrWhiteSpace(connectionString))
// return Array.Empty<HrmsMenuItem>();
// const string sql = @"
// SELECT
// mi.id,
// mi.item_name,
// mi.item_type,
// mi.price,
// li.menu_date,
// li.day_of_week
// FROM lunch_menu_week w
// JOIN lunch_menu_item li ON li.lunch_menu_week_id = w.id
// JOIN menu_item mi ON mi.id = li.menu_item_id
// WHERE w.location_site_id = @siteId
// AND @menuDate BETWEEN w.week_start_date AND w.week_end_date
// AND li.menu_date = @menuDate
// AND mi.item_for = @itemFor
// ORDER BY mi.item_name;";
// var itemFor = GetItemForFromGradeType(gradeType);
// await using var conn = new MySqlConnection(connectionString);
// await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
// await using var cmd = new MySqlCommand(sql, conn);
// cmd.Parameters.AddWithValue("@siteId", siteIdNumeric);
// cmd.Parameters.AddWithValue("@menuDate", menuDateLocal.Date);
// cmd.Parameters.AddWithValue("@itemFor", itemFor);
// var list = new List<HrmsMenuItem>();
// await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
// while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
// {
// list.Add(new HrmsMenuItem
// {
// MenuItemId = reader.IsDBNull(0) ? 0 : reader.GetInt32(0),
// ItemName = GetString(reader, 1),
// ItemType = GetString(reader, 2),
// Price = GetDecimal(reader, 3),
// MenuDate = GetString(reader, 4),
// DayOfWeek = GetString(reader, 5),
// MealName = string.Empty
// });
// }
// return list;
//}
public async Task<IReadOnlyList<HrmsMenuItem>> GetMenuItemsForSiteAndDateAsync(
int siteIdNumeric,
DateTime menuDateLocal,
string gradeType,
string mealName, // Add mealName parameter
CancellationToken cancellationToken = default)
int siteIdNumeric,
DateTime menuDateLocal,
string gradeType,
string mealName,
CancellationToken cancellationToken = default)
{
var connectionString = _configService.GetHrmsLookupConnectionString();
if (string.IsNullOrWhiteSpace(connectionString))
if (siteIdNumeric <= 0)
return Array.Empty<HrmsMenuItem>();
// Updated SQL to include meal_name filter and select meal_name
const string sql = @"
SELECT
mi.id,
mi.item_name,
mi.item_type,
mi.price,
li.menu_date,
li.day_of_week,
li.meal_name -- Add meal_name to SELECT
FROM lunch_menu_week w
JOIN lunch_menu_item li ON li.lunch_menu_week_id = w.id
JOIN menu_item mi ON mi.id = li.menu_item_id
WHERE w.location_site_id = @siteId
AND @menuDate BETWEEN w.week_start_date AND w.week_end_date
AND li.menu_date = @menuDate
AND li.meal_name = @mealName -- Filter by meal session
AND mi.item_for = @itemFor
ORDER BY mi.item_name;";
await using var db = await _dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
if (!await db.MenuItemCache.AnyAsync(cancellationToken).ConfigureAwait(false))
return Array.Empty<HrmsMenuItem>();
var menuDate = menuDateLocal.Date;
var itemFor = HrmsMenuItemForMapping.FromGradeType(gradeType);
var meal = mealName?.Trim() ?? string.Empty;
await using var conn = new MySqlConnection(connectionString);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var cmd = new MySqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@siteId", siteIdNumeric);
cmd.Parameters.AddWithValue("@menuDate", menuDateLocal.Date);
cmd.Parameters.AddWithValue("@mealName", mealName); // Add mealName parameter
cmd.Parameters.AddWithValue("@itemFor", itemFor);
var list = new List<HrmsMenuItem>();
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
list.Add(new HrmsMenuItem
var query =
from w in db.LunchMenuWeekCache.AsNoTracking()
join li in db.LunchMenuItemCache.AsNoTracking() on w.HrmsId equals li.LunchMenuWeekHrmsId
join mi in db.MenuItemCache.AsNoTracking() on li.MenuItemHrmsId equals mi.HrmsId
where w.LocationSiteId == siteIdNumeric
&& w.WeekStartDate != null
&& w.WeekEndDate != null
&& menuDate >= w.WeekStartDate.Value.Date
&& menuDate <= w.WeekEndDate.Value.Date
&& li.MenuDate != null
&& li.MenuDate.Value.Date == menuDate
&& li.MealName == meal
&& mi.ItemFor == itemFor
orderby mi.ItemName
select new HrmsMenuItem
{
MenuItemId = reader.IsDBNull(0) ? 0 : reader.GetInt32(0),
ItemName = GetString(reader, 1),
ItemType = GetString(reader, 2),
Price = GetDecimal(reader, 3),
MenuDate = GetString(reader, 4),
DayOfWeek = GetString(reader, 5),
MealName = GetString(reader, 6) // Map meal_name from column index 6
});
}
MenuItemId = (int)mi.HrmsId,
ItemName = mi.ItemName,
ItemType = mi.ItemType,
Price = mi.Price,
MenuDate = li.MenuDate != null ? li.MenuDate.Value.ToString("yyyy-MM-dd") : string.Empty,
DayOfWeek = li.DayOfWeek,
MealName = li.MealName
};
return list;
return await query.ToListAsync(cancellationToken).ConfigureAwait(false);
}
private static string GetString(MySqlDataReader reader, int ordinal)
{
if (reader.IsDBNull(ordinal)) return string.Empty;
var v = reader.GetValue(ordinal);
return v?.ToString() ?? string.Empty;
}
private static decimal GetDecimal(MySqlDataReader reader, int ordinal)
{
if (reader.IsDBNull(ordinal)) return 0m;
var v = reader.GetValue(ordinal);
return v is decimal d ? d : Convert.ToDecimal(v);
}
}
}

30
Services/SiteIdHelper.cs Normal file
View File

@ -0,0 +1,30 @@
namespace UtopiaCanteenSystem.Services;
internal static class SiteIdHelper
{
public static int ToInt(string? siteId)
{
var s = (siteId ?? string.Empty).Trim();
if (string.IsNullOrEmpty(s))
return 0;
var digits = new string(s.Where(char.IsDigit).ToArray());
return int.TryParse(digits, out var n) ? n : 0;
}
public static string ToDisplayString(int siteIdInt)
{
if (siteIdInt <= 0)
return "01";
return siteIdInt <= 99 ? siteIdInt.ToString("D2") : siteIdInt.ToString();
}
public static string NormalizeSiteId(string? siteId)
{
var normalized = (siteId ?? string.Empty).Trim();
if (string.IsNullOrEmpty(normalized))
normalized = "01";
if (normalized.Length == 1 && char.IsDigit(normalized[0]))
normalized = normalized.PadLeft(2, '0');
return normalized;
}
}