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

Reviewed-on: #9
pull/10/head^2
SYED MUSTUFA AHMED NAQVI 2026-04-20 12:10:41 +00:00
commit bda42b2a84
5 changed files with 169 additions and 25 deletions

View File

@ -47,8 +47,8 @@ public partial class App : Application
var menuLookupService = new MenuLookupService(configService);
var mealScheduleService = new ProductionMealScheduleService(configService);
var mealSessionResolver = new DbMealSessionResolver(mealScheduleService);
var rfidService = new RfidService(dbFactory, configService, employeeLookupService, mealSessionResolver, menuLookupService);
var syncService = new SyncService(dbFactory, configService);
var rfidService = new RfidService(dbFactory, configService, employeeLookupService, mealSessionResolver, menuLookupService, syncService);
var adminAuditService = new AdminAuditService(dbFactory);
var session = new AppSession();
var authenticationUrl = "https://portal.utopiaindustries.pk/uind/rest/auth/user/";
@ -76,7 +76,9 @@ public partial class App : Application
mainWindow.Show();
// Background sync: every 15 minutes, POST unsynced lunch_order_transactions to API
_syncTimer = new System.Timers.Timer(TimeSpan.FromMinutes(15).TotalMilliseconds)
if (configService.GetSyncServiceEnabled())
{
_syncTimer = new System.Timers.Timer(TimeSpan.FromMinutes(1).TotalMilliseconds)
{
AutoReset = true
};
@ -101,6 +103,7 @@ public partial class App : Application
};
_syncTimer.Start();
}
}
protected override void OnExit(ExitEventArgs e)
{

View File

@ -33,6 +33,14 @@ public class ConfigService : IConfigService
SaveConfig();
}
public bool GetSyncServiceEnabled() => _config.SyncServiceEnabled;
public void SetSyncServiceEnabled(bool enabled)
{
_config.SyncServiceEnabled = enabled;
SaveConfig();
}
public bool GetScannerConnected() => _config.ScannerConnected;
public void SetScannerConnected(bool connected)
@ -362,6 +370,7 @@ public class ConfigService : IConfigService
private sealed class AppConfig
{
public string SyncApiEndpoint { get; set; } = "https://api.example.com/uind/sync";
public bool SyncServiceEnabled { get; set; } = true;
public bool ScannerConnected { get; set; } = false;
/// <summary>Legacy: migrated to ScanIntervalDays/Hours/Minutes on first load. Kept for JSON deserialization.</summary>
public int ScanTimeoutSeconds { get; set; } = 0;

View File

@ -7,6 +7,8 @@ public interface IConfigService
{
string GetSyncApiEndpoint();
void SetSyncApiEndpoint(string endpoint);
bool GetSyncServiceEnabled();
void SetSyncServiceEnabled(bool enabled);
bool GetScannerConnected();
void SetScannerConnected(bool connected);
/// <summary>Scan interval: minimum time between scans. Days (0365), Hours (023), Minutes (059), Seconds (059).</summary>

View File

@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore;
using MySqlConnector;
using System.Net;
using System.Net.Sockets;
using System.Threading;
@ -21,19 +22,22 @@ public class RfidService : IRfidService
private readonly IEmployeeLookupService _employeeLookup;
private readonly IMealSessionResolver _mealSessionResolver;
private readonly IMenuLookupService _menuLookup;
private readonly ISyncService _syncService;
public RfidService(
IDbContextFactory<AppDbContext> dbFactory,
IConfigService configService,
IEmployeeLookupService employeeLookup,
IMealSessionResolver mealSessionResolver,
IMenuLookupService menuLookup)
IMenuLookupService menuLookup,
ISyncService syncService)
{
_dbFactory = dbFactory;
_configService = configService;
_employeeLookup = employeeLookup;
_mealSessionResolver = mealSessionResolver;
_menuLookup = menuLookup;
_syncService = syncService;
}
public (bool Success, string Message) ProcessScan(string cardId)
@ -269,6 +273,7 @@ public class RfidService : IRfidService
var session = resolvedSession.Session;
var sessionCode = (int)session;
var mealLabel = resolvedSession.MealName;
using var db = _dbFactory.CreateDbContext();
@ -314,13 +319,21 @@ public class RfidService : IRfidService
session);
}
// Production duplicate check by employee serial + date + meal + site.
// Different meal on same day is allowed.
if (int.TryParse(new string(siteIdForSession.Where(char.IsDigit).ToArray()), out var siteNumericForDup) &&
siteNumericForDup > 0 &&
HasProductionMealAlreadyTaken(employee.EmployeeId, siteNumericForDup, mealLabel, nowLocal.Date))
{
return new ScanResult(false, "Meal already taken", 0, 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
@ -410,9 +423,80 @@ public class RfidService : IRfidService
db.LunchOrderTransactions.Add(record);
db.SaveChanges();
// If we can reach production now, try posting immediately.
// On success (or duplicate already in production), SyncService will mark IsSynced=1.
if (CanReachProduction())
{
try
{
_syncService.SyncNowAsync().GetAwaiter().GetResult();
}
catch (Exception ex)
{
Logger.Log(ex, "RfidService.ProcessScanDetailed immediate sync");
}
}
return new ScanResult(true, "Order recorded successfully.", 0, employee, session, normalizedEmployeeSite, normalizedCurrentSite);
}
private bool HasProductionMealAlreadyTaken(string? employeeSerialNumber, int siteIdNumeric, string mealName, DateTime orderDateLocal)
{
var employeeSerial = employeeSerialNumber?.Trim() ?? string.Empty;
var meal = mealName?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(employeeSerial) || string.IsNullOrWhiteSpace(meal) || siteIdNumeric <= 0)
return false;
var hrmsConnStr = _configService.GetHrmsLookupConnectionString();
if (string.IsNullOrWhiteSpace(hrmsConnStr))
return false;
const string sql = @"
SELECT 1
FROM lunch_order
WHERE employee_serial_number = @EmployeeSerialNumber
AND order_date = @OrderDate
AND meal_name = @MealName
AND location_site_id = @LocationSiteId
LIMIT 1";
try
{
using var conn = new MySqlConnection(hrmsConnStr);
conn.Open();
using var cmd = new MySqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@EmployeeSerialNumber", employeeSerial);
cmd.Parameters.AddWithValue("@OrderDate", orderDateLocal.Date);
cmd.Parameters.AddWithValue("@MealName", meal);
cmd.Parameters.AddWithValue("@LocationSiteId", siteIdNumeric);
var exists = cmd.ExecuteScalar();
return exists != null && exists != DBNull.Value;
}
catch (Exception ex)
{
Logger.Log(ex, "RfidService.HasProductionMealAlreadyTaken");
return false;
}
}
private bool CanReachProduction()
{
var connStr = _configService.GetMySqlConnectionString();
if (string.IsNullOrWhiteSpace(connStr))
return false;
try
{
using var conn = new MySqlConnection(connStr);
conn.Open();
return true;
}
catch
{
return false;
}
}
//public ScanResult ProcessScanDetailed(string cardId)
//{

