Sync scans to lunch_order and enforce per-session daily scans

Extend HRMS RFID lookup to include uind_serial, function_id, department_id, created_by, and tag created timestamp
Persist new tag fields + meal session code in SQLite scan records; auto-migrate schema and drop deprecated EmployeeDbId column
Enforce “once per meal session per local day” scan rule (keep short per-card debounce; remove global cooldown)
Enhance sync: insert scans into production lunch_order_transactions (MySqlConnectionString) and into HRMS/local lunch_order (HrmsLookupConnectionString) with duplicate guard; include location_site_id from SQLite SiteId
pull/1/head
SYED MUSTUFA AHMED NAQVI 2026-03-09 13:01:08 +05:00
parent 18a6b7a668
commit a42f43b2cc
16 changed files with 497 additions and 127 deletions

View File

@ -1,4 +1,5 @@
using System.Data; using System.Data;
using System.Data.Common;
using System.IO; using System.IO;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using UtopiaCanteenSystem.Models; using UtopiaCanteenSystem.Models;
@ -163,6 +164,13 @@ public class AppDbContext : DbContext
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
} }
if (columns.Count > 0 && !columns.Contains("MealSessionCode", StringComparer.OrdinalIgnoreCase))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN MealSessionCode INTEGER DEFAULT -1";
cmd.ExecuteNonQuery();
}
if (columns.Count > 0 && !columns.Contains("ParentDocumentId", StringComparer.OrdinalIgnoreCase)) if (columns.Count > 0 && !columns.Contains("ParentDocumentId", StringComparer.OrdinalIgnoreCase))
{ {
using var cmd = conn.CreateCommand(); using var cmd = conn.CreateCommand();
@ -177,6 +185,41 @@ public class AppDbContext : DbContext
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
} }
if (columns.Count > 0 && !columns.Contains("UindSerial", StringComparer.OrdinalIgnoreCase))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN UindSerial TEXT DEFAULT ''";
cmd.ExecuteNonQuery();
}
if (columns.Count > 0 && !columns.Contains("FunctionId", StringComparer.OrdinalIgnoreCase))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN FunctionId INTEGER DEFAULT 0";
cmd.ExecuteNonQuery();
}
if (columns.Count > 0 && !columns.Contains("DepartmentId", StringComparer.OrdinalIgnoreCase))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN DepartmentId INTEGER DEFAULT 0";
cmd.ExecuteNonQuery();
}
if (columns.Count > 0 && !columns.Contains("TagCreatedAtUtc", StringComparer.OrdinalIgnoreCase))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN TagCreatedAtUtc TEXT";
cmd.ExecuteNonQuery();
}
if (columns.Count > 0 && !columns.Contains("TagCreatedBy", StringComparer.OrdinalIgnoreCase))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN TagCreatedBy TEXT DEFAULT ''";
cmd.ExecuteNonQuery();
}
if (columns.Count > 0 && !columns.Contains("EmployeeName", StringComparer.OrdinalIgnoreCase)) if (columns.Count > 0 && !columns.Contains("EmployeeName", StringComparer.OrdinalIgnoreCase))
{ {
using var cmd = conn.CreateCommand(); using var cmd = conn.CreateCommand();
@ -218,6 +261,9 @@ public class AppDbContext : DbContext
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN TotalPrice REAL DEFAULT 0"; cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN TotalPrice REAL DEFAULT 0";
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
} }
// Remove deprecated column if it exists (older installs).
RemoveEmployeeDbIdColumnIfPresent(conn, columns);
} }
catch catch
{ {
@ -225,6 +271,110 @@ public class AppDbContext : DbContext
} }
} }
/// <summary>
/// Removes deprecated EmployeeDbId column from lunch_order_transactions if present.
/// Uses DROP COLUMN when supported; otherwise rebuilds the table without that column.
/// Best-effort and non-fatal.
/// </summary>
private static void RemoveEmployeeDbIdColumnIfPresent(DbConnection conn, List<string> existingColumns)
{
if (existingColumns.Count == 0 || !existingColumns.Contains("EmployeeDbId", StringComparer.OrdinalIgnoreCase))
return;
// Try modern SQLite DROP COLUMN first.
try
{
using var dropCmd = conn.CreateCommand();
dropCmd.CommandText = "ALTER TABLE lunch_order_transactions DROP COLUMN EmployeeDbId";
dropCmd.ExecuteNonQuery();
return;
}
catch
{
// Fall back to table rebuild.
}
try
{
using var tx = conn.BeginTransaction();
using (var createCmd = conn.CreateCommand())
{
createCmd.Transaction = tx;
createCmd.CommandText = @"
CREATE TABLE IF NOT EXISTS lunch_order_transactions__new (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
CardId TEXT,
ScanTime TEXT,
IsSynced INTEGER,
SiteId TEXT,
DeviceId TEXT,
IpAddress TEXT,
ParentDocumentId TEXT,
EmployeeId TEXT,
UindSerial TEXT,
FunctionId INTEGER,
DepartmentId INTEGER,
TagCreatedAtUtc TEXT,
TagCreatedBy TEXT,
EmployeeName TEXT,
Department TEXT,
DepartmentType TEXT,
MealLabel TEXT,
MealItems TEXT,
TotalPrice REAL
)";
createCmd.ExecuteNonQuery();
}
using (var copyCmd = conn.CreateCommand())
{
copyCmd.Transaction = tx;
copyCmd.CommandText = @"
INSERT INTO lunch_order_transactions__new
(Id, CardId, ScanTime, IsSynced, SiteId, DeviceId, IpAddress, ParentDocumentId, EmployeeId, UindSerial, FunctionId, DepartmentId, TagCreatedAtUtc, TagCreatedBy, EmployeeName, Department, DepartmentType, MealLabel, MealItems, TotalPrice)
SELECT
Id, CardId, ScanTime, IsSynced, SiteId, DeviceId, IpAddress, ParentDocumentId, EmployeeId, UindSerial, FunctionId, DepartmentId, TagCreatedAtUtc, TagCreatedBy, EmployeeName, Department, DepartmentType, MealLabel, MealItems, TotalPrice
FROM lunch_order_transactions";
copyCmd.ExecuteNonQuery();
}
using (var dropOldCmd = conn.CreateCommand())
{
dropOldCmd.Transaction = tx;
dropOldCmd.CommandText = "DROP TABLE lunch_order_transactions";
dropOldCmd.ExecuteNonQuery();
}
using (var renameCmd = conn.CreateCommand())
{
renameCmd.Transaction = tx;
renameCmd.CommandText = "ALTER TABLE lunch_order_transactions__new RENAME TO lunch_order_transactions";
renameCmd.ExecuteNonQuery();
}
// Recreate basic indexes used by the app.
using (var idxCmd = conn.CreateCommand())
{
idxCmd.Transaction = tx;
idxCmd.CommandText = "CREATE INDEX IF NOT EXISTS IX_lunch_order_transactions_IsSynced ON lunch_order_transactions(IsSynced)";
idxCmd.ExecuteNonQuery();
}
using (var idxCmd = conn.CreateCommand())
{
idxCmd.Transaction = tx;
idxCmd.CommandText = "CREATE INDEX IF NOT EXISTS IX_lunch_order_transactions_ScanTime ON lunch_order_transactions(ScanTime)";
idxCmd.ExecuteNonQuery();
}
tx.Commit();
}
catch
{
// Best-effort; ignore failures.
}
}
/// <summary> /// <summary>
/// Lightweight creation of AdminLoginRecords table for existing databases. /// Lightweight creation of AdminLoginRecords table for existing databases.
/// </summary> /// </summary>

