commit
db28c3488c
|
|
@ -44,7 +44,7 @@ public partial class App : Application
|
||||||
var menuLookupService = new MenuLookupService(configService);
|
var menuLookupService = new MenuLookupService(configService);
|
||||||
var mealScheduleService = new ProductionMealScheduleService(configService);
|
var mealScheduleService = new ProductionMealScheduleService(configService);
|
||||||
var mealSessionResolver = new DbMealSessionResolver(mealScheduleService);
|
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 syncService = new SyncService(dbFactory, configService);
|
||||||
var adminAuditService = new AdminAuditService(dbFactory);
|
var adminAuditService = new AdminAuditService(dbFactory);
|
||||||
var session = new AppSession();
|
var session = new AppSession();
|
||||||
|
|
@ -60,7 +60,7 @@ public partial class App : Application
|
||||||
() => new MainDashboardViewModel(navigationService, rfidService, configService, session),
|
() => new MainDashboardViewModel(navigationService, rfidService, configService, session),
|
||||||
() => new AdminSettingsAuthViewModel(authService, session, navigationService, configService),
|
() => new AdminSettingsAuthViewModel(authService, session, navigationService, configService),
|
||||||
() => new SettingsViewModel(configService, navigationService, adminAuditService, syncService),
|
() => new SettingsViewModel(configService, navigationService, adminAuditService, syncService),
|
||||||
() => new MealSchedulesViewModel(mealScheduleService, navigationService));
|
() => new MealSchedulesViewModel(mealScheduleService, navigationService, configService));
|
||||||
|
|
||||||
var mainViewModel = new MainViewModel(navigationService);
|
var mainViewModel = new MainViewModel(navigationService);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
/// <summary>
|
||||||
|
/// View-only display of meal schedules from production (hrms.meal_schedule).
|
||||||
|
/// No CRUD operations.
|
||||||
|
/// Loads all schedules on open; optional Site filter (local).
|
||||||
|
/// </summary>
|
||||||
|
public partial class MealSchedulesViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
// Only these properties remain:
|
||||||
|
[ObservableProperty] private ObservableCollection<MealSchedule> _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
|
||||||
|
<!-- REMOVED: -->
|
||||||
|
<Button Grid.Column="3"
|
||||||
|
Command="{Binding AddNewCommand}"
|
||||||
|
Content="+ Add New"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Actions Column in DataGrid** (Edit/Delete icons)
|
||||||
|
```xml
|
||||||
|
<!-- REMOVED: -->
|
||||||
|
<DataGridTemplateColumn Header="Actions" Width="100">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<Button Style="{StaticResource GridActionEditButton}" ... />
|
||||||
|
<Button Style="{StaticResource GridActionDeleteButton}" ... />
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Entire Add/Edit Form Card**
|
||||||
|
```xml
|
||||||
|
<!-- REMOVED: Complete "Add or Edit Meal Schedule" card with:
|
||||||
|
- Location Site ID input
|
||||||
|
- Meal Session input
|
||||||
|
- Start Time input
|
||||||
|
- End Time input
|
||||||
|
- Add New / Save / Delete buttons
|
||||||
|
-->
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **Updated DataGrid Columns**
|
||||||
|
```xml
|
||||||
|
<!-- BEFORE (with Actions column): -->
|
||||||
|
<DataGridTextColumn Header="Site" Binding="{Binding LocationSiteId}" Width="*" />
|
||||||
|
<DataGridTextColumn Header="Session" Binding="{Binding MealName}" Width="*" />
|
||||||
|
<DataGridTextColumn Header="Start" Binding="{Binding StartTime}" Width="*" />
|
||||||
|
<DataGridTextColumn Header="End" Binding="{Binding EndTime}" Width="*" />
|
||||||
|
<DataGridTemplateColumn Header="Actions" Width="100">...edit/delete buttons...</DataGridTemplateColumn>
|
||||||
|
|
||||||
|
<!-- AFTER (no Actions column): -->
|
||||||
|
<DataGridTextColumn Header="Site" Binding="{Binding LocationSiteId}" Width="*" />
|
||||||
|
<DataGridTextColumn Header="Session" Binding="{Binding MealName}" Width="*" />
|
||||||
|
<DataGridTextColumn Header="Start Time" Binding="{Binding StartTime}" Width="*" />
|
||||||
|
<DataGridTextColumn Header="End Time" Binding="{Binding EndTime}" Width="*" />
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **Updated Header Text**
|
||||||
|
```xml
|
||||||
|
<!-- BEFORE: -->
|
||||||
|
<TextBlock Text="Meal Schedules" ... />
|
||||||
|
|
||||||
|
<!-- AFTER: -->
|
||||||
|
<TextBlock Text="Meal Schedules" ... />
|
||||||
|
<TextBlock Text="View-only mode - configured meal windows per site" ... />
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **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
|
||||||
|
<!-- Kept but noted as potentially removable: -->
|
||||||
|
- 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<long> 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<IReadOnlyList<MealSchedule>> 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.
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -19,8 +19,62 @@ public class DbMealSessionResolver : IMealSessionResolver
|
||||||
_mealScheduleService = mealScheduleService;
|
_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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
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();
|
var normalizedSite = (siteId ?? string.Empty).Trim();
|
||||||
if (string.IsNullOrEmpty(normalizedSite))
|
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))
|
if (!TryParseTime(s.StartTime, out var start) || !TryParseTime(s.EndTime, out var end))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (t >= start && t < end)
|
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<MealSchedule> GetSchedulesForSiteCached(string siteId)
|
private List<MealSchedule> GetSchedulesForSiteCached(string siteId)
|
||||||
{
|
{
|
||||||
lock (_cacheLock)
|
lock (_cacheLock)
|
||||||
|
|
|
||||||
|
|
@ -10,5 +10,6 @@ public interface IMealSessionResolver
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the active meal session for the given local time and site, or MealSession.None if outside all windows.
|
/// Returns the active meal session for the given local time and site, or MealSession.None if outside all windows.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
MealSession GetCurrentSession(DateTime nowLocal, string siteId);
|
//MealSession GetCurrentSession(DateTime nowLocal, string siteId);
|
||||||
|
ResolvedMealSession? GetCurrentSession(DateTime nowLocal, string siteId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,10 @@ public interface IMenuLookupService
|
||||||
/// siteIdNumeric: match to lunch_menu_week.location_site_id (e.g. 2 for site "02").
|
/// siteIdNumeric: match to lunch_menu_week.location_site_id (e.g. 2 for site "02").
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<IReadOnlyList<HrmsMenuItem>> GetMenuItemsForSiteAsync(int siteIdNumeric, CancellationToken cancellationToken = default);
|
Task<IReadOnlyList<HrmsMenuItem>> GetMenuItemsForSiteAsync(int siteIdNumeric, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets menu items for the given site and specific local date (yyyy-MM-dd).
|
||||||
|
/// Avoids server time mismatch by not relying on CURDATE().
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<HrmsMenuItem>> GetMenuItemsForSiteAndDateAsync(int siteIdNumeric, DateTime menuDateLocal, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -18,6 +18,13 @@ public class MenuLookupService : IMenuLookupService
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<HrmsMenuItem>> GetMenuItemsForSiteAsync(int siteIdNumeric, CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<HrmsMenuItem>> GetMenuItemsForSiteAsync(int siteIdNumeric, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// Backwards-compatible: use today's local date.
|
||||||
|
return await GetMenuItemsForSiteAndDateAsync(siteIdNumeric, DateTime.Today, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<HrmsMenuItem>> GetMenuItemsForSiteAndDateAsync(int siteIdNumeric, DateTime menuDateLocal, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var connectionString = _configService.GetHrmsLookupConnectionString();
|
var connectionString = _configService.GetHrmsLookupConnectionString();
|
||||||
if (string.IsNullOrWhiteSpace(connectionString))
|
if (string.IsNullOrWhiteSpace(connectionString))
|
||||||
|
|
@ -31,28 +38,45 @@ public class MenuLookupService : IMenuLookupService
|
||||||
// WHERE w.location_site_id = @siteId
|
// WHERE w.location_site_id = @siteId
|
||||||
// AND CURDATE() BETWEEN w.week_start_date AND w.week_end_date
|
// AND CURDATE() BETWEEN w.week_start_date AND w.week_end_date
|
||||||
// ORDER BY mi.item_type, mi.item_name";
|
// ORDER BY mi.item_type, mi.item_name";
|
||||||
|
|
||||||
const string sql = @"
|
const string sql = @"
|
||||||
SELECT
|
SELECT
|
||||||
mi.id,
|
mi.id,
|
||||||
mi.item_name,
|
mi.item_name,
|
||||||
mi.item_type,
|
mi.item_type,
|
||||||
mi.price,
|
mi.price,
|
||||||
li.meal_name,
|
|
||||||
li.menu_date,
|
li.menu_date,
|
||||||
li.day_of_week
|
li.day_of_week
|
||||||
FROM lunch_menu_week w
|
FROM lunch_menu_week w
|
||||||
JOIN lunch_menu_item li ON li.lunch_menu_week_id = w.id
|
JOIN lunch_menu_item li ON li.lunch_menu_week_id = w.id
|
||||||
JOIN menu_item mi ON mi.id = li.menu_item_id
|
JOIN menu_item mi ON mi.id = li.menu_item_id
|
||||||
WHERE w.location_site_id = @siteId
|
WHERE w.location_site_id = @siteId
|
||||||
AND CURDATE() BETWEEN w.week_start_date AND w.week_end_date
|
AND @menuDate BETWEEN w.week_start_date AND w.week_end_date
|
||||||
AND li.menu_date = DATE_FORMAT(CURDATE(), '%Y-%m-%d')
|
AND li.menu_date = @menuDate
|
||||||
ORDER BY li.meal_name, mi.item_name;
|
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 using var conn = new MySqlConnection(connectionString);
|
||||||
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
|
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
|
||||||
await using var cmd = new MySqlCommand(sql, conn);
|
await using var cmd = new MySqlCommand(sql, conn);
|
||||||
cmd.Parameters.AddWithValue("@siteId", siteIdNumeric);
|
cmd.Parameters.AddWithValue("@siteId", siteIdNumeric);
|
||||||
|
cmd.Parameters.AddWithValue("@menuDate", menuDateLocal.Date);
|
||||||
|
|
||||||
var list = new List<HrmsMenuItem>();
|
var list = new List<HrmsMenuItem>();
|
||||||
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
@ -72,11 +96,22 @@ public class MenuLookupService : IMenuLookupService
|
||||||
ItemName = GetString(reader, 1),
|
ItemName = GetString(reader, 1),
|
||||||
ItemType = GetString(reader, 2),
|
ItemType = GetString(reader, 2),
|
||||||
Price = GetDecimal(reader, 3),
|
Price = GetDecimal(reader, 3),
|
||||||
|
MenuDate = GetString(reader, 4),
|
||||||
MealName = GetString(reader, 4),
|
DayOfWeek = GetString(reader, 5),
|
||||||
MenuDate = GetString(reader, 5),
|
MealName = string.Empty
|
||||||
DayOfWeek = GetString(reader, 6),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//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;
|
return list;
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,20 @@ public class RfidService : IRfidService
|
||||||
private readonly IConfigService _configService;
|
private readonly IConfigService _configService;
|
||||||
private readonly IEmployeeLookupService _employeeLookup;
|
private readonly IEmployeeLookupService _employeeLookup;
|
||||||
private readonly IMealSessionResolver _mealSessionResolver;
|
private readonly IMealSessionResolver _mealSessionResolver;
|
||||||
|
private readonly IMenuLookupService _menuLookup;
|
||||||
|
|
||||||
public RfidService(IDbContextFactory<AppDbContext> dbFactory, IConfigService configService, IEmployeeLookupService employeeLookup, IMealSessionResolver mealSessionResolver)
|
public RfidService(
|
||||||
|
IDbContextFactory<AppDbContext> dbFactory,
|
||||||
|
IConfigService configService,
|
||||||
|
IEmployeeLookupService employeeLookup,
|
||||||
|
IMealSessionResolver mealSessionResolver,
|
||||||
|
IMenuLookupService menuLookup)
|
||||||
{
|
{
|
||||||
_dbFactory = dbFactory;
|
_dbFactory = dbFactory;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_employeeLookup = employeeLookup;
|
_employeeLookup = employeeLookup;
|
||||||
_mealSessionResolver = mealSessionResolver;
|
_mealSessionResolver = mealSessionResolver;
|
||||||
|
_menuLookup = menuLookup;
|
||||||
}
|
}
|
||||||
|
|
||||||
public (bool Success, string Message) ProcessScan(string cardId)
|
public (bool Success, string Message) ProcessScan(string cardId)
|
||||||
|
|
@ -54,10 +61,17 @@ public class RfidService : IRfidService
|
||||||
var siteIdForSession = !string.IsNullOrWhiteSpace(employee.LocationSiteId)
|
var siteIdForSession = !string.IsNullOrWhiteSpace(employee.LocationSiteId)
|
||||||
? employee.LocationSiteId.Trim()
|
? employee.LocationSiteId.Trim()
|
||||||
: _configService.GetSiteId();
|
: _configService.GetSiteId();
|
||||||
var session = _mealSessionResolver.GetCurrentSession(nowLocal, siteIdForSession);
|
//var session = _mealSessionResolver.GetCurrentSession(nowLocal, siteIdForSession);
|
||||||
if (session == MealSession.None)
|
//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);
|
return new ScanResult(false, "This scan is outside of valid meal timings.", 0);
|
||||||
|
|
||||||
|
var session = resolvedSession.Session;
|
||||||
var sessionCode = (int)session;
|
var sessionCode = (int)session;
|
||||||
|
|
||||||
using var db = _dbFactory.CreateDbContext();
|
using var db = _dbFactory.CreateDbContext();
|
||||||
|
|
@ -75,9 +89,15 @@ public class RfidService : IRfidService
|
||||||
.OrderByDescending(r => r.ScanTime)
|
.OrderByDescending(r => r.ScanTime)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
//if (alreadyScannedThisSessionToday != null)
|
||||||
|
//{
|
||||||
|
// var sessionName = GetMealSessionDisplayName(session);
|
||||||
|
// return new ScanResult(false, $"This card is already scanned for {sessionName} today.", 0);
|
||||||
|
//}
|
||||||
if (alreadyScannedThisSessionToday != null)
|
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);
|
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)
|
var siteId = !string.IsNullOrWhiteSpace(employee.LocationSiteId)
|
||||||
? employee.LocationSiteId.Trim()
|
? employee.LocationSiteId.Trim()
|
||||||
: _configService.GetSiteId();
|
: _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
|
var record = new ScanRecord
|
||||||
{
|
{
|
||||||
CardId = cardId,
|
CardId = cardId,
|
||||||
|
|
@ -124,7 +183,10 @@ public class RfidService : IRfidService
|
||||||
TagCreatedBy = employee.TagCreatedBy ?? string.Empty,
|
TagCreatedBy = employee.TagCreatedBy ?? string.Empty,
|
||||||
EmployeeName = string.IsNullOrEmpty(fullName) ? string.Empty : fullName,
|
EmployeeName = string.IsNullOrEmpty(fullName) ? string.Empty : fullName,
|
||||||
Department = employee.DepartmentTitle ?? string.Empty,
|
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.LunchOrderTransactions.Add(record);
|
||||||
db.SaveChanges();
|
db.SaveChanges();
|
||||||
|
|
@ -132,17 +194,17 @@ public class RfidService : IRfidService
|
||||||
return new ScanResult(true, "Order recorded successfully.", 0, employee, session);
|
return new ScanResult(true, "Order recorded successfully.", 0, employee, session);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetMealSessionDisplayName(MealSession session)
|
//private static string GetMealSessionDisplayName(MealSession session)
|
||||||
{
|
//{
|
||||||
return session switch
|
// return session switch
|
||||||
{
|
// {
|
||||||
MealSession.Breakfast => "Sehri",
|
// MealSession.Breakfast => "Sehri",
|
||||||
MealSession.Lunch => "Iftari",
|
// MealSession.Lunch => "Iftari",
|
||||||
MealSession.Tea => "Tea",
|
// MealSession.Tea => "Tea",
|
||||||
MealSession.Dinner => "Dinner",
|
// MealSession.Dinner => "Dinner",
|
||||||
_ => "this meal"
|
// _ => "this meal"
|
||||||
};
|
// };
|
||||||
}
|
//}
|
||||||
|
|
||||||
public ScanRecord? GetLastScan()
|
public ScanRecord? GetLastScan()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
///// <summary>
|
||||||
|
///// Admin CRUD for meal schedules in production (hrms.meal_schedule). No SQLite. Load all on open; optional Site filter (local).
|
||||||
|
///// </summary>
|
||||||
|
//public partial class MealSchedulesViewModel : ObservableObject
|
||||||
|
//{
|
||||||
|
// private readonly IMealScheduleService _mealScheduleService;
|
||||||
|
// private readonly INavigationService _navigation;
|
||||||
|
|
||||||
|
// [ObservableProperty]
|
||||||
|
// private ObservableCollection<MealSchedule> _schedules = new();
|
||||||
|
|
||||||
|
// /// <summary>Full list from production; Schedules is filtered by SelectedSiteFilter.</summary>
|
||||||
|
// private List<MealSchedule> _allSchedules = new();
|
||||||
|
|
||||||
|
// [ObservableProperty]
|
||||||
|
// private ObservableCollection<string> _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<MealSchedule>(_allSchedules);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// var filtered = _allSchedules.Where(s => string.Equals(s.LocationSiteId?.Trim(), SelectedSiteFilter.Trim(), StringComparison.OrdinalIgnoreCase)).ToList();
|
||||||
|
// Schedules = new ObservableCollection<MealSchedule>(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<string>(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);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /// <summary>Selects a schedule for editing (used by Actions column Edit button).</summary>
|
||||||
|
// [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.Collections.ObjectModel;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
@ -9,17 +250,19 @@ using UtopiaCanteenSystem.Services;
|
||||||
namespace UtopiaCanteenSystem.ViewModels;
|
namespace UtopiaCanteenSystem.ViewModels;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class MealSchedulesViewModel : ObservableObject
|
public partial class MealSchedulesViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
private readonly IMealScheduleService _mealScheduleService;
|
private readonly IMealScheduleService _mealScheduleService;
|
||||||
private readonly INavigationService _navigation;
|
private readonly INavigationService _navigation;
|
||||||
|
private readonly IConfigService _configService;
|
||||||
|
private readonly string _currentSiteId;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private ObservableCollection<MealSchedule> _schedules = new();
|
private ObservableCollection<MealSchedule> _schedules = new();
|
||||||
|
|
||||||
/// <summary>Full list from production; Schedules is filtered by SelectedSiteFilter.</summary>
|
|
||||||
private List<MealSchedule> _allSchedules = new();
|
private List<MealSchedule> _allSchedules = new();
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
|
|
@ -31,18 +274,6 @@ public partial class MealSchedulesViewModel : ObservableObject
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private MealSchedule? _selectedSchedule;
|
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]
|
[ObservableProperty]
|
||||||
private string _message = string.Empty;
|
private string _message = string.Empty;
|
||||||
|
|
||||||
|
|
@ -52,10 +283,12 @@ public partial class MealSchedulesViewModel : ObservableObject
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _isLoading;
|
private bool _isLoading;
|
||||||
|
|
||||||
public MealSchedulesViewModel(IMealScheduleService mealScheduleService, INavigationService navigation)
|
public MealSchedulesViewModel(IMealScheduleService mealScheduleService, INavigationService navigation, IConfigService configService)
|
||||||
{
|
{
|
||||||
_mealScheduleService = mealScheduleService;
|
_mealScheduleService = mealScheduleService;
|
||||||
_navigation = navigation;
|
_navigation = navigation;
|
||||||
|
_configService = configService;
|
||||||
|
_currentSiteId = (_configService.GetSiteId() ?? string.Empty).Trim();
|
||||||
_ = LoadSchedulesAsync();
|
_ = LoadSchedulesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,23 +297,12 @@ public partial class MealSchedulesViewModel : ObservableObject
|
||||||
ApplyFilter();
|
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()
|
private void ApplyFilter()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(SelectedSiteFilter) || SelectedSiteFilter == "All")
|
var site = string.IsNullOrWhiteSpace(_currentSiteId) ? "01" : _currentSiteId;
|
||||||
{
|
var filtered = _allSchedules
|
||||||
Schedules = new ObservableCollection<MealSchedule>(_allSchedules);
|
.Where(s => string.Equals(s.LocationSiteId?.Trim(), site, StringComparison.OrdinalIgnoreCase))
|
||||||
return;
|
.ToList();
|
||||||
}
|
|
||||||
var filtered = _allSchedules.Where(s => string.Equals(s.LocationSiteId?.Trim(), SelectedSiteFilter.Trim(), StringComparison.OrdinalIgnoreCase)).ToList();
|
|
||||||
Schedules = new ObservableCollection<MealSchedule>(filtered);
|
Schedules = new ObservableCollection<MealSchedule>(filtered);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,24 +311,36 @@ public partial class MealSchedulesViewModel : ObservableObject
|
||||||
IsLoading = true;
|
IsLoading = true;
|
||||||
Message = string.Empty;
|
Message = string.Empty;
|
||||||
IsError = false;
|
IsError = false;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var list = await _mealScheduleService.GetAllSchedulesAsync().ConfigureAwait(true);
|
var list = await _mealScheduleService.GetAllSchedulesAsync().ConfigureAwait(true);
|
||||||
_allSchedules = list.ToList();
|
_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<string>(new[] { "All" }.Concat(siteIds));
|
var siteIds = _allSchedules
|
||||||
SelectedSiteFilter = "All";
|
.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<string>(new[] { string.IsNullOrWhiteSpace(_currentSiteId) ? "01" : _currentSiteId });
|
||||||
|
SelectedSiteFilter = SiteFilterChoices.First();
|
||||||
ApplyFilter();
|
ApplyFilter();
|
||||||
|
|
||||||
if (_allSchedules.Count == 0)
|
if (_allSchedules.Count == 0)
|
||||||
Message = "No schedules in production. Add one below (HRMS MySQL must be configured).";
|
Message = "No schedules configured.";
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
IsError = true;
|
IsError = true;
|
||||||
|
|
||||||
var msg = ex is MySqlException mysql
|
var msg = ex is MySqlException mysql
|
||||||
? $"{mysql.Message}{Environment.NewLine}{Environment.NewLine}{mysql.StackTrace}"
|
? $"{mysql.Message}{Environment.NewLine}{Environment.NewLine}{mysql.StackTrace}"
|
||||||
: $"{ex.Message}{Environment.NewLine}{Environment.NewLine}{ex.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);
|
MessageBox.Show(msg, "Meal Schedules – Load Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
@ -115,121 +349,6 @@ 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Selects a schedule for editing (used by Actions column Edit button).</summary>
|
|
||||||
[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]
|
[RelayCommand]
|
||||||
private void Back()
|
private void Back()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -276,8 +276,8 @@ public partial class ScannerDashboardViewModel : ObservableObject
|
||||||
//};
|
//};
|
||||||
mealLabel = activeSession switch
|
mealLabel = activeSession switch
|
||||||
{
|
{
|
||||||
MealSession.Breakfast => "Sehri",
|
MealSession.Breakfast => "Breakfast",
|
||||||
MealSession.Lunch => "Iftari",
|
MealSession.Lunch => "Lunch",
|
||||||
MealSession.Tea => "Tea",
|
MealSession.Tea => "Tea",
|
||||||
MealSession.Dinner => "Dinner",
|
MealSession.Dinner => "Dinner",
|
||||||
_ => "No active meal session"
|
_ => "No active meal session"
|
||||||
|
|
@ -298,9 +298,10 @@ public partial class ScannerDashboardViewModel : ObservableObject
|
||||||
// i.ItemType.IndexOf(mealLabel, StringComparison.OrdinalIgnoreCase) >= 0)
|
// i.ItemType.IndexOf(mealLabel, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||||
// .ToList();
|
// .ToList();
|
||||||
|
|
||||||
var matchingItems = items
|
// var matchingItems = items
|
||||||
.Where(i => string.Equals(i.MealName, mealLabel, StringComparison.OrdinalIgnoreCase))
|
//.Where(i => string.Equals(i.MealName, mealLabel, StringComparison.OrdinalIgnoreCase))
|
||||||
.ToList();
|
//.ToList();
|
||||||
|
var matchingItems = items.ToList();
|
||||||
|
|
||||||
var matchingItemNames = matchingItems
|
var matchingItemNames = matchingItems
|
||||||
.Select(i => i.ItemName)
|
.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.
|
// 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).
|
// Only do this when we are handling an actual scan (sessionFromScan has a value).
|
||||||
if (sessionFromScan.HasValue && activeSession != MealSession.None)
|
//if (sessionFromScan.HasValue && activeSession != MealSession.None)
|
||||||
{
|
//{
|
||||||
_rfidService.UpdateLastScanMealInfo(mealLabel, historyDisplay, totalPrice);
|
// _rfidService.UpdateLastScanMealInfo(mealLabel, historyDisplay, totalPrice);
|
||||||
}
|
//}
|
||||||
_uiDispatcher.Invoke(() =>
|
_uiDispatcher.Invoke(() =>
|
||||||
{
|
{
|
||||||
EmployeeOrderItem = mealLabel;
|
EmployeeOrderItem = mealLabel;
|
||||||
|
|
@ -534,7 +535,7 @@ public partial class ScannerDashboardViewModel : ObservableObject
|
||||||
EmployeeDepartmentType = !string.IsNullOrWhiteSpace(info.DepartmentType) ? info.DepartmentType : "—";
|
EmployeeDepartmentType = !string.IsNullOrWhiteSpace(info.DepartmentType) ? info.DepartmentType : "—";
|
||||||
// Load menu using site from employee_rfid_tag.location_site_id (not config)
|
// Load menu using site from employee_rfid_tag.location_site_id (not config)
|
||||||
if (int.TryParse(info.LocationSiteId?.Trim(), out var siteFromRfid))
|
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)
|
// Load employee photo from hrms.employee_photo by parent_document_id (employee document id)
|
||||||
_ = LoadEmployeePhotoAsync(info.ParentDocumentId);
|
_ = LoadEmployeePhotoAsync(info.ParentDocumentId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
<UserControl x:Class="UtopiaCanteenSystem.Views.MealSchedulesView"
|
<!--<UserControl x:Class="UtopiaCanteenSystem.Views.MealSchedulesView"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:converters="clr-namespace:UtopiaCanteenSystem.Converters">
|
xmlns:converters="clr-namespace:UtopiaCanteenSystem.Converters">
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
<converters:MealSessionToNameConverter x:Key="MealSessionConverter"/>
|
<converters:MealSessionToNameConverter x:Key="MealSessionConverter"/>
|
||||||
|
|
||||||
<!-- Palette -->
|
--><!-- Palette --><!--
|
||||||
<SolidColorBrush x:Key="PrimaryText" Color="#1F2933"/>
|
<SolidColorBrush x:Key="PrimaryText" Color="#1F2933"/>
|
||||||
<SolidColorBrush x:Key="MutedText" Color="#718096"/>
|
<SolidColorBrush x:Key="MutedText" Color="#718096"/>
|
||||||
<SolidColorBrush x:Key="PrimaryAccent" Color="#5BA3A0"/>
|
<SolidColorBrush x:Key="PrimaryAccent" Color="#5BA3A0"/>
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
<SolidColorBrush x:Key="ErrorBrush" Color="#E53E3E"/>
|
<SolidColorBrush x:Key="ErrorBrush" Color="#E53E3E"/>
|
||||||
<SolidColorBrush x:Key="CardBackground" Color="#FFFFFF"/>
|
<SolidColorBrush x:Key="CardBackground" Color="#FFFFFF"/>
|
||||||
|
|
||||||
<!-- Card style -->
|
--><!-- Card style --><!--
|
||||||
<Style x:Key="CardBorder" TargetType="Border">
|
<Style x:Key="CardBorder" TargetType="Border">
|
||||||
<Setter Property="Background" Value="{StaticResource CardBackground}"/>
|
<Setter Property="Background" Value="{StaticResource CardBackground}"/>
|
||||||
<Setter Property="CornerRadius" Value="16"/>
|
<Setter Property="CornerRadius" Value="16"/>
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- Buttons -->
|
--><!-- Buttons --><!--
|
||||||
<Style x:Key="PrimaryButton" TargetType="Button">
|
<Style x:Key="PrimaryButton" TargetType="Button">
|
||||||
<Setter Property="Foreground" Value="White"/>
|
<Setter Property="Foreground" Value="White"/>
|
||||||
<Setter Property="Background" Value="{StaticResource PrimaryAccent}"/>
|
<Setter Property="Background" Value="{StaticResource PrimaryAccent}"/>
|
||||||
|
|
@ -88,7 +88,7 @@
|
||||||
<Setter Property="Background" Value="#FFF5F5"/>
|
<Setter Property="Background" Value="#FFF5F5"/>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- Back button: pill style with hover/pressed -->
|
--><!-- Back button: pill style with hover/pressed --><!--
|
||||||
<Style x:Key="BackButton" TargetType="Button" BasedOn="{StaticResource SecondaryButton}">
|
<Style x:Key="BackButton" TargetType="Button" BasedOn="{StaticResource SecondaryButton}">
|
||||||
<Setter Property="Padding" Value="18,10"/>
|
<Setter Property="Padding" Value="18,10"/>
|
||||||
<Setter Property="MinWidth" Value="100"/>
|
<Setter Property="MinWidth" Value="100"/>
|
||||||
|
|
@ -130,7 +130,7 @@
|
||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- Small icon action buttons for DataGrid Actions column -->
|
--><!-- Small icon action buttons for DataGrid Actions column --><!--
|
||||||
<Style x:Key="GridActionButton" TargetType="Button">
|
<Style x:Key="GridActionButton" TargetType="Button">
|
||||||
<Setter Property="Width" Value="28"/>
|
<Setter Property="Width" Value="28"/>
|
||||||
<Setter Property="Height" Value="28"/>
|
<Setter Property="Height" Value="28"/>
|
||||||
|
|
@ -169,7 +169,7 @@
|
||||||
<Setter Property="ToolTip" Value="Delete"/>
|
<Setter Property="ToolTip" Value="Delete"/>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- Inputs -->
|
--><!-- Inputs --><!--
|
||||||
<Style x:Key="ModernTextBox" TargetType="TextBox">
|
<Style x:Key="ModernTextBox" TargetType="TextBox">
|
||||||
<Setter Property="Margin" Value="0"/>
|
<Setter Property="Margin" Value="0"/>
|
||||||
<Setter Property="Padding" Value="10,8"/>
|
<Setter Property="Padding" Value="10,8"/>
|
||||||
|
|
@ -192,7 +192,7 @@
|
||||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- DataGrid styling -->
|
--><!-- DataGrid styling --><!--
|
||||||
<Style x:Key="ModernDataGrid" TargetType="DataGrid">
|
<Style x:Key="ModernDataGrid" TargetType="DataGrid">
|
||||||
<Setter Property="AutoGenerateColumns" Value="False"/>
|
<Setter Property="AutoGenerateColumns" Value="False"/>
|
||||||
<Setter Property="CanUserAddRows" Value="False"/>
|
<Setter Property="CanUserAddRows" Value="False"/>
|
||||||
|
|
@ -276,7 +276,7 @@
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- HEADER BAR -->
|
--><!-- HEADER BAR --><!--
|
||||||
<Grid Grid.Row="0" Margin="24,20,24,16">
|
<Grid Grid.Row="0" Margin="24,20,24,16">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
|
@ -285,25 +285,25 @@
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<!-- Back -->
|
--><!-- Back --><!--
|
||||||
<Button Grid.Column="0"
|
<Button Grid.Column="0"
|
||||||
Command="{Binding BackCommand}"
|
Command="{Binding BackCommand}"
|
||||||
Style="{StaticResource BackButton}"
|
Style="{StaticResource BackButton}"
|
||||||
Content="Back"/>
|
Content="Back"/>
|
||||||
|
|
||||||
<!-- Title + subtitle -->
|
--><!-- Title + subtitle --><!--
|
||||||
<StackPanel Grid.Column="1" Margin="16,0,0,0" VerticalAlignment="Center">
|
<StackPanel Grid.Column="1" Margin="16,0,0,0" VerticalAlignment="Center">
|
||||||
<TextBlock Text="Meal Schedules"
|
<TextBlock Text="Meal Schedules"
|
||||||
FontSize="24"
|
FontSize="24"
|
||||||
FontWeight="SemiBold"
|
FontWeight="SemiBold"
|
||||||
Foreground="{StaticResource PrimaryText}"/>
|
Foreground="{StaticResource PrimaryText}"/>
|
||||||
<!--<TextBlock Text="(production hrms.meal_schedule)"
|
--><!--<TextBlock Text="(production hrms.meal_schedule)"
|
||||||
FontSize="13"
|
FontSize="13"
|
||||||
Foreground="{StaticResource MutedText}"
|
Foreground="{StaticResource MutedText}"
|
||||||
Margin="0,2,0,0"/>-->
|
Margin="0,2,0,0"/>--><!--
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Site filter -->
|
--><!-- Site filter --><!--
|
||||||
<StackPanel Grid.Column="2"
|
<StackPanel Grid.Column="2"
|
||||||
Orientation="Horizontal"
|
Orientation="Horizontal"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
|
|
@ -320,7 +320,7 @@
|
||||||
Style="{StaticResource ModernComboBox}"/>
|
Style="{StaticResource ModernComboBox}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Add New -->
|
--><!-- Add New --><!--
|
||||||
<Button Grid.Column="3"
|
<Button Grid.Column="3"
|
||||||
Command="{Binding AddNewCommand}"
|
Command="{Binding AddNewCommand}"
|
||||||
Style="{StaticResource PrimaryButton}">
|
Style="{StaticResource PrimaryButton}">
|
||||||
|
|
@ -331,12 +331,12 @@
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- MAIN CONTENT -->
|
--><!-- MAIN CONTENT --><!--
|
||||||
<ScrollViewer Grid.Row="1"
|
<ScrollViewer Grid.Row="1"
|
||||||
Margin="24,0,24,16"
|
Margin="24,0,24,16"
|
||||||
VerticalScrollBarVisibility="Auto">
|
VerticalScrollBarVisibility="Auto">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<!-- Schedules card -->
|
--><!-- Schedules card --><!--
|
||||||
<Border Style="{StaticResource CardBorder}">
|
<Border Style="{StaticResource CardBorder}">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
|
|
@ -344,7 +344,7 @@
|
||||||
<RowDefinition Height="*"/>
|
<RowDefinition Height="*"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- Card header -->
|
--><!-- Card header --><!--
|
||||||
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,12">
|
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,12">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="Schedules"
|
<TextBlock Text="Schedules"
|
||||||
|
|
@ -357,7 +357,7 @@
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- DataGrid -->
|
--><!-- DataGrid --><!--
|
||||||
<DataGrid Grid.Row="1"
|
<DataGrid Grid.Row="1"
|
||||||
Style="{StaticResource ModernDataGrid}"
|
Style="{StaticResource ModernDataGrid}"
|
||||||
ItemsSource="{Binding Schedules}"
|
ItemsSource="{Binding Schedules}"
|
||||||
|
|
@ -429,7 +429,7 @@
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Add / Edit card -->
|
--><!-- Add / Edit card --><!--
|
||||||
<Border Style="{StaticResource CardBorder}">
|
<Border Style="{StaticResource CardBorder}">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
|
|
@ -438,7 +438,7 @@
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- Card header -->
|
--><!-- Card header --><!--
|
||||||
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,16">
|
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,16">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="Add or Edit Meal Schedule"
|
<TextBlock Text="Add or Edit Meal Schedule"
|
||||||
|
|
@ -448,16 +448,16 @@
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Form -->
|
--><!-- Form --><!--
|
||||||
<Grid Grid.Row="1">
|
<Grid Grid.Row="1">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<!-- Left column -->
|
--><!-- Left column --><!--
|
||||||
<StackPanel Grid.Column="0" Margin="0,0,16,0">
|
<StackPanel Grid.Column="0" Margin="0,0,16,0">
|
||||||
<!-- Location Site ID -->
|
--><!-- Location Site ID --><!--
|
||||||
<StackPanel Margin="0,0,0,12">
|
<StackPanel Margin="0,0,0,12">
|
||||||
<TextBlock Text="Location Site ID"
|
<TextBlock Text="Location Site ID"
|
||||||
FontSize="12"
|
FontSize="12"
|
||||||
|
|
@ -467,7 +467,7 @@
|
||||||
Style="{StaticResource ModernTextBox}"/>
|
Style="{StaticResource ModernTextBox}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Meal Session -->
|
--><!-- Meal Session --><!--
|
||||||
<StackPanel Margin="0,0,0,12">
|
<StackPanel Margin="0,0,0,12">
|
||||||
<TextBlock Text="Meal Session"
|
<TextBlock Text="Meal Session"
|
||||||
FontSize="12"
|
FontSize="12"
|
||||||
|
|
@ -478,9 +478,9 @@
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Right column -->
|
--><!-- Right column --><!--
|
||||||
<StackPanel Grid.Column="1" Margin="0,0,0,0">
|
<StackPanel Grid.Column="1" Margin="0,0,0,0">
|
||||||
<!-- Start time -->
|
--><!-- Start time --><!--
|
||||||
<StackPanel Margin="0,0,0,12">
|
<StackPanel Margin="0,0,0,12">
|
||||||
<TextBlock Text="Start (HH:mm:ss)"
|
<TextBlock Text="Start (HH:mm:ss)"
|
||||||
FontSize="12"
|
FontSize="12"
|
||||||
|
|
@ -503,7 +503,7 @@
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- End time -->
|
--><!-- End time --><!--
|
||||||
<StackPanel Margin="0,0,0,12">
|
<StackPanel Margin="0,0,0,12">
|
||||||
<TextBlock Text="End (HH:mm:ss)"
|
<TextBlock Text="End (HH:mm:ss)"
|
||||||
FontSize="12"
|
FontSize="12"
|
||||||
|
|
@ -528,7 +528,7 @@
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Buttons -->
|
--><!-- Buttons --><!--
|
||||||
<StackPanel Grid.Row="2"
|
<StackPanel Grid.Row="2"
|
||||||
Orientation="Horizontal"
|
Orientation="Horizontal"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
|
|
@ -550,7 +550,7 @@
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
||||||
<!-- Message -->
|
--><!-- Message --><!--
|
||||||
<Border Grid.Row="2"
|
<Border Grid.Row="2"
|
||||||
Margin="24,0,24,20"
|
Margin="24,0,24,20"
|
||||||
Background="Transparent">
|
Background="Transparent">
|
||||||
|
|
@ -572,4 +572,181 @@
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
</UserControl>-->
|
||||||
|
|
||||||
|
|
||||||
|
<UserControl x:Class="UtopiaCanteenSystem.Views.MealSchedulesView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:converters="clr-namespace:UtopiaCanteenSystem.Converters">
|
||||||
|
<UserControl.Resources>
|
||||||
|
<converters:MealSessionToNameConverter x:Key="MealSessionConverter"/>
|
||||||
|
|
||||||
|
<SolidColorBrush x:Key="PrimaryText" Color="#1F2933"/>
|
||||||
|
<SolidColorBrush x:Key="MutedText" Color="#718096"/>
|
||||||
|
<SolidColorBrush x:Key="PrimaryAccent" Color="#5BA3A0"/>
|
||||||
|
<SolidColorBrush x:Key="PrimaryAccentLight" Color="#E3F5F4"/>
|
||||||
|
<SolidColorBrush x:Key="BorderBrush" Color="#E2E8F0"/>
|
||||||
|
<SolidColorBrush x:Key="ErrorBrush" Color="#E53E3E"/>
|
||||||
|
<SolidColorBrush x:Key="CardBackground" Color="#FFFFFF"/>
|
||||||
|
|
||||||
|
<Style x:Key="CardBorder" TargetType="Border">
|
||||||
|
<Setter Property="Background" Value="{StaticResource CardBackground}"/>
|
||||||
|
<Setter Property="CornerRadius" Value="16"/>
|
||||||
|
<Setter Property="Padding" Value="20"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,0,18"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="SecondaryButton" TargetType="Button">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource PrimaryText}"/>
|
||||||
|
<Setter Property="Background" Value="#FFFFFF"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Padding" Value="16,9"/>
|
||||||
|
<Setter Property="MinHeight" Value="36"/>
|
||||||
|
<Setter Property="MinWidth" Value="100"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="BackButton" TargetType="Button" BasedOn="{StaticResource SecondaryButton}">
|
||||||
|
<Setter Property="Padding" Value="18,10"/>
|
||||||
|
<Setter Property="MinWidth" Value="100"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ModernComboBox" TargetType="ComboBox">
|
||||||
|
<Setter Property="Padding" Value="10,4"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Background" Value="White"/>
|
||||||
|
<Setter Property="MinHeight" Value="34"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ModernDataGrid" TargetType="DataGrid">
|
||||||
|
<Setter Property="AutoGenerateColumns" Value="False"/>
|
||||||
|
<Setter Property="CanUserAddRows" Value="False"/>
|
||||||
|
<Setter Property="CanUserDeleteRows" Value="False"/>
|
||||||
|
<Setter Property="IsReadOnly" Value="True"/>
|
||||||
|
<Setter Property="HeadersVisibility" Value="Column"/>
|
||||||
|
<Setter Property="GridLinesVisibility" Value="None"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="RowBackground" Value="White"/>
|
||||||
|
<Setter Property="AlternatingRowBackground" Value="#F7FAFC"/>
|
||||||
|
<Setter Property="RowHeight" Value="44"/>
|
||||||
|
<Setter Property="ColumnHeaderHeight" Value="44"/>
|
||||||
|
</Style>
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<Grid Background="#F4F7FB">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<Grid Grid.Row="0" Margin="24,20,24,16">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<Button Grid.Column="0"
|
||||||
|
Command="{Binding BackCommand}"
|
||||||
|
Style="{StaticResource BackButton}"
|
||||||
|
Content="← Back"/>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="1" Margin="16,0,0,0" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="Meal Schedules"
|
||||||
|
FontSize="24"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource PrimaryText}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="2"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="Site"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="0,0,8,0"
|
||||||
|
Foreground="{StaticResource MutedText}"
|
||||||
|
FontSize="13"/>
|
||||||
|
<ComboBox ItemsSource="{Binding SiteFilterChoices}"
|
||||||
|
SelectedItem="{Binding SelectedSiteFilter, Mode=TwoWay}"
|
||||||
|
MinWidth="100"
|
||||||
|
Style="{StaticResource ModernComboBox}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<ScrollViewer Grid.Row="1"
|
||||||
|
Margin="24,0,24,16"
|
||||||
|
VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel>
|
||||||
|
<Border Style="{StaticResource CardBorder}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="0" Margin="0,0,0,12">
|
||||||
|
<TextBlock Text="Meal Schedules"
|
||||||
|
FontSize="16"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource PrimaryText}"/>
|
||||||
|
<TextBlock Text="View-only mode - configured meal windows per site"
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="{StaticResource MutedText}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<DataGrid Grid.Row="1"
|
||||||
|
Style="{StaticResource ModernDataGrid}"
|
||||||
|
ItemsSource="{Binding Schedules}"
|
||||||
|
SelectedItem="{Binding SelectedSchedule, Mode=TwoWay}">
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTextColumn Header="Site"
|
||||||
|
Binding="{Binding LocationSiteId}"
|
||||||
|
Width="*"/>
|
||||||
|
<DataGridTextColumn Header="Session"
|
||||||
|
Binding="{Binding MealName}"
|
||||||
|
Width="*"/>
|
||||||
|
<DataGridTextColumn Header="Start Time"
|
||||||
|
Binding="{Binding StartTime}"
|
||||||
|
Width="*"/>
|
||||||
|
<DataGridTextColumn Header="End Time"
|
||||||
|
Binding="{Binding EndTime}"
|
||||||
|
Width="*"/>
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<!-- Message -->
|
||||||
|
<Border Grid.Row="2"
|
||||||
|
Margin="24,0,24,20"
|
||||||
|
Background="Transparent">
|
||||||
|
<TextBlock Text="{Binding Message}" FontSize="14">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource PrimaryText}"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsError}" Value="True">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource ErrorBrush}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding Message}" Value="">
|
||||||
|
<Setter Property="Visibility" Value="Collapsed"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|
@ -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).
|
||||||
|
|
@ -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
|
||||||
|
);
|
||||||
Loading…
Reference in New Issue