191 lines
6.5 KiB
C#
191 lines
6.5 KiB
C#
using System.Data;
|
|
using System.IO;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using UtopiaCanteenSystem.Models;
|
|
|
|
namespace UtopiaCanteenSystem.Data;
|
|
|
|
/// <summary>
|
|
/// 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
|
|
{
|
|
private static readonly string DbPath = Path.Combine(
|
|
AppDomain.CurrentDomain.BaseDirectory,
|
|
"utopia_canteen.db");
|
|
|
|
public DbSet<Labour> Labour { get; set; }
|
|
public DbSet<ScanRecord> LunchOrderTransactions { get; set; }
|
|
public DbSet<AdminLoginRecord> AdminLoginRecords { get; set; }
|
|
|
|
public AppDbContext() { }
|
|
|
|
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
|
|
|
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
|
{
|
|
if (!optionsBuilder.IsConfigured)
|
|
optionsBuilder.UseSqlite($"Data Source={DbPath}");
|
|
}
|
|
|
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
{
|
|
// Labour: CardId should be indexed for lookups.
|
|
modelBuilder.Entity<Labour>(e =>
|
|
{
|
|
e.HasKey(x => x.Id);
|
|
e.HasIndex(x => x.CardId);
|
|
});
|
|
|
|
// 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);
|
|
});
|
|
|
|
modelBuilder.Entity<AdminLoginRecord>(e =>
|
|
{
|
|
e.HasKey(x => x.Id);
|
|
e.HasIndex(x => x.LoginTimeUtc);
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensures database exists and is migrated. Call on app startup.
|
|
/// </summary>
|
|
public void EnsureDatabaseCreated()
|
|
{
|
|
Database.EnsureCreated();
|
|
MigrateScanRecordsToLunchOrderTransactionsIfNeeded();
|
|
UpgradeLunchOrderTransactionsSchemaIfNeeded();
|
|
EnsureAdminLoginTableExists();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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 UpgradeLunchOrderTransactionsSchemaIfNeeded()
|
|
{
|
|
try
|
|
{
|
|
var conn = Database.GetDbConnection();
|
|
if (conn.State != ConnectionState.Open)
|
|
conn.Open();
|
|
|
|
var columns = new List<string>();
|
|
using (var cmd = conn.CreateCommand())
|
|
{
|
|
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.Count > 0 && !columns.Contains("SiteId", StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
using var cmd = conn.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN SiteId TEXT DEFAULT ''";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
if (columns.Count > 0 && !columns.Contains("DeviceId", StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
using var cmd = conn.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN DeviceId TEXT DEFAULT ''";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// Ignore; existing DB may already have columns or be incompatible
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lightweight creation of AdminLoginRecords table for existing databases.
|
|
/// </summary>
|
|
private void EnsureAdminLoginTableExists()
|
|
{
|
|
try
|
|
{
|
|
var conn = Database.GetDbConnection();
|
|
if (conn.State != ConnectionState.Open)
|
|
conn.Open();
|
|
|
|
using var cmd = conn.CreateCommand();
|
|
cmd.CommandText =
|
|
"CREATE TABLE IF NOT EXISTS AdminLoginRecords (" +
|
|
"Id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
|
"Username TEXT NOT NULL, " +
|
|
"EmployeeId TEXT NOT NULL, " +
|
|
"LoginTimeUtc TEXT NOT NULL)";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
catch
|
|
{
|
|
// Ignore; table may already exist or DB may be read-only.
|
|
}
|
|
}
|
|
}
|