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
SYED MUSTUFA AHMED NAQVI 2026-03-03 13:47:43 +05:00
parent e496445b35
commit a1ff310afb
14 changed files with 864 additions and 107 deletions

View File

@ -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),

View File

@ -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();

View File

@ -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;

View File

@ -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;
}

View File

@ -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>

View File

@ -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>

View File

@ -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)
};
}

View File

@ -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,&lt;payload&gt;", 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;
}
}
}

View File

@ -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);
}

View File

@ -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,

View File

@ -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);

View File

@ -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()
{

View File

@ -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>

View File

@ -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"/>