View File

@ -9,9 +9,18 @@ public class HrmsEmployeeInfo
public string ParentDocumentId { get; set; } = string.Empty; public string ParentDocumentId { get; set; } = string.Empty;
/// <summary>Employee serial number (employee.serial_number).</summary> /// <summary>Employee serial number (employee.serial_number).</summary>
public string EmployeeId { get; set; } = string.Empty; public string EmployeeId { get; set; } = string.Empty;
/// <summary>uind_serial from employee_rfid_tag (production lunch_order.employee_serial_number).</summary>
public string UindSerial { get; set; } = string.Empty;
/// <summary>function_id from employee_rfid_tag.</summary>
public int FunctionId { get; set; }
/// <summary>department_id from employee_rfid_tag (production lunch_order.department_id).</summary>
public int DepartmentId { get; set; }
/// <summary>date_time_created from employee_rfid_tag (production lunch_order.created_at).</summary>
public DateTime? TagCreatedAtUtc { get; set; }
/// <summary>created_by from employee_rfid_tag (production lunch_order.created_by).</summary>
public string TagCreatedBy { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty; public string FirstName { get; set; } = string.Empty;
public string MiddleName { get; set; } = string.Empty; public string MiddleName { get; set; } = string.Empty;
public string DepartmentId { get; set; } = string.Empty;
public string DepartmentTitle { get; set; } = string.Empty; public string DepartmentTitle { get; set; } = string.Empty;
public string DepartmentType { get; set; } = string.Empty; public string DepartmentType { get; set; } = string.Empty;
/// <summary>Site from employee_rfid_tag.location_site_id; used for menu lookup (lunch_menu_week).</summary> /// <summary>Site from employee_rfid_tag.location_site_id; used for menu lookup (lunch_menu_week).</summary>

View File

@ -6,9 +6,19 @@ namespace UtopiaCanteenSystem.Models;
public class HrmsMenuItem public class HrmsMenuItem
{ {
/// <summary>menu_item.id</summary> /// <summary>menu_item.id</summary>
//public int MenuItemId { get; set; }
//public string ItemName { get; set; } = string.Empty;
//public string ItemType { get; set; } = string.Empty;
///// <summary>menu_item.price (HRMS). Decimal is safest for money.</summary>
//public decimal Price { get; set; }
public int MenuItemId { get; set; } public int MenuItemId { get; set; }
public string ItemName { get; set; } = string.Empty; public string ItemName { get; set; } = string.Empty;
public string ItemType { get; set; } = string.Empty; public string ItemType { get; set; } = string.Empty;
/// <summary>menu_item.price (HRMS). Decimal is safest for money.</summary>
public decimal Price { get; set; } public decimal Price { get; set; }
// NEW:
public string MealName { get; set; } = string.Empty; // Sehri / Iftari / Dinner
public string MenuDate { get; set; } = string.Empty; // "2026-03-05"
public string DayOfWeek { get; set; } = string.Empty; // Thursday
} }

View File

@ -6,6 +6,8 @@ namespace UtopiaCanteenSystem.Models;
public class MealSchedule public class MealSchedule
{ {
public long Id { get; set; } public long Id { get; set; }
/// <summary>Raw meal_name from hrms.meal_schedule (e.g. \"Sehri\", \"Iftari\", \"Tea\", \"Dinner\").</summary>
public string MealName { get; set; } = string.Empty;
/// <summary>Site identifier (matches ScanRecord.SiteId / config).</summary> /// <summary>Site identifier (matches ScanRecord.SiteId / config).</summary>
public string LocationSiteId { get; set; } = string.Empty; public string LocationSiteId { get; set; } = string.Empty;
/// <summary>Session type: 0=Breakfast, 1=Lunch, 2=Tea, 3=Dinner (MealSession enum value).</summary> /// <summary>Session type: 0=Breakfast, 1=Lunch, 2=Tea, 3=Dinner (MealSession enum value).</summary>

View File

@ -18,10 +18,22 @@ public class ScanRecord
public string DeviceId { get; set; } = string.Empty; public string DeviceId { get; set; } = string.Empty;
/// <summary>IP address of the device at scan time.</summary> /// <summary>IP address of the device at scan time.</summary>
public string IpAddress { get; set; } = string.Empty; public string IpAddress { get; set; } = string.Empty;
/// <summary>Meal session enum value at scan time (int cast of MealSession).</summary>
public int MealSessionCode { get; set; }
/// <summary>HRMS employee document id (employee.id) at scan time; used to load photo from hrms.employee_photo.</summary> /// <summary>HRMS employee document id (employee.id) at scan time; used to load photo from hrms.employee_photo.</summary>
public string ParentDocumentId { get; set; } = string.Empty; public string ParentDocumentId { get; set; } = string.Empty;
/// <summary>HRMS employee ID (serial_number) at scan time, when lookup succeeded.</summary> /// <summary>HRMS employee ID (serial_number) at scan time, when lookup succeeded.</summary>
public string EmployeeId { get; set; } = string.Empty; public string EmployeeId { get; set; } = string.Empty;
/// <summary>uind_serial from employee_rfid_tag at scan time.</summary>
public string UindSerial { get; set; } = string.Empty;
/// <summary>function_id from employee_rfid_tag at scan time.</summary>
public int FunctionId { get; set; }
/// <summary>department_id from employee_rfid_tag at scan time.</summary>
public int DepartmentId { get; set; }
/// <summary>date_time_created from employee_rfid_tag (stored as DateTime; naming kept for clarity).</summary>
public DateTime? TagCreatedAtUtc { get; set; }
/// <summary>created_by from employee_rfid_tag.</summary>
public string TagCreatedBy { get; set; } = string.Empty;
/// <summary>Full name (first + middle) from HRMS at scan time, when lookup succeeded.</summary> /// <summary>Full name (first + middle) from HRMS at scan time, when lookup succeeded.</summary>
public string EmployeeName { get; set; } = string.Empty; public string EmployeeName { get; set; } = string.Empty;
/// <summary>HRMS department title at scan time.</summary> /// <summary>HRMS department title at scan time.</summary>

View File

@ -2,37 +2,33 @@
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. --> <!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<ApplicationRevision>13</ApplicationRevision> <ApplicationRevision>0</ApplicationRevision>
<ApplicationVersion>1.0.0.*</ApplicationVersion> <ApplicationVersion>1.0.0.*</ApplicationVersion>
<BootstrapperEnabled>True</BootstrapperEnabled> <BootstrapperEnabled>True</BootstrapperEnabled>
<Configuration>Release</Configuration> <Configuration>Release</Configuration>
<CreateDesktopShortcut>True</CreateDesktopShortcut>
<CreateWebPageOnPublish>False</CreateWebPageOnPublish> <CreateWebPageOnPublish>False</CreateWebPageOnPublish>
<GenerateManifests>true</GenerateManifests> <GenerateManifests>true</GenerateManifests>
<Install>True</Install> <Install>True</Install>
<InstallFrom>Unc</InstallFrom> <InstallFrom>Disk</InstallFrom>
<InstallUrl>\\FileServer\Edata\Deployments-by-DotNet-Team\UtopiaCanteenSystem\</InstallUrl>
<IsRevisionIncremented>True</IsRevisionIncremented> <IsRevisionIncremented>True</IsRevisionIncremented>
<IsWebBootstrapper>False</IsWebBootstrapper> <IsWebBootstrapper>False</IsWebBootstrapper>
<MapFileExtensions>True</MapFileExtensions> <MapFileExtensions>True</MapFileExtensions>
<OpenBrowserOnPublish>False</OpenBrowserOnPublish> <OpenBrowserOnPublish>False</OpenBrowserOnPublish>
<Platform>Any CPU</Platform> <Platform>Any CPU</Platform>
<PublishDir>bin\Release\net8.0-windows\win-x86\app.publish\</PublishDir> <PublishDir>bin\Release\net8.0-windows\win-x86\app.publish\</PublishDir>
<PublishUrl>\\FileServer\Edata\Deployments-by-DotNet-Team\UtopiaCanteenSystem\</PublishUrl> <PublishUrl>D:\Publish\UtopiaCanteenSystem\</PublishUrl>
<PublishProtocol>ClickOnce</PublishProtocol> <PublishProtocol>ClickOnce</PublishProtocol>
<PublishReadyToRun>False</PublishReadyToRun> <PublishReadyToRun>False</PublishReadyToRun>
<PublishSingleFile>True</PublishSingleFile> <PublishSingleFile>False</PublishSingleFile>
<RuntimeIdentifier>win-x86</RuntimeIdentifier> <RuntimeIdentifier>win-x86</RuntimeIdentifier>
<SelfContained>True</SelfContained> <SelfContained>True</SelfContained>
<SignatureAlgorithm>(none)</SignatureAlgorithm> <SignatureAlgorithm>(none)</SignatureAlgorithm>
<SignManifests>False</SignManifests> <SignManifests>False</SignManifests>
<SkipPublishVerification>false</SkipPublishVerification> <SkipPublishVerification>false</SkipPublishVerification>
<TargetFramework>net8.0-windows</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
<UpdateEnabled>True</UpdateEnabled> <UpdateEnabled>False</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode> <UpdateMode>Foreground</UpdateMode>
<UpdateRequired>False</UpdateRequired> <UpdateRequired>False</UpdateRequired>
<UpdateUrl>\\FileServer\Edata\Deployments-by-DotNet-Team\UtopiaCanteenSystem\</UpdateUrl>
<WebPageFileName>Publish.html</WebPageFileName> <WebPageFileName>Publish.html</WebPageFileName>
<History>False|2026-03-03T06:08:36.2813025Z||;False|2026-03-03T11:07:21.6156279+05:00||;True|2026-02-18T16:44:11.3509970+05:00||;True|2026-02-12T11:25:40.9881813+05:00||;True|2026-02-12T11:22:02.6537306+05:00||;True|2026-02-12T10:57:16.2775876+05:00||;True|2026-02-12T10:50:58.6968682+05:00||;False|2026-02-12T10:49:27.1867616+05:00||;True|2026-02-12T10:44:55.6022990+05:00||;True|2026-02-11T17:00:04.2786466+05:00||;True|2026-02-11T16:54:37.5052808+05:00||;True|2026-02-11T16:39:26.1892892+05:00||;True|2026-02-11T16:30:57.2420414+05:00||;True|2026-02-11T16:20:44.4749529+05:00||;</History>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project>
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>D:\</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<_TargetId>Folder</_TargetId>
<TargetFramework>net8.0-windows</TargetFramework>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile>
<PublishReadyToRun>false</PublishReadyToRun>
</PropertyGroup>
</Project>

View File

@ -224,7 +224,17 @@ public class ConfigService : IConfigService
SaveConfig(); SaveConfig();
} }
public string GetHrmsLookupConnectionString() => (_config.HrmsLookupConnectionString ?? string.Empty).Trim(); //public string GetHrmsLookupConnectionString() => (_config.HrmsLookupConnectionString ?? string.Empty).Trim();
public string GetHrmsLookupConnectionString()
{
var hrms = (_config.HrmsLookupConnectionString ?? string.Empty).Trim();
if (!string.IsNullOrWhiteSpace(hrms))
return hrms;
// fallback to production/AWS connection string
return GetMySqlConnectionString();
}
public void SetHrmsLookupConnectionString(string connectionString) public void SetHrmsLookupConnectionString(string connectionString)
{ {

View File

@ -30,7 +30,11 @@ public class EmployeeLookupService : IEmployeeLookupService
e.serial_number AS employee_id, e.serial_number AS employee_id,
e.concatenated_name AS first_name, e.concatenated_name AS first_name,
'' AS middle_name, '' AS middle_name,
e.department_id, 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.title AS department_title,
d.department_type, d.department_type,
r.location_site_id AS location_site_id r.location_site_id AS location_site_id
@ -50,16 +54,21 @@ public class EmployeeLookupService : IEmployeeLookupService
if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
return null; return null;
var parentDocumentId = GetString(reader, 0);
return new HrmsEmployeeInfo return new HrmsEmployeeInfo
{ {
ParentDocumentId = GetString(reader, 0), ParentDocumentId = parentDocumentId,
EmployeeId = GetString(reader, 1), EmployeeId = GetString(reader, 1),
FirstName = GetString(reader, 2), FirstName = GetString(reader, 2),
MiddleName = GetString(reader, 3), MiddleName = GetString(reader, 3),
DepartmentId = GetString(reader, 4), UindSerial = GetString(reader, 4),
DepartmentTitle = GetString(reader, 5), FunctionId = GetInt(reader, 5),
DepartmentType = GetString(reader, 6), DepartmentId = GetInt(reader, 6),
LocationSiteId = GetString(reader, 7) TagCreatedAtUtc = GetDateTimeNullable(reader, 7),
TagCreatedBy = GetString(reader, 8),
DepartmentTitle = GetString(reader, 9),
DepartmentType = GetString(reader, 10),
LocationSiteId = GetString(reader, 11)
}; };
} }
@ -69,4 +78,36 @@ public class EmployeeLookupService : IEmployeeLookupService
var v = reader.GetValue(ordinal); var v = reader.GetValue(ordinal);
return v?.ToString() ?? string.Empty; return v?.ToString() ?? string.Empty;
} }
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 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;
}
}
} }

