Merge pull request 'fixes' (#4) from fixes into main

Reviewed-on: #4
pull/5/head^2
SYED MUSTUFA AHMED NAQVI 2026-03-31 14:30:18 +00:00
commit e467831cfb
10 changed files with 317 additions and 107 deletions

View File

@ -1,6 +1,7 @@
using System.Windows;
using System.Threading;
using Microsoft.EntityFrameworkCore;
using System.Net.Http;
using System.Threading;
using System.Windows;
using UtopiaCanteenSystem.Data;
using UtopiaCanteenSystem.Services;
using UtopiaCanteenSystem.ViewModels;
@ -40,7 +41,9 @@ public partial class App : Application
var configService = new ConfigService();
var employeeLookupService = new EmployeeLookupService(configService);
var employeePhotoService = new EmployeePhotoService(configService);
//var employeePhotoService = new EmployeePhotoService(configService);
var httpClient = new HttpClient();
var employeePhotoService = new EmployeePhotoService(configService, httpClient);
var menuLookupService = new MenuLookupService(configService);
var mealScheduleService = new ProductionMealScheduleService(configService);
var mealSessionResolver = new DbMealSessionResolver(mealScheduleService);

View File

@ -25,4 +25,5 @@ public class HrmsEmployeeInfo
public string DepartmentType { get; set; } = string.Empty;
/// <summary>Site from employee_rfid_tag.location_site_id; used for menu lookup (lunch_menu_week).</summary>
public string LocationSiteId { get; set; } = string.Empty;
public string GradeType { get; set; } = string.Empty;
}

View File

@ -46,4 +46,5 @@ public class ScanRecord
public string MealItems { get; set; } = string.Empty;
/// <summary>Total price for this scan's meal menu.</summary>
public double TotalPrice { get; set; }
public string grade_type { get; set; } = string.Empty;
}

View File

@ -37,7 +37,8 @@ public class EmployeeLookupService : IEmployeeLookupService
r.created_by AS tag_created_by,
d.title AS department_title,
d.department_type,
r.location_site_id AS location_site_id
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
@ -68,7 +69,8 @@ public class EmployeeLookupService : IEmployeeLookupService
TagCreatedBy = GetString(reader, 8),
DepartmentTitle = GetString(reader, 9),
DepartmentType = GetString(reader, 10),
LocationSiteId = GetString(reader, 11)
LocationSiteId = GetString(reader, 11),
GradeType = GetString(reader, 12)
};
}

View File

@ -1,6 +1,7 @@
using System.Collections.Concurrent;
using System.Text;
using MySqlConnector;
using System.Collections.Concurrent;
using System.Net.Http;
using System.Text;
namespace UtopiaCanteenSystem.Services;
@ -13,13 +14,83 @@ public class EmployeePhotoService : IEmployeePhotoService
private readonly IConfigService _configService;
private readonly ConcurrentDictionary<string, byte[]> _cache = new(StringComparer.OrdinalIgnoreCase);
private const int MaxCacheEntries = 50;
private readonly HttpClient _httpClient;
public EmployeePhotoService(IConfigService configService)
public EmployeePhotoService(IConfigService configService, HttpClient httpClient)
{
_configService = configService;
_httpClient = httpClient;
_httpClient = httpClient;
}
/// <inheritdoc />
//public async Task<byte[]?> GetPhotoBytesAsync(string parentDocumentId, CancellationToken cancellationToken = default)
//{
// if (string.IsNullOrWhiteSpace(parentDocumentId))
// return null;
// var key = parentDocumentId.Trim();
// if (_cache.TryGetValue(key, out var cached))
// return cached;
// var connectionString = _configService.GetHrmsLookupConnectionString();
// if (string.IsNullOrWhiteSpace(connectionString))
// return null;
// try
// {
// await using var conn = new MySqlConnection(connectionString);
// await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
// // employee_photo.employee_id stores the employee document id (parent_document_id from employee lookup)
// const string sql = "SELECT photo_blob FROM hrms.employee_photo WHERE employee_id = @parentDocumentId LIMIT 1";
// await using var cmd = new MySqlCommand(sql, conn);
// cmd.Parameters.AddWithValue("@parentDocumentId", key);
// 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;
// byte[] blob;
// try
// {
// var len = reader.GetBytes(0, 0, null, 0, 0);
// blob = new byte[len];
// reader.GetBytes(0, 0, blob, 0, (int)len);
// }
// catch
// {
// return null;
// }
// var imageBytes = DecodeBlobToImageBytes(blob);
// if (imageBytes == null || imageBytes.Length == 0)
// return null;
// // Cache (evict old if needed)
// while (_cache.Count >= MaxCacheEntries && _cache.Count > 0)
// {
// var first = _cache.Keys.FirstOrDefault();
// if (first != null)
// _cache.TryRemove(first, out _);
// else
// break;
// }
// _cache[key] = imageBytes;
// return imageBytes;
// }
// catch
// {
// return null;
// }
//}
public async Task<byte[]?> GetPhotoBytesAsync(string parentDocumentId, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(parentDocumentId))
@ -29,44 +100,25 @@ public class EmployeePhotoService : IEmployeePhotoService
if (_cache.TryGetValue(key, out var cached))
return cached;
var connectionString = _configService.GetHrmsLookupConnectionString();
if (string.IsNullOrWhiteSpace(connectionString))
return null;
try
{
await using var conn = new MySqlConnection(connectionString);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
var url = $"https://portal.utopiaindustries.pk/uind/employee-photo/{Uri.EscapeDataString(key)}.jpeg";
// employee_photo.employee_id stores the employee document id (parent_document_id from employee lookup)
const string sql = "SELECT photo_blob FROM hrms.employee_photo WHERE employee_id = @parentDocumentId LIMIT 1";
await using var cmd = new MySqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@parentDocumentId", key);
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
using var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
return null;
if (reader.IsDBNull(0))
return null;
byte[] blob;
try
{
var len = reader.GetBytes(0, 0, null, 0, 0);
blob = new byte[len];
reader.GetBytes(0, 0, blob, 0, (int)len);
}
catch
var contentType = response.Content.Headers.ContentType?.MediaType;
if (!string.IsNullOrWhiteSpace(contentType) &&
!contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
{
return null;
}
var imageBytes = DecodeBlobToImageBytes(blob);
var imageBytes = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
if (imageBytes == null || imageBytes.Length == 0)
return null;
// Cache (evict old if needed)
while (_cache.Count >= MaxCacheEntries && _cache.Count > 0)
{
var first = _cache.Keys.FirstOrDefault();
@ -75,6 +127,7 @@ public class EmployeePhotoService : IEmployeePhotoService
else
break;
}
_cache[key] = imageBytes;
return imageBytes;
}
@ -83,7 +136,6 @@ public class EmployeePhotoService : IEmployeePhotoService
return null;
}
}
/// <summary>
/// Converts blob to image bytes: if blob is UTF-8 "data:image...;base64,&lt;payload&gt;", decodes base64; else treats as raw image.
/// </summary>

