diff --git a/Services/SyncService.cs b/Services/SyncService.cs index e034333..e415305 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 = GradeTypeToItemFor(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,62 @@ public class SyncService : ISyncService } } + private static string GradeTypeToItemFor(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 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). ///