View File

@ -23,14 +23,31 @@ public class MenuLookupService : IMenuLookupService
if (string.IsNullOrWhiteSpace(connectionString)) if (string.IsNullOrWhiteSpace(connectionString))
return Array.Empty<HrmsMenuItem>(); 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 = @" const string sql = @"
SELECT DISTINCT mi.id, mi.item_name, mi.item_type, mi.price SELECT
FROM lunch_menu_week w mi.id,
JOIN lunch_menu_item li ON li.lunch_menu_week_id = w.id mi.item_name,
JOIN menu_item mi ON mi.id = li.menu_item_id mi.item_type,
WHERE w.location_site_id = @siteId mi.price,
AND CURDATE() BETWEEN w.week_start_date AND w.week_end_date li.meal_name,
ORDER BY mi.item_type, mi.item_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 CURDATE() BETWEEN w.week_start_date AND w.week_end_date
AND li.menu_date = DATE_FORMAT(CURDATE(), '%Y-%m-%d')
ORDER BY li.meal_name, mi.item_name;
";
await using var conn = new MySqlConnection(connectionString); await using var conn = new MySqlConnection(connectionString);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false); await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
@ -41,12 +58,24 @@ public class MenuLookupService : IMenuLookupService
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
while (await reader.ReadAsync(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 list.Add(new HrmsMenuItem
{ {
MenuItemId = reader.IsDBNull(0) ? 0 : reader.GetInt32(0), MenuItemId = reader.IsDBNull(0) ? 0 : reader.GetInt32(0),
ItemName = GetString(reader, 1), ItemName = GetString(reader, 1),
ItemType = GetString(reader, 2), ItemType = GetString(reader, 2),
Price = GetDecimal(reader, 3) Price = GetDecimal(reader, 3),
MealName = GetString(reader, 4),
MenuDate = GetString(reader, 5),
DayOfWeek = GetString(reader, 6),
}); });
} }

