chnage in layout of view all 'order' modal and chnage the local table name from ScanRecords to lunch_order_transactions
parent
cb35b6581a
commit
306e03e71a
|
|
@ -54,7 +54,7 @@ public partial class App : Application
|
|||
};
|
||||
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)
|
||||
{
|
||||
AutoReset = true
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ using UtopiaCanteenSystem.Models;
|
|||
namespace UtopiaCanteenSystem.Data;
|
||||
|
||||
/// <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.
|
||||
/// </summary>
|
||||
public class AppDbContext : DbContext
|
||||
|
|
@ -16,7 +16,7 @@ public class AppDbContext : DbContext
|
|||
"utopia_canteen.db");
|
||||
|
||||
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 AppDbContext() { }
|
||||
|
|
@ -38,9 +38,10 @@ public class AppDbContext : DbContext
|
|||
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 =>
|
||||
{
|
||||
e.ToTable("lunch_order_transactions");
|
||||
e.HasKey(x => x.Id);
|
||||
e.HasIndex(x => x.IsSynced);
|
||||
e.HasIndex(x => x.ScanTime);
|
||||
|
|
@ -59,15 +60,72 @@ public class AppDbContext : DbContext
|
|||
public void EnsureDatabaseCreated()
|
||||
{
|
||||
Database.EnsureCreated();
|
||||
UpgradeScanRecordsSchemaIfNeeded();
|
||||
MigrateScanRecordsToLunchOrderTransactionsIfNeeded();
|
||||
UpgradeLunchOrderTransactionsSchemaIfNeeded();
|
||||
EnsureAdminLoginTableExists();
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// </summary>
|
||||
private void UpgradeScanRecordsSchemaIfNeeded()
|
||||
private void UpgradeLunchOrderTransactionsSchemaIfNeeded()
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -78,23 +136,23 @@ public class AppDbContext : DbContext
|
|||
var columns = new List<string>();
|
||||
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();
|
||||
while (r.Read())
|
||||
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();
|
||||
cmd.CommandText = "ALTER TABLE ScanRecords ADD COLUMN SiteId TEXT DEFAULT ''";
|
||||
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN SiteId TEXT DEFAULT ''";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
if (!columns.Contains("DeviceId", StringComparer.OrdinalIgnoreCase))
|
||||
if (columns.Count > 0 && !columns.Contains("DeviceId", StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ public interface IRfidService
|
|||
{
|
||||
/// <summary>
|
||||
/// 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>
|
||||
(bool Success, string Message) ProcessScan(string cardId);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
namespace UtopiaCanteenSystem.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Syncs unsynced ScanRecords to the configured UIND API endpoint.
|
||||
/// Syncs unsynced lunch_order_transactions to the configured UIND API endpoint.
|
||||
/// </summary>
|
||||
public interface ISyncService
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace UtopiaCanteenSystem.Services;
|
|||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
public class RfidService : IRfidService
|
||||
{
|
||||
|
|
@ -45,7 +45,7 @@ public class RfidService : IRfidService
|
|||
var windowStart = nowUtc.AddSeconds(-timeoutSeconds);
|
||||
|
||||
// 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.ScanTime >= windowStart)
|
||||
.OrderByDescending(r => r.ScanTime)
|
||||
|
|
@ -61,7 +61,7 @@ public class RfidService : IRfidService
|
|||
}
|
||||
|
||||
// Optional: prevent any scan within the timeout window (any card).
|
||||
var lastAnyScanInWindow = db.ScanRecords
|
||||
var lastAnyScanInWindow = db.LunchOrderTransactions
|
||||
.Where(r => r.ScanTime >= windowStart)
|
||||
.OrderByDescending(r => r.ScanTime)
|
||||
.FirstOrDefault();
|
||||
|
|
@ -83,7 +83,7 @@ public class RfidService : IRfidService
|
|||
SiteId = _configService.GetSiteId(),
|
||||
DeviceId = _configService.GetDeviceId()
|
||||
};
|
||||
db.ScanRecords.Add(record);
|
||||
db.LunchOrderTransactions.Add(record);
|
||||
db.SaveChanges();
|
||||
|
||||
return new ScanResult(true, "Order recorded successfully.", 0);
|
||||
|
|
@ -92,7 +92,7 @@ public class RfidService : IRfidService
|
|||
public ScanRecord? GetLastScan()
|
||||
{
|
||||
using var db = _dbFactory.CreateDbContext();
|
||||
return db.ScanRecords
|
||||
return db.LunchOrderTransactions
|
||||
.OrderByDescending(r => r.ScanTime)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
|
@ -101,7 +101,7 @@ public class RfidService : IRfidService
|
|||
{
|
||||
if (count <= 0) return Array.Empty<ScanRecord>();
|
||||
using var db = _dbFactory.CreateDbContext();
|
||||
return db.ScanRecords
|
||||
return db.LunchOrderTransactions
|
||||
.OrderByDescending(r => r.ScanTime)
|
||||
.Take(count)
|
||||
.ToList();
|
||||
|
|
@ -114,7 +114,7 @@ public class RfidService : IRfidService
|
|||
var startUtc = startOfTodayLocal.ToUniversalTime();
|
||||
var endUtc = startOfTomorrowLocal.ToUniversalTime();
|
||||
using var db = _dbFactory.CreateDbContext();
|
||||
return db.ScanRecords
|
||||
return db.LunchOrderTransactions
|
||||
.Where(r => r.ScanTime >= startUtc && r.ScanTime < endUtc)
|
||||
.OrderByDescending(r => r.ScanTime)
|
||||
.ToList();
|
||||
|
|
@ -130,7 +130,7 @@ public class RfidService : IRfidService
|
|||
var endUtc = startOfTomorrowLocal.ToUniversalTime();
|
||||
|
||||
using var db = _dbFactory.CreateDbContext();
|
||||
return db.ScanRecords
|
||||
return db.LunchOrderTransactions
|
||||
.Count(r => r.ScanTime >= startUtc && r.ScanTime < endUtc);
|
||||
}
|
||||
|
||||
|
|
@ -144,7 +144,7 @@ public class RfidService : IRfidService
|
|||
var endUtc = startOfTomorrowLocal.ToUniversalTime();
|
||||
|
||||
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)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
|
@ -152,7 +152,7 @@ public class RfidService : IRfidService
|
|||
public async Task<int> GetTotalScanCountAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
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)
|
||||
|
|
@ -163,7 +163,7 @@ public class RfidService : IRfidService
|
|||
cardId = cardId.Trim();
|
||||
|
||||
await using var db = await _dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||
return await db.ScanRecords
|
||||
return await db.LunchOrderTransactions
|
||||
.CountAsync(r => r.CardId == cardId, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
|
@ -183,7 +183,7 @@ public class RfidService : IRfidService
|
|||
var endUtc = startOfTomorrowLocal.ToUniversalTime();
|
||||
|
||||
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)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ using System.Net.Http;
|
|||
namespace UtopiaCanteenSystem.Services;
|
||||
|
||||
/// <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.
|
||||
/// </summary>
|
||||
public class SyncService : ISyncService
|
||||
|
|
@ -32,7 +32,7 @@ public class SyncService : ISyncService
|
|||
List<ScanRecord> toSync;
|
||||
using (var db = _dbFactory.CreateDbContext())
|
||||
{
|
||||
toSync = await db.ScanRecords
|
||||
toSync = await db.LunchOrderTransactions
|
||||
.Where(r => !r.IsSynced)
|
||||
.OrderBy(r => r.ScanTime)
|
||||
.ToListAsync(cancellationToken)
|
||||
|
|
@ -63,12 +63,12 @@ public class SyncService : ISyncService
|
|||
var ids = toSync.Select(r => r.Id).ToList();
|
||||
using (var db = _dbFactory.CreateDbContext())
|
||||
{
|
||||
var records = await db.ScanRecords
|
||||
var records = await db.LunchOrderTransactions
|
||||
.Where(r => ids.Contains(r.Id))
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
// On successful upload, delete uploaded scan records from local SQLite.
|
||||
db.ScanRecords.RemoveRange(records);
|
||||
// On successful upload, delete uploaded scan events from local SQLite.
|
||||
db.LunchOrderTransactions.RemoveRange(records);
|
||||
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -454,11 +454,13 @@
|
|||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" MinWidth="0"/>
|
||||
<ColumnDefinition Width="110"/>
|
||||
<ColumnDefinition Width="140"/>
|
||||
<ColumnDefinition Width="90"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<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="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>
|
||||
<ItemsControl ItemsSource="{Binding TodayOrderHistory}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
|
|
@ -468,6 +470,7 @@
|
|||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" MinWidth="0"/>
|
||||
<ColumnDefinition Width="110"/>
|
||||
<ColumnDefinition Width="140"/>
|
||||
<ColumnDefinition Width="90"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
|
|
@ -475,10 +478,8 @@
|
|||
<TextBlock Text="—" FontSize="14" Foreground="{StaticResource AccentBrush}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<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 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>
|
||||
<TextBlock Grid.Column="1"
|
||||
|
|
@ -489,7 +490,15 @@
|
|||
HorizontalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
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 RelativeDateLabel}" FontSize="11" Foreground="{StaticResource MutedTextBrush}" Margin="0,1,0,0"/>
|
||||
</StackPanel>
|
||||
|
|
|
|||
Loading…
Reference in New Issue