View File

@ -134,6 +134,15 @@ public class SyncService : ISyncService
{
try
{
// Duplicate-safe sync rule:
// if this meal is already present in production for the same employee serial/date/site/session,
// mark SQLite as synced and skip re-insert.
if (await IsDuplicateMealInProductionAsync(record, hrmsConnStr, cancellationToken).ConfigureAwait(false))
{
syncedIds.Add(record.Id);
continue;
}
// 1) Insert into lunch_order_transactions (production MySqlConnectionString)
await using (var prodConn = new MySqlConnection(productionConnStr))
{
@ -379,6 +388,43 @@ public class SyncService : ISyncService
return (lunchMenuItemId, resolvedItemName, itemType, price);
}
private async Task<bool> IsDuplicateMealInProductionAsync(ScanRecord record, string hrmsConnStr, CancellationToken cancellationToken)
{
var employeeSerial = (record.EmployeeId ?? string.Empty).Trim();
var mealName = (record.MealLabel ?? string.Empty).Trim();
var siteId = SiteIdStringToInt(record.SiteId ?? string.Empty);
var orderDate = record.ScanTime.ToLocalTime().Date;
if (string.IsNullOrWhiteSpace(employeeSerial) || string.IsNullOrWhiteSpace(mealName) || siteId <= 0)
return false;
const string sql = @"
SELECT 1
FROM lunch_order
WHERE employee_serial_number = @EmployeeSerialNumber
AND order_date = @OrderDate
AND meal_name = @MealName
AND location_site_id = @LocationSiteId
LIMIT 1";
try
{
await using var conn = new MySqlConnection(hrmsConnStr);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var cmd = new MySqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@EmployeeSerialNumber", employeeSerial);
cmd.Parameters.AddWithValue("@OrderDate", orderDate);
cmd.Parameters.AddWithValue("@MealName", mealName);
cmd.Parameters.AddWithValue("@LocationSiteId", siteId);
var exists = await cmd.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
return exists != null && exists != DBNull.Value;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Duplicate check failed for SQLite Id={record.Id}: {ex.Message}");
return false;
}
}
/// <summary>
/// Deletes only synced rows (IsSynced = 1) where ScanTime is before the start of today (local day).
/// </summary>