chnage in layout of view all 'order' modal and chnage the local table name from ScanRecords to lunch_order_transactions

pull/1/head
SYED MUSTUFA AHMED NAQVI 2026-02-09 12:17:08 +05:00
parent cb35b6581a
commit 306e03e71a
7 changed files with 106 additions and 39 deletions

View File

@ -54,7 +54,7 @@ public partial class App : Application
}; };
mainWindow.Show(); mainWindow.Show();
// Background sync: every 15 minutes, POST unsynced ScanRecords to API // Background sync: every 15 minutes, POST unsynced lunch_order_transactions to API
_syncTimer = new System.Timers.Timer(TimeSpan.FromMinutes(15).TotalMilliseconds) _syncTimer = new System.Timers.Timer(TimeSpan.FromMinutes(15).TotalMilliseconds)
{ {
AutoReset = true AutoReset = true

View File

@ -6,7 +6,7 @@ using UtopiaCanteenSystem.Models;
namespace UtopiaCanteenSystem.Data; namespace UtopiaCanteenSystem.Data;
/// <summary> /// <summary>
/// SQLite DbContext for Labour and ScanRecord tables. /// SQLite DbContext for Labour and lunch_order_transactions (scan records) tables.
/// Database file is created in application directory on first run. /// Database file is created in application directory on first run.
/// </summary> /// </summary>
public class AppDbContext : DbContext public class AppDbContext : DbContext
@ -16,7 +16,7 @@ public class AppDbContext : DbContext
"utopia_canteen.db"); "utopia_canteen.db");
public DbSet<Labour> Labour { get; set; } public DbSet<Labour> Labour { get; set; }
public DbSet<ScanRecord> ScanRecords { get; set; } public DbSet<ScanRecord> LunchOrderTransactions { get; set; }
public DbSet<AdminLoginRecord> AdminLoginRecords { get; set; } public DbSet<AdminLoginRecord> AdminLoginRecords { get; set; }
public AppDbContext() { } public AppDbContext() { }
@ -38,9 +38,10 @@ public class AppDbContext : DbContext
e.HasIndex(x => x.CardId); e.HasIndex(x => x.CardId);
}); });
// ScanRecord: Index for unsynced queries and by ScanTime. // lunch_order_transactions (scan records): Index for unsynced queries and by ScanTime.
modelBuilder.Entity<ScanRecord>(e => modelBuilder.Entity<ScanRecord>(e =>
{ {
e.ToTable("lunch_order_transactions");
e.HasKey(x => x.Id); e.HasKey(x => x.Id);
e.HasIndex(x => x.IsSynced); e.HasIndex(x => x.IsSynced);
e.HasIndex(x => x.ScanTime); e.HasIndex(x => x.ScanTime);
@ -59,15 +60,72 @@ public class AppDbContext : DbContext
public void EnsureDatabaseCreated() public void EnsureDatabaseCreated()
{ {
Database.EnsureCreated(); Database.EnsureCreated();
UpgradeScanRecordsSchemaIfNeeded(); MigrateScanRecordsToLunchOrderTransactionsIfNeeded();
UpgradeLunchOrderTransactionsSchemaIfNeeded();
EnsureAdminLoginTableExists(); EnsureAdminLoginTableExists();
} }
/// <summary> /// <summary>
/// Lightweight schema upgrade: add SiteId and DeviceId to ScanRecords if missing (no EF migrations). /// One-time migration: rename old tables (ScanRecords or CanteenScanEvents) to lunch_order_transactions for existing databases.
/// </summary>
private void MigrateScanRecordsToLunchOrderTransactionsIfNeeded()
{
try
{
var conn = Database.GetDbConnection();
if (conn.State != ConnectionState.Open)
conn.Open();
var hasOldScanRecords = false;
var hasOldCanteenScanEvents = false;
var hasNew = false;
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "SELECT name FROM sqlite_master WHERE type='table' AND name='ScanRecords'";
using var r = cmd.ExecuteReader();
hasOldScanRecords = r.Read();
}
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "SELECT name FROM sqlite_master WHERE type='table' AND name='CanteenScanEvents'";
using var r = cmd.ExecuteReader();
hasOldCanteenScanEvents = r.Read();
}
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "SELECT name FROM sqlite_master WHERE type='table' AND name='lunch_order_transactions'";
using var r = cmd.ExecuteReader();
hasNew = r.Read();
}
if (!hasNew)
{
if (hasOldScanRecords)
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE ScanRecords RENAME TO lunch_order_transactions";
cmd.ExecuteNonQuery();
}
else if (hasOldCanteenScanEvents)
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE CanteenScanEvents RENAME TO lunch_order_transactions";
cmd.ExecuteNonQuery();
}
}
}
catch
{
// Ignore; DB may be new or already migrated
}
}
/// <summary>
/// Lightweight schema upgrade: add SiteId and DeviceId to lunch_order_transactions if missing (no EF migrations).
/// Does not delete any data. /// Does not delete any data.
/// </summary> /// </summary>
private void UpgradeScanRecordsSchemaIfNeeded() private void UpgradeLunchOrderTransactionsSchemaIfNeeded()
{ {
try try
{ {
@ -78,23 +136,23 @@ public class AppDbContext : DbContext
var columns = new List<string>(); var columns = new List<string>();
using (var cmd = conn.CreateCommand()) using (var cmd = conn.CreateCommand())
{ {
cmd.CommandText = "SELECT name FROM pragma_table_info('ScanRecords')"; cmd.CommandText = "SELECT name FROM pragma_table_info('lunch_order_transactions')";
using var r = cmd.ExecuteReader(); using var r = cmd.ExecuteReader();
while (r.Read()) while (r.Read())
columns.Add(r.GetString(0)); columns.Add(r.GetString(0));
} }
if (!columns.Contains("SiteId", StringComparer.OrdinalIgnoreCase)) if (columns.Count > 0 && !columns.Contains("SiteId", StringComparer.OrdinalIgnoreCase))
{ {
using var cmd = conn.CreateCommand(); using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE ScanRecords ADD COLUMN SiteId TEXT DEFAULT ''"; cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN SiteId TEXT DEFAULT ''";
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
} }
if (!columns.Contains("DeviceId", StringComparer.OrdinalIgnoreCase)) if (columns.Count > 0 && !columns.Contains("DeviceId", StringComparer.OrdinalIgnoreCase))
{ {
using var cmd = conn.CreateCommand(); using var cmd = conn.CreateCommand();
cmd.CommandText = "ALTER TABLE ScanRecords ADD COLUMN DeviceId TEXT DEFAULT ''"; cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN DeviceId TEXT DEFAULT ''";
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
} }
} }

View File

@ -11,7 +11,7 @@ public interface IRfidService
{ {
/// <summary> /// <summary>
/// Processes a scan for the given card ID. Applies configured timeout, /// Processes a scan for the given card ID. Applies configured timeout,
/// saves to ScanRecords if allowed, and returns result message. /// saves to lunch_order_transactions if allowed, and returns result message.
/// </summary> /// </summary>
(bool Success, string Message) ProcessScan(string cardId); (bool Success, string Message) ProcessScan(string cardId);

View File

@ -1,7 +1,7 @@
namespace UtopiaCanteenSystem.Services; namespace UtopiaCanteenSystem.Services;
/// <summary> /// <summary>
/// Syncs unsynced ScanRecords to the configured UIND API endpoint. /// Syncs unsynced lunch_order_transactions to the configured UIND API endpoint.
/// </summary> /// </summary>
public interface ISyncService public interface ISyncService
{ {

View File

@ -9,7 +9,7 @@ namespace UtopiaCanteenSystem.Services;
/// <summary> /// <summary>
/// Handles RFID scan logic: validates input, enforces configurable timeout, /// Handles RFID scan logic: validates input, enforces configurable timeout,
/// and saves ScanRecord to SQLite. /// and saves scan events to SQLite (lunch_order_transactions table).
/// </summary> /// </summary>
public class RfidService : IRfidService public class RfidService : IRfidService
{ {
@ -45,7 +45,7 @@ public class RfidService : IRfidService
var windowStart = nowUtc.AddSeconds(-timeoutSeconds); var windowStart = nowUtc.AddSeconds(-timeoutSeconds);
// Enforce timeout: no duplicate scan within the timeout window // Enforce timeout: no duplicate scan within the timeout window
var lastInWindow = db.ScanRecords var lastInWindow = db.LunchOrderTransactions
.Where(r => r.CardId == cardId) .Where(r => r.CardId == cardId)
.Where(r => r.ScanTime >= windowStart) .Where(r => r.ScanTime >= windowStart)
.OrderByDescending(r => r.ScanTime) .OrderByDescending(r => r.ScanTime)
@ -61,7 +61,7 @@ public class RfidService : IRfidService
} }
// Optional: prevent any scan within the timeout window (any card). // Optional: prevent any scan within the timeout window (any card).
var lastAnyScanInWindow = db.ScanRecords var lastAnyScanInWindow = db.LunchOrderTransactions
.Where(r => r.ScanTime >= windowStart) .Where(r => r.ScanTime >= windowStart)
.OrderByDescending(r => r.ScanTime) .OrderByDescending(r => r.ScanTime)
.FirstOrDefault(); .FirstOrDefault();
@ -83,7 +83,7 @@ public class RfidService : IRfidService
SiteId = _configService.GetSiteId(), SiteId = _configService.GetSiteId(),
DeviceId = _configService.GetDeviceId() DeviceId = _configService.GetDeviceId()
}; };
db.ScanRecords.Add(record); db.LunchOrderTransactions.Add(record);
db.SaveChanges(); db.SaveChanges();
return new ScanResult(true, "Order recorded successfully.", 0); return new ScanResult(true, "Order recorded successfully.", 0);
@ -92,7 +92,7 @@ public class RfidService : IRfidService
public ScanRecord? GetLastScan() public ScanRecord? GetLastScan()
{ {
using var db = _dbFactory.CreateDbContext(); using var db = _dbFactory.CreateDbContext();
return db.ScanRecords return db.LunchOrderTransactions
.OrderByDescending(r => r.ScanTime) .OrderByDescending(r => r.ScanTime)
.FirstOrDefault(); .FirstOrDefault();
} }
@ -101,7 +101,7 @@ public class RfidService : IRfidService
{ {
if (count <= 0) return Array.Empty<ScanRecord>(); if (count <= 0) return Array.Empty<ScanRecord>();
using var db = _dbFactory.CreateDbContext(); using var db = _dbFactory.CreateDbContext();
return db.ScanRecords return db.LunchOrderTransactions
.OrderByDescending(r => r.ScanTime) .OrderByDescending(r => r.ScanTime)
.Take(count) .Take(count)
.ToList(); .ToList();
@ -114,7 +114,7 @@ public class RfidService : IRfidService
var startUtc = startOfTodayLocal.ToUniversalTime(); var startUtc = startOfTodayLocal.ToUniversalTime();
var endUtc = startOfTomorrowLocal.ToUniversalTime(); var endUtc = startOfTomorrowLocal.ToUniversalTime();
using var db = _dbFactory.CreateDbContext(); using var db = _dbFactory.CreateDbContext();
return db.ScanRecords return db.LunchOrderTransactions
.Where(r => r.ScanTime >= startUtc && r.ScanTime < endUtc) .Where(r => r.ScanTime >= startUtc && r.ScanTime < endUtc)
.OrderByDescending(r => r.ScanTime) .OrderByDescending(r => r.ScanTime)
.ToList(); .ToList();
@ -130,7 +130,7 @@ public class RfidService : IRfidService
var endUtc = startOfTomorrowLocal.ToUniversalTime(); var endUtc = startOfTomorrowLocal.ToUniversalTime();
using var db = _dbFactory.CreateDbContext(); using var db = _dbFactory.CreateDbContext();
return db.ScanRecords return db.LunchOrderTransactions
.Count(r => r.ScanTime >= startUtc && r.ScanTime < endUtc); .Count(r => r.ScanTime >= startUtc && r.ScanTime < endUtc);
} }
@ -144,7 +144,7 @@ public class RfidService : IRfidService
var endUtc = startOfTomorrowLocal.ToUniversalTime(); var endUtc = startOfTomorrowLocal.ToUniversalTime();
await using var db = await _dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); await using var db = await _dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
return await db.ScanRecords return await db.LunchOrderTransactions
.CountAsync(r => r.ScanTime >= startUtc && r.ScanTime < endUtc, cancellationToken) .CountAsync(r => r.ScanTime >= startUtc && r.ScanTime < endUtc, cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
} }
@ -152,7 +152,7 @@ public class RfidService : IRfidService
public async Task<int> GetTotalScanCountAsync(CancellationToken cancellationToken = default) public async Task<int> GetTotalScanCountAsync(CancellationToken cancellationToken = default)
{ {
await using var db = await _dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); await using var db = await _dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
return await db.ScanRecords.CountAsync(cancellationToken).ConfigureAwait(false); return await db.LunchOrderTransactions.CountAsync(cancellationToken).ConfigureAwait(false);
} }
public async Task<int> GetTotalScanCountForCardAsync(string cardId, CancellationToken cancellationToken = default) public async Task<int> GetTotalScanCountForCardAsync(string cardId, CancellationToken cancellationToken = default)
@ -163,7 +163,7 @@ public class RfidService : IRfidService
cardId = cardId.Trim(); cardId = cardId.Trim();
await using var db = await _dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); await using var db = await _dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
return await db.ScanRecords return await db.LunchOrderTransactions
.CountAsync(r => r.CardId == cardId, cancellationToken) .CountAsync(r => r.CardId == cardId, cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
} }
@ -183,7 +183,7 @@ public class RfidService : IRfidService
var endUtc = startOfTomorrowLocal.ToUniversalTime(); var endUtc = startOfTomorrowLocal.ToUniversalTime();
await using var db = await _dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); await using var db = await _dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
return await db.ScanRecords return await db.LunchOrderTransactions
.CountAsync(r => r.CardId == cardId && r.ScanTime >= startUtc && r.ScanTime < endUtc, cancellationToken) .CountAsync(r => r.CardId == cardId && r.ScanTime >= startUtc && r.ScanTime < endUtc, cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
} }

