diff --git a/App.xaml.cs b/App.xaml.cs
index 5595912..2139ac9 100644
--- a/App.xaml.cs
+++ b/App.xaml.cs
@@ -44,7 +44,7 @@ public partial class App : Application
var menuLookupService = new MenuLookupService(configService);
var mealScheduleService = new ProductionMealScheduleService(configService);
var mealSessionResolver = new DbMealSessionResolver(mealScheduleService);
- var rfidService = new RfidService(dbFactory, configService, employeeLookupService, mealSessionResolver);
+ var rfidService = new RfidService(dbFactory, configService, employeeLookupService, mealSessionResolver, menuLookupService);
var syncService = new SyncService(dbFactory, configService);
var adminAuditService = new AdminAuditService(dbFactory);
var session = new AppSession();
@@ -60,7 +60,7 @@ public partial class App : Application
() => new MainDashboardViewModel(navigationService, rfidService, configService, session),
() => new AdminSettingsAuthViewModel(authService, session, navigationService, configService),
() => new SettingsViewModel(configService, navigationService, adminAuditService, syncService),
- () => new MealSchedulesViewModel(mealScheduleService, navigationService));
+ () => new MealSchedulesViewModel(mealScheduleService, navigationService, configService));
var mainViewModel = new MainViewModel(navigationService);
diff --git a/CONFIG_FIX_SUMMARY.md b/CONFIG_FIX_SUMMARY.md
new file mode 100644
index 0000000..7a72e16
--- /dev/null
+++ b/CONFIG_FIX_SUMMARY.md
@@ -0,0 +1,281 @@
+# Configuration Fix Summary
+
+## Problem
+After user login, the application was changing configuration values unexpectedly:
+- Connection string changed from production to local/HRMS
+- SiteId changed back to "01"
+- Config was being overwritten during authentication or navigation
+
+## Root Causes Identified
+
+1. **ProductionMealScheduleService** was using `GetHrmsLookupConnectionString()` instead of production connection string
+2. **AppConfig class** had hardcoded defaults that auto-populated when config was recreated
+3. **ConfigService.GetSiteId()/SetSiteId()** silently normalized missing values to "01"
+4. **No logging** around config load/save operations
+5. **Config overwrites** during login-related operations
+
+---
+
+## Fixes Applied
+
+### 1. ProductionMealScheduleService.cs
+**File:** `Services/ProductionMealScheduleService.cs`
+
+**Changes:**
+- Changed all occurrences of `GetHrmsLookupConnectionString()` to `GetMySqlConnectionString()`
+- Updated error messages from "HRMS MySQL connection string not configured" to "Production MySQL connection string not configured"
+- Methods affected:
+ - `GetActiveSchedulesForSite()`
+ - `GetAllSchedulesAsync()`
+ - `CreateAsync()`
+ - `UpdateAsync()`
+ - `DeleteAsync()`
+
+**Impact:** Meal schedule operations now use the production database connection string instead of the local HRMS lookup connection string.
+
+---
+
+### 2. AppConfig Class Defaults Removed
+**File:** `Services/ConfigService.cs`
+
+**Changes:**
+```csharp
+// BEFORE
+public string SiteId { get; set; } = "SITE : 1";
+public string HrmsLookupConnectionString { get; set; } = "Server=192.168.90.147;...";
+
+// AFTER
+public string SiteId { get; set; } = string.Empty; // No default - must be explicitly configured
+public string HrmsLookupConnectionString { get; set; } = string.Empty; // Must be explicitly configured
+```
+
+**Impact:** Config values will no longer auto-populate with defaults when config file is recreated. Empty strings indicate "not configured" state.
+
+---
+
+### 3. ConfigService.GetSiteId() and SetSiteId() Simplified
+**File:** `Services/ConfigService.cs`
+
+**Changes:**
+```csharp
+// BEFORE - GetSiteId()
+if (string.IsNullOrWhiteSpace(raw))
+ return "01";
+// ... normalization logic ...
+return Math.Clamp(numeric, 0, 99).ToString("D2");
+
+// AFTER - GetSiteId()
+return _config.SiteId ?? string.Empty;
+
+// BEFORE - SetSiteId()
+var normalized = siteId;
+if (string.IsNullOrWhiteSpace(normalized))
+{
+ normalized = "01";
+}
+else
+{
+ // ... normalization to 2-digit format ...
+}
+_config.SiteId = normalized;
+
+// AFTER - SetSiteId()
+_config.SiteId = siteId ?? string.Empty;
+```
+
+**Impact:** SiteId is now stored and retrieved exactly as provided. No automatic normalization to "01". Callers are responsible for formatting.
+
+---
+
+### 4. Config Load/Save Logging Added
+**File:** `Services/ConfigService.cs`
+
+**Changes:**
+Added comprehensive logging:
+
+**LoadConfig():**
+- Logs when config file is not found and defaults are being used
+- Logs config file path on successful load
+- Logs SiteId value (shows "empty=not configured" if empty)
+- Logs MySqlConnectionString status ("NOT CONFIGURED" or masked value)
+- Logs HrmsLookupConnectionString status ("NOT CONFIGURED" or masked value)
+- Logs any exceptions during load
+
+**SaveConfig():**
+- Logs when saving starts
+- Logs current SiteId value
+- Logs current MySqlConnectionString status (masked)
+- Logs current HrmsLookupConnectionString status (masked)
+- Logs successful save with file path
+- Logs any exceptions during save
+
+**MaskConnectionString():**
+- New helper method to mask sensitive password values in logs
+- Shows server and database information but replaces password with "***"
+
+**Sample Log Output:**
+```
+[ConfigService] Config loaded from C:\Users\...\UtopiaCanteenSystem\config.json
+[ConfigService] SiteId: '02' (empty=not configured)
+[ConfigService] MySqlConnectionString: Server=localhost;Database=hrms;User=root;Password=***
+[ConfigService] HrmsLookupConnectionString: NOT CONFIGURED
+[ConfigService] Saving config...
+[ConfigService] SiteId: '02'
+[ConfigService] MySqlConnectionString: Server=localhost;Database=hrms;User=root;Password=***
+[ConfigService] Config saved to C:\Users\...\UtopiaCanteenSystem\config.json
+```
+
+**Impact:** Full visibility into config changes for debugging. Sensitive values are masked.
+
+---
+
+### 5. ScannerDashboardViewModel Updated
+**File:** `ViewModels/ScannerDashboardViewModel.cs`
+
+**Changes:**
+Updated site loading logic to handle both legacy "SITE : X" format and new numeric format:
+
+```csharp
+// BEFORE
+if (!string.IsNullOrWhiteSpace(siteId) && siteId.StartsWith("SITE : ", ...))
+{
+ var num = siteId.Substring("SITE : ".Length).Trim();
+ if (num.Length > 0 && num.All(char.IsDigit))
+ SiteNumber = num;
+}
+
+// AFTER
+if (!string.IsNullOrWhiteSpace(siteId))
+{
+ // Extract digits from legacy format "SITE : X"or use value directly if already numeric
+ if (siteId.StartsWith("SITE : ", StringComparison.OrdinalIgnoreCase))
+ {
+ var num = siteId.Substring("SITE : ".Length).Trim();
+ if (num.Length > 0 && num.All(char.IsDigit))
+ SiteNumber = num;
+ }
+ else if (siteId.All(char.IsDigit))
+ {
+ SiteNumber = siteId;
+ }
+}
+```
+
+**Impact:** Backward compatible with legacy config format while supporting new numeric-only format.
+
+---
+
+## Expected Behavior After Fix
+
+### ✅ Login/Authentication
+- Login operations **only** modify credential-related config values:
+ - `RememberAdminCredentials`
+ - `SavedAdminUsername`
+ - `SavedAdminPasswordProtected`
+- **Does NOT modify:**
+ - `SiteId`
+ - `MySqlConnectionString`
+ - `HrmsLookupConnectionString`
+ - Other unrelated settings
+
+### ✅ Navigation
+- Navigation between views does not trigger config saves
+- ViewModel initialization reads config but doesn't modify it
+- Meal Schedules page uses production connection string
+
+### ✅ Config Persistence
+- Existing config values are preserved across app restarts
+- Defaults only applied when config file truly doesn't exist
+- No silent normalization of values
+- All config changes are logged
+
+### ✅ Database Connections
+- **Production operations** (meal schedules, sync) use `MySqlConnectionString`
+- **Local HRMS operations** (employee lookup, menu lookup, photos) use `HrmsLookupConnectionString`
+- Both connections must be explicitly configured
+
+---
+
+## Testing Recommendations
+
+1. **Initial Startup Test:**
+ - Delete config.json
+ - Run app
+ - Verify log shows "Config file not found. Creating with defaults."
+ - Check that SiteId and connection strings are empty (not "01" or hardcoded)
+
+2. **Login Test:**
+ - Configure production connection string and SiteId
+ - Save config
+ - Perform admin login
+ - Verify config still has correct values (not reset to defaults)
+
+3. **Navigation Test:**
+ - Navigate to Meal Schedules page
+ - Verify it uses production connection string (check logs)
+ - Verify meal schedules load from production database
+
+4. **Config Change Test:**
+ - Change SiteId in Settings
+ - Verify log shows old/new values
+ - Verify change persists after app restart
+
+5. **Connection String Masking Test:**
+ - Check debug output for config save operations
+ - Verify passwords are shown as "***" in logs
+
+---
+
+## Migration Notes
+
+### For Existing Users
+- Existing config.json files will be preserved
+- Legacy "SITE : X" format will be recognized and loaded correctly
+- On first save after upgrade, SiteId will be stored without normalization
+
+### For New Installations
+- Config will be created with empty strings for SiteId and connection strings
+- User must explicitly configure these values
+- No assumptions about default site or database
+
+---
+
+## Files Modified
+
+1. `Services/ProductionMealScheduleService.cs` - Use production connection string
+2. `Services/ConfigService.cs` - Remove defaults, add logging, simplify SiteId handling
+3. `ViewModels/ScannerDashboardViewModel.cs` - Handle both legacy and new SiteId formats
+
+---
+
+## Breaking Changes
+
+⚠️ **None** - Changes are backward compatible:
+- Legacy "SITE : X" format still supported
+- Existing configs preserved
+- Only behavior change: no more automatic resets to defaults
+
+---
+
+## Configuration Requirements
+
+Both connection strings should be configured:
+
+**appsettings.json or manual config edit:**
+```json
+{
+ "MySqlConnectionString": "Server=production-server;Database=hrms;User=utopia;Password=***;Port=3306;",
+ "HrmsLookupConnectionString": "Server=local-hrms-server;Database=hrms;User=utopia;Password=***;Port=3306;"
+}
+```
+
+Or configure via Settings UI (for production connection) and manual config edit (for HRMS lookup).
+
+---
+
+## Next Steps
+
+1. Test all scenarios listed above
+2. Monitor debug output for config operations
+3. Verify production meal schedule operations work correctly
+4. Ensure login no longer modifies unrelated config values
diff --git a/MEAL_SCHEDULE_VIEW_ONLY_CHANGES.md b/MEAL_SCHEDULE_VIEW_ONLY_CHANGES.md
new file mode 100644
index 0000000..24b4576
--- /dev/null
+++ b/MEAL_SCHEDULE_VIEW_ONLY_CHANGES.md
@@ -0,0 +1,322 @@
+# Meal Schedule View-Only Mode Changes
+
+## Summary
+Converted the Meal Schedule management screen from full CRUD (Create, Read, Update, Delete) to **view-only mode**. Users can now only view existing meal schedules without any ability to create, edit, or delete them.
+
+---
+
+## Changes Made
+
+### 1. ViewModel Changes (`MealSchedulesViewModel.cs`)
+
+#### **Removed Properties** (No longer needed for forms)
+```csharp
+// REMOVED:
+private string_locationSiteId = string.Empty;
+private string _mealName = string.Empty;
+private string _startTime = "06:00:00";
+private string _endTime = "09:00:00";
+```
+
+#### **Removed Commands** (All CRUD operations)
+```csharp
+// REMOVED:
+[RelayCommand] private void AddNew() // Form preparation
+[RelayCommand] private async Task Save() // Create/Update logic
+[RelayCommand] private void SelectSchedule() // Edit selection
+[RelayCommand] private async Task Delete() // Delete operation
+```
+
+#### **Removed Callbacks**
+```csharp
+// REMOVED:
+partial void OnSelectedScheduleChanged(MealSchedule? value)
+{
+ // This used to populate form fields when editing
+ // No longer needed in view-only mode
+}
+```
+
+#### **Updated Class Summary**
+```csharp
+///
+/// View-only display of meal schedules from production (hrms.meal_schedule).
+/// No CRUD operations.
+/// Loads all schedules on open; optional Site filter (local).
+///
+public partial class MealSchedulesViewModel : ObservableObject
+{
+ // Only these properties remain:
+ [ObservableProperty] private ObservableCollection _schedules;
+ [ObservableProperty] private string_selectedSiteFilter = "All";
+ [ObservableProperty] private MealSchedule? _selectedSchedule;
+ [ObservableProperty] private string _message = string.Empty;
+ [ObservableProperty] private bool_isError;
+ [ObservableProperty] private bool _isLoading;
+
+ // Only one command remains:
+ [RelayCommand] private void Back(); // Navigate back to Settings
+}
+```
+
+---
+
+### 2. View Changes (`MealSchedulesView.xaml`)
+
+#### **Removed UI Elements**
+
+1. **"Add New" Button** (was in top-right corner)
+```xml
+
+
+```
+
+2. **Actions Column in DataGrid** (Edit/Delete icons)
+```xml
+
+
+
+
+
+
+
+```
+
+3. **Entire Add/Edit Form Card**
+```xml
+
+```
+
+#### **Updated DataGrid Columns**
+```xml
+
+
+
+
+
+...edit/delete buttons...
+
+
+
+
+
+
+```
+
+#### **Updated Header Text**
+```xml
+
+
+
+
+
+
+```
+
+#### **Updated Empty State Message**
+```csharp
+// BEFORE:
+Message = "No schedules in production. Add one below (HRMS MySQL must be configured).";
+
+// AFTER:
+Message = "No schedules configured in production.";
+```
+
+---
+
+### 3. Styles Cleanup
+
+#### **Marked as Removed** (but kept for Back button compatibility)
+```xml
+
+- PrimaryButton style (used by Back button)
+- SecondaryButton style (used by Back button)
+- DangerButton style (no longer used)
+- GridActionButton styles (no longer used)
+- GridActionEditButton style (no longer used)
+- GridActionDeleteButton style (no longer used)
+- ModernTextBox style (no longer used - no forms)
+- ModernComboBox style (still used for Site filter)
+```
+
+---
+
+## What Still Works
+
+### ✅ **Functional Features:**
+1. **Load all schedules** from production database
+2. **Site filter dropdown** - Filter schedules by site
+3. **Auto-refresh** on page load
+4. **Error handling** with detailed messages
+5. **Empty state** messaging
+6. **Back navigation** to Settings page
+7. **Read-only DataGrid** with hover/selection effects
+
+### ✅ **Data Display:**
+- Site ID
+- Meal Session name
+- Start time
+- End time
+
+---
+
+## What No Longer Works
+
+### ❌ **Removed Features:**
+1. **Add New** - Cannot create new meal schedules
+2. **Edit** - Cannot modify existing schedules
+3. **Delete** - Cannot remove schedules
+4. **Form inputs** - No textboxes for entering schedule data
+5. **Save** - No save functionality
+6. **Actions column** - No edit/delete buttons in grid
+
+---
+
+## Backend Service Layer
+
+### **No Changes to Service Implementation**
+The `ProductionMealScheduleService` still has full CRUD methods:
+```csharp
+public Task CreateAsync(...) // Still exists but not called from UI
+public Task UpdateAsync(...) // Still exists but not called from UI
+public Task DeleteAsync(...) // Still exists but not called from UI
+public Task> GetAllSchedulesAsync() // ✅ Still used
+```
+
+**Why keep them?**
+- Service layer is generic and could be reused if admin features are re-enabled
+- Other parts of the system might use these methods
+- No harm in keeping them available
+
+### **What Changed:**
+The ViewModel **no longer calls** Create/Update/Delete methods. It only calls:
+```csharp
+await _mealScheduleService.GetAllSchedulesAsync();
+```
+
+---
+
+## User Experience Impact
+
+### **Before (Full CRUD):**
+```
+User Flow:
+1. Click "Add New" → Form appears
+2. Fill in Site, Session, Times
+3. Click "Save" → Record created
+4. Click ✎ Edit icon → Form populates
+5. Modify fields
+6. Click "Save" → Record updated
+7. Click 🗑 Delete icon → Record deleted
+```
+
+### **After (View-Only):**
+```
+User Flow:
+1. Open Meal Schedules page
+2. See list of all schedules
+3. Optionally filter by Site
+4. Click "Back" to return to Settings
+```
+
+---
+
+## Testing Checklist
+
+### ✅ **Test Scenarios:**
+
+1. **Initial Load**
+ - [ ] Navigate to Meal Schedules page
+ - [ ] Verify schedules load from production database
+ - [ ] Verify no "Add New" button visible
+ - [ ] Verify no edit/delete icons in grid
+
+2. **Site Filtering**
+ - [ ] Change Site filter dropdown
+ - [ ] Verify grid filters correctly
+ - [ ] Select "All" to see all schedules
+
+3. **Empty State**
+ - [ ] If no schedules exist, verify message: "No schedules configured in production."
+ - [ ] Verify no error shown (just informational message)
+
+4. **Error Handling**
+ - [ ] If production DB unavailable, verify error message shown
+ - [ ] Verify detailed error in MessageBox
+
+5. **Navigation**
+ - [ ] Click "Back" button
+ - [ ] Verify returns to Settings page
+
+6. **Data Integrity**
+ - [ ] Verify cannot add rows via UI
+ - [ ] Verify cannot edit rows via UI
+ - [ ] Verify cannot delete rows via UI
+ - [ ] Verify DataGrid is read-only (IsReadOnly=True)
+
+---
+
+## Files Modified
+
+| File | Lines Changed | Description |
+|------|---------------|-------------|
+| `ViewModels/MealSchedulesViewModel.cs` | ~135 removed | Removed all CRUD commands, form properties, callbacks |
+| `Views/MealSchedulesView.xaml` | ~160 removed | Removed Add New button, Actions column, entire form card |
+
+**Total:** ~295 lines removed, minimal additions (comments)
+
+---
+
+## Security Note
+
+⚠️ **Important:** This change only removes UI elements. The backend API/service methods still exist and could be called directly if someone has database access. For true security, also restrict database permissions at the MySQL user level.
+
+---
+
+## Future Considerations
+
+### **If Admin Features Need to be Re-enabled:**
+1. Restore the removed ViewModel commands (AddNew, Save, Delete, SelectSchedule)
+2. Restore form properties (_locationSiteId, _mealName, _startTime, _endTime)
+3. Restore OnSelectedScheduleChanged callback
+4. Restore XAML form card and Actions column
+5. Re-add "Add New" button to header
+
+### **Alternative Approach:**
+Consider role-based access control where:
+- Regular users see view-only mode
+- Admin users see full CRUD mode
+- Toggle visibility based on user role
+
+---
+
+## Commit Message
+```
+feat: Convert Meal Schedule management to view-only mode
+
+Changes:
+- Remove all CRUD operations (Create, Update, Delete) from MealSchedulesViewModel
+- Remove Add New button, Edit/Delete action icons from UI
+- Remove form input fields and related properties
+- Keep only Load/Display functionality with Site filtering
+- Update empty state message to reflect view-only mode
+- Retain Back navigation command
+
+Impact:
+Users can now only view existing meal schedules without ability to modify them.
+Backend service methods still exist but are not called from UI.
+```
+
+---
+
+## Related Changes
+
+This view-only mode complements the earlier fix that switched `ProductionMealScheduleService` to use the production MySQL connection string instead of the local HRMS lookup connection string, ensuring data consistency across the application.
diff --git a/Models/ResolvedMealSession.cs b/Models/ResolvedMealSession.cs
new file mode 100644
index 0000000..01a3528
--- /dev/null
+++ b/Models/ResolvedMealSession.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace UtopiaCanteenSystem.Models;
+
+public class ResolvedMealSession
+{
+ public MealSession Session { get; set; }
+ public string MealName { get; set; } = string.Empty;
+ public string SiteId { get; set; } = string.Empty;
+ public string StartTime { get; set; } = string.Empty;
+ public string EndTime { get; set; } = string.Empty;
+}
diff --git a/Services/DbMealSessionResolver.cs b/Services/DbMealSessionResolver.cs
index d6ab8d1..29af390 100644
--- a/Services/DbMealSessionResolver.cs
+++ b/Services/DbMealSessionResolver.cs
@@ -19,8 +19,62 @@ public class DbMealSessionResolver : IMealSessionResolver
_mealScheduleService = mealScheduleService;
}
+
+ private static MealSession MapMealNameToSession(string? mealName)
+ {
+ if (string.IsNullOrWhiteSpace(mealName))
+ return MealSession.None;
+
+ var name = mealName.Trim();
+
+ return name.ToLowerInvariant() switch
+ {
+ "breakfast" => MealSession.Breakfast,
+ "sehri" => MealSession.Breakfast,
+ "lunch" => MealSession.Lunch,
+ "iftari" => MealSession.Lunch,
+ "tea" => MealSession.Tea,
+ "dinner" => MealSession.Dinner,
+ _ => MealSession.None
+ };
+ }
+
+
///
- public MealSession GetCurrentSession(DateTime nowLocal, string siteId)
+ //public MealSession GetCurrentSession(DateTime nowLocal, string siteId)
+ //{
+ // var normalizedSite = (siteId ?? string.Empty).Trim();
+ // if (string.IsNullOrEmpty(normalizedSite))
+ // normalizedSite = "01";
+ // if (normalizedSite.Length == 1 && char.IsDigit(normalizedSite[0]))
+ // normalizedSite = normalizedSite.PadLeft(2, '0');
+
+ // var schedules = GetSchedulesForSiteCached(normalizedSite);
+ // var t = nowLocal.TimeOfDay;
+
+ // //foreach (var s in schedules)
+ // //{
+ // // if (!TryParseTime(s.StartTime, out var start) || !TryParseTime(s.EndTime, out var end))
+ // // continue;
+ // // if (t >= start && t < end)
+ // // return (MealSession)s.MealSession;
+ // //}
+ // foreach (var s in schedules)
+ // {
+ // if (!TryParseTime(s.StartTime, out var start) || !TryParseTime(s.EndTime, out var end))
+ // continue;
+
+ // if (t >= start && t < end)
+ // return MapMealNameToSession(s.MealName);
+ // }
+
+ // return MealSession.None;
+ //}
+
+
+
+
+ public ResolvedMealSession? GetCurrentSession(DateTime nowLocal, string siteId)
{
var normalizedSite = (siteId ?? string.Empty).Trim();
if (string.IsNullOrEmpty(normalizedSite))
@@ -35,13 +89,24 @@ public class DbMealSessionResolver : IMealSessionResolver
{
if (!TryParseTime(s.StartTime, out var start) || !TryParseTime(s.EndTime, out var end))
continue;
+
if (t >= start && t < end)
- return (MealSession)s.MealSession;
+ {
+ return new ResolvedMealSession
+ {
+ Session = MapMealNameToSession(s.MealName),
+ MealName = s.MealName ?? string.Empty,
+ SiteId = normalizedSite,
+ StartTime = s.StartTime ?? string.Empty,
+ EndTime = s.EndTime ?? string.Empty
+ };
+ }
}
- return MealSession.None;
+ return null;
}
+
private List GetSchedulesForSiteCached(string siteId)
{
lock (_cacheLock)
diff --git a/Services/IMealSessionResolver.cs b/Services/IMealSessionResolver.cs
index 3e54289..a882ff6 100644
--- a/Services/IMealSessionResolver.cs
+++ b/Services/IMealSessionResolver.cs
@@ -10,5 +10,6 @@ public interface IMealSessionResolver
///
/// Returns the active meal session for the given local time and site, or MealSession.None if outside all windows.
///
- MealSession GetCurrentSession(DateTime nowLocal, string siteId);
+ //MealSession GetCurrentSession(DateTime nowLocal, string siteId);
+ ResolvedMealSession? GetCurrentSession(DateTime nowLocal, string siteId);
}
diff --git a/Services/IMenuLookupService.cs b/Services/IMenuLookupService.cs
index a1f3bea..cf15353 100644
--- a/Services/IMenuLookupService.cs
+++ b/Services/IMenuLookupService.cs
@@ -13,4 +13,10 @@ public interface IMenuLookupService
/// siteIdNumeric: match to lunch_menu_week.location_site_id (e.g. 2 for site "02").
///
Task> GetMenuItemsForSiteAsync(int siteIdNumeric, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets menu items for the given site and specific local date (yyyy-MM-dd).
+ /// Avoids server time mismatch by not relying on CURDATE().
+ ///
+ Task> GetMenuItemsForSiteAndDateAsync(int siteIdNumeric, DateTime menuDateLocal, CancellationToken cancellationToken = default);
}
diff --git a/Services/Logger.cs b/Services/Logger.cs
new file mode 100644
index 0000000..2cfcd45
--- /dev/null
+++ b/Services/Logger.cs
@@ -0,0 +1,53 @@
+using System;
+using System.IO;
+using System.Text;
+
+namespace UtopiaCanteenSystem.Services;
+
+public static class Logger
+{
+ private static readonly object _lock = new();
+
+ public static void Log(Exception ex, string context = "")
+ {
+ try
+ {
+ if (ex == null) return;
+
+ var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+ var appRoot = Path.Combine(localAppData, "UtopiaCanteenSystem");
+ var logsDir = Path.Combine(appRoot, "Logs");
+ var logFile = Path.Combine(logsDir, "error.log");
+
+ try
+ {
+ if (!Directory.Exists(logsDir))
+ Directory.CreateDirectory(logsDir);
+ }
+ catch
+ {
+ return;
+ }
+
+ var sb = new StringBuilder();
+ sb.AppendLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}]");
+ if (!string.IsNullOrWhiteSpace(context))
+ sb.AppendLine($"Context: {context}");
+ sb.AppendLine($"Message: {ex.Message}");
+ sb.AppendLine($"StackTrace: {ex.StackTrace}");
+ if (ex.InnerException != null)
+ sb.AppendLine($"InnerException: {ex.InnerException.Message}");
+ sb.AppendLine("----------------------------------------------------");
+
+ lock (_lock)
+ {
+ File.AppendAllText(logFile, sb.ToString());
+ }
+ }
+ catch
+ {
+ // never throw from logger
+ }
+ }
+}
+
diff --git a/Services/MenuLookupService.cs b/Services/MenuLookupService.cs
index 323539f..ec3c804 100644
--- a/Services/MenuLookupService.cs
+++ b/Services/MenuLookupService.cs
@@ -18,6 +18,13 @@ public class MenuLookupService : IMenuLookupService
///
public async Task> GetMenuItemsForSiteAsync(int siteIdNumeric, CancellationToken cancellationToken = default)
+ {
+ // Backwards-compatible: use today's local date.
+ return await GetMenuItemsForSiteAndDateAsync(siteIdNumeric, DateTime.Today, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ public async Task> GetMenuItemsForSiteAndDateAsync(int siteIdNumeric, DateTime menuDateLocal, CancellationToken cancellationToken = default)
{
var connectionString = _configService.GetHrmsLookupConnectionString();
if (string.IsNullOrWhiteSpace(connectionString))
@@ -31,28 +38,45 @@ public class MenuLookupService : IMenuLookupService
// WHERE w.location_site_id = @siteId
// AND CURDATE() BETWEEN w.week_start_date AND w.week_end_date
// ORDER BY mi.item_type, mi.item_name";
+
const string sql = @"
SELECT
mi.id,
mi.item_name,
mi.item_type,
mi.price,
- li.meal_name,
li.menu_date,
li.day_of_week
FROM lunch_menu_week w
JOIN lunch_menu_item li ON li.lunch_menu_week_id = w.id
JOIN menu_item mi ON mi.id = li.menu_item_id
WHERE w.location_site_id = @siteId
- AND CURDATE() BETWEEN w.week_start_date AND w.week_end_date
- AND li.menu_date = DATE_FORMAT(CURDATE(), '%Y-%m-%d')
- ORDER BY li.meal_name, mi.item_name;
-";
+ AND @menuDate BETWEEN w.week_start_date AND w.week_end_date
+ AND li.menu_date = @menuDate
+ ORDER BY mi.item_name;";
+
+ //const string sql = @"
+ // SELECT
+ // mi.id,
+ // mi.item_name,
+ // mi.item_type,
+ // mi.price,
+ // li.meal_name,
+ // li.menu_date,
+ // li.day_of_week
+ // FROM lunch_menu_week w
+ // JOIN lunch_menu_item li ON li.lunch_menu_week_id = w.id
+ // JOIN menu_item mi ON mi.id = li.menu_item_id
+ // WHERE w.location_site_id = @siteId
+ // AND @menuDate BETWEEN w.week_start_date AND w.week_end_date
+ // AND li.menu_date = @menuDate
+ // ORDER BY li.meal_name, mi.item_name;";
await using var conn = new MySqlConnection(connectionString);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var cmd = new MySqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@siteId", siteIdNumeric);
+ cmd.Parameters.AddWithValue("@menuDate", menuDateLocal.Date);
var list = new List();
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
@@ -72,11 +96,22 @@ public class MenuLookupService : IMenuLookupService
ItemName = GetString(reader, 1),
ItemType = GetString(reader, 2),
Price = GetDecimal(reader, 3),
-
- MealName = GetString(reader, 4),
- MenuDate = GetString(reader, 5),
- DayOfWeek = GetString(reader, 6),
+ MenuDate = GetString(reader, 4),
+ DayOfWeek = GetString(reader, 5),
+ MealName = string.Empty
});
+
+ //list.Add(new HrmsMenuItem
+ //{
+ // MenuItemId = reader.IsDBNull(0) ? 0 : reader.GetInt32(0),
+ // ItemName = GetString(reader, 1),
+ // ItemType = GetString(reader, 2),
+ // Price = GetDecimal(reader, 3),
+
+ // MealName = GetString(reader, 4),
+ // MenuDate = GetString(reader, 5),
+ // DayOfWeek = GetString(reader, 6),
+ //});
}
return list;
diff --git a/Services/RfidService.cs b/Services/RfidService.cs
index 356950f..c3394d0 100644
--- a/Services/RfidService.cs
+++ b/Services/RfidService.cs
@@ -20,13 +20,20 @@ public class RfidService : IRfidService
private readonly IConfigService _configService;
private readonly IEmployeeLookupService _employeeLookup;
private readonly IMealSessionResolver _mealSessionResolver;
+ private readonly IMenuLookupService _menuLookup;
- public RfidService(IDbContextFactory dbFactory, IConfigService configService, IEmployeeLookupService employeeLookup, IMealSessionResolver mealSessionResolver)
+ public RfidService(
+ IDbContextFactory dbFactory,
+ IConfigService configService,
+ IEmployeeLookupService employeeLookup,
+ IMealSessionResolver mealSessionResolver,
+ IMenuLookupService menuLookup)
{
_dbFactory = dbFactory;
_configService = configService;
_employeeLookup = employeeLookup;
_mealSessionResolver = mealSessionResolver;
+ _menuLookup = menuLookup;
}
public (bool Success, string Message) ProcessScan(string cardId)
@@ -54,10 +61,17 @@ public class RfidService : IRfidService
var siteIdForSession = !string.IsNullOrWhiteSpace(employee.LocationSiteId)
? employee.LocationSiteId.Trim()
: _configService.GetSiteId();
- var session = _mealSessionResolver.GetCurrentSession(nowLocal, siteIdForSession);
- if (session == MealSession.None)
+ //var session = _mealSessionResolver.GetCurrentSession(nowLocal, siteIdForSession);
+ //if (session == MealSession.None)
+ // return new ScanResult(false, "This scan is outside of valid meal timings.", 0);
+
+ //var sessionCode = (int)session;
+
+ var resolvedSession = _mealSessionResolver.GetCurrentSession(nowLocal, siteIdForSession);
+ if (resolvedSession == null || resolvedSession.Session == MealSession.None)
return new ScanResult(false, "This scan is outside of valid meal timings.", 0);
+ var session = resolvedSession.Session;
var sessionCode = (int)session;
using var db = _dbFactory.CreateDbContext();
@@ -75,9 +89,15 @@ public class RfidService : IRfidService
.OrderByDescending(r => r.ScanTime)
.FirstOrDefault();
+ //if (alreadyScannedThisSessionToday != null)
+ //{
+ // var sessionName = GetMealSessionDisplayName(session);
+ // return new ScanResult(false, $"This card is already scanned for {sessionName} today.", 0);
+ //}
if (alreadyScannedThisSessionToday != null)
{
- var sessionName = GetMealSessionDisplayName(session);
+ //var sessionName = session.ToString();
+ var sessionName = resolvedSession.MealName;
return new ScanResult(false, $"This card is already scanned for {sessionName} today.", 0);
}
@@ -106,6 +126,45 @@ public class RfidService : IRfidService
var siteId = !string.IsNullOrWhiteSpace(employee.LocationSiteId)
? employee.LocationSiteId.Trim()
: _configService.GetSiteId();
+
+ // Resolve menu items for this scan (store on record so order history shows actual items)
+ //var mealLabel = GetMealSessionDisplayName(session);
+ //var mealLabel = session.ToString();
+ var mealLabel = resolvedSession.MealName;
+ var mealItemsDisplay = string.Empty;
+ double totalPrice = 0;
+ try
+ {
+ if (int.TryParse(new string(siteIdForSession.Where(char.IsDigit).ToArray()), out var siteNumeric) && siteNumeric > 0)
+ {
+ var menuItems = _menuLookup
+ .GetMenuItemsForSiteAndDateAsync(siteNumeric, nowLocal.Date)
+ .GetAwaiter()
+ .GetResult();
+
+ //var matching = menuItems
+ // .Where(i => string.Equals(i.MealName, mealLabel, StringComparison.OrdinalIgnoreCase))
+ // .ToList();
+ var matching = menuItems.ToList();
+
+ var names = matching
+ .Select(i => i.ItemName)
+ .Where(n => !string.IsNullOrWhiteSpace(n))
+ .Distinct()
+ .ToList();
+
+ if (names.Count > 0)
+ {
+ mealItemsDisplay = string.Join(" + ", names);
+ totalPrice = matching.Sum(i => (double)i.Price);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.Log(ex, "RfidService.ProcessScanDetailed menu lookup");
+ }
+
var record = new ScanRecord
{
CardId = cardId,
@@ -124,7 +183,10 @@ public class RfidService : IRfidService
TagCreatedBy = employee.TagCreatedBy ?? string.Empty,
EmployeeName = string.IsNullOrEmpty(fullName) ? string.Empty : fullName,
Department = employee.DepartmentTitle ?? string.Empty,
- DepartmentType = employee.DepartmentType ?? string.Empty
+ DepartmentType = employee.DepartmentType ?? string.Empty,
+ MealLabel = mealLabel,
+ MealItems = mealItemsDisplay,
+ TotalPrice = totalPrice
};
db.LunchOrderTransactions.Add(record);
db.SaveChanges();
@@ -132,17 +194,17 @@ public class RfidService : IRfidService
return new ScanResult(true, "Order recorded successfully.", 0, employee, session);
}
- private static string GetMealSessionDisplayName(MealSession session)
- {
- return session switch
- {
- MealSession.Breakfast => "Sehri",
- MealSession.Lunch => "Iftari",
- MealSession.Tea => "Tea",
- MealSession.Dinner => "Dinner",
- _ => "this meal"
- };
- }
+ //private static string GetMealSessionDisplayName(MealSession session)
+ //{
+ // return session switch
+ // {
+ // MealSession.Breakfast => "Sehri",
+ // MealSession.Lunch => "Iftari",
+ // MealSession.Tea => "Tea",
+ // MealSession.Dinner => "Dinner",
+ // _ => "this meal"
+ // };
+ //}
public ScanRecord? GetLastScan()
{
diff --git a/ViewModels/MealSchedulesViewModel.cs b/ViewModels/MealSchedulesViewModel.cs
index f6d3adf..e867893 100644
--- a/ViewModels/MealSchedulesViewModel.cs
+++ b/ViewModels/MealSchedulesViewModel.cs
@@ -1,3 +1,244 @@
+//using System.Collections.ObjectModel;
+//using System.Windows;
+//using CommunityToolkit.Mvvm.ComponentModel;
+//using CommunityToolkit.Mvvm.Input;
+//using MySqlConnector;
+//using UtopiaCanteenSystem.Models;
+//using UtopiaCanteenSystem.Services;
+
+//namespace UtopiaCanteenSystem.ViewModels;
+
+/////
+///// Admin CRUD for meal schedules in production (hrms.meal_schedule). No SQLite. Load all on open; optional Site filter (local).
+/////
+//public partial class MealSchedulesViewModel : ObservableObject
+//{
+// private readonly IMealScheduleService _mealScheduleService;
+// private readonly INavigationService _navigation;
+
+// [ObservableProperty]
+// private ObservableCollection _schedules = new();
+
+// /// Full list from production; Schedules is filtered by SelectedSiteFilter.
+// private List _allSchedules = new();
+
+// [ObservableProperty]
+// private ObservableCollection _siteFilterChoices = new() { "All" };
+
+// [ObservableProperty]
+// private string _selectedSiteFilter = "All";
+
+// [ObservableProperty]
+// private MealSchedule? _selectedSchedule;
+
+// [ObservableProperty]
+// private string _locationSiteId = string.Empty;
+
+// [ObservableProperty]
+// private string _mealName = string.Empty;
+
+// [ObservableProperty]
+// private string _startTime = "06:00:00";
+
+// [ObservableProperty]
+// private string _endTime = "09:00:00";
+
+// [ObservableProperty]
+// private string _message = string.Empty;
+
+// [ObservableProperty]
+// private bool _isError;
+
+// [ObservableProperty]
+// private bool _isLoading;
+
+// public MealSchedulesViewModel(IMealScheduleService mealScheduleService, INavigationService navigation)
+// {
+// _mealScheduleService = mealScheduleService;
+// _navigation = navigation;
+// _ = LoadSchedulesAsync();
+// }
+
+// partial void OnSelectedSiteFilterChanged(string value)
+// {
+// ApplyFilter();
+// }
+
+// partial void OnSelectedScheduleChanged(MealSchedule? value)
+// {
+// if (value == null) return;
+// LocationSiteId = value.LocationSiteId ?? string.Empty;
+// MealName = value.MealName ?? string.Empty;
+// StartTime = value.StartTime ?? "00:00:00";
+// EndTime = value.EndTime ?? "23:59:59";
+// }
+
+// private void ApplyFilter()
+// {
+// if (string.IsNullOrEmpty(SelectedSiteFilter) || SelectedSiteFilter == "All")
+// {
+// Schedules = new ObservableCollection(_allSchedules);
+// return;
+// }
+// var filtered = _allSchedules.Where(s => string.Equals(s.LocationSiteId?.Trim(), SelectedSiteFilter.Trim(), StringComparison.OrdinalIgnoreCase)).ToList();
+// Schedules = new ObservableCollection(filtered);
+// }
+
+// private async Task LoadSchedulesAsync()
+// {
+// IsLoading = true;
+// Message = string.Empty;
+// IsError = false;
+// try
+// {
+// var list = await _mealScheduleService.GetAllSchedulesAsync().ConfigureAwait(true);
+// _allSchedules = list.ToList();
+// var siteIds = _allSchedules.Select(s => s.LocationSiteId?.Trim() ?? "").Where(s => !string.IsNullOrEmpty(s)).Distinct().OrderBy(s => s, StringComparer.Ordinal).ToList();
+// SiteFilterChoices = new ObservableCollection(new[] { "All" }.Concat(siteIds));
+// SelectedSiteFilter = "All";
+// ApplyFilter();
+// if (_allSchedules.Count == 0)
+// Message = "No schedules in production. Add one below (HRMS MySQL must be configured).";
+// }
+// catch (Exception ex)
+// {
+// IsError = true;
+// var msg = ex is MySqlException mysql
+// ? $"{mysql.Message}{Environment.NewLine}{Environment.NewLine}{mysql.StackTrace}"
+// : $"{ex.Message}{Environment.NewLine}{Environment.NewLine}{ex.StackTrace}";
+// Message = "Could not load schedules from production.";
+// MessageBox.Show(msg, "Meal Schedules – Load Error", MessageBoxButton.OK, MessageBoxImage.Error);
+// }
+// finally
+// {
+// IsLoading = false;
+// }
+// }
+
+// [RelayCommand]
+// private void AddNew()
+// {
+// SelectedSchedule = null;
+// LocationSiteId = "02";
+// MealName = string.Empty;
+// StartTime = "06:00:00";
+// EndTime = "09:00:00";
+// Message = string.Empty;
+// }
+
+// [RelayCommand]
+// private async Task Save()
+// {
+// Message = string.Empty;
+// IsError = false;
+
+// if (!TimeSpan.TryParse(StartTime?.Trim(), out var start) || !TimeSpan.TryParse(EndTime?.Trim(), out var end))
+// {
+// Message = "Start time and end time must be in HH:mm:ss format.";
+// IsError = true;
+// return;
+// }
+// if (start >= end)
+// {
+// Message = "Start time must be before end time.";
+// IsError = true;
+// return;
+// }
+
+// var siteId = (LocationSiteId ?? string.Empty).Trim();
+// if (string.IsNullOrEmpty(siteId))
+// {
+// Message = "Location site ID is required.";
+// IsError = true;
+// return;
+// }
+
+// try
+// {
+// if (SelectedSchedule != null)
+// {
+// var dto = new MealSchedule
+// {
+// Id = SelectedSchedule.Id,
+// MealName = (MealName ?? string.Empty).Trim(),
+// LocationSiteId = siteId,
+// StartTime = StartTime.Trim(),
+// EndTime = EndTime.Trim()
+// };
+// await _mealScheduleService.UpdateAsync(dto).ConfigureAwait(true);
+// Message = "Schedule updated in production.";
+// }
+// else
+// {
+// var dto = new MealSchedule
+// {
+// MealName = (MealName ?? string.Empty).Trim(),
+// LocationSiteId = siteId,
+// StartTime = StartTime.Trim(),
+// EndTime = EndTime.Trim()
+// };
+// await _mealScheduleService.CreateAsync(dto).ConfigureAwait(true);
+// Message = "Schedule added to production.";
+// }
+// await LoadSchedulesAsync().ConfigureAwait(true);
+// }
+// catch (Exception ex)
+// {
+// IsError = true;
+// var msg = ex is MySqlException mysql
+// ? $"{mysql.Message}{Environment.NewLine}{Environment.NewLine}{mysql.StackTrace}"
+// : $"{ex.Message}{Environment.NewLine}{Environment.NewLine}{ex.StackTrace}";
+// Message = "Failed to save to production.";
+// MessageBox.Show(msg, "Meal Schedules – Save Error", MessageBoxButton.OK, MessageBoxImage.Error);
+// }
+// }
+
+// /// Selects a schedule for editing (used by Actions column Edit button).
+// [RelayCommand]
+// private void SelectSchedule(MealSchedule? schedule)
+// {
+// SelectedSchedule = schedule;
+// }
+
+// [RelayCommand]
+// private async Task Delete(MealSchedule? row)
+// {
+// var toDelete = row ?? SelectedSchedule;
+// if (toDelete == null)
+// {
+// Message = "Select a schedule to delete.";
+// IsError = true;
+// return;
+// }
+// Message = string.Empty;
+// IsError = false;
+// try
+// {
+// await _mealScheduleService.DeleteAsync(toDelete.Id).ConfigureAwait(true);
+// Message = "Schedule deleted from production.";
+// SelectedSchedule = null;
+// await LoadSchedulesAsync().ConfigureAwait(true);
+// }
+// catch (Exception ex)
+// {
+// IsError = true;
+// var msg = ex is MySqlException mysql
+// ? $"{mysql.Message}{Environment.NewLine}{Environment.NewLine}{mysql.StackTrace}"
+// : $"{ex.Message}{Environment.NewLine}{Environment.NewLine}{ex.StackTrace}";
+// Message = "Failed to delete.";
+// MessageBox.Show(msg, "Meal Schedules – Delete Error", MessageBoxButton.OK, MessageBoxImage.Error);
+// }
+// }
+
+// [RelayCommand]
+// private void Back()
+// {
+// _navigation.NavigateToSettings();
+// }
+//}
+
+
+
using System.Collections.ObjectModel;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
@@ -9,17 +250,19 @@ using UtopiaCanteenSystem.Services;
namespace UtopiaCanteenSystem.ViewModels;
///
-/// Admin CRUD for meal schedules in production (hrms.meal_schedule). No SQLite. Load all on open; optional Site filter (local).
+/// View-only display of meal schedules from production.
+/// Loads all schedules on open; optional Site filter.
///
public partial class MealSchedulesViewModel : ObservableObject
{
private readonly IMealScheduleService _mealScheduleService;
private readonly INavigationService _navigation;
+ private readonly IConfigService _configService;
+ private readonly string _currentSiteId;
[ObservableProperty]
private ObservableCollection _schedules = new();
- /// Full list from production; Schedules is filtered by SelectedSiteFilter.
private List _allSchedules = new();
[ObservableProperty]
@@ -31,18 +274,6 @@ public partial class MealSchedulesViewModel : ObservableObject
[ObservableProperty]
private MealSchedule? _selectedSchedule;
- [ObservableProperty]
- private string _locationSiteId = string.Empty;
-
- [ObservableProperty]
- private string _mealName = string.Empty;
-
- [ObservableProperty]
- private string _startTime = "06:00:00";
-
- [ObservableProperty]
- private string _endTime = "09:00:00";
-
[ObservableProperty]
private string _message = string.Empty;
@@ -52,10 +283,12 @@ public partial class MealSchedulesViewModel : ObservableObject
[ObservableProperty]
private bool _isLoading;
- public MealSchedulesViewModel(IMealScheduleService mealScheduleService, INavigationService navigation)
+ public MealSchedulesViewModel(IMealScheduleService mealScheduleService, INavigationService navigation, IConfigService configService)
{
_mealScheduleService = mealScheduleService;
_navigation = navigation;
+ _configService = configService;
+ _currentSiteId = (_configService.GetSiteId() ?? string.Empty).Trim();
_ = LoadSchedulesAsync();
}
@@ -64,23 +297,12 @@ public partial class MealSchedulesViewModel : ObservableObject
ApplyFilter();
}
- partial void OnSelectedScheduleChanged(MealSchedule? value)
- {
- if (value == null) return;
- LocationSiteId = value.LocationSiteId ?? string.Empty;
- MealName = value.MealName ?? string.Empty;
- StartTime = value.StartTime ?? "00:00:00";
- EndTime = value.EndTime ?? "23:59:59";
- }
-
private void ApplyFilter()
{
- if (string.IsNullOrEmpty(SelectedSiteFilter) || SelectedSiteFilter == "All")
- {
- Schedules = new ObservableCollection(_allSchedules);
- return;
- }
- var filtered = _allSchedules.Where(s => string.Equals(s.LocationSiteId?.Trim(), SelectedSiteFilter.Trim(), StringComparison.OrdinalIgnoreCase)).ToList();
+ var site = string.IsNullOrWhiteSpace(_currentSiteId) ? "01" : _currentSiteId;
+ var filtered = _allSchedules
+ .Where(s => string.Equals(s.LocationSiteId?.Trim(), site, StringComparison.OrdinalIgnoreCase))
+ .ToList();
Schedules = new ObservableCollection(filtered);
}
@@ -89,24 +311,36 @@ public partial class MealSchedulesViewModel : ObservableObject
IsLoading = true;
Message = string.Empty;
IsError = false;
+
try
{
var list = await _mealScheduleService.GetAllSchedulesAsync().ConfigureAwait(true);
_allSchedules = list.ToList();
- var siteIds = _allSchedules.Select(s => s.LocationSiteId?.Trim() ?? "").Where(s => !string.IsNullOrEmpty(s)).Distinct().OrderBy(s => s, StringComparer.Ordinal).ToList();
- SiteFilterChoices = new ObservableCollection(new[] { "All" }.Concat(siteIds));
- SelectedSiteFilter = "All";
+
+ var siteIds = _allSchedules
+ .Select(s => s.LocationSiteId?.Trim() ?? "")
+ .Where(s => !string.IsNullOrEmpty(s))
+ .Distinct()
+ .OrderBy(s => s, StringComparer.Ordinal)
+ .ToList();
+
+ // Only show schedules for current configured site; hide other sites from filter choices.
+ SiteFilterChoices = new ObservableCollection(new[] { string.IsNullOrWhiteSpace(_currentSiteId) ? "01" : _currentSiteId });
+ SelectedSiteFilter = SiteFilterChoices.First();
ApplyFilter();
+
if (_allSchedules.Count == 0)
- Message = "No schedules in production. Add one below (HRMS MySQL must be configured).";
+ Message = "No schedules configured.";
}
catch (Exception ex)
{
IsError = true;
+
var msg = ex is MySqlException mysql
? $"{mysql.Message}{Environment.NewLine}{Environment.NewLine}{mysql.StackTrace}"
: $"{ex.Message}{Environment.NewLine}{Environment.NewLine}{ex.StackTrace}";
- Message = "Could not load schedules from production.";
+
+ Message = "Could not load schedules.";
MessageBox.Show(msg, "Meal Schedules – Load Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
@@ -115,124 +349,9 @@ public partial class MealSchedulesViewModel : ObservableObject
}
}
- [RelayCommand]
- private void AddNew()
- {
- SelectedSchedule = null;
- LocationSiteId = "02";
- MealName = string.Empty;
- StartTime = "06:00:00";
- EndTime = "09:00:00";
- Message = string.Empty;
- }
-
- [RelayCommand]
- private async Task Save()
- {
- Message = string.Empty;
- IsError = false;
-
- if (!TimeSpan.TryParse(StartTime?.Trim(), out var start) || !TimeSpan.TryParse(EndTime?.Trim(), out var end))
- {
- Message = "Start time and end time must be in HH:mm:ss format.";
- IsError = true;
- return;
- }
- if (start >= end)
- {
- Message = "Start time must be before end time.";
- IsError = true;
- return;
- }
-
- var siteId = (LocationSiteId ?? string.Empty).Trim();
- if (string.IsNullOrEmpty(siteId))
- {
- Message = "Location site ID is required.";
- IsError = true;
- return;
- }
-
- try
- {
- if (SelectedSchedule != null)
- {
- var dto = new MealSchedule
- {
- Id = SelectedSchedule.Id,
- MealName = (MealName ?? string.Empty).Trim(),
- LocationSiteId = siteId,
- StartTime = StartTime.Trim(),
- EndTime = EndTime.Trim()
- };
- await _mealScheduleService.UpdateAsync(dto).ConfigureAwait(true);
- Message = "Schedule updated in production.";
- }
- else
- {
- var dto = new MealSchedule
- {
- MealName = (MealName ?? string.Empty).Trim(),
- LocationSiteId = siteId,
- StartTime = StartTime.Trim(),
- EndTime = EndTime.Trim()
- };
- await _mealScheduleService.CreateAsync(dto).ConfigureAwait(true);
- Message = "Schedule added to production.";
- }
- await LoadSchedulesAsync().ConfigureAwait(true);
- }
- catch (Exception ex)
- {
- IsError = true;
- var msg = ex is MySqlException mysql
- ? $"{mysql.Message}{Environment.NewLine}{Environment.NewLine}{mysql.StackTrace}"
- : $"{ex.Message}{Environment.NewLine}{Environment.NewLine}{ex.StackTrace}";
- Message = "Failed to save to production.";
- MessageBox.Show(msg, "Meal Schedules – Save Error", MessageBoxButton.OK, MessageBoxImage.Error);
- }
- }
-
- /// Selects a schedule for editing (used by Actions column Edit button).
- [RelayCommand]
- private void SelectSchedule(MealSchedule? schedule)
- {
- SelectedSchedule = schedule;
- }
-
- [RelayCommand]
- private async Task Delete(MealSchedule? row)
- {
- var toDelete = row ?? SelectedSchedule;
- if (toDelete == null)
- {
- Message = "Select a schedule to delete.";
- IsError = true;
- return;
- }
- Message = string.Empty;
- IsError = false;
- try
- {
- await _mealScheduleService.DeleteAsync(toDelete.Id).ConfigureAwait(true);
- Message = "Schedule deleted from production.";
- SelectedSchedule = null;
- await LoadSchedulesAsync().ConfigureAwait(true);
- }
- catch (Exception ex)
- {
- IsError = true;
- var msg = ex is MySqlException mysql
- ? $"{mysql.Message}{Environment.NewLine}{Environment.NewLine}{mysql.StackTrace}"
- : $"{ex.Message}{Environment.NewLine}{Environment.NewLine}{ex.StackTrace}";
- Message = "Failed to delete.";
- MessageBox.Show(msg, "Meal Schedules – Delete Error", MessageBoxButton.OK, MessageBoxImage.Error);
- }
- }
-
[RelayCommand]
private void Back()
{
_navigation.NavigateToSettings();
}
-}
+}
\ No newline at end of file
diff --git a/ViewModels/ScannerDashboardViewModel.cs b/ViewModels/ScannerDashboardViewModel.cs
index a38ca40..cc4cccd 100644
--- a/ViewModels/ScannerDashboardViewModel.cs
+++ b/ViewModels/ScannerDashboardViewModel.cs
@@ -276,8 +276,8 @@ public partial class ScannerDashboardViewModel : ObservableObject
//};
mealLabel = activeSession switch
{
- MealSession.Breakfast => "Sehri",
- MealSession.Lunch => "Iftari",
+ MealSession.Breakfast => "Breakfast",
+ MealSession.Lunch => "Lunch",
MealSession.Tea => "Tea",
MealSession.Dinner => "Dinner",
_ => "No active meal session"
@@ -298,9 +298,10 @@ public partial class ScannerDashboardViewModel : ObservableObject
// i.ItemType.IndexOf(mealLabel, StringComparison.OrdinalIgnoreCase) >= 0)
// .ToList();
- var matchingItems = items
- .Where(i => string.Equals(i.MealName, mealLabel, StringComparison.OrdinalIgnoreCase))
- .ToList();
+ // var matchingItems = items
+ //.Where(i => string.Equals(i.MealName, mealLabel, StringComparison.OrdinalIgnoreCase))
+ //.ToList();
+ var matchingItems = items.ToList();
var matchingItemNames = matchingItems
.Select(i => i.ItemName)
@@ -325,10 +326,10 @@ public partial class ScannerDashboardViewModel : ObservableObject
// Persist these values on the latest scan record so history rows keep their own Meal/Price.
// Only do this when we are handling an actual scan (sessionFromScan has a value).
- if (sessionFromScan.HasValue && activeSession != MealSession.None)
- {
- _rfidService.UpdateLastScanMealInfo(mealLabel, historyDisplay, totalPrice);
- }
+ //if (sessionFromScan.HasValue && activeSession != MealSession.None)
+ //{
+ // _rfidService.UpdateLastScanMealInfo(mealLabel, historyDisplay, totalPrice);
+ //}
_uiDispatcher.Invoke(() =>
{
EmployeeOrderItem = mealLabel;
@@ -534,7 +535,7 @@ public partial class ScannerDashboardViewModel : ObservableObject
EmployeeDepartmentType = !string.IsNullOrWhiteSpace(info.DepartmentType) ? info.DepartmentType : "—";
// Load menu using site from employee_rfid_tag.location_site_id (not config)
if (int.TryParse(info.LocationSiteId?.Trim(), out var siteFromRfid))
- _ = LoadMenuForSiteAsync(siteFromRfid, result.MealSession);
+ //_ = LoadMenuForSiteAsync(siteFromRfid, result.MealSession);
// Load employee photo from hrms.employee_photo by parent_document_id (employee document id)
_ = LoadEmployeePhotoAsync(info.ParentDocumentId);
}
diff --git a/Views/MealSchedulesView.xaml b/Views/MealSchedulesView.xaml
index 327424d..0039805 100644
--- a/Views/MealSchedulesView.xaml
+++ b/Views/MealSchedulesView.xaml
@@ -1,11 +1,11 @@
-
-
+ -->
+ -->
+ -->
+ -->
+ -->
+ -->
+ -->
+ -->
+ -->
+ -->
+ Margin="0,2,0,0"/>-->
+ -->
+ -->
+ -->
+ -->
+ -->
+ -->
+ -->
+ -->
+ -->
+ -->
+ -->
+ -->
+ -->
+ -->
+ -->
+ -->
+ -->
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/scripts/mysql_meal_schedule_table.sql b/scripts/mysql_meal_schedule_table.sql
new file mode 100644
index 0000000..c0e7214
--- /dev/null
+++ b/scripts/mysql_meal_schedule_table.sql
@@ -0,0 +1,14 @@
+-- Production meal_schedule table (already exists on POD).
+-- Schema used by ProductionMealScheduleService (Settings → Meal Schedules).
+--
+-- Columns:
+-- id bigint PK auto_increment
+-- meal_name varchar(255) e.g. 'Breakfast', 'Lunch', 'Dinner'
+-- start_time time
+-- end_time time
+-- created_at timestamp
+-- updated_at timestamp
+-- location_site_id int default 0
+--
+-- No is_active column; all rows are treated as active.
+-- App maps meal_name <-> MealSession (0=Breakfast, 1=Lunch, 2=Tea, 3=Dinner).
diff --git a/scripts/mysql_migrate_remove_device_local_row_id.sql b/scripts/mysql_migrate_remove_device_local_row_id.sql
new file mode 100644
index 0000000..eb40aab
--- /dev/null
+++ b/scripts/mysql_migrate_remove_device_local_row_id.sql
@@ -0,0 +1,32 @@
+-- Migration: remove device_local_row_id, rename columns, and update unique index.
+-- Run this on hrms (production) before deploying the app that uses new column names.
+-- Production still has: unique index (device_id, device_local_row_id) and column device_local_row_id.
+--
+-- Order: 1) Drop unique index 2) Drop column 3) Rename scan_time_utc 4) Rename received_at_utc 5) Add new unique key
+
+USE hrms;
+
+-- 1) Drop the unique index on (device_id, device_local_row_id)
+ALTER TABLE lunch_order_transactions
+ DROP INDEX ux_lunch_order_tx_device_device_local_row_id;
+
+-- 2) Drop the column
+ALTER TABLE lunch_order_transactions
+ DROP COLUMN device_local_row_id;
+
+-- 3) Rename scan_time_utc → scan_date
+ALTER TABLE lunch_order_transactions
+ CHANGE COLUMN scan_time_utc scan_date DATETIME(3) NOT NULL;
+
+-- 4) Rename received_at_utc → received_date
+ALTER TABLE lunch_order_transactions
+ CHANGE COLUMN received_at_utc received_date DATETIME(3) NOT NULL DEFAULT (UTC_TIMESTAMP(3));
+
+-- 5) Add new unique key so INSERT IGNORE still deduplicates (same site/device/card/time = one row)
+ALTER TABLE lunch_order_transactions
+ ADD UNIQUE KEY ux_lunch_order_tx_site_device_card_scan_date (
+ site_id,
+ device_id,
+ card_id,
+ scan_date
+ );