diff --git a/Services/HrmsMenuItemForMapping.cs b/Services/HrmsMenuItemForMapping.cs new file mode 100644 index 0000000..29d66e0 --- /dev/null +++ b/Services/HrmsMenuItemForMapping.cs @@ -0,0 +1,45 @@ +namespace UtopiaCanteenSystem.Services; + +/// +/// Maps HRMS employee_rfid_tag.grade_type to menu_item.item_for. +/// Menu rows only use MANAGEMENT / NON_MANAGEMENT; other grades must map to one of those. +/// Contractual and MTO/TE/Intern use the management menu; Unassigned uses non-management. +/// +public static class HrmsMenuItemForMapping +{ + /// + /// Returns MANAGEMENT or NON_MANAGEMENT for menu queries. Never returns empty. + /// + public static string FromGradeType(string? gradeType) + { + var compact = ToCompactAlphaNum(gradeType); + if (string.IsNullOrEmpty(compact)) + return "NON_MANAGEMENT"; + + if (compact == "management") + return "MANAGEMENT"; + + if (compact == "nonmanagement") + return "NON_MANAGEMENT"; + + if (compact == "contractual" || compact == "mtoteintern") + return "MANAGEMENT"; + + if (compact == "unassigned") + return "NON_MANAGEMENT"; + + if (compact.StartsWith("non", StringComparison.Ordinal) && compact.Contains("management", StringComparison.Ordinal)) + return "NON_MANAGEMENT"; + + // Any other label (e.g. future grade codes) — use non-management menu so items still resolve. + return "NON_MANAGEMENT"; + } + + private static string ToCompactAlphaNum(string? s) + { + if (string.IsNullOrWhiteSpace(s)) + return string.Empty; + var chars = s.Trim().ToLowerInvariant().Where(char.IsLetterOrDigit).ToArray(); + return new string(chars); + } +} diff --git a/Services/IMenuLookupService.cs b/Services/IMenuLookupService.cs index a96089a..a299041 100644 --- a/Services/IMenuLookupService.cs +++ b/Services/IMenuLookupService.cs @@ -20,5 +20,5 @@ public interface IMenuLookupService /// //Task> GetMenuItemsForSiteAndDateAsync(int siteIdNumeric, DateTime menuDateLocal, CancellationToken cancellationToken = default); - Task> GetMenuItemsForSiteAndDateAsync(int siteIdNumeric, DateTime menuDateLocal, string gradeType, CancellationToken cancellationToken = default); + Task> GetMenuItemsForSiteAndDateAsync(int siteIdNumeric, DateTime menuDateLocal, string gradeType, string mealName ,CancellationToken cancellationToken = default); } diff --git a/Services/MenuLookupService.cs b/Services/MenuLookupService.cs index 8d36774..3b10075 100644 --- a/Services/MenuLookupService.cs +++ b/Services/MenuLookupService.cs @@ -147,40 +147,104 @@ public class MenuLookupService : IMenuLookupService public async Task> GetMenuItemsForSiteAsync(int siteIdNumeric, CancellationToken cancellationToken = default) { // Backwards-compatible: use today's local date. - return await GetMenuItemsForSiteAndDateAsync(siteIdNumeric, DateTime.Today, "NonManagement", 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> GetMenuItemsForSiteAndDateAsync(int siteIdNumeric, DateTime menuDateLocal, string gradeType, CancellationToken cancellationToken = default) + //public async Task> GetMenuItemsForSiteAndDateAsync(int siteIdNumeric, DateTime menuDateLocal, string gradeType, CancellationToken cancellationToken = default) + //{ + // var connectionString = _configService.GetHrmsLookupConnectionString(); + // if (string.IsNullOrWhiteSpace(connectionString)) + // return Array.Empty(); + + // 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(); + // 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> GetMenuItemsForSiteAndDateAsync( + int siteIdNumeric, + DateTime menuDateLocal, + string gradeType, + string mealName, // Add mealName parameter + CancellationToken cancellationToken = default) { var connectionString = _configService.GetHrmsLookupConnectionString(); if (string.IsNullOrWhiteSpace(connectionString)) return Array.Empty(); + // 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 - 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;"; + 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;"; - var itemFor = GetItemForFromGradeType(gradeType); + var itemFor = HrmsMenuItemForMapping.FromGradeType(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("@mealName", mealName); // Add mealName parameter cmd.Parameters.AddWithValue("@itemFor", itemFor); var list = new List(); @@ -195,26 +259,12 @@ public class MenuLookupService : IMenuLookupService Price = GetDecimal(reader, 3), MenuDate = GetString(reader, 4), DayOfWeek = GetString(reader, 5), - MealName = string.Empty + MealName = GetString(reader, 6) // Map meal_name from column index 6 }); } return list; } - - private static string GetItemForFromGradeType(string? gradeType) - { - var value = (gradeType ?? string.Empty).Trim().ToLowerInvariant(); - - return value switch - { - "management" => "MANAGEMENT", - "nonmanagement" => "NON_MANAGEMENT", - "non_management" => "NON_MANAGEMENT", - _ => string.Empty - }; - } - private static string GetString(MySqlDataReader reader, int ordinal) { if (reader.IsDBNull(ordinal)) return string.Empty; diff --git a/Services/RfidService.cs b/Services/RfidService.cs index 7041308..24aea08 100644 --- a/Services/RfidService.cs +++ b/Services/RfidService.cs @@ -195,6 +195,7 @@ public class RfidService : IRfidService //} + public ScanResult ProcessScanDetailed(string cardId) { if (string.IsNullOrWhiteSpace(cardId)) @@ -240,9 +241,9 @@ public class RfidService : IRfidService { // 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"); + 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: /* @@ -326,8 +327,9 @@ public class RfidService : IRfidService { 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) + .GetMenuItemsForSiteAndDateAsync(siteNumeric, nowLocal.Date, employee.GradeType, mealLabel) .GetAwaiter() .GetResult(); @@ -381,6 +383,193 @@ public class RfidService : IRfidService 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); + //} + /// /// Normalizes a site ID to a consistent format for comparison /// Handles formats like "1", "01", "SITE : 1", "Site 01", etc. diff --git a/Services/SyncService.cs b/Services/SyncService.cs index e034333..9cbbdf0 100644 --- a/Services/SyncService.cs +++ b/Services/SyncService.cs @@ -81,6 +81,52 @@ public class SyncService : ISyncService SET code = @Code WHERE id = @Id"; + const string findLunchMenuItemSql = @" + SELECT + li.id AS lunch_menu_item_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 @MenuDate BETWEEN w.week_start_date AND w.week_end_date + AND li.menu_date = @MenuDate + AND li.meal_name = @MealName + AND mi.item_name = @ItemName + AND (@ItemFor = '' OR mi.item_for = @ItemFor) + LIMIT 1"; + + const string findLunchMenuItemFallbackSql = @" + SELECT + li.id AS lunch_menu_item_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 @MenuDate BETWEEN w.week_start_date AND w.week_end_date + AND li.menu_date = @MenuDate + AND mi.item_name = @ItemName + AND (@ItemFor = '' OR mi.item_for = @ItemFor) + LIMIT 1"; + + const string existsOrderItemSql = @" + SELECT id + FROM lunch_order_item + WHERE lunch_order_id = @LunchOrderId + AND lunch_menu_item_id = @LunchMenuItemId + LIMIT 1"; + + const string insertOrderItemSql = @" + INSERT INTO lunch_order_item + (lunch_order_id, lunch_menu_item_id, quantity, price_at_order_time, item_name, item_type) + VALUES + (@LunchOrderId, @LunchMenuItemId, @Quantity, @PriceAtOrderTime, @ItemName, @ItemType)"; + var syncedIds = new List(capacity: toSync.Count); @@ -120,7 +166,7 @@ public class SyncService : ISyncService { await hrmsConn.OpenAsync(cancellationToken).ConfigureAwait(false); - var alreadyExists = false; + long lunchOrderId = 0; await using (var existsCmd = new MySqlCommand(existsOrderSql, hrmsConn)) { existsCmd.Parameters.AddWithValue("@EmployeeId", employeeIdBigint); @@ -129,10 +175,11 @@ public class SyncService : ISyncService existsCmd.Parameters.AddWithValue("@CreatedAt", createdAt); existsCmd.Parameters.AddWithValue("@MealName", mealName); var existing = await existsCmd.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false); - alreadyExists = existing != null && existing != DBNull.Value; + if (existing != null && existing != DBNull.Value) + lunchOrderId = Convert.ToInt64(existing); } - if (!alreadyExists) + if (lunchOrderId <= 0) { await using var orderCmd = new MySqlCommand(insertOrderSql, hrmsConn); orderCmd.Parameters.AddWithValue("@EmployeeId", employeeIdBigint); @@ -152,6 +199,7 @@ public class SyncService : ISyncService var insertedId = orderCmd.LastInsertedId; if (insertedId > 0) { + lunchOrderId = insertedId; var code = GenerateLunchOrderCode(insertedId, createdAt); await using var updateCodeCmd = new MySqlCommand(updateOrderCodeSql, hrmsConn); @@ -160,6 +208,65 @@ public class SyncService : ISyncService await updateCodeCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); } } + + if (lunchOrderId > 0) + { + var mealItems = SplitMealItems(record.MealItems); + var itemFor = HrmsMenuItemForMapping.FromGradeType(record.grade_type); + + foreach (var itemName in mealItems) + { + var menuLookup = await FindLunchMenuItemAsync( + hrmsConn, + findLunchMenuItemSql, + locationSiteId, + orderDateLocal, + mealName, + itemName, + itemFor, + cancellationToken).ConfigureAwait(false); + + if (menuLookup is null) + { + menuLookup = await FindLunchMenuItemAsync( + hrmsConn, + findLunchMenuItemFallbackSql, + locationSiteId, + orderDateLocal, + mealName, + itemName, + itemFor, + cancellationToken).ConfigureAwait(false); + } + + if (menuLookup is null) + { + System.Diagnostics.Debug.WriteLine($"Menu item mapping not found for '{itemName}' (site={locationSiteId}, date={orderDateLocal:yyyy-MM-dd}, meal={mealName})."); + continue; + } + + var itemExists = false; + await using (var existsItemCmd = new MySqlCommand(existsOrderItemSql, hrmsConn)) + { + existsItemCmd.Parameters.AddWithValue("@LunchOrderId", lunchOrderId); + existsItemCmd.Parameters.AddWithValue("@LunchMenuItemId", menuLookup.Value.LunchMenuItemId); + var existingItem = await existsItemCmd.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false); + itemExists = existingItem != null && existingItem != DBNull.Value; + } + + if (itemExists) + continue; + + await using var insertItemCmd = new MySqlCommand(insertOrderItemSql, hrmsConn); + insertItemCmd.Parameters.AddWithValue("@LunchOrderId", lunchOrderId); + insertItemCmd.Parameters.AddWithValue("@LunchMenuItemId", menuLookup.Value.LunchMenuItemId); + insertItemCmd.Parameters.AddWithValue("@Quantity", 1); + insertItemCmd.Parameters.AddWithValue("@PriceAtOrderTime", menuLookup.Value.Price); + insertItemCmd.Parameters.AddWithValue("@ItemName", menuLookup.Value.ItemName); + insertItemCmd.Parameters.AddWithValue("@ItemType", menuLookup.Value.ItemType); + await insertItemCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + } + } } syncedIds.Add(record.Id); @@ -228,6 +335,50 @@ public class SyncService : ISyncService } } + private static IReadOnlyList SplitMealItems(string? mealItems) + { + if (string.IsNullOrWhiteSpace(mealItems)) + return Array.Empty(); + + return mealItems + .Split('+', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Trim()) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + } + + private static async Task<(long LunchMenuItemId, string ItemName, string ItemType, decimal Price)?> FindLunchMenuItemAsync( + MySqlConnection conn, + string sql, + int siteId, + DateTime menuDate, + string mealName, + string itemName, + string itemFor, + CancellationToken cancellationToken) + { + await using var cmd = new MySqlCommand(sql, conn); + cmd.Parameters.AddWithValue("@SiteId", siteId); + cmd.Parameters.AddWithValue("@MenuDate", menuDate.Date); + cmd.Parameters.AddWithValue("@MealName", mealName); + cmd.Parameters.AddWithValue("@ItemName", itemName); + cmd.Parameters.AddWithValue("@ItemFor", itemFor); + + await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); + if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + return null; + + var lunchMenuItemId = reader.IsDBNull(0) ? 0 : Convert.ToInt64(reader.GetValue(0)); + if (lunchMenuItemId <= 0) + return null; + + var resolvedItemName = reader.IsDBNull(1) ? string.Empty : reader.GetValue(1)?.ToString() ?? string.Empty; + var itemType = reader.IsDBNull(2) ? string.Empty : reader.GetValue(2)?.ToString() ?? string.Empty; + var price = reader.IsDBNull(3) ? 0m : Convert.ToDecimal(reader.GetValue(3)); + return (lunchMenuItemId, resolvedItemName, itemType, price); + } + /// /// Deletes only synced rows (IsSynced = 1) where ScanTime is before the start of today (local day). ///