commit
db28c3488c
|
|
@ -44,7 +44,7 @@ public partial class App : Application
|
|||
var menuLookupService = new MenuLookupService(configService);
|
||||
var mealScheduleService = new ProductionMealScheduleService(configService);
|
||||
var mealSessionResolver = new DbMealSessionResolver(mealScheduleService);
|
||||
var rfidService = new RfidService(dbFactory, configService, employeeLookupService, mealSessionResolver);
|
||||
var rfidService = new RfidService(dbFactory, configService, employeeLookupService, mealSessionResolver, menuLookupService);
|
||||
var syncService = new SyncService(dbFactory, configService);
|
||||
var adminAuditService = new AdminAuditService(dbFactory);
|
||||
var session = new AppSession();
|
||||
|
|
@ -60,7 +60,7 @@ public partial class App : Application
|
|||
() => new MainDashboardViewModel(navigationService, rfidService, configService, session),
|
||||
() => new AdminSettingsAuthViewModel(authService, session, navigationService, configService),
|
||||
() => new SettingsViewModel(configService, navigationService, adminAuditService, syncService),
|
||||
() => new MealSchedulesViewModel(mealScheduleService, navigationService));
|
||||
() => new MealSchedulesViewModel(mealScheduleService, navigationService, configService));
|
||||
|
||||
var mainViewModel = new MainViewModel(navigationService);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
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 />
|
||||
public MealSession GetCurrentSession(DateTime nowLocal, string siteId)
|
||||
//public MealSession GetCurrentSession(DateTime nowLocal, string siteId)
|
||||
//{
|
||||
// var normalizedSite = (siteId ?? string.Empty).Trim();
|
||||
// if (string.IsNullOrEmpty(normalizedSite))
|
||||
// normalizedSite = "01";
|
||||
// if (normalizedSite.Length == 1 && char.IsDigit(normalizedSite[0]))
|
||||
// normalizedSite = normalizedSite.PadLeft(2, '0');
|
||||
|
||||
// var schedules = GetSchedulesForSiteCached(normalizedSite);
|
||||
// var t = nowLocal.TimeOfDay;
|
||||
|
||||
// //foreach (var s in schedules)
|
||||
// //{
|
||||
// // if (!TryParseTime(s.StartTime, out var start) || !TryParseTime(s.EndTime, out var end))
|
||||
// // continue;
|
||||
// // if (t >= start && t < end)
|
||||
// // return (MealSession)s.MealSession;
|
||||
// //}
|
||||
// foreach (var s in schedules)
|
||||
// {
|
||||
// if (!TryParseTime(s.StartTime, out var start) || !TryParseTime(s.EndTime, out var end))
|
||||
// continue;
|
||||
|
||||
// if (t >= start && t < end)
|
||||
// return MapMealNameToSession(s.MealName);
|
||||
// }
|
||||
|
||||
// return MealSession.None;
|
||||
//}
|
||||
|
||||
|
||||
|
||||
|
||||
public ResolvedMealSession? GetCurrentSession(DateTime nowLocal, string siteId)
|
||||
{
|
||||
var normalizedSite = (siteId ?? string.Empty).Trim();
|
||||
if (string.IsNullOrEmpty(normalizedSite))
|
||||
|
|
@ -35,13 +89,24 @@ public class DbMealSessionResolver : IMealSessionResolver
|
|||
{
|
||||
if (!TryParseTime(s.StartTime, out var start) || !TryParseTime(s.EndTime, out var end))
|
||||
continue;
|
||||
|
||||
if (t >= start && t < end)
|
||||
return (MealSession)s.MealSession;
|
||||
{
|
||||
return new ResolvedMealSession
|
||||
{
|
||||
Session = MapMealNameToSession(s.MealName),
|
||||
MealName = s.MealName ?? string.Empty,
|
||||
SiteId = normalizedSite,
|
||||
StartTime = s.StartTime ?? string.Empty,
|
||||
EndTime = s.EndTime ?? string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return MealSession.None;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private List<MealSchedule> GetSchedulesForSiteCached(string siteId)
|
||||
{
|
||||
lock (_cacheLock)
|
||||
|
|
|
|||
|
|
@ -10,5 +10,6 @@ public interface IMealSessionResolver
|
|||
/// <summary>
|
||||
/// Returns the active meal session for the given local time and site, or MealSession.None if outside all windows.
|
||||
/// </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").
|
||||
/// </summary>
|
||||
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 />
|
||||
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();
|
||||
if (string.IsNullOrWhiteSpace(connectionString))
|
||||
|
|
@ -31,28 +38,45 @@ public class MenuLookupService : IMenuLookupService
|
|||
// WHERE w.location_site_id = @siteId
|
||||
// AND CURDATE() BETWEEN w.week_start_date AND w.week_end_date
|
||||
// ORDER BY mi.item_type, mi.item_name";
|
||||
|
||||
const string sql = @"
|
||||
SELECT
|
||||
mi.id,
|
||||
mi.item_name,
|
||||
mi.item_type,
|
||||
mi.price,
|
||||
li.meal_name,
|
||||
li.menu_date,
|
||||
li.day_of_week
|
||||
FROM lunch_menu_week w
|
||||
JOIN lunch_menu_item li ON li.lunch_menu_week_id = w.id
|
||||
JOIN menu_item mi ON mi.id = li.menu_item_id
|
||||
WHERE w.location_site_id = @siteId
|
||||
AND CURDATE() BETWEEN w.week_start_date AND w.week_end_date
|
||||
AND li.menu_date = DATE_FORMAT(CURDATE(), '%Y-%m-%d')
|
||||
ORDER BY li.meal_name, mi.item_name;
|
||||
";
|
||||
AND @menuDate BETWEEN w.week_start_date AND w.week_end_date
|
||||
AND li.menu_date = @menuDate
|
||||
ORDER BY mi.item_name;";
|
||||
|
||||
//const string sql = @"
|
||||
// SELECT
|
||||
// mi.id,
|
||||
// mi.item_name,
|
||||
// mi.item_type,
|
||||
// mi.price,
|
||||
// li.meal_name,
|
||||
// li.menu_date,
|
||||
// li.day_of_week
|
||||
// FROM lunch_menu_week w
|
||||
// JOIN lunch_menu_item li ON li.lunch_menu_week_id = w.id
|
||||
// JOIN menu_item mi ON mi.id = li.menu_item_id
|
||||
// WHERE w.location_site_id = @siteId
|
||||
// AND @menuDate BETWEEN w.week_start_date AND w.week_end_date
|
||||
// AND li.menu_date = @menuDate
|
||||
// ORDER BY li.meal_name, mi.item_name;";
|
||||
|
||||
await using var conn = new MySqlConnection(connectionString);
|
||||
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var cmd = new MySqlCommand(sql, conn);
|
||||
cmd.Parameters.AddWithValue("@siteId", siteIdNumeric);
|
||||
cmd.Parameters.AddWithValue("@menuDate", menuDateLocal.Date);
|
||||
|
||||
var list = new List<HrmsMenuItem>();
|
||||
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
|
@ -72,11 +96,22 @@ public class MenuLookupService : IMenuLookupService
|
|||
ItemName = GetString(reader, 1),
|
||||
ItemType = GetString(reader, 2),
|
||||
Price = GetDecimal(reader, 3),
|
||||
|
||||
MealName = GetString(reader, 4),
|
||||
MenuDate = GetString(reader, 5),
|
||||
DayOfWeek = GetString(reader, 6),
|
||||
MenuDate = GetString(reader, 4),
|
||||
DayOfWeek = GetString(reader, 5),
|
||||
MealName = string.Empty
|
||||
});
|
||||
|
||||
//list.Add(new HrmsMenuItem
|
||||
//{
|
||||
// MenuItemId = reader.IsDBNull(0) ? 0 : reader.GetInt32(0),
|
||||
// ItemName = GetString(reader, 1),
|
||||
// ItemType = GetString(reader, 2),
|
||||
// Price = GetDecimal(reader, 3),
|
||||
|
||||
// MealName = GetString(reader, 4),
|
||||
// MenuDate = GetString(reader, 5),
|
||||
// DayOfWeek = GetString(reader, 6),
|
||||
//});
|
||||
}
|
||||
|
||||
return list;
|
||||
|
|
|
|||
|
|
@ -20,13 +20,20 @@ public class RfidService : IRfidService
|
|||
private readonly IConfigService _configService;
|
||||
private readonly IEmployeeLookupService _employeeLookup;
|
||||
private readonly IMealSessionResolver _mealSessionResolver;
|
||||
private readonly IMenuLookupService _menuLookup;
|
||||
|
||||
public RfidService(IDbContextFactory<AppDbContext> dbFactory, IConfigService configService, IEmployeeLookupService employeeLookup, IMealSessionResolver mealSessionResolver)
|
||||
public RfidService(
|
||||
IDbContextFactory<AppDbContext> dbFactory,
|
||||
IConfigService configService,
|
||||
IEmployeeLookupService employeeLookup,
|
||||
IMealSessionResolver mealSessionResolver,
|
||||
IMenuLookupService menuLookup)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
_configService = configService;
|
||||
_employeeLookup = employeeLookup;
|
||||
_mealSessionResolver = mealSessionResolver;
|
||||
_menuLookup = menuLookup;
|
||||
}
|
||||
|
||||
public (bool Success, string Message) ProcessScan(string cardId)
|
||||
|
|
@ -54,10 +61,17 @@ public class RfidService : IRfidService
|
|||
var siteIdForSession = !string.IsNullOrWhiteSpace(employee.LocationSiteId)
|
||||
? employee.LocationSiteId.Trim()
|
||||
: _configService.GetSiteId();
|
||||
var session = _mealSessionResolver.GetCurrentSession(nowLocal, siteIdForSession);
|
||||
if (session == MealSession.None)
|
||||
//var session = _mealSessionResolver.GetCurrentSession(nowLocal, siteIdForSession);
|
||||
//if (session == MealSession.None)
|
||||
// return new ScanResult(false, "This scan is outside of valid meal timings.", 0);
|
||||
|
||||
//var sessionCode = (int)session;
|
||||
|
||||
var resolvedSession = _mealSessionResolver.GetCurrentSession(nowLocal, siteIdForSession);
|
||||
if (resolvedSession == null || resolvedSession.Session == MealSession.None)
|
||||
return new ScanResult(false, "This scan is outside of valid meal timings.", 0);
|
||||
|
||||
var session = resolvedSession.Session;
|
||||
var sessionCode = (int)session;
|
||||
|
||||
using var db = _dbFactory.CreateDbContext();
|
||||
|
|
@ -75,9 +89,15 @@ public class RfidService : IRfidService
|
|||
.OrderByDescending(r => r.ScanTime)
|
||||
.FirstOrDefault();
|
||||
|
||||
//if (alreadyScannedThisSessionToday != null)
|
||||
//{
|
||||
// var sessionName = GetMealSessionDisplayName(session);
|
||||
// return new ScanResult(false, $"This card is already scanned for {sessionName} today.", 0);
|
||||
//}
|
||||
if (alreadyScannedThisSessionToday != null)
|
||||
{
|
||||
var sessionName = GetMealSessionDisplayName(session);
|
||||
//var sessionName = session.ToString();
|
||||
var sessionName = resolvedSession.MealName;
|
||||
return new ScanResult(false, $"This card is already scanned for {sessionName} today.", 0);
|
||||
}
|
||||
|
||||
|
|
@ -106,6 +126,45 @@ public class RfidService : IRfidService
|
|||
var siteId = !string.IsNullOrWhiteSpace(employee.LocationSiteId)
|
||||
? employee.LocationSiteId.Trim()
|
||||
: _configService.GetSiteId();
|
||||
|
||||
// Resolve menu items for this scan (store on record so order history shows actual items)
|
||||
//var mealLabel = GetMealSessionDisplayName(session);
|
||||
//var mealLabel = session.ToString();
|
||||
var mealLabel = resolvedSession.MealName;
|
||||
var mealItemsDisplay = string.Empty;
|
||||
double totalPrice = 0;
|
||||
try
|
||||
{
|
||||
if (int.TryParse(new string(siteIdForSession.Where(char.IsDigit).ToArray()), out var siteNumeric) && siteNumeric > 0)
|
||||
{
|
||||
var menuItems = _menuLookup
|
||||
.GetMenuItemsForSiteAndDateAsync(siteNumeric, nowLocal.Date)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
|
||||
//var matching = menuItems
|
||||
// .Where(i => string.Equals(i.MealName, mealLabel, StringComparison.OrdinalIgnoreCase))
|
||||
// .ToList();
|
||||
var matching = menuItems.ToList();
|
||||
|
||||
var names = matching
|
||||
.Select(i => i.ItemName)
|
||||
.Where(n => !string.IsNullOrWhiteSpace(n))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (names.Count > 0)
|
||||
{
|
||||
mealItemsDisplay = string.Join(" + ", names);
|
||||
totalPrice = matching.Sum(i => (double)i.Price);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log(ex, "RfidService.ProcessScanDetailed menu lookup");
|
||||
}
|
||||
|
||||
var record = new ScanRecord
|
||||
{
|
||||
CardId = cardId,
|
||||
|
|
@ -124,7 +183,10 @@ public class RfidService : IRfidService
|
|||
TagCreatedBy = employee.TagCreatedBy ?? string.Empty,
|
||||
EmployeeName = string.IsNullOrEmpty(fullName) ? string.Empty : fullName,
|
||||
Department = employee.DepartmentTitle ?? string.Empty,
|
||||
DepartmentType = employee.DepartmentType ?? string.Empty
|
||||
DepartmentType = employee.DepartmentType ?? string.Empty,
|
||||
MealLabel = mealLabel,
|
||||
MealItems = mealItemsDisplay,
|
||||
TotalPrice = totalPrice
|
||||
};
|
||||
db.LunchOrderTransactions.Add(record);
|
||||
db.SaveChanges();
|
||||
|
|
@ -132,17 +194,17 @@ public class RfidService : IRfidService
|
|||
return new ScanResult(true, "Order recorded successfully.", 0, employee, session);
|
||||
}
|
||||
|
||||
private static string GetMealSessionDisplayName(MealSession session)
|
||||
{
|
||||
return session switch
|
||||
{
|
||||
MealSession.Breakfast => "Sehri",
|
||||
MealSession.Lunch => "Iftari",
|
||||
MealSession.Tea => "Tea",
|
||||
MealSession.Dinner => "Dinner",
|
||||
_ => "this meal"
|
||||
};
|
||||
}
|
||||
//private static string GetMealSessionDisplayName(MealSession session)
|
||||
//{
|
||||
// return session switch
|
||||
// {
|
||||
// MealSession.Breakfast => "Sehri",
|
||||
// MealSession.Lunch => "Iftari",
|
||||
// MealSession.Tea => "Tea",
|
||||
// MealSession.Dinner => "Dinner",
|
||||
// _ => "this meal"
|
||||
// };
|
||||
//}
|
||||
|
||||
public ScanRecord? GetLastScan()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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.Windows;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
|
@ -9,17 +250,19 @@ 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).
|
||||
/// View-only display of meal schedules from production.
|
||||
/// Loads all schedules on open; optional Site filter.
|
||||
/// </summary>
|
||||
public partial class MealSchedulesViewModel : ObservableObject
|
||||
{
|
||||
private readonly IMealScheduleService _mealScheduleService;
|
||||
private readonly INavigationService _navigation;
|
||||
private readonly IConfigService _configService;
|
||||
private readonly string _currentSiteId;
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<MealSchedule> _schedules = new();
|
||||
|
||||
/// <summary>Full list from production; Schedules is filtered by SelectedSiteFilter.</summary>
|
||||
private List<MealSchedule> _allSchedules = new();
|
||||
|
||||
[ObservableProperty]
|
||||
|
|
@ -31,18 +274,6 @@ public partial class MealSchedulesViewModel : ObservableObject
|
|||
[ObservableProperty]
|
||||
private MealSchedule? _selectedSchedule;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _locationSiteId = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _mealName = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _startTime = "06:00:00";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _endTime = "09:00:00";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _message = string.Empty;
|
||||
|
||||
|
|
@ -52,10 +283,12 @@ public partial class MealSchedulesViewModel : ObservableObject
|
|||
[ObservableProperty]
|
||||
private bool _isLoading;
|
||||
|
||||
public MealSchedulesViewModel(IMealScheduleService mealScheduleService, INavigationService navigation)
|
||||
public MealSchedulesViewModel(IMealScheduleService mealScheduleService, INavigationService navigation, IConfigService configService)
|
||||
{
|
||||
_mealScheduleService = mealScheduleService;
|
||||
_navigation = navigation;
|
||||
_configService = configService;
|
||||
_currentSiteId = (_configService.GetSiteId() ?? string.Empty).Trim();
|
||||
_ = LoadSchedulesAsync();
|
||||
}
|
||||
|
||||
|
|
@ -64,23 +297,12 @@ public partial class MealSchedulesViewModel : ObservableObject
|
|||
ApplyFilter();
|
||||
}
|
||||
|
||||
partial void OnSelectedScheduleChanged(MealSchedule? value)
|
||||
{
|
||||
if (value == null) return;
|
||||
LocationSiteId = value.LocationSiteId ?? string.Empty;
|
||||
MealName = value.MealName ?? string.Empty;
|
||||
StartTime = value.StartTime ?? "00:00:00";
|
||||
EndTime = value.EndTime ?? "23:59:59";
|
||||
}
|
||||
|
||||
private void ApplyFilter()
|
||||
{
|
||||
if (string.IsNullOrEmpty(SelectedSiteFilter) || SelectedSiteFilter == "All")
|
||||
{
|
||||
Schedules = new ObservableCollection<MealSchedule>(_allSchedules);
|
||||
return;
|
||||
}
|
||||
var filtered = _allSchedules.Where(s => string.Equals(s.LocationSiteId?.Trim(), SelectedSiteFilter.Trim(), StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
var site = string.IsNullOrWhiteSpace(_currentSiteId) ? "01" : _currentSiteId;
|
||||
var filtered = _allSchedules
|
||||
.Where(s => string.Equals(s.LocationSiteId?.Trim(), site, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
Schedules = new ObservableCollection<MealSchedule>(filtered);
|
||||
}
|
||||
|
||||
|
|
@ -89,24 +311,36 @@ public partial class MealSchedulesViewModel : ObservableObject
|
|||
IsLoading = true;
|
||||
Message = string.Empty;
|
||||
IsError = false;
|
||||
|
||||
try
|
||||
{
|
||||
var list = await _mealScheduleService.GetAllSchedulesAsync().ConfigureAwait(true);
|
||||
_allSchedules = list.ToList();
|
||||
var siteIds = _allSchedules.Select(s => s.LocationSiteId?.Trim() ?? "").Where(s => !string.IsNullOrEmpty(s)).Distinct().OrderBy(s => s, StringComparer.Ordinal).ToList();
|
||||
SiteFilterChoices = new ObservableCollection<string>(new[] { "All" }.Concat(siteIds));
|
||||
SelectedSiteFilter = "All";
|
||||
|
||||
var siteIds = _allSchedules
|
||||
.Select(s => s.LocationSiteId?.Trim() ?? "")
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.Distinct()
|
||||
.OrderBy(s => s, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
// Only show schedules for current configured site; hide other sites from filter choices.
|
||||
SiteFilterChoices = new ObservableCollection<string>(new[] { string.IsNullOrWhiteSpace(_currentSiteId) ? "01" : _currentSiteId });
|
||||
SelectedSiteFilter = SiteFilterChoices.First();
|
||||
ApplyFilter();
|
||||
|
||||
if (_allSchedules.Count == 0)
|
||||
Message = "No schedules in production. Add one below (HRMS MySQL must be configured).";
|
||||
Message = "No schedules configured.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
IsError = true;
|
||||
|
||||
var msg = ex is MySqlException mysql
|
||||
? $"{mysql.Message}{Environment.NewLine}{Environment.NewLine}{mysql.StackTrace}"
|
||||
: $"{ex.Message}{Environment.NewLine}{Environment.NewLine}{ex.StackTrace}";
|
||||
Message = "Could not load schedules from production.";
|
||||
|
||||
Message = "Could not load schedules.";
|
||||
MessageBox.Show(msg, "Meal Schedules – Load Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
finally
|
||||
|
|
@ -115,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]
|
||||
private void Back()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -276,8 +276,8 @@ public partial class ScannerDashboardViewModel : ObservableObject
|
|||
//};
|
||||
mealLabel = activeSession switch
|
||||
{
|
||||
MealSession.Breakfast => "Sehri",
|
||||
MealSession.Lunch => "Iftari",
|
||||
MealSession.Breakfast => "Breakfast",
|
||||
MealSession.Lunch => "Lunch",
|
||||
MealSession.Tea => "Tea",
|
||||
MealSession.Dinner => "Dinner",
|
||||
_ => "No active meal session"
|
||||
|
|
@ -298,9 +298,10 @@ public partial class ScannerDashboardViewModel : ObservableObject
|
|||
// i.ItemType.IndexOf(mealLabel, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
// .ToList();
|
||||
|
||||
var matchingItems = items
|
||||
.Where(i => string.Equals(i.MealName, mealLabel, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
// var matchingItems = items
|
||||
//.Where(i => string.Equals(i.MealName, mealLabel, StringComparison.OrdinalIgnoreCase))
|
||||
//.ToList();
|
||||
var matchingItems = items.ToList();
|
||||
|
||||
var matchingItemNames = matchingItems
|
||||
.Select(i => i.ItemName)
|
||||
|
|
@ -325,10 +326,10 @@ public partial class ScannerDashboardViewModel : ObservableObject
|
|||
|
||||
// Persist these values on the latest scan record so history rows keep their own Meal/Price.
|
||||
// Only do this when we are handling an actual scan (sessionFromScan has a value).
|
||||
if (sessionFromScan.HasValue && activeSession != MealSession.None)
|
||||
{
|
||||
_rfidService.UpdateLastScanMealInfo(mealLabel, historyDisplay, totalPrice);
|
||||
}
|
||||
//if (sessionFromScan.HasValue && activeSession != MealSession.None)
|
||||
//{
|
||||
// _rfidService.UpdateLastScanMealInfo(mealLabel, historyDisplay, totalPrice);
|
||||
//}
|
||||
_uiDispatcher.Invoke(() =>
|
||||
{
|
||||
EmployeeOrderItem = mealLabel;
|
||||
|
|
@ -534,7 +535,7 @@ public partial class ScannerDashboardViewModel : ObservableObject
|
|||
EmployeeDepartmentType = !string.IsNullOrWhiteSpace(info.DepartmentType) ? info.DepartmentType : "—";
|
||||
// Load menu using site from employee_rfid_tag.location_site_id (not config)
|
||||
if (int.TryParse(info.LocationSiteId?.Trim(), out var siteFromRfid))
|
||||
_ = LoadMenuForSiteAsync(siteFromRfid, result.MealSession);
|
||||
//_ = LoadMenuForSiteAsync(siteFromRfid, result.MealSession);
|
||||
// Load employee photo from hrms.employee_photo by parent_document_id (employee document id)
|
||||
_ = LoadEmployeePhotoAsync(info.ParentDocumentId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="clr-namespace:UtopiaCanteenSystem.Converters">
|
||||
<UserControl.Resources>
|
||||
<converters:MealSessionToNameConverter x:Key="MealSessionConverter"/>
|
||||
|
||||
<!-- Palette -->
|
||||
--><!-- Palette --><!--
|
||||
<SolidColorBrush x:Key="PrimaryText" Color="#1F2933"/>
|
||||
<SolidColorBrush x:Key="MutedText" Color="#718096"/>
|
||||
<SolidColorBrush x:Key="PrimaryAccent" Color="#5BA3A0"/>
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
<SolidColorBrush x:Key="ErrorBrush" Color="#E53E3E"/>
|
||||
<SolidColorBrush x:Key="CardBackground" Color="#FFFFFF"/>
|
||||
|
||||
<!-- Card style -->
|
||||
--><!-- Card style --><!--
|
||||
<Style x:Key="CardBorder" TargetType="Border">
|
||||
<Setter Property="Background" Value="{StaticResource CardBackground}"/>
|
||||
<Setter Property="CornerRadius" Value="16"/>
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Buttons -->
|
||||
--><!-- Buttons --><!--
|
||||
<Style x:Key="PrimaryButton" TargetType="Button">
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="Background" Value="{StaticResource PrimaryAccent}"/>
|
||||
|
|
@ -88,7 +88,7 @@
|
|||
<Setter Property="Background" Value="#FFF5F5"/>
|
||||
</Style>
|
||||
|
||||
<!-- Back button: pill style with hover/pressed -->
|
||||
--><!-- Back button: pill style with hover/pressed --><!--
|
||||
<Style x:Key="BackButton" TargetType="Button" BasedOn="{StaticResource SecondaryButton}">
|
||||
<Setter Property="Padding" Value="18,10"/>
|
||||
<Setter Property="MinWidth" Value="100"/>
|
||||
|
|
@ -130,7 +130,7 @@
|
|||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Small icon action buttons for DataGrid Actions column -->
|
||||
--><!-- Small icon action buttons for DataGrid Actions column --><!--
|
||||
<Style x:Key="GridActionButton" TargetType="Button">
|
||||
<Setter Property="Width" Value="28"/>
|
||||
<Setter Property="Height" Value="28"/>
|
||||
|
|
@ -169,7 +169,7 @@
|
|||
<Setter Property="ToolTip" Value="Delete"/>
|
||||
</Style>
|
||||
|
||||
<!-- Inputs -->
|
||||
--><!-- Inputs --><!--
|
||||
<Style x:Key="ModernTextBox" TargetType="TextBox">
|
||||
<Setter Property="Margin" Value="0"/>
|
||||
<Setter Property="Padding" Value="10,8"/>
|
||||
|
|
@ -192,7 +192,7 @@
|
|||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
</Style>
|
||||
|
||||
<!-- DataGrid styling -->
|
||||
--><!-- DataGrid styling --><!--
|
||||
<Style x:Key="ModernDataGrid" TargetType="DataGrid">
|
||||
<Setter Property="AutoGenerateColumns" Value="False"/>
|
||||
<Setter Property="CanUserAddRows" Value="False"/>
|
||||
|
|
@ -276,7 +276,7 @@
|
|||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- HEADER BAR -->
|
||||
--><!-- HEADER BAR --><!--
|
||||
<Grid Grid.Row="0" Margin="24,20,24,16">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
|
|
@ -285,25 +285,25 @@
|
|||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Back -->
|
||||
--><!-- Back --><!--
|
||||
<Button Grid.Column="0"
|
||||
Command="{Binding BackCommand}"
|
||||
Style="{StaticResource BackButton}"
|
||||
Content="Back"/>
|
||||
|
||||
<!-- Title + subtitle -->
|
||||
--><!-- Title + subtitle --><!--
|
||||
<StackPanel Grid.Column="1" Margin="16,0,0,0" VerticalAlignment="Center">
|
||||
<TextBlock Text="Meal Schedules"
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource PrimaryText}"/>
|
||||
<!--<TextBlock Text="(production hrms.meal_schedule)"
|
||||
--><!--<TextBlock Text="(production hrms.meal_schedule)"
|
||||
FontSize="13"
|
||||
Foreground="{StaticResource MutedText}"
|
||||
Margin="0,2,0,0"/>-->
|
||||
Margin="0,2,0,0"/>--><!--
|
||||
</StackPanel>
|
||||
|
||||
<!-- Site filter -->
|
||||
--><!-- Site filter --><!--
|
||||
<StackPanel Grid.Column="2"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
|
|
@ -320,7 +320,7 @@
|
|||
Style="{StaticResource ModernComboBox}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Add New -->
|
||||
--><!-- Add New --><!--
|
||||
<Button Grid.Column="3"
|
||||
Command="{Binding AddNewCommand}"
|
||||
Style="{StaticResource PrimaryButton}">
|
||||
|
|
@ -331,12 +331,12 @@
|
|||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- MAIN CONTENT -->
|
||||
--><!-- MAIN CONTENT --><!--
|
||||
<ScrollViewer Grid.Row="1"
|
||||
Margin="24,0,24,16"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel>
|
||||
<!-- Schedules card -->
|
||||
--><!-- Schedules card --><!--
|
||||
<Border Style="{StaticResource CardBorder}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
|
|
@ -344,7 +344,7 @@
|
|||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Card header -->
|
||||
--><!-- Card header --><!--
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,12">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Schedules"
|
||||
|
|
@ -357,7 +357,7 @@
|
|||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- DataGrid -->
|
||||
--><!-- DataGrid --><!--
|
||||
<DataGrid Grid.Row="1"
|
||||
Style="{StaticResource ModernDataGrid}"
|
||||
ItemsSource="{Binding Schedules}"
|
||||
|
|
@ -429,7 +429,7 @@
|
|||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Add / Edit card -->
|
||||
--><!-- Add / Edit card --><!--
|
||||
<Border Style="{StaticResource CardBorder}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
|
|
@ -438,7 +438,7 @@
|
|||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Card header -->
|
||||
--><!-- Card header --><!--
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,16">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Add or Edit Meal Schedule"
|
||||
|
|
@ -448,16 +448,16 @@
|
|||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Form -->
|
||||
--><!-- Form --><!--
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Left column -->
|
||||
--><!-- Left column --><!--
|
||||
<StackPanel Grid.Column="0" Margin="0,0,16,0">
|
||||
<!-- Location Site ID -->
|
||||
--><!-- Location Site ID --><!--
|
||||
<StackPanel Margin="0,0,0,12">
|
||||
<TextBlock Text="Location Site ID"
|
||||
FontSize="12"
|
||||
|
|
@ -467,7 +467,7 @@
|
|||
Style="{StaticResource ModernTextBox}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Meal Session -->
|
||||
--><!-- Meal Session --><!--
|
||||
<StackPanel Margin="0,0,0,12">
|
||||
<TextBlock Text="Meal Session"
|
||||
FontSize="12"
|
||||
|
|
@ -478,9 +478,9 @@
|
|||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Right column -->
|
||||
--><!-- Right column --><!--
|
||||
<StackPanel Grid.Column="1" Margin="0,0,0,0">
|
||||
<!-- Start time -->
|
||||
--><!-- Start time --><!--
|
||||
<StackPanel Margin="0,0,0,12">
|
||||
<TextBlock Text="Start (HH:mm:ss)"
|
||||
FontSize="12"
|
||||
|
|
@ -503,7 +503,7 @@
|
|||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<!-- End time -->
|
||||
--><!-- End time --><!--
|
||||
<StackPanel Margin="0,0,0,12">
|
||||
<TextBlock Text="End (HH:mm:ss)"
|
||||
FontSize="12"
|
||||
|
|
@ -528,7 +528,7 @@
|
|||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Buttons -->
|
||||
--><!-- Buttons --><!--
|
||||
<StackPanel Grid.Row="2"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
|
|
@ -550,7 +550,7 @@
|
|||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- Message -->
|
||||
--><!-- Message --><!--
|
||||
<Border Grid.Row="2"
|
||||
Margin="24,0,24,20"
|
||||
Background="Transparent">
|
||||
|
|
@ -572,4 +572,181 @@
|
|||
</TextBlock>
|
||||
</Border>
|
||||
</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>
|
||||
|
|
@ -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