View File

@ -72,7 +72,7 @@ public class ProductionMealScheduleService : IMealScheduleService
throw new InvalidOperationException("HRMS MySQL connection string not configured."); throw new InvalidOperationException("HRMS MySQL connection string not configured.");
var utc = DateTime.UtcNow; var utc = DateTime.UtcNow;
var mealName = MealSessionToName(schedule.MealSession); var mealName = (schedule.MealName ?? string.Empty).Trim();
var siteIdInt = SiteIdStringToInt(schedule.LocationSiteId); var siteIdInt = SiteIdStringToInt(schedule.LocationSiteId);
await using var conn = new MySqlConnection(connStr); await using var conn = new MySqlConnection(connStr);
@ -98,7 +98,7 @@ public class ProductionMealScheduleService : IMealScheduleService
throw new InvalidOperationException("HRMS MySQL connection string not configured."); throw new InvalidOperationException("HRMS MySQL connection string not configured.");
var utc = DateTime.UtcNow; var utc = DateTime.UtcNow;
var mealName = MealSessionToName(schedule.MealSession); var mealName = (schedule.MealName ?? string.Empty).Trim();
var siteIdInt = SiteIdStringToInt(schedule.LocationSiteId); var siteIdInt = SiteIdStringToInt(schedule.LocationSiteId);
await using var conn = new MySqlConnection(connStr); await using var conn = new MySqlConnection(connStr);
@ -135,6 +135,7 @@ public class ProductionMealScheduleService : IMealScheduleService
return new MealSchedule return new MealSchedule
{ {
Id = r.GetInt64(0), Id = r.GetInt64(0),
MealName = GetString(r, 1),
MealSession = MealNameToSession(GetString(r, 1)), MealSession = MealNameToSession(GetString(r, 1)),
StartTime = GetTimeString(r, 2), StartTime = GetTimeString(r, 2),
EndTime = GetTimeString(r, 3), EndTime = GetTimeString(r, 3),
@ -171,8 +172,15 @@ public class ProductionMealScheduleService : IMealScheduleService
{ {
if (string.IsNullOrWhiteSpace(mealName)) return 0; if (string.IsNullOrWhiteSpace(mealName)) return 0;
var n = mealName.Trim(); var n = mealName.Trim();
if (n.Equals("Breakfast", StringComparison.OrdinalIgnoreCase) || n.Equals("BreakFast", StringComparison.OrdinalIgnoreCase)) return (int)MealSession.Breakfast; // Map legacy names as well as Ramadan labels used in production (Sehri/Iftari)
if (n.Equals("Lunch", StringComparison.OrdinalIgnoreCase)) return (int)MealSession.Lunch; if (n.Equals("Breakfast", StringComparison.OrdinalIgnoreCase)
|| n.Equals("BreakFast", StringComparison.OrdinalIgnoreCase)
|| n.Equals("Sehri", StringComparison.OrdinalIgnoreCase))
return (int)MealSession.Breakfast;
if (n.Equals("Lunch", StringComparison.OrdinalIgnoreCase)
|| n.Equals("Iftari", StringComparison.OrdinalIgnoreCase)
|| n.Equals("Iftar", StringComparison.OrdinalIgnoreCase))
return (int)MealSession.Lunch;
if (n.Equals("Tea", StringComparison.OrdinalIgnoreCase)) return (int)MealSession.Tea; if (n.Equals("Tea", StringComparison.OrdinalIgnoreCase)) return (int)MealSession.Tea;
if (n.Equals("Dinner", StringComparison.OrdinalIgnoreCase)) return (int)MealSession.Dinner; if (n.Equals("Dinner", StringComparison.OrdinalIgnoreCase)) return (int)MealSession.Dinner;
return 0; return 0;

View File

@ -58,14 +58,35 @@ public class RfidService : IRfidService
if (session == MealSession.None) if (session == MealSession.None)
return new ScanResult(false, "This scan is outside of valid meal timings.", 0); return new ScanResult(false, "This scan is outside of valid meal timings.", 0);
var sessionCode = (int)session;
using var db = _dbFactory.CreateDbContext(); 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);
}
var interval = _configService.GetScanInterval(); var interval = _configService.GetScanInterval();
var timeoutSeconds = (int)Math.Max(1, interval.TotalSeconds); var timeoutSeconds = (int)Math.Max(1, interval.TotalSeconds);
var windowStart = nowUtc.AddSeconds(-timeoutSeconds); var windowStart = nowUtc.AddSeconds(-timeoutSeconds);
// Enforce timeout: no duplicate scan within the timeout window // Safety debounce: short cooldown per card to prevent accidental double-tap.
var lastInWindow = db.LunchOrderTransactions var lastInWindow = db.LunchOrderTransactions
.Where(r => r.CardId == cardId) .Where(r => r.CardId == cardId)
.Where(r => r.ScanTime >= windowStart) .Where(r => r.ScanTime >= windowStart)
@ -81,21 +102,6 @@ public class RfidService : IRfidService
remaining); remaining);
} }
// Optional: prevent any scan within the timeout window (any card).
var lastAnyScanInWindow = db.LunchOrderTransactions
.Where(r => r.ScanTime >= windowStart)
.OrderByDescending(r => r.ScanTime)
.FirstOrDefault();
if (lastAnyScanInWindow != null)
{
var remaining = GetCooldownRemainingSeconds(nowUtc, lastAnyScanInWindow.ScanTime, timeoutSeconds);
return new ScanResult(
false,
$"Only one order at a time 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 fullName = string.Join(" ", new[] { employee.FirstName, employee.MiddleName }.Where(s => !string.IsNullOrWhiteSpace(s))).Trim();
var siteId = !string.IsNullOrWhiteSpace(employee.LocationSiteId) var siteId = !string.IsNullOrWhiteSpace(employee.LocationSiteId)
? employee.LocationSiteId.Trim() ? employee.LocationSiteId.Trim()
@ -108,8 +114,14 @@ public class RfidService : IRfidService
SiteId = siteId, SiteId = siteId,
DeviceId = _configService.GetDeviceId(), DeviceId = _configService.GetDeviceId(),
IpAddress = GetLocalIpAddress(), IpAddress = GetLocalIpAddress(),
MealSessionCode = sessionCode,
ParentDocumentId = employee.ParentDocumentId ?? string.Empty, ParentDocumentId = employee.ParentDocumentId ?? string.Empty,
EmployeeId = employee.EmployeeId ?? 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, EmployeeName = string.IsNullOrEmpty(fullName) ? string.Empty : fullName,
Department = employee.DepartmentTitle ?? string.Empty, Department = employee.DepartmentTitle ?? string.Empty,
DepartmentType = employee.DepartmentType ?? string.Empty DepartmentType = employee.DepartmentType ?? string.Empty
@ -120,6 +132,18 @@ public class RfidService : IRfidService
return new ScanResult(true, "Order recorded successfully.", 0, employee, session); return new ScanResult(true, "Order recorded successfully.", 0, employee, session);
} }
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() public ScanRecord? GetLastScan()
{ {
using var db = _dbFactory.CreateDbContext(); using var db = _dbFactory.CreateDbContext();

View File

@ -21,11 +21,18 @@ public class SyncService : ISyncService
} }
public async Task SyncNowAsync(CancellationToken cancellationToken = default) public async Task SyncNowAsync(CancellationToken cancellationToken = default)
{ {
var connectionString = _configService.GetMySqlConnectionString(); var productionConnStr = _configService.GetMySqlConnectionString();
if (string.IsNullOrWhiteSpace(connectionString)) if (string.IsNullOrWhiteSpace(productionConnStr))
{ {
System.Diagnostics.Debug.WriteLine("MySQL connection string not configured; skipping sync."); System.Diagnostics.Debug.WriteLine("MySQL connection string (production) not configured; skipping sync.");
return;
}
var hrmsConnStr = _configService.GetHrmsLookupConnectionString();
if (string.IsNullOrWhiteSpace(hrmsConnStr))
{
System.Diagnostics.Debug.WriteLine("HRMS lookup connection string not configured; skipping sync (need both for lunch_order).");
return; return;
} }
@ -45,85 +52,136 @@ public class SyncService : ISyncService
if (toSync.Count == 0) if (toSync.Count == 0)
return; return;
try // Production: lunch_order_transactions (GetMySqlConnectionString)
const string insertTxnSql = @"
INSERT IGNORE INTO lunch_order_transactions
(scan_date, site_id, device_id, card_id, ip_address, received_date)
VALUES
(@ScanTimeUtc, @SiteId, @DeviceId, @CardId, @IpAddress, UTC_TIMESTAMP(3))";
// HRMS/local: lunch_order (GetHrmsLookupConnectionString)
const string existsOrderSql = @"
SELECT id
FROM lunch_order
WHERE employee_id = @EmployeeId
AND order_date = @OrderDate
AND total_cost = @TotalCost
AND created_at = @CreatedAt
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)
VALUES
(@EmployeeId, @EmployeeSerialNumber, @OrderDate, @Shift, @CreatedAt, @TotalCost, 0, @CancelledBy, @CreatedBy, @FunctionId, @DepartmentId, @LocationSiteId)";
var syncedIds = new List<int>(capacity: toSync.Count);
foreach (var record in toSync)
{ {
// Insert into MySQL using transaction for atomicity
await using var mysqlConn = new MySqlConnection(connectionString);
await mysqlConn.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var transaction = await mysqlConn.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
try try
{ {
// INSERT IGNORE: duplicates avoided by UNIQUE(site_id, device_id, card_id, scan_date) in production. // 1) Insert into lunch_order_transactions (production MySqlConnectionString)
// Column names match production: hrms.lunch_order_transactions (snake_case). No device_local_row_id. await using (var prodConn = new MySqlConnection(productionConnStr))
var insertSql = @"
INSERT IGNORE INTO lunch_order_transactions
(scan_date, site_id, device_id, card_id, ip_address, received_date)
VALUES
(@ScanTimeUtc, @SiteId, @DeviceId, @CardId, @IpAddress, UTC_TIMESTAMP(3))";
await using var cmd = new MySqlCommand(insertSql, mysqlConn, transaction);
foreach (var record in toSync)
{ {
cmd.Parameters.Clear(); await prodConn.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var cmd = new MySqlCommand(insertTxnSql, prodConn);
cmd.Parameters.AddWithValue("@ScanTimeUtc", record.ScanTime); cmd.Parameters.AddWithValue("@ScanTimeUtc", record.ScanTime);
cmd.Parameters.AddWithValue("@SiteId", record.SiteId ?? string.Empty); cmd.Parameters.AddWithValue("@SiteId", record.SiteId ?? string.Empty);
cmd.Parameters.AddWithValue("@DeviceId", record.DeviceId ?? string.Empty); cmd.Parameters.AddWithValue("@DeviceId", record.DeviceId ?? string.Empty);
cmd.Parameters.AddWithValue("@CardId", record.CardId ?? string.Empty); cmd.Parameters.AddWithValue("@CardId", record.CardId ?? string.Empty);
cmd.Parameters.AddWithValue("@IpAddress", record.IpAddress ?? string.Empty); cmd.Parameters.AddWithValue("@IpAddress", record.IpAddress ?? string.Empty);
await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
var rowsAffected = await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
// Note: INSERT IGNORE returns 0 if row already exists (duplicate), 1 if inserted
} }
// Commit MySQL transaction // 2) Insert into lunch_order (HRMS/local HrmsLookupConnectionString)
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); if (string.IsNullOrWhiteSpace(record.ParentDocumentId) || record.FunctionId <= 0 || record.DepartmentId <= 0)
throw new InvalidOperationException("Missing required HRMS tag fields for lunch_order insert.");
// Only mark as synced in SQLite after successful MySQL insert if (!long.TryParse(record.ParentDocumentId.Trim(), out var employeeIdBigint) || employeeIdBigint <= 0)
var ids = toSync.Select(r => r.Id).ToList(); throw new InvalidOperationException("Invalid ParentDocumentId for lunch_order.employee_id.");
using (var db = _dbFactory.CreateDbContext())
var orderDateLocal = record.ScanTime.ToLocalTime().Date;
var totalCost = Math.Round((decimal)record.TotalPrice, 2, MidpointRounding.AwayFromZero);
var createdAt = record.TagCreatedAtUtc ?? record.ScanTime;
var locationSiteId = SiteIdStringToInt(record.SiteId ?? string.Empty);
await using (var hrmsConn = new MySqlConnection(hrmsConnStr))
{ {
var records = await db.LunchOrderTransactions await hrmsConn.OpenAsync(cancellationToken).ConfigureAwait(false);
.Where(r => ids.Contains(r.Id))
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
foreach (var record in records)
record.IsSynced = true;
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); var alreadyExists = false;
await using (var existsCmd = new MySqlCommand(existsOrderSql, hrmsConn))
{
existsCmd.Parameters.AddWithValue("@EmployeeId", employeeIdBigint);
existsCmd.Parameters.AddWithValue("@OrderDate", orderDateLocal);
existsCmd.Parameters.AddWithValue("@TotalCost", totalCost);
existsCmd.Parameters.AddWithValue("@CreatedAt", createdAt);
var existing = await existsCmd.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
alreadyExists = existing != null && existing != DBNull.Value;
}
if (!alreadyExists)
{
await using var orderCmd = new MySqlCommand(insertOrderSql, hrmsConn);
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("@CreatedAt", createdAt);
orderCmd.Parameters.AddWithValue("@TotalCost", totalCost);
orderCmd.Parameters.AddWithValue("@CancelledBy", string.Empty);
orderCmd.Parameters.AddWithValue("@CreatedBy", record.TagCreatedBy ?? string.Empty);
orderCmd.Parameters.AddWithValue("@FunctionId", record.FunctionId);
orderCmd.Parameters.AddWithValue("@DepartmentId", record.DepartmentId);
orderCmd.Parameters.AddWithValue("@LocationSiteId", locationSiteId);
await orderCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
} }
// Day-end cleanup: delete only synced rows from previous days. syncedIds.Add(record.Id);
await CleanupOldSyncedRowsAsync(cancellationToken).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {
// Rollback MySQL transaction on error System.Diagnostics.Debug.WriteLine($"Sync record failed (SQLite Id={record.Id}): {ex.Message}");
try // Leave unsynced; will retry later.
{
await transaction.RollbackAsync(cancellationToken).ConfigureAwait(false);
}
catch
{
// Ignore rollback errors
}
// Log error for debugging (you can replace with proper logging)
System.Diagnostics.Debug.WriteLine($"Sync error: {ex.Message}");
System.Diagnostics.Debug.WriteLine($"Stack trace: {ex.StackTrace}");
throw; // Re-throw to be caught by outer catch
} }
} }
if (syncedIds.Count > 0)
{
using var db = _dbFactory.CreateDbContext();
var records = await db.LunchOrderTransactions
.Where(r => syncedIds.Contains(r.Id))
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
foreach (var r in records)
r.IsSynced = true;
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
try
{
await CleanupOldSyncedRowsAsync(cancellationToken).ConfigureAwait(false);
}
catch (Exception ex) catch (Exception ex)
{ {
// Log error for debugging (you can replace with proper logging) System.Diagnostics.Debug.WriteLine($"Sync cleanup failed: {ex.Message}");
System.Diagnostics.Debug.WriteLine($"Sync failed: {ex.Message}"); }
System.Diagnostics.Debug.WriteLine($"Stack trace: {ex.StackTrace}"); }
// Leave records intact; will retry on next run private static int SiteIdStringToInt(string siteId)
{
try
{
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;
}
catch
{
return 0;
} }
} }

