495 lines
20 KiB
C#
495 lines
20 KiB
C#
using System.Data;
|
|
using System.Data.Common;
|
|
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 stored under LocalApplicationData so it persists across app updates
|
|
/// when deployed from a file server.
|
|
/// </summary>
|
|
public class AppDbContext : DbContext
|
|
{
|
|
private static string DbPath => DatabasePath.GetDbPath();
|
|
|
|
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();
|
|
RemoveLegacyMealRelatedSchemaIfNeeded();
|
|
}
|
|
|
|
/// <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, DeviceId and IpAddress 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();
|
|
}
|
|
|
|
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("MealSessionCode", StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
using var cmd = conn.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN MealSessionCode INTEGER DEFAULT -1";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
if (columns.Count > 0 && !columns.Contains("ParentDocumentId", StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
using var cmd = conn.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN ParentDocumentId 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("UindSerial", StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
using var cmd = conn.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN UindSerial TEXT DEFAULT ''";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
if (columns.Count > 0 && !columns.Contains("FunctionId", StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
using var cmd = conn.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN FunctionId INTEGER DEFAULT 0";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
if (columns.Count > 0 && !columns.Contains("DepartmentId", StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
using var cmd = conn.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN DepartmentId INTEGER DEFAULT 0";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
if (columns.Count > 0 && !columns.Contains("TagCreatedAtUtc", StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
using var cmd = conn.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN TagCreatedAtUtc TEXT";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
if (columns.Count > 0 && !columns.Contains("TagCreatedBy", StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
using var cmd = conn.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN TagCreatedBy 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();
|
|
}
|
|
|
|
// Remove deprecated column if it exists (older installs).
|
|
RemoveEmployeeDbIdColumnIfPresent(conn, columns);
|
|
}
|
|
catch
|
|
{
|
|
// Ignore; existing DB may already have columns or be incompatible
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes deprecated EmployeeDbId column from lunch_order_transactions if present.
|
|
/// Uses DROP COLUMN when supported; otherwise rebuilds the table without that column.
|
|
/// Best-effort and non-fatal.
|
|
/// </summary>
|
|
private static void RemoveEmployeeDbIdColumnIfPresent(DbConnection conn, List<string> existingColumns)
|
|
{
|
|
if (existingColumns.Count == 0 || !existingColumns.Contains("EmployeeDbId", StringComparer.OrdinalIgnoreCase))
|
|
return;
|
|
|
|
// Try modern SQLite DROP COLUMN first.
|
|
try
|
|
{
|
|
using var dropCmd = conn.CreateCommand();
|
|
dropCmd.CommandText = "ALTER TABLE lunch_order_transactions DROP COLUMN EmployeeDbId";
|
|
dropCmd.ExecuteNonQuery();
|
|
return;
|
|
}
|
|
catch
|
|
{
|
|
// Fall back to table rebuild.
|
|
}
|
|
|
|
try
|
|
{
|
|
using var tx = conn.BeginTransaction();
|
|
|
|
using (var createCmd = conn.CreateCommand())
|
|
{
|
|
createCmd.Transaction = tx;
|
|
createCmd.CommandText = @"
|
|
CREATE TABLE IF NOT EXISTS lunch_order_transactions__new (
|
|
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
CardId TEXT,
|
|
ScanTime TEXT,
|
|
IsSynced INTEGER,
|
|
SiteId TEXT,
|
|
DeviceId TEXT,
|
|
IpAddress TEXT,
|
|
ParentDocumentId TEXT,
|
|
EmployeeId TEXT,
|
|
UindSerial TEXT,
|
|
FunctionId INTEGER,
|
|
DepartmentId INTEGER,
|
|
TagCreatedAtUtc TEXT,
|
|
TagCreatedBy TEXT,
|
|
EmployeeName TEXT,
|
|
Department TEXT,
|
|
DepartmentType TEXT,
|
|
MealLabel TEXT,
|
|
MealItems TEXT,
|
|
TotalPrice REAL
|
|
)";
|
|
createCmd.ExecuteNonQuery();
|
|
}
|
|
|
|
using (var copyCmd = conn.CreateCommand())
|
|
{
|
|
copyCmd.Transaction = tx;
|
|
copyCmd.CommandText = @"
|
|
INSERT INTO lunch_order_transactions__new
|
|
(Id, CardId, ScanTime, IsSynced, SiteId, DeviceId, IpAddress, ParentDocumentId, EmployeeId, UindSerial, FunctionId, DepartmentId, TagCreatedAtUtc, TagCreatedBy, EmployeeName, Department, DepartmentType, MealLabel, MealItems, TotalPrice)
|
|
SELECT
|
|
Id, CardId, ScanTime, IsSynced, SiteId, DeviceId, IpAddress, ParentDocumentId, EmployeeId, UindSerial, FunctionId, DepartmentId, TagCreatedAtUtc, TagCreatedBy, EmployeeName, Department, DepartmentType, MealLabel, MealItems, TotalPrice
|
|
FROM lunch_order_transactions";
|
|
copyCmd.ExecuteNonQuery();
|
|
}
|
|
|
|
using (var dropOldCmd = conn.CreateCommand())
|
|
{
|
|
dropOldCmd.Transaction = tx;
|
|
dropOldCmd.CommandText = "DROP TABLE lunch_order_transactions";
|
|
dropOldCmd.ExecuteNonQuery();
|
|
}
|
|
|
|
using (var renameCmd = conn.CreateCommand())
|
|
{
|
|
renameCmd.Transaction = tx;
|
|
renameCmd.CommandText = "ALTER TABLE lunch_order_transactions__new RENAME TO lunch_order_transactions";
|
|
renameCmd.ExecuteNonQuery();
|
|
}
|
|
|
|
// Recreate basic indexes used by the app.
|
|
using (var idxCmd = conn.CreateCommand())
|
|
{
|
|
idxCmd.Transaction = tx;
|
|
idxCmd.CommandText = "CREATE INDEX IF NOT EXISTS IX_lunch_order_transactions_IsSynced ON lunch_order_transactions(IsSynced)";
|
|
idxCmd.ExecuteNonQuery();
|
|
}
|
|
using (var idxCmd = conn.CreateCommand())
|
|
{
|
|
idxCmd.Transaction = tx;
|
|
idxCmd.CommandText = "CREATE INDEX IF NOT EXISTS IX_lunch_order_transactions_ScanTime ON lunch_order_transactions(ScanTime)";
|
|
idxCmd.ExecuteNonQuery();
|
|
}
|
|
|
|
tx.Commit();
|
|
}
|
|
catch
|
|
{
|
|
// Best-effort; ignore failures.
|
|
}
|
|
}
|
|
|
|
/// <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.
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<string>();
|
|
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<string>(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.
|
|
}
|
|
}
|
|
}
|