using System.Data; using System.IO; using Microsoft.EntityFrameworkCore; using UtopiaCanteenSystem.Models; namespace UtopiaCanteenSystem.Data; /// /// SQLite DbContext for Labour and lunch_order_transactions (scan records) tables. /// Database file is stored under LocalApplicationData so it persists across app updates /// when deployed from a file server. /// public class AppDbContext : DbContext { private static string DbPath => DatabasePath.GetDbPath(); public DbSet Labour { get; set; } public DbSet LunchOrderTransactions { get; set; } public DbSet AdminLoginRecords { get; set; } public AppDbContext() { } public AppDbContext(DbContextOptions 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(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(e => { e.ToTable("lunch_order_transactions"); e.HasKey(x => x.Id); e.HasIndex(x => x.IsSynced); e.HasIndex(x => x.ScanTime); }); modelBuilder.Entity(e => { e.HasKey(x => x.Id); e.HasIndex(x => x.LoginTimeUtc); }); } /// /// Ensures database exists and is migrated. Call on app startup. /// public void EnsureDatabaseCreated() { Database.EnsureCreated(); MigrateScanRecordsToLunchOrderTransactionsIfNeeded(); UpgradeLunchOrderTransactionsSchemaIfNeeded(); EnsureAdminLoginTableExists(); RemoveLegacyMealRelatedSchemaIfNeeded(); } /// /// One-time migration: rename old tables (ScanRecords or CanteenScanEvents) to lunch_order_transactions for existing databases. /// 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 } } /// /// Lightweight schema upgrade: add SiteId, DeviceId and IpAddress to lunch_order_transactions if missing (no EF migrations). /// Does not delete any data. /// private void UpgradeLunchOrderTransactionsSchemaIfNeeded() { try { var conn = Database.GetDbConnection(); if (conn.State != ConnectionState.Open) conn.Open(); var columns = new List(); 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(); } if (columns.Count > 0 && !columns.Contains("IpAddress", StringComparer.OrdinalIgnoreCase)) { using var cmd = conn.CreateCommand(); cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN IpAddress TEXT DEFAULT ''"; cmd.ExecuteNonQuery(); } if (columns.Count > 0 && !columns.Contains("EmployeeId", StringComparer.OrdinalIgnoreCase)) { using var cmd = conn.CreateCommand(); cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN EmployeeId TEXT DEFAULT ''"; cmd.ExecuteNonQuery(); } if (columns.Count > 0 && !columns.Contains("EmployeeName", StringComparer.OrdinalIgnoreCase)) { using var cmd = conn.CreateCommand(); cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN EmployeeName TEXT DEFAULT ''"; cmd.ExecuteNonQuery(); } if (columns.Count > 0 && !columns.Contains("Department", StringComparer.OrdinalIgnoreCase)) { using var cmd = conn.CreateCommand(); cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN Department TEXT DEFAULT ''"; cmd.ExecuteNonQuery(); } if (columns.Count > 0 && !columns.Contains("DepartmentType", StringComparer.OrdinalIgnoreCase)) { using var cmd = conn.CreateCommand(); cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN DepartmentType TEXT DEFAULT ''"; cmd.ExecuteNonQuery(); } if (columns.Count > 0 && !columns.Contains("MealLabel", StringComparer.OrdinalIgnoreCase)) { using var cmd = conn.CreateCommand(); cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN MealLabel TEXT DEFAULT ''"; cmd.ExecuteNonQuery(); } if (columns.Count > 0 && !columns.Contains("MealItems", StringComparer.OrdinalIgnoreCase)) { using var cmd = conn.CreateCommand(); cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN MealItems TEXT DEFAULT ''"; cmd.ExecuteNonQuery(); } if (columns.Count > 0 && !columns.Contains("TotalPrice", StringComparer.OrdinalIgnoreCase)) { using var cmd = conn.CreateCommand(); cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN TotalPrice REAL DEFAULT 0"; cmd.ExecuteNonQuery(); } } catch { // Ignore; existing DB may already have columns or be incompatible } } /// /// Lightweight creation of AdminLoginRecords table for existing databases. /// 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. } } /// /// Removes legacy meal-related tables and columns that should never be present in production. /// This is a best-effort cleanup that runs on every startup for existing SQLite databases. /// private void RemoveLegacyMealRelatedSchemaIfNeeded() { try { var conn = Database.GetDbConnection(); if (conn.State != ConnectionState.Open) conn.Open(); // 1) Drop whole legacy tables if they exist. var legacyTables = new[] { "Employees", "MealRates", // Support both spellings of the temporary meal plan overrides table. "TemporaryMealPlanOverirdes", "TemporaryMealPlanOverrides", // Old local-only tables we no longer use; all meal timings now come from production HRMS. "MealSchedules", "OrderItems" }; foreach (var table in legacyTables) { using var dropCmd = conn.CreateCommand(); dropCmd.CommandText = $"DROP TABLE IF EXISTS \"{table}\""; dropCmd.ExecuteNonQuery(); } // 2) Strip specific columns from any user tables that might contain them. // We don't assume which table they belong to; instead we scan all non-system tables. var legacyColumns = new[] { "MealSession", "MealPlanType", "AppliedRate", "TotalAmount" }; var tableNames = new List(); using (var tablesCmd = conn.CreateCommand()) { tablesCmd.CommandText = "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"; using var reader = tablesCmd.ExecuteReader(); while (reader.Read()) tableNames.Add(reader.GetString(0)); } foreach (var tableName in tableNames) { var existingColumns = new HashSet(StringComparer.OrdinalIgnoreCase); using (var columnsCmd = conn.CreateCommand()) { // Use pragma_table_info to discover columns for the current table. var safeTableName = tableName.Replace("'", "''"); columnsCmd.CommandText = $"SELECT name FROM pragma_table_info('{safeTableName}')"; using var reader = columnsCmd.ExecuteReader(); while (reader.Read()) existingColumns.Add(reader.GetString(0)); } foreach (var legacyColumn in legacyColumns) { if (!existingColumns.Contains(legacyColumn)) continue; try { using var dropColumnCmd = conn.CreateCommand(); dropColumnCmd.CommandText = $"ALTER TABLE \"{tableName}\" DROP COLUMN \"{legacyColumn}\""; dropColumnCmd.ExecuteNonQuery(); } catch { // Older SQLite versions may not support DROP COLUMN; ignore in that case. // If needed in the future, a more invasive recreate-table migration can be added. } } } } catch { // Best-effort only: if anything fails, do not block app startup. } } }