Show HRMS employee photo in dashboard and order history
Add EmployeePhotoService to load and decode hrms.employee_photo blobs (data URI or raw bytes) using parent_document_id Wire ScannerDashboardViewModel to load photos after RFID scan and on app restart, binding to EmployeeProfileImage Display employee photos in left panel and order-history rows with graceful placeholder fallback Store ParentDocumentId in scan records and expose it through HrmsEmployeeInfo / ScanRecord / OrderHistoryItem for photo lookups Use employee.concatenated_name from HRMS for full employee name in lookups Add CRUD support for meal schedules backed by hrms.meal_schedule (Add New + Save creates/updates schedules for a site and session)pull/1/head
parent
e496445b35
commit
a1ff310afb
|
|
@ -40,6 +40,7 @@ public partial class App : Application
|
|||
|
||||
var configService = new ConfigService();
|
||||
var employeeLookupService = new EmployeeLookupService(configService);
|
||||
var employeePhotoService = new EmployeePhotoService(configService);
|
||||
var menuLookupService = new MenuLookupService(configService);
|
||||
var mealScheduleService = new ProductionMealScheduleService(configService);
|
||||
var mealSessionResolver = new DbMealSessionResolver(mealScheduleService);
|
||||
|
|
@ -55,7 +56,7 @@ public partial class App : Application
|
|||
navigationService = new NavigationService(
|
||||
session,
|
||||
() => new AdminLoginViewModel(authService, session, navigationService, configService, adminAuditService),
|
||||
() => new ScannerDashboardViewModel(rfidService, navigationService, session, configService, menuLookupService),
|
||||
() => new ScannerDashboardViewModel(rfidService, navigationService, session, configService, menuLookupService, employeePhotoService),
|
||||
() => new MainDashboardViewModel(navigationService, rfidService, configService, session),
|
||||
() => new AdminSettingsAuthViewModel(authService, session, navigationService, configService),
|
||||
() => new SettingsViewModel(configService, navigationService, adminAuditService, syncService),
|
||||
|
|
|
|||
|
|
@ -163,6 +163,13 @@ public class AppDbContext : DbContext
|
|||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
if (columns.Count > 0 && !columns.Contains("ParentDocumentId", StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = "ALTER TABLE lunch_order_transactions ADD COLUMN ParentDocumentId TEXT DEFAULT ''";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
if (columns.Count > 0 && !columns.Contains("EmployeeId", StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
using var cmd = conn.CreateCommand();
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ namespace UtopiaCanteenSystem.Models;
|
|||
/// </summary>
|
||||
public class HrmsEmployeeInfo
|
||||
{
|
||||
/// <summary>Employee document id (employee.id); used to fetch photo from hrms.employee_photo.</summary>
|
||||
public string ParentDocumentId { get; set; } = string.Empty;
|
||||
/// <summary>Employee serial number (employee.serial_number).</summary>
|
||||
public string EmployeeId { get; set; } = string.Empty;
|
||||
public string FirstName { get; set; } = string.Empty;
|
||||
public string MiddleName { get; set; } = string.Empty;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using System.Windows.Media;
|
||||
|
||||
namespace UtopiaCanteenSystem.Models;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -5,6 +7,8 @@ namespace UtopiaCanteenSystem.Models;
|
|||
/// </summary>
|
||||
public class OrderHistoryItem
|
||||
{
|
||||
/// <summary>HRMS employee document id (employee.id) for this order; used to load photo.</summary>
|
||||
public string ParentDocumentId { get; set; } = string.Empty;
|
||||
public string EmployeeId { get; set; } = string.Empty;
|
||||
public string EmployeeName { get; set; } = string.Empty;
|
||||
public string Department { get; set; } = string.Empty;
|
||||
|
|
@ -18,6 +22,8 @@ public class OrderHistoryItem
|
|||
public string RelativeDateLabel { get; set; } = string.Empty;
|
||||
/// <summary>Time only, e.g. "09:54 AM".</summary>
|
||||
public string TimeDisplay { get; set; } = string.Empty;
|
||||
/// <summary>Optional avatar path; null = show placeholder.</summary>
|
||||
public string? AvatarPath { get; set; }
|
||||
/// <summary>Optional employee photo for this row; null = show placeholder.</summary>
|
||||
public ImageSource? EmployeePhoto { get; set; }
|
||||
/// <summary>True when EmployeePhoto is not null.</summary>
|
||||
public bool HasEmployeePhoto => EmployeePhoto != null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ public class ScanRecord
|
|||
public string DeviceId { get; set; } = string.Empty;
|
||||
/// <summary>IP address of the device at scan time.</summary>
|
||||
public string IpAddress { get; set; } = string.Empty;
|
||||
/// <summary>HRMS employee document id (employee.id) at scan time; used to load photo from hrms.employee_photo.</summary>
|
||||
public string ParentDocumentId { get; set; } = string.Empty;
|
||||
/// <summary>HRMS employee ID (serial_number) at scan time, when lookup succeeded.</summary>
|
||||
public string EmployeeId { get; set; } = string.Empty;
|
||||
/// <summary>Full name (first + middle) from HRMS at scan time, when lookup succeeded.</summary>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ApplicationRevision>11</ApplicationRevision>
|
||||
<ApplicationRevision>13</ApplicationRevision>
|
||||
<ApplicationVersion>1.0.0.*</ApplicationVersion>
|
||||
<BootstrapperEnabled>True</BootstrapperEnabled>
|
||||
<Configuration>Release</Configuration>
|
||||
|
|
@ -33,6 +33,6 @@
|
|||
<UpdateRequired>False</UpdateRequired>
|
||||
<UpdateUrl>\\FileServer\Edata\Deployments-by-DotNet-Team\UtopiaCanteenSystem\</UpdateUrl>
|
||||
<WebPageFileName>Publish.html</WebPageFileName>
|
||||
<History>True|2026-02-12T06:25:40.9881813Z||;True|2026-02-12T11:22:02.6537306+05:00||;True|2026-02-12T10:57:16.2775876+05:00||;True|2026-02-12T10:50:58.6968682+05:00||;False|2026-02-12T10:49:27.1867616+05:00||;True|2026-02-12T10:44:55.6022990+05:00||;True|2026-02-11T17:00:04.2786466+05:00||;True|2026-02-11T16:54:37.5052808+05:00||;True|2026-02-11T16:39:26.1892892+05:00||;True|2026-02-11T16:30:57.2420414+05:00||;True|2026-02-11T16:20:44.4749529+05:00||;</History>
|
||||
<History>False|2026-03-03T06:08:36.2813025Z||;False|2026-03-03T11:07:21.6156279+05:00||;True|2026-02-18T16:44:11.3509970+05:00||;True|2026-02-12T11:25:40.9881813+05:00||;True|2026-02-12T11:22:02.6537306+05:00||;True|2026-02-12T10:57:16.2775876+05:00||;True|2026-02-12T10:50:58.6968682+05:00||;False|2026-02-12T10:49:27.1867616+05:00||;True|2026-02-12T10:44:55.6022990+05:00||;True|2026-02-11T17:00:04.2786466+05:00||;True|2026-02-11T16:54:37.5052808+05:00||;True|2026-02-11T16:39:26.1892892+05:00||;True|2026-02-11T16:30:57.2420414+05:00||;True|2026-02-11T16:20:44.4749529+05:00||;</History>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
@ -26,9 +26,10 @@ public class EmployeeLookupService : IEmployeeLookupService
|
|||
|
||||
const string sql = @"
|
||||
SELECT
|
||||
e.id AS parent_document_id,
|
||||
e.serial_number AS employee_id,
|
||||
e.first_name,
|
||||
e.middle_name,
|
||||
e.concatenated_name AS first_name,
|
||||
'' AS middle_name,
|
||||
e.department_id,
|
||||
d.title AS department_title,
|
||||
d.department_type,
|
||||
|
|
@ -51,13 +52,14 @@ public class EmployeeLookupService : IEmployeeLookupService
|
|||
|
||||
return new HrmsEmployeeInfo
|
||||
{
|
||||
EmployeeId = GetString(reader, 0),
|
||||
FirstName = GetString(reader, 1),
|
||||
MiddleName = GetString(reader, 2),
|
||||
DepartmentId = GetString(reader, 3),
|
||||
DepartmentTitle = GetString(reader, 4),
|
||||
DepartmentType = GetString(reader, 5),
|
||||
LocationSiteId = GetString(reader, 6)
|
||||
ParentDocumentId = GetString(reader, 0),
|
||||
EmployeeId = GetString(reader, 1),
|
||||
FirstName = GetString(reader, 2),
|
||||
MiddleName = GetString(reader, 3),
|
||||
DepartmentId = GetString(reader, 4),
|
||||
DepartmentTitle = GetString(reader, 5),
|
||||
DepartmentType = GetString(reader, 6),
|
||||
LocationSiteId = GetString(reader, 7)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using MySqlConnector;
|
||||
|
||||
namespace UtopiaCanteenSystem.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Fetches employee photo from hrms.employee_photo by parent_document_id (employee document id from lookup).
|
||||
/// Handles data URI (data:image/...;base64,...) or raw image bytes. Caches by key to avoid repeated DB calls.
|
||||
/// </summary>
|
||||
public class EmployeePhotoService : IEmployeePhotoService
|
||||
{
|
||||
private readonly IConfigService _configService;
|
||||
private readonly ConcurrentDictionary<string, byte[]> _cache = new(StringComparer.OrdinalIgnoreCase);
|
||||
private const int MaxCacheEntries = 50;
|
||||
|
||||
public EmployeePhotoService(IConfigService configService)
|
||||
{
|
||||
_configService = configService;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<byte[]?> GetPhotoBytesAsync(string parentDocumentId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(parentDocumentId))
|
||||
return null;
|
||||
|
||||
var key = parentDocumentId.Trim();
|
||||
if (_cache.TryGetValue(key, out var cached))
|
||||
return cached;
|
||||
|
||||
var connectionString = _configService.GetHrmsLookupConnectionString();
|
||||
if (string.IsNullOrWhiteSpace(connectionString))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
await using var conn = new MySqlConnection(connectionString);
|
||||
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// employee_photo.employee_id stores the employee document id (parent_document_id from employee lookup)
|
||||
const string sql = "SELECT photo_blob FROM hrms.employee_photo WHERE employee_id = @parentDocumentId LIMIT 1";
|
||||
await using var cmd = new MySqlCommand(sql, conn);
|
||||
cmd.Parameters.AddWithValue("@parentDocumentId", key);
|
||||
|
||||
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
return null;
|
||||
|
||||
if (reader.IsDBNull(0))
|
||||
return null;
|
||||
|
||||
byte[] blob;
|
||||
try
|
||||
{
|
||||
var len = reader.GetBytes(0, 0, null, 0, 0);
|
||||
blob = new byte[len];
|
||||
reader.GetBytes(0, 0, blob, 0, (int)len);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var imageBytes = DecodeBlobToImageBytes(blob);
|
||||
if (imageBytes == null || imageBytes.Length == 0)
|
||||
return null;
|
||||
|
||||
// Cache (evict old if needed)
|
||||
while (_cache.Count >= MaxCacheEntries && _cache.Count > 0)
|
||||
{
|
||||
var first = _cache.Keys.FirstOrDefault();
|
||||
if (first != null)
|
||||
_cache.TryRemove(first, out _);
|
||||
else
|
||||
break;
|
||||
}
|
||||
_cache[key] = imageBytes;
|
||||
return imageBytes;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts blob to image bytes: if blob is UTF-8 "data:image...;base64,<payload>", decodes base64; else treats as raw image.
|
||||
/// </summary>
|
||||
internal static byte[]? DecodeBlobToImageBytes(byte[] blob)
|
||||
{
|
||||
if (blob == null || blob.Length == 0)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
var str = Encoding.UTF8.GetString(blob);
|
||||
if (str.StartsWith("data:image", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var commaIndex = str.IndexOf(',');
|
||||
if (commaIndex < 0)
|
||||
return null;
|
||||
var base64 = str.Substring(commaIndex + 1).Trim();
|
||||
return Convert.FromBase64String(base64);
|
||||
}
|
||||
|
||||
return blob;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
namespace UtopiaCanteenSystem.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Fetches employee profile photo from hrms.employee_photo by parent_document_id (employee document id from RFID lookup).
|
||||
/// Photo blob may be a data URI (data:image/...;base64,...) or raw image bytes.
|
||||
/// </summary>
|
||||
public interface IEmployeePhotoService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets photo bytes for the given parent_document_id (employee.id from lookup), or null if not found / decode failed.
|
||||
/// May return cached bytes for the same key.
|
||||
/// </summary>
|
||||
Task<byte[]?> GetPhotoBytesAsync(string parentDocumentId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -108,6 +108,7 @@ public class RfidService : IRfidService
|
|||
SiteId = siteId,
|
||||
DeviceId = _configService.GetDeviceId(),
|
||||
IpAddress = GetLocalIpAddress(),
|
||||
ParentDocumentId = employee.ParentDocumentId ?? string.Empty,
|
||||
EmployeeId = employee.EmployeeId ?? string.Empty,
|
||||
EmployeeName = string.IsNullOrEmpty(fullName) ? string.Empty : fullName,
|
||||
Department = employee.DepartmentTitle ?? string.Empty,
|
||||
|
|
|
|||
|
|
@ -195,10 +195,18 @@ public partial class MealSchedulesViewModel : ObservableObject
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>Selects a schedule for editing (used by Actions column Edit button).</summary>
|
||||
[RelayCommand]
|
||||
private async Task Delete()
|
||||
private void SelectSchedule(MealSchedule? schedule)
|
||||
{
|
||||
if (SelectedSchedule == null)
|
||||
SelectedSchedule = schedule;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task Delete(MealSchedule? row)
|
||||
{
|
||||
var toDelete = row ?? SelectedSchedule;
|
||||
if (toDelete == null)
|
||||
{
|
||||
Message = "Select a schedule to delete.";
|
||||
IsError = true;
|
||||
|
|
@ -208,7 +216,7 @@ public partial class MealSchedulesViewModel : ObservableObject
|
|||
IsError = false;
|
||||
try
|
||||
{
|
||||
await _mealScheduleService.DeleteAsync(SelectedSchedule.Id).ConfigureAwait(true);
|
||||
await _mealScheduleService.DeleteAsync(toDelete.Id).ConfigureAwait(true);
|
||||
Message = "Schedule deleted from production.";
|
||||
SelectedSchedule = null;
|
||||
await LoadSchedulesAsync().ConfigureAwait(true);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ using System.Text;
|
|||
using DebounceTimer = System.Timers.Timer;
|
||||
using Microsoft.Win32;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
|
@ -138,6 +140,13 @@ public partial class ScannerDashboardViewModel : ObservableObject
|
|||
[ObservableProperty]
|
||||
private string? _employeeProfileImagePath;
|
||||
|
||||
/// <summary>Employee photo from hrms.employee_photo (in-memory); null = show placeholder.</summary>
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(HasEmployeeProfileImage))]
|
||||
private ImageSource? _employeeProfileImage;
|
||||
|
||||
public bool HasEmployeeProfileImage => EmployeeProfileImage != null;
|
||||
|
||||
/// <summary>Last 4 orders for the Order History card (newest first).</summary>
|
||||
public ObservableCollection<OrderHistoryItem> OrderHistory { get; } = new();
|
||||
|
||||
|
|
@ -154,19 +163,22 @@ public partial class ScannerDashboardViewModel : ObservableObject
|
|||
private bool _isOrderHistoryModalOpen;
|
||||
|
||||
private readonly IMenuLookupService _menuLookupService;
|
||||
private readonly IEmployeePhotoService _employeePhotoService;
|
||||
|
||||
public ScannerDashboardViewModel(
|
||||
IRfidService rfidService,
|
||||
INavigationService navigation,
|
||||
AppSession session,
|
||||
IConfigService configService,
|
||||
IMenuLookupService menuLookupService)
|
||||
IMenuLookupService menuLookupService,
|
||||
IEmployeePhotoService employeePhotoService)
|
||||
{
|
||||
_rfidService = rfidService;
|
||||
_navigation = navigation;
|
||||
_session = session;
|
||||
_configService = configService;
|
||||
_menuLookupService = menuLookupService;
|
||||
_employeePhotoService = employeePhotoService;
|
||||
_uiDispatcher = Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher;
|
||||
|
||||
// Debounce auto-submit (keyboard wedge scanners).
|
||||
|
|
@ -503,6 +515,8 @@ public partial class ScannerDashboardViewModel : ObservableObject
|
|||
// 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);
|
||||
// Load employee photo from hrms.employee_photo by parent_document_id (employee document id)
|
||||
_ = LoadEmployeePhotoAsync(info.ParentDocumentId);
|
||||
}
|
||||
// Refresh dashboard stats and order history
|
||||
_ = RefreshDashboardAfterSuccessfulScanAsync(cardId);
|
||||
|
|
@ -575,6 +589,7 @@ public partial class ScannerDashboardViewModel : ObservableObject
|
|||
EmployeeDepartment = "—";
|
||||
EmployeeDepartmentType = "—";
|
||||
EmployeeOrderItem = "—";
|
||||
EmployeeProfileImage = null;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -591,6 +606,53 @@ public partial class ScannerDashboardViewModel : ObservableObject
|
|||
EmployeeOrderItem = last.MealItems;
|
||||
else
|
||||
EmployeeOrderItem = "—";
|
||||
// Load photo for last scanned employee (e.g. on app restart) using parent_document_id
|
||||
_ = LoadEmployeePhotoAsync(string.IsNullOrWhiteSpace(last.ParentDocumentId) ? null : last.ParentDocumentId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads employee photo from hrms.employee_photo by parent_document_id and sets EmployeeProfileImage on the UI thread.
|
||||
/// Fails gracefully: no photo or decode error leaves placeholder (null).
|
||||
/// </summary>
|
||||
private async Task LoadEmployeePhotoAsync(string? parentDocumentId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(parentDocumentId))
|
||||
{
|
||||
_uiDispatcher.Invoke(() => EmployeeProfileImage = null);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var bytes = await _employeePhotoService.GetPhotoBytesAsync(parentDocumentId.Trim()).ConfigureAwait(false);
|
||||
_uiDispatcher.Invoke(() =>
|
||||
{
|
||||
if (bytes == null || bytes.Length == 0)
|
||||
{
|
||||
EmployeeProfileImage = null;
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
using var ms = new MemoryStream(bytes);
|
||||
var bmp = new BitmapImage();
|
||||
bmp.BeginInit();
|
||||
bmp.CacheOption = BitmapCacheOption.OnLoad;
|
||||
bmp.StreamSource = ms;
|
||||
bmp.EndInit();
|
||||
bmp.Freeze();
|
||||
EmployeeProfileImage = bmp;
|
||||
}
|
||||
catch
|
||||
{
|
||||
EmployeeProfileImage = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
_uiDispatcher.Invoke(() => EmployeeProfileImage = null);
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadOrderHistory()
|
||||
|
|
@ -602,9 +664,11 @@ public partial class ScannerDashboardViewModel : ObservableObject
|
|||
var local = r.ScanTime.ToLocalTime();
|
||||
var employeeId = !string.IsNullOrWhiteSpace(r.EmployeeId) ? r.EmployeeId : "—";
|
||||
var employeeName = !string.IsNullOrWhiteSpace(r.EmployeeName) ? r.EmployeeName : "—";
|
||||
var photo = CreateImageSourceFromPhoto(r.ParentDocumentId);
|
||||
|
||||
OrderHistory.Add(new OrderHistoryItem
|
||||
{
|
||||
ParentDocumentId = r.ParentDocumentId ?? string.Empty,
|
||||
EmployeeId = employeeId,
|
||||
EmployeeName = employeeName,
|
||||
Department = string.IsNullOrWhiteSpace(r.Department) ? "—" : r.Department,
|
||||
|
|
@ -615,7 +679,8 @@ public partial class ScannerDashboardViewModel : ObservableObject
|
|||
: (!string.IsNullOrWhiteSpace(r.MealLabel) ? r.MealLabel : "—"),
|
||||
TotalPrice = r.TotalPrice > 0 ? r.TotalPrice : 0,
|
||||
TimeDisplay = local.ToString("hh:mm tt"),
|
||||
RelativeDateLabel = GetRelativeDateLabel(r.ScanTime)
|
||||
RelativeDateLabel = GetRelativeDateLabel(r.ScanTime),
|
||||
EmployeePhoto = photo
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -631,9 +696,11 @@ public partial class ScannerDashboardViewModel : ObservableObject
|
|||
var local = r.ScanTime.ToLocalTime();
|
||||
var employeeId = !string.IsNullOrWhiteSpace(r.EmployeeId) ? r.EmployeeId : "—";
|
||||
var employeeName = !string.IsNullOrWhiteSpace(r.EmployeeName) ? r.EmployeeName : "—";
|
||||
var photo = CreateImageSourceFromPhoto(r.ParentDocumentId);
|
||||
|
||||
TodayOrderHistory.Add(new OrderHistoryItem
|
||||
{
|
||||
ParentDocumentId = r.ParentDocumentId ?? string.Empty,
|
||||
EmployeeId = employeeId,
|
||||
EmployeeName = employeeName,
|
||||
Department = string.IsNullOrWhiteSpace(r.Department) ? "—" : r.Department,
|
||||
|
|
@ -644,11 +711,45 @@ public partial class ScannerDashboardViewModel : ObservableObject
|
|||
: (!string.IsNullOrWhiteSpace(r.MealLabel) ? r.MealLabel : "—"),
|
||||
TotalPrice = r.TotalPrice > 0 ? r.TotalPrice : 0,
|
||||
TimeDisplay = local.ToString("hh:mm tt"),
|
||||
RelativeDateLabel = GetRelativeDateLabel(r.ScanTime)
|
||||
RelativeDateLabel = GetRelativeDateLabel(r.ScanTime),
|
||||
EmployeePhoto = photo
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper to synchronously create an ImageSource for order history rows using cached employee photos.
|
||||
/// Returns null on any failure and does not throw.
|
||||
/// </summary>
|
||||
private ImageSource? CreateImageSourceFromPhoto(string? parentDocumentId)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(parentDocumentId))
|
||||
return null;
|
||||
|
||||
var bytes = _employeePhotoService.GetPhotoBytesAsync(parentDocumentId.Trim())
|
||||
.ConfigureAwait(false)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
if (bytes == null || bytes.Length == 0)
|
||||
return null;
|
||||
|
||||
using var ms = new MemoryStream(bytes);
|
||||
var bmp = new BitmapImage();
|
||||
bmp.BeginInit();
|
||||
bmp.CacheOption = BitmapCacheOption.OnLoad;
|
||||
bmp.StreamSource = ms;
|
||||
bmp.EndInit();
|
||||
bmp.Freeze();
|
||||
return bmp;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenOrderHistoryModal()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,114 +4,577 @@
|
|||
xmlns:converters="clr-namespace:UtopiaCanteenSystem.Converters">
|
||||
<UserControl.Resources>
|
||||
<converters:MealSessionToNameConverter x:Key="MealSessionConverter"/>
|
||||
<SolidColorBrush x:Key="PrimaryText" Color="#2D3748"/>
|
||||
|
||||
<!-- Palette -->
|
||||
<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"/>
|
||||
<Style x:Key="AccentButton" TargetType="Button">
|
||||
<Setter Property="Background" Value="{StaticResource PrimaryAccent}"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Padding" Value="16,10"/>
|
||||
<Setter Property="MinHeight" Value="36"/>
|
||||
</Style>
|
||||
<Style x:Key="OutlineButton" TargetType="Button">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource PrimaryText}"/>
|
||||
<SolidColorBrush x:Key="CardBackground" Color="#FFFFFF"/>
|
||||
|
||||
<!-- Card style -->
|
||||
<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"/>
|
||||
<Setter Property="Padding" Value="16,10"/>
|
||||
<Setter Property="SnapsToDevicePixels" Value="True"/>
|
||||
<Setter Property="Effect">
|
||||
<Setter.Value>
|
||||
<DropShadowEffect BlurRadius="18"
|
||||
ShadowDepth="0"
|
||||
Opacity="0.08"
|
||||
Color="#000000"/>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Buttons -->
|
||||
<Style x:Key="PrimaryButton" TargetType="Button">
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="Background" Value="{StaticResource PrimaryAccent}"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Padding" Value="18,10"/>
|
||||
<Setter Property="MinHeight" Value="36"/>
|
||||
<Setter Property="MinWidth" Value="110"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Right"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="8"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</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"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="8"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="DangerButton" TargetType="Button" BasedOn="{StaticResource SecondaryButton}">
|
||||
<Setter Property="Foreground" Value="#E53E3E"/>
|
||||
<Setter Property="BorderBrush" Value="#FED7D7"/>
|
||||
<Setter Property="Background" Value="#FFF5F5"/>
|
||||
</Style>
|
||||
|
||||
<!-- 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"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Left"/>
|
||||
<Setter Property="Background" Value="#F7FAFC"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource PrimaryText}"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="BackBtnBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
CornerRadius="20"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||
<TextBlock Text="←" FontSize="14" Margin="0,0,6,0" VerticalAlignment="Center"/>
|
||||
<ContentPresenter VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="BackBtnBorder" Property="Background" Value="#EDF2F7"/>
|
||||
<Setter TargetName="BackBtnBorder" Property="BorderBrush" Value="{StaticResource PrimaryAccent}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource PrimaryAccent}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="BackBtnBorder" Property="Background" Value="#E2E8F0"/>
|
||||
<Setter TargetName="BackBtnBorder" Property="BorderBrush" Value="#CBD5E0"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Small icon action buttons for DataGrid Actions column -->
|
||||
<Style x:Key="GridActionButton" TargetType="Button">
|
||||
<Setter Property="Width" Value="28"/>
|
||||
<Setter Property="Height" Value="28"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="Margin" Value="2,0"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="BtnBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
CornerRadius="14"
|
||||
BorderThickness="0">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="BtnBorder" Property="Background" Value="#E2E8F0"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="BtnBorder" Property="Background" Value="#CBD5E0"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style x:Key="GridActionEditButton" TargetType="Button" BasedOn="{StaticResource GridActionButton}">
|
||||
<Setter Property="Foreground" Value="{StaticResource PrimaryAccent}"/>
|
||||
<Setter Property="ToolTip" Value="Edit"/>
|
||||
</Style>
|
||||
<Style x:Key="GridActionDeleteButton" TargetType="Button" BasedOn="{StaticResource GridActionButton}">
|
||||
<Setter Property="Foreground" Value="{StaticResource ErrorBrush}"/>
|
||||
<Setter Property="ToolTip" Value="Delete"/>
|
||||
</Style>
|
||||
|
||||
<!-- Inputs -->
|
||||
<Style x:Key="ModernTextBox" TargetType="TextBox">
|
||||
<Setter Property="Margin" Value="0"/>
|
||||
<Setter Property="Padding" Value="10,8"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Background" Value="White"/>
|
||||
<Setter Property="MinHeight" Value="34"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
<Setter Property="SnapsToDevicePixels" Value="True"/>
|
||||
</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"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
</Style>
|
||||
|
||||
<!-- DataGrid styling -->
|
||||
<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"/>
|
||||
<Setter Property="Margin" Value="0"/>
|
||||
<Setter Property="SnapsToDevicePixels" Value="True"/>
|
||||
<Setter Property="HorizontalGridLinesBrush" Value="{StaticResource BorderBrush}"/>
|
||||
<Setter Property="ColumnHeaderStyle">
|
||||
<Setter.Value>
|
||||
<Style TargetType="DataGridColumnHeader">
|
||||
<Setter Property="Background" Value="#F7FAFC"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource MutedText}"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="BorderThickness" Value="0,0,0,1"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
<Setter Property="Padding" Value="16,10"/>
|
||||
</Style>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Property="CellStyle">
|
||||
<Setter.Value>
|
||||
<Style TargetType="DataGridCell">
|
||||
<Setter Property="Padding" Value="16,10"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="DataGridCell">
|
||||
<Border Padding="{TemplateBinding Padding}"
|
||||
Background="{TemplateBinding Background}">
|
||||
<ContentPresenter VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Property="RowStyle">
|
||||
<Setter.Value>
|
||||
<Style TargetType="DataGridRow">
|
||||
<Setter Property="SnapsToDevicePixels" Value="True"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="MinHeight" Value="44"/>
|
||||
<Setter Property="BorderThickness" Value="0,0,0,1"/>
|
||||
<Setter Property="BorderBrush" Value="#EDF2F7"/>
|
||||
<Setter Property="Background" Value="White"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{StaticResource PrimaryAccentLight}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="#E0F2F1"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource PrimaryAccent}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
<Grid Background="#F0F2F5" Margin="24">
|
||||
|
||||
<Grid Background="#F4F7FB">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Title + Back + Site filter -->
|
||||
<Grid Grid.Row="0" Margin="0,0,0,16">
|
||||
<!-- HEADER BAR -->
|
||||
<Grid Grid.Row="0" Margin="24,20,24,16">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button Grid.Column="0" Content="← Back" Command="{Binding BackCommand}" Style="{StaticResource OutlineButton}" Margin="0,0,16,0"/>
|
||||
<TextBlock Grid.Column="1" Text="Meal Schedules (production hrms.meal_schedule)" FontSize="22" FontWeight="SemiBold" Foreground="{StaticResource PrimaryText}" VerticalAlignment="Center"/>
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="Site:" VerticalAlignment="Center" Margin="0,0,8,0" Foreground="{StaticResource MutedText}"/>
|
||||
<ComboBox ItemsSource="{Binding SiteFilterChoices}" SelectedItem="{Binding SelectedSiteFilter, Mode=TwoWay}" MinWidth="80" MinHeight="32" Padding="8,4"/>
|
||||
|
||||
<!-- Back -->
|
||||
<Button Grid.Column="0"
|
||||
Command="{Binding BackCommand}"
|
||||
Style="{StaticResource BackButton}"
|
||||
Content="Back"/>
|
||||
|
||||
<!-- 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)"
|
||||
FontSize="13"
|
||||
Foreground="{StaticResource MutedText}"
|
||||
Margin="0,2,0,0"/>-->
|
||||
</StackPanel>
|
||||
|
||||
<!-- Site filter -->
|
||||
<StackPanel Grid.Column="2"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,16,0">
|
||||
<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>
|
||||
|
||||
<!-- Add New -->
|
||||
<Button Grid.Column="3"
|
||||
Command="{Binding AddNewCommand}"
|
||||
Style="{StaticResource PrimaryButton}">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="+" Margin="0,0,6,0" FontWeight="Bold"/>
|
||||
<TextBlock Text="Add New"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- DataGrid -->
|
||||
<DataGrid Grid.Row="1" ItemsSource="{Binding Schedules}" SelectedItem="{Binding SelectedSchedule, Mode=TwoWay}"
|
||||
AutoGenerateColumns="False" IsReadOnly="True" CanUserAddRows="False" CanUserDeleteRows="False"
|
||||
HeadersVisibility="Column" GridLinesVisibility="Horizontal" BorderThickness="1" BorderBrush="{StaticResource BorderBrush}"
|
||||
Margin="0,0,0,16" MinHeight="120">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Site" Binding="{Binding LocationSiteId}" Width="*"/>
|
||||
<DataGridTextColumn Header="Session" Binding="{Binding MealSession, Converter={StaticResource MealSessionConverter}}" Width="*"/>
|
||||
<DataGridTextColumn Header="Start" Binding="{Binding StartTime}" Width="*"/>
|
||||
<DataGridTextColumn Header="End" Binding="{Binding EndTime}" Width="*"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
<!-- MAIN CONTENT -->
|
||||
<ScrollViewer Grid.Row="1"
|
||||
Margin="24,0,24,16"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel>
|
||||
<!-- Schedules card -->
|
||||
<Border Style="{StaticResource CardBorder}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Edit form -->
|
||||
<Border Grid.Row="2" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1" CornerRadius="8" Padding="16" Margin="0,0,0,12" Background="White">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0" Margin="0,0,8,0">
|
||||
<TextBlock Text="Location Site ID" FontSize="12" Foreground="{StaticResource MutedText}" Margin="0,0,0,4"/>
|
||||
<TextBox Text="{Binding LocationSiteId, UpdateSourceTrigger=PropertyChanged}" Padding="8,6" MinHeight="32"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Margin="8,0">
|
||||
<TextBlock Text="Meal Session" FontSize="12" Foreground="{StaticResource MutedText}" Margin="0,0,0,4"/>
|
||||
<ComboBox SelectedIndex="{Binding MealSessionIndex, Mode=TwoWay}" MinHeight="32" Padding="8,4">
|
||||
<ComboBoxItem Content="Breakfast"/>
|
||||
<ComboBoxItem Content="Lunch"/>
|
||||
<ComboBoxItem Content="Tea"/>
|
||||
<ComboBoxItem Content="Dinner"/>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Margin="8,0">
|
||||
<TextBlock Text="Start (HH:mm:ss)" FontSize="12" Foreground="{StaticResource MutedText}" Margin="0,0,0,4"/>
|
||||
<TextBox Text="{Binding StartTime, UpdateSourceTrigger=PropertyChanged}" Padding="8,6" MinHeight="32"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="3" Margin="8,0">
|
||||
<TextBlock Text="End (HH:mm:ss)" FontSize="12" Foreground="{StaticResource MutedText}" Margin="0,0,0,4"/>
|
||||
<TextBox Text="{Binding EndTime, UpdateSourceTrigger=PropertyChanged}" Padding="8,6" MinHeight="32"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="4" VerticalAlignment="Bottom" Orientation="Horizontal">
|
||||
<Button Content="Add new" Command="{Binding AddNewCommand}" Style="{StaticResource OutlineButton}" Margin="0,0,8,0"/>
|
||||
<Button Content="Save" Command="{Binding SaveCommand}" Style="{StaticResource AccentButton}" Margin="0,0,8,0"/>
|
||||
<Button Content="Delete" Command="{Binding DeleteCommand}" Style="{StaticResource OutlineButton}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
<!-- Card header -->
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,12">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Schedules"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource PrimaryText}"/>
|
||||
<TextBlock Text="Configured meal windows per site"
|
||||
FontSize="12"
|
||||
Foreground="{StaticResource MutedText}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- DataGrid -->
|
||||
<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 MealSession, Converter={StaticResource MealSessionConverter}}"
|
||||
Width="*"/>
|
||||
<DataGridTextColumn Header="Start"
|
||||
Binding="{Binding StartTime}"
|
||||
Width="*"/>
|
||||
<DataGridTextColumn Header="End"
|
||||
Binding="{Binding EndTime}"
|
||||
Width="*"/>
|
||||
<DataGridTemplateColumn Header="Actions" Width="100" MinWidth="100">
|
||||
<DataGridTemplateColumn.HeaderStyle>
|
||||
<Style TargetType="DataGridColumnHeader">
|
||||
<Setter Property="Background" Value="#F7FAFC"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource MutedText}"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="Padding" Value="16,10"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Right"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
<Setter Property="BorderThickness" Value="0,0,0,1"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||
</Style>
|
||||
</DataGridTemplateColumn.HeaderStyle>
|
||||
<DataGridTemplateColumn.CellStyle>
|
||||
<Style TargetType="DataGridCell">
|
||||
<Setter Property="Padding" Value="8,6"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Right"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="DataGridCell">
|
||||
<Border Padding="{TemplateBinding Padding}" Background="{TemplateBinding Background}">
|
||||
<ContentPresenter VerticalAlignment="Center" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</DataGridTemplateColumn.CellStyle>
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||
<Button Style="{StaticResource GridActionEditButton}"
|
||||
Command="{Binding DataContext.SelectScheduleCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="✎"
|
||||
FontSize="12"/>
|
||||
<Button Style="{StaticResource GridActionDeleteButton}"
|
||||
Command="{Binding DataContext.DeleteCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="🗑"
|
||||
FontSize="12"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Add / Edit card -->
|
||||
<Border Style="{StaticResource CardBorder}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Card header -->
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,16">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Add or Edit Meal Schedule"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource PrimaryText}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Form -->
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Left column -->
|
||||
<StackPanel Grid.Column="0" Margin="0,0,16,0">
|
||||
<!-- Location Site ID -->
|
||||
<StackPanel Margin="0,0,0,12">
|
||||
<TextBlock Text="Location Site ID"
|
||||
FontSize="12"
|
||||
Foreground="{StaticResource MutedText}"
|
||||
Margin="0,0,0,4"/>
|
||||
<TextBox Text="{Binding LocationSiteId, UpdateSourceTrigger=PropertyChanged}"
|
||||
Style="{StaticResource ModernTextBox}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Meal Session -->
|
||||
<StackPanel Margin="0,0,0,12">
|
||||
<TextBlock Text="Meal Session"
|
||||
FontSize="12"
|
||||
Foreground="{StaticResource MutedText}"
|
||||
Margin="0,0,0,4"/>
|
||||
<ComboBox SelectedIndex="{Binding MealSessionIndex, Mode=TwoWay}"
|
||||
Style="{StaticResource ModernComboBox}">
|
||||
<ComboBoxItem Content="Breakfast"/>
|
||||
<ComboBoxItem Content="Lunch"/>
|
||||
<ComboBoxItem Content="Tea"/>
|
||||
<ComboBoxItem Content="Dinner"/>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Right column -->
|
||||
<StackPanel Grid.Column="1" Margin="0,0,0,0">
|
||||
<!-- Start time -->
|
||||
<StackPanel Margin="0,0,0,12">
|
||||
<TextBlock Text="Start (HH:mm:ss)"
|
||||
FontSize="12"
|
||||
Foreground="{StaticResource MutedText}"
|
||||
Margin="0,0,0,4"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox Grid.Column="0"
|
||||
Text="{Binding StartTime, UpdateSourceTrigger=PropertyChanged}"
|
||||
Style="{StaticResource ModernTextBox}"
|
||||
Margin="0"/>
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="🕒"
|
||||
Margin="6,0,4,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{StaticResource MutedText}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<!-- End time -->
|
||||
<StackPanel Margin="0,0,0,12">
|
||||
<TextBlock Text="End (HH:mm:ss)"
|
||||
FontSize="12"
|
||||
Foreground="{StaticResource MutedText}"
|
||||
Margin="0,0,0,4"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox Grid.Column="0"
|
||||
Text="{Binding EndTime, UpdateSourceTrigger=PropertyChanged}"
|
||||
Style="{StaticResource ModernTextBox}"
|
||||
Margin="0"/>
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="🕒"
|
||||
Margin="6,0,4,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{StaticResource MutedText}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Buttons -->
|
||||
<StackPanel Grid.Row="2"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0,18,0,0">
|
||||
<Button Content="Add New"
|
||||
Command="{Binding AddNewCommand}"
|
||||
Style="{StaticResource SecondaryButton}"
|
||||
Margin="0,0,8,0"/>
|
||||
<Button Content="Save"
|
||||
Command="{Binding SaveCommand}"
|
||||
Style="{StaticResource PrimaryButton}"
|
||||
Margin="0,0,8,0"/>
|
||||
<Button Content="Delete"
|
||||
Command="{Binding DeleteCommand}"
|
||||
Style="{StaticResource DangerButton}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- Message -->
|
||||
<TextBlock Grid.Row="3" Text="{Binding Message}" Foreground="{StaticResource PrimaryText}" 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>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -165,15 +165,26 @@
|
|||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*" MinWidth="0"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<!-- Profile image (circular); set EmployeeProfileImagePath to show photo -->
|
||||
<!-- Profile image (circular); EmployeeProfileImage from hrms.employee_photo, else placeholder -->
|
||||
<Grid Grid.Column="0" Width="110" Height="110" Margin="0,0,18,0">
|
||||
<Ellipse Fill="#e2e8f0"/>
|
||||
<Image Source="{Binding EmployeeProfileImagePath, Converter={StaticResource StringToImageSource}}" Stretch="UniformToFill" Visibility="{Binding EmployeeProfileImagePath, Converter={StaticResource StringToVisibility}}">
|
||||
<Image Source="{Binding EmployeeProfileImage}" Stretch="UniformToFill" Visibility="{Binding HasEmployeeProfileImage, Converter={StaticResource BoolToVisibility}}">
|
||||
<Image.Clip>
|
||||
<EllipseGeometry Center="55,55" RadiusX="55" RadiusY="55"/>
|
||||
</Image.Clip>
|
||||
</Image>
|
||||
<TextBlock Text="—" FontSize="32" Foreground="{StaticResource MutedTextBrush}" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="{Binding EmployeeProfileImagePath, Converter={StaticResource StringToVisibility}, ConverterParameter=Invert}"/>
|
||||
<TextBlock Text="—" FontSize="32" Foreground="{StaticResource MutedTextBrush}" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding HasEmployeeProfileImage}" Value="True">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" MinWidth="0">
|
||||
<TextBlock Text="Name" FontSize="15" Foreground="{StaticResource MutedTextBrush}" Margin="0,0,0,4"/>
|
||||
|
|
@ -491,7 +502,31 @@
|
|||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Border Width="36" Height="36" Margin="0,0,10,0" CornerRadius="18" Background="{StaticResource AccentLightBrush}" BorderBrush="{StaticResource AccentBrush}" BorderThickness="1">
|
||||
<TextBlock Text="—" FontSize="14" Foreground="{StaticResource AccentBrush}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Grid>
|
||||
<Image Source="{Binding EmployeePhoto}"
|
||||
Stretch="UniformToFill"
|
||||
Visibility="{Binding HasEmployeePhoto, Converter={StaticResource BoolToVisibility}}">
|
||||
<Image.Clip>
|
||||
<EllipseGeometry Center="18,18" RadiusX="18" RadiusY="18"/>
|
||||
</Image.Clip>
|
||||
</Image>
|
||||
<TextBlock Text="—"
|
||||
FontSize="14"
|
||||
Foreground="{StaticResource AccentBrush}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding HasEmployeePhoto}" Value="True">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</Border>
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding EmployeeId}" FontSize="14" FontWeight="Bold" Foreground="{StaticResource TitleTextBrush}" TextTrimming="CharacterEllipsis"/>
|
||||
|
|
|
|||
Loading…
Reference in New Issue