View File

@ -35,7 +35,7 @@ public partial class MealSchedulesViewModel : ObservableObject
private string _locationSiteId = string.Empty; private string _locationSiteId = string.Empty;
[ObservableProperty] [ObservableProperty]
private int _mealSessionIndex; // 0=Breakfast, 1=Lunch, 2=Tea, 3=Dinner private string _mealName = string.Empty;
[ObservableProperty] [ObservableProperty]
private string _startTime = "06:00:00"; private string _startTime = "06:00:00";
@ -52,8 +52,6 @@ public partial class MealSchedulesViewModel : ObservableObject
[ObservableProperty] [ObservableProperty]
private bool _isLoading; private bool _isLoading;
public static readonly string[] MealSessionNames = { "Breakfast", "Lunch", "Tea", "Dinner" };
public MealSchedulesViewModel(IMealScheduleService mealScheduleService, INavigationService navigation) public MealSchedulesViewModel(IMealScheduleService mealScheduleService, INavigationService navigation)
{ {
_mealScheduleService = mealScheduleService; _mealScheduleService = mealScheduleService;
@ -70,7 +68,7 @@ public partial class MealSchedulesViewModel : ObservableObject
{ {
if (value == null) return; if (value == null) return;
LocationSiteId = value.LocationSiteId ?? string.Empty; LocationSiteId = value.LocationSiteId ?? string.Empty;
MealSessionIndex = value.MealSession; MealName = value.MealName ?? string.Empty;
StartTime = value.StartTime ?? "00:00:00"; StartTime = value.StartTime ?? "00:00:00";
EndTime = value.EndTime ?? "23:59:59"; EndTime = value.EndTime ?? "23:59:59";
} }
@ -122,7 +120,7 @@ public partial class MealSchedulesViewModel : ObservableObject
{ {
SelectedSchedule = null; SelectedSchedule = null;
LocationSiteId = "02"; LocationSiteId = "02";
MealSessionIndex = 0; MealName = string.Empty;
StartTime = "06:00:00"; StartTime = "06:00:00";
EndTime = "09:00:00"; EndTime = "09:00:00";
Message = string.Empty; Message = string.Empty;
@ -162,8 +160,8 @@ public partial class MealSchedulesViewModel : ObservableObject
var dto = new MealSchedule var dto = new MealSchedule
{ {
Id = SelectedSchedule.Id, Id = SelectedSchedule.Id,
MealName = (MealName ?? string.Empty).Trim(),
LocationSiteId = siteId, LocationSiteId = siteId,
MealSession = MealSessionIndex,
StartTime = StartTime.Trim(), StartTime = StartTime.Trim(),
EndTime = EndTime.Trim() EndTime = EndTime.Trim()
}; };
@ -174,8 +172,8 @@ public partial class MealSchedulesViewModel : ObservableObject
{ {
var dto = new MealSchedule var dto = new MealSchedule
{ {
MealName = (MealName ?? string.Empty).Trim(),
LocationSiteId = siteId, LocationSiteId = siteId,
MealSession = MealSessionIndex,
StartTime = StartTime.Trim(), StartTime = StartTime.Trim(),
EndTime = EndTime.Trim() EndTime = EndTime.Trim()
}; };

View File

@ -258,10 +258,18 @@ public partial class ScannerDashboardViewModel : ObservableObject
} }
else else
{ {
//mealLabel = activeSession switch
//{
// MealSession.Breakfast => "Breakfast",
// MealSession.Lunch => "Lunch",
// MealSession.Tea => "Tea",
// MealSession.Dinner => "Dinner",
// _ => "No active meal session"
//};
mealLabel = activeSession switch mealLabel = activeSession switch
{ {
MealSession.Breakfast => "Breakfast", MealSession.Breakfast => "Sehri",
MealSession.Lunch => "Lunch", MealSession.Lunch => "Iftari",
MealSession.Tea => "Tea", MealSession.Tea => "Tea",
MealSession.Dinner => "Dinner", MealSession.Dinner => "Dinner",
_ => "No active meal session" _ => "No active meal session"
@ -276,11 +284,15 @@ public partial class ScannerDashboardViewModel : ObservableObject
!string.Equals(mealLabel, "No active meal session", StringComparison.OrdinalIgnoreCase) && !string.Equals(mealLabel, "No active meal session", StringComparison.OrdinalIgnoreCase) &&
items.Count > 0) items.Count > 0)
{ {
//var matchingItems = items
// .Where(i =>
// !string.IsNullOrWhiteSpace(i.ItemType) &&
// i.ItemType.IndexOf(mealLabel, StringComparison.OrdinalIgnoreCase) >= 0)
// .ToList();
var matchingItems = items var matchingItems = items
.Where(i => .Where(i => string.Equals(i.MealName, mealLabel, StringComparison.OrdinalIgnoreCase))
!string.IsNullOrWhiteSpace(i.ItemType) && .ToList();
i.ItemType.IndexOf(mealLabel, StringComparison.OrdinalIgnoreCase) >= 0)
.ToList();
var matchingItemNames = matchingItems var matchingItemNames = matchingItems
.Select(i => i.ItemName) .Select(i => i.ItemName)

View File

@ -367,7 +367,7 @@
Binding="{Binding LocationSiteId}" Binding="{Binding LocationSiteId}"
Width="*"/> Width="*"/>
<DataGridTextColumn Header="Session" <DataGridTextColumn Header="Session"
Binding="{Binding MealSession, Converter={StaticResource MealSessionConverter}}" Binding="{Binding MealName}"
Width="*"/> Width="*"/>
<DataGridTextColumn Header="Start" <DataGridTextColumn Header="Start"
Binding="{Binding StartTime}" Binding="{Binding StartTime}"
@ -473,13 +473,8 @@
FontSize="12" FontSize="12"
Foreground="{StaticResource MutedText}" Foreground="{StaticResource MutedText}"
Margin="0,0,0,4"/> Margin="0,0,0,4"/>
<ComboBox SelectedIndex="{Binding MealSessionIndex, Mode=TwoWay}" <TextBox Text="{Binding MealName, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource ModernComboBox}"> Style="{StaticResource ModernTextBox}"/>
<ComboBoxItem Content="Breakfast"/>
<ComboBoxItem Content="Lunch"/>
<ComboBoxItem Content="Tea"/>
<ComboBoxItem Content="Dinner"/>
</ComboBox>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>