View File

@ -8,7 +8,7 @@ using System.Net.Http;
namespace UtopiaCanteenSystem.Services; namespace UtopiaCanteenSystem.Services;
/// <summary> /// <summary>
/// Scheduled sync: fetches unsynced ScanRecords, POSTs them to the configured API, /// Scheduled sync: fetches unsynced lunch_order_transactions, POSTs them to the configured API,
/// and deletes uploaded records from local SQLite on success. /// and deletes uploaded records from local SQLite on success.
/// </summary> /// </summary>
public class SyncService : ISyncService public class SyncService : ISyncService
@ -32,7 +32,7 @@ public class SyncService : ISyncService
List<ScanRecord> toSync; List<ScanRecord> toSync;
using (var db = _dbFactory.CreateDbContext()) using (var db = _dbFactory.CreateDbContext())
{ {
toSync = await db.ScanRecords toSync = await db.LunchOrderTransactions
.Where(r => !r.IsSynced) .Where(r => !r.IsSynced)
.OrderBy(r => r.ScanTime) .OrderBy(r => r.ScanTime)
.ToListAsync(cancellationToken) .ToListAsync(cancellationToken)
@ -63,12 +63,12 @@ public class SyncService : ISyncService
var ids = toSync.Select(r => r.Id).ToList(); var ids = toSync.Select(r => r.Id).ToList();
using (var db = _dbFactory.CreateDbContext()) using (var db = _dbFactory.CreateDbContext())
{ {
var records = await db.ScanRecords var records = await db.LunchOrderTransactions
.Where(r => ids.Contains(r.Id)) .Where(r => ids.Contains(r.Id))
.ToListAsync(cancellationToken) .ToListAsync(cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
// On successful upload, delete uploaded scan records from local SQLite. // On successful upload, delete uploaded scan events from local SQLite.
db.ScanRecords.RemoveRange(records); db.LunchOrderTransactions.RemoveRange(records);
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
} }
} }

View File

@ -454,11 +454,13 @@
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MinWidth="0"/> <ColumnDefinition Width="*" MinWidth="0"/>
<ColumnDefinition Width="110"/> <ColumnDefinition Width="110"/>
<ColumnDefinition Width="140"/>
<ColumnDefinition Width="90"/> <ColumnDefinition Width="90"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Employee" FontSize="12" FontWeight="SemiBold" Foreground="{StaticResource MutedTextBrush}"/> <TextBlock Grid.Column="0" Text="Employee" FontSize="12" FontWeight="SemiBold" Foreground="{StaticResource MutedTextBrush}"/>
<TextBlock Grid.Column="1" Text="Scan ID" FontSize="12" FontWeight="SemiBold" Foreground="{StaticResource MutedTextBrush}" HorizontalAlignment="Center"/> <TextBlock Grid.Column="1" Text="Scan ID" FontSize="12" FontWeight="SemiBold" Foreground="{StaticResource MutedTextBrush}" HorizontalAlignment="Center"/>
<TextBlock Grid.Column="2" Text="Time" FontSize="12" FontWeight="SemiBold" Foreground="{StaticResource MutedTextBrush}" HorizontalAlignment="Right"/> <TextBlock Grid.Column="2" Text="Order" FontSize="12" FontWeight="SemiBold" Foreground="{StaticResource MutedTextBrush}" HorizontalAlignment="Center"/>
<TextBlock Grid.Column="3" Text="Time" FontSize="12" FontWeight="SemiBold" Foreground="{StaticResource MutedTextBrush}" HorizontalAlignment="Right"/>
</Grid> </Grid>
<ItemsControl ItemsSource="{Binding TodayOrderHistory}"> <ItemsControl ItemsSource="{Binding TodayOrderHistory}">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
@ -468,6 +470,7 @@
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MinWidth="0"/> <ColumnDefinition Width="*" MinWidth="0"/>
<ColumnDefinition Width="110"/> <ColumnDefinition Width="110"/>
<ColumnDefinition Width="140"/>
<ColumnDefinition Width="90"/> <ColumnDefinition Width="90"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center"> <StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
@ -475,10 +478,8 @@
<TextBlock Text="—" FontSize="14" Foreground="{StaticResource AccentBrush}" HorizontalAlignment="Center" VerticalAlignment="Center"/> <TextBlock Text="—" FontSize="14" Foreground="{StaticResource AccentBrush}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border> </Border>
<StackPanel VerticalAlignment="Center"> <StackPanel VerticalAlignment="Center">
<!-- Employee column: show EmployeeId (top) + EmployeeName (middle) + Order item (bottom, muted) -->
<TextBlock Text="{Binding EmployeeId}" FontSize="14" FontWeight="Bold" Foreground="{StaticResource TitleTextBrush}" TextTrimming="CharacterEllipsis"/> <TextBlock Text="{Binding EmployeeId}" FontSize="14" FontWeight="Bold" Foreground="{StaticResource TitleTextBrush}" TextTrimming="CharacterEllipsis"/>
<TextBlock Text="{Binding EmployeeName}" FontSize="12" Foreground="{StaticResource MutedTextBrush}" TextTrimming="CharacterEllipsis" Margin="0,1,0,0"/> <TextBlock Text="{Binding EmployeeName}" FontSize="12" Foreground="{StaticResource MutedTextBrush}" TextTrimming="CharacterEllipsis" Margin="0,1,0,0"/>
<TextBlock Text="{Binding OrderItem, StringFormat='Order: {0}'}" FontSize="11" Foreground="{StaticResource MutedTextBrush}" TextTrimming="CharacterEllipsis" Margin="0,1,0,0"/>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
<TextBlock Grid.Column="1" <TextBlock Grid.Column="1"
@ -489,7 +490,15 @@
HorizontalAlignment="Center" HorizontalAlignment="Center"
TextTrimming="CharacterEllipsis" TextTrimming="CharacterEllipsis"
MaxWidth="110"/> MaxWidth="110"/>
<StackPanel Grid.Column="2" HorizontalAlignment="Right" VerticalAlignment="Center"> <TextBlock Grid.Column="2"
Text="{Binding OrderItem}"
FontSize="13"
Foreground="{StaticResource TitleTextBrush}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
TextTrimming="CharacterEllipsis"
MaxWidth="140"/>
<StackPanel Grid.Column="3" HorizontalAlignment="Right" VerticalAlignment="Center">
<TextBlock Text="{Binding TimeDisplay}" FontSize="13" FontWeight="Bold" Foreground="{StaticResource TitleTextBrush}"/> <TextBlock Text="{Binding TimeDisplay}" FontSize="13" FontWeight="Bold" Foreground="{StaticResource TitleTextBrush}"/>
<TextBlock Text="{Binding RelativeDateLabel}" FontSize="11" Foreground="{StaticResource MutedTextBrush}" Margin="0,1,0,0"/> <TextBlock Text="{Binding RelativeDateLabel}" FontSize="11" Foreground="{StaticResource MutedTextBrush}" Margin="0,1,0,0"/>
</StackPanel> </StackPanel>