View File

@ -18,5 +18,7 @@ public interface IMenuLookupService
/// Gets menu items for the given site and specific local date (yyyy-MM-dd).
/// Avoids server time mismatch by not relying on CURDATE().
/// </summary>
Task<IReadOnlyList<HrmsMenuItem>> GetMenuItemsForSiteAndDateAsync(int siteIdNumeric, DateTime menuDateLocal, CancellationToken cancellationToken = default);
//Task<IReadOnlyList<HrmsMenuItem>> GetMenuItemsForSiteAndDateAsync(int siteIdNumeric, DateTime menuDateLocal, CancellationToken cancellationToken = default);
Task<IReadOnlyList<HrmsMenuItem>> GetMenuItemsForSiteAndDateAsync(int siteIdNumeric, DateTime menuDateLocal, string gradeType, CancellationToken cancellationToken = default);
}

View File

@ -7,6 +7,133 @@ namespace UtopiaCanteenSystem.Services;
/// 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.
/// </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;
@ -16,80 +143,50 @@ public class MenuLookupService : IMenuLookupService
_configService = configService;
}
/// <inheritdoc />
// 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, cancellationToken).ConfigureAwait(false);
return await GetMenuItemsForSiteAndDateAsync(siteIdNumeric, DateTime.Today, "NonManagement", cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task<IReadOnlyList<HrmsMenuItem>> GetMenuItemsForSiteAndDateAsync(int siteIdNumeric, DateTime menuDateLocal, CancellationToken cancellationToken = default)
// 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 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;";
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;";
//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;";
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)
//});
list.Add(new HrmsMenuItem
{
MenuItemId = reader.IsDBNull(0) ? 0 : reader.GetInt32(0),
@ -100,23 +197,24 @@ public class MenuLookupService : IMenuLookupService
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 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;

View File

@ -327,7 +327,7 @@ public class RfidService : IRfidService
if (int.TryParse(new string(siteIdForSession.Where(char.IsDigit).ToArray()), out var siteNumeric) && siteNumeric > 0)
{
var menuItems = _menuLookup
.GetMenuItemsForSiteAndDateAsync(siteNumeric, nowLocal.Date)
.GetMenuItemsForSiteAndDateAsync(siteNumeric, nowLocal.Date, employee.GradeType)
.GetAwaiter()
.GetResult();
@ -372,7 +372,8 @@ public class RfidService : IRfidService
DepartmentType = employee.DepartmentType ?? string.Empty,
MealLabel = mealLabel,
MealItems = mealItemsDisplay,
TotalPrice = totalPrice
TotalPrice = totalPrice,
grade_type = employee.GradeType ?? string.Empty
};
db.LunchOrderTransactions.Add(record);
db.SaveChanges();

View File

@ -67,13 +67,20 @@ public class SyncService : ISyncService
AND order_date = @OrderDate
AND total_cost = @TotalCost
AND created_at = @CreatedAt
AND meal_name = @MealName
LIMIT 1";
const string insertOrderSql = @"
INSERT INTO lunch_order
(employee_id, employee_serial_number, order_date, shift, created_at, total_cost, is_cancelled, cancelled_by, created_by, function_id, department_id, location_site_id)
(employee_id, employee_serial_number, order_date, shift, created_at, total_cost, is_cancelled, cancelled_by, created_by, function_id, department_id, location_site_id, meal_name)
VALUES
(@EmployeeId, @EmployeeSerialNumber, @OrderDate, @Shift, @CreatedAt, @TotalCost, 0, @CancelledBy, @CreatedBy, @FunctionId, @DepartmentId, @LocationSiteId)";
(@EmployeeId, @EmployeeSerialNumber, @OrderDate, @Shift, @CreatedAt, @TotalCost, 0, @CancelledBy, @CreatedBy, @FunctionId, @DepartmentId, @LocationSiteId, @MealName)";
const string updateOrderCodeSql = @"
UPDATE lunch_order
SET code = @Code
WHERE id = @Id";
var syncedIds = new List<int>(capacity: toSync.Count);
@ -103,8 +110,11 @@ public class SyncService : ISyncService
var orderDateLocal = record.ScanTime.ToLocalTime().Date;
var totalCost = Math.Round((decimal)record.TotalPrice, 2, MidpointRounding.AwayFromZero);
var createdAt = record.TagCreatedAtUtc ?? record.ScanTime;
//var createdAt = record.TagCreatedAtUtc ?? record.ScanTime;
var createdAt = record.ScanTime.ToLocalTime();
var locationSiteId = SiteIdStringToInt(record.SiteId ?? string.Empty);
var mealName = (record.MealLabel ?? string.Empty).Trim();
var shift = GetShiftFromMeal(mealName);
await using (var hrmsConn = new MySqlConnection(hrmsConnStr))
{
@ -117,6 +127,7 @@ public class SyncService : ISyncService
existsCmd.Parameters.AddWithValue("@OrderDate", orderDateLocal);
existsCmd.Parameters.AddWithValue("@TotalCost", totalCost);
existsCmd.Parameters.AddWithValue("@CreatedAt", createdAt);
existsCmd.Parameters.AddWithValue("@MealName", mealName);
var existing = await existsCmd.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
alreadyExists = existing != null && existing != DBNull.Value;
}
@ -127,7 +138,7 @@ public class SyncService : ISyncService
orderCmd.Parameters.AddWithValue("@EmployeeId", employeeIdBigint);
orderCmd.Parameters.AddWithValue("@EmployeeSerialNumber", record.UindSerial ?? string.Empty);
orderCmd.Parameters.AddWithValue("@OrderDate", orderDateLocal);
orderCmd.Parameters.AddWithValue("@Shift", string.Empty);
orderCmd.Parameters.AddWithValue("@Shift", shift);
orderCmd.Parameters.AddWithValue("@CreatedAt", createdAt);
orderCmd.Parameters.AddWithValue("@TotalCost", totalCost);
orderCmd.Parameters.AddWithValue("@CancelledBy", string.Empty);
@ -135,7 +146,19 @@ public class SyncService : ISyncService
orderCmd.Parameters.AddWithValue("@FunctionId", record.FunctionId);
orderCmd.Parameters.AddWithValue("@DepartmentId", record.DepartmentId);
orderCmd.Parameters.AddWithValue("@LocationSiteId", locationSiteId);
orderCmd.Parameters.AddWithValue("@MealName", mealName);
await orderCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
var insertedId = orderCmd.LastInsertedId;
if (insertedId > 0)
{
var code = GenerateLunchOrderCode(insertedId, createdAt);
await using var updateCodeCmd = new MySqlCommand(updateOrderCodeSql, hrmsConn);
updateCodeCmd.Parameters.AddWithValue("@Code", code);
updateCodeCmd.Parameters.AddWithValue("@Id", insertedId);
await updateCodeCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
}
}
@ -170,6 +193,26 @@ public class SyncService : ISyncService
}
}
private static string GenerateLunchOrderCode(long id, DateTime date)
{
return $"LO-{date:yyyy-MM}-{id:D6}";
}
private static string GetShiftFromMeal(string mealName)
{
if (string.IsNullOrWhiteSpace(mealName))
return string.Empty;
mealName = mealName.Trim().ToLowerInvariant();
return mealName switch
{
"breakfast" => "MORNING",
"lunch" => "MORNING",
_ => "EVENING"
};
}
private static int SiteIdStringToInt(string siteId)
{
try

View File

@ -923,6 +923,13 @@ public partial class ScannerDashboardViewModel : ObservableObject
}
}
private static string ToExcelText(string? value)
{
var safe = value ?? string.Empty;
safe = safe.Replace("\"", "\"\"");
return $"=\"{safe}\"";
}
private static string BuildCsvFromScanRecords(IReadOnlyList<ScanRecord> records)
{
var sb = new StringBuilder();
@ -931,7 +938,7 @@ public partial class ScannerDashboardViewModel : ObservableObject
{
sb.Append(r.Id);
sb.Append(',');
sb.Append(EscapeCsv(r.CardId));
sb.Append(ToExcelText(r.CardId));
sb.Append(',');
sb.Append(EscapeCsv(r.ScanTime.ToString("O", CultureInfo.InvariantCulture)));
sb.Append(',');