fix: schedule pages — real API data, role display, time formatting

- Rewrite StaffSchedule.razor from hardcoded stub to real API integration
  (profile → shop schedules → filter by staffId)
- Fix admin ShopSchedule role column: use staff role from merchant data
  instead of showing "—"
- Add FormatTime() helper to strip seconds from time display (08:00:00 → 08:00)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ho Ngoc Hai
2026-03-13 17:06:40 +07:00
parent deffb9de4a
commit fffedea785
2 changed files with 137 additions and 47 deletions

View File

@@ -82,10 +82,10 @@
var schedStaffName = schedStaff != null && (!string.IsNullOrWhiteSpace(schedStaff.LastName) || !string.IsNullOrWhiteSpace(schedStaff.FirstName)) ? $"{schedStaff.LastName} {schedStaff.FirstName}".Trim() : (s.EmployeeCode ?? s.StaffId.ToString()[..8]);
<tr style="border-top:1px solid var(--admin-border-subtle);">
<td style="padding:12px 16px;font-weight:600;">@schedStaffName</td>
<td style="padding:12px 16px;font-size:13px;color:var(--admin-text-tertiary);">@(s.Role ?? "—")</td>
<td style="padding:12px 16px;font-size:13px;color:var(--admin-text-tertiary);">@(schedStaff?.Role ?? s.Role ?? "—")</td>
<td style="padding:12px 16px;text-align:center;font-weight:600;color:var(--admin-orange-primary);">@ShopHelpers.DayLabel(s.DayOfWeek)</td>
<td style="padding:12px 16px;text-align:center;">@s.StartTime</td>
<td style="padding:12px 16px;text-align:center;">@s.EndTime</td>
<td style="padding:12px 16px;text-align:center;">@FormatTime(s.StartTime)</td>
<td style="padding:12px 16px;text-align:center;">@FormatTime(s.EndTime)</td>
<td style="padding:12px 16px;text-align:center;"><button @onclick='() => DeleteScheduleItem(s.Id)' style="background:rgba(239,68,68,0.1);border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;"><i data-lucide="trash-2" style="color:#EF4444;width:14px;height:14px;"></i></button></td>
</tr>
}
@@ -278,4 +278,15 @@ else if (SubSection == "shifts")
}
catch (Exception ex) { Console.Error.WriteLine($"Không thể xóa lịch làm việc: {ex.Message}"); }
}
/// <summary>
/// EN: Format time string — remove seconds if present (08:00:00 → 08:00).
/// VI: Định dạng giờ — bỏ giây nếu có (08:00:00 → 08:00).
/// </summary>
private static string FormatTime(string? time)
{
if (string.IsNullOrEmpty(time)) return "—";
var parts = time.Split(':');
return parts.Length >= 2 ? $"{parts[0]}:{parts[1]}" : time;
}
}

View File

@@ -1,11 +1,14 @@
@page "/staff/schedule"
@layout StaffLayout
@inject WebClientTpos.Client.Services.PosDataService DataService
@using WebClientTpos.Client.Services
@inject PosDataService DataService
@inject AuthService AuthSvc
@inject AuthStateService AuthState
@inject IJSRuntime JS
@*
EN: Staff schedule page — view weekly work schedule.
VI: Trang lịch làm việc — xem lịch làm việc hàng tuần.
EN: Staff schedule page — view weekly work schedule from real API data.
VI: Trang lịch làm việc — xem lịch làm việc hàng tuần từ dữ liệu API thật.
*@
<PageTitle>Lịch làm việc</PageTitle>
@@ -16,60 +19,124 @@
<p class="staff-page-subtitle">Lịch trình làm việc của bạn</p>
</div>
@* ═══ WEEK VIEW ═══ *@
<div style="display:grid;grid-template-columns:repeat(7,1fr);gap:8px;margin-bottom:24px;">
@for (int i = 0; i < 7; i++)
{
var day = _weekStart.AddDays(i);
var isToday = day.Date == DateTime.Now.Date;
var isWeekend = day.DayOfWeek == DayOfWeek.Sunday;
<div style="background:var(--admin-bg-elevated);border:1px solid @(isToday ? "#22C55E" : "var(--admin-border-default)");border-radius:var(--admin-radius-lg);padding:16px;text-align:center;">
<div style="font-size:11px;color:var(--admin-text-tertiary);margin-bottom:4px;">@GetDayName(day)</div>
<div style="font-size:20px;font-weight:700;color:@(isToday ? "#22C55E" : "var(--admin-text-primary)");margin-bottom:8px;">@day.Day</div>
@if (isWeekend)
{
<span class="staff-status staff-status--neutral">Nghỉ</span>
}
else
{
<div style="font-size:12px;color:var(--admin-text-secondary);">08:00 - 17:00</div>
}
@if (_loading)
{
<div style="text-align:center;padding:60px 20px;">
<div class="staff-spinner"></div>
<p style="margin-top:12px;color:var(--admin-text-tertiary);">Đang tải lịch...</p>
</div>
}
else if (!_mySchedules.Any())
{
<div style="text-align:center;padding:60px 20px;">
<div style="width:80px;height:80px;border-radius:24px;background:rgba(139,92,246,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 20px;">
<i data-lucide="calendar-clock" style="width:36px;height:36px;color:#8B5CF6;"></i>
</div>
}
</div>
<h2 style="font-size:20px;font-weight:700;margin:0 0 8px;color:var(--admin-text-primary);">Chưa có lịch làm việc</h2>
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0;">Quản lý chưa phân lịch cho bạn</p>
</div>
}
else
{
@* ═══ WEEK VIEW ═══ *@
<div style="display:grid;grid-template-columns:repeat(7,1fr);gap:8px;margin-bottom:24px;">
@for (int i = 0; i < 7; i++)
{
var day = _weekStart.AddDays(i);
var isToday = day.Date == DateTime.Now.Date;
var dow = (int)day.DayOfWeek;
var daySchedules = _mySchedules.Where(s => s.DayOfWeek == dow).OrderBy(s => s.StartTime).ToList();
var hasSchedule = daySchedules.Any();
<div style="background:var(--admin-bg-elevated);border:1px solid @(isToday ? "#22C55E" : "var(--admin-border-default)");border-radius:var(--admin-radius-lg);padding:16px;text-align:center;">
<div style="font-size:11px;color:var(--admin-text-tertiary);margin-bottom:4px;">@GetDayName(day)</div>
<div style="font-size:20px;font-weight:700;color:@(isToday ? "#22C55E" : "var(--admin-text-primary)");margin-bottom:8px;">@day.Day</div>
@if (hasSchedule)
{
@foreach (var sched in daySchedules)
{
<div style="font-size:12px;color:var(--admin-text-secondary);margin-bottom:2px;">@FormatTime(sched.StartTime) - @FormatTime(sched.EndTime)</div>
}
}
else
{
<span class="staff-status staff-status--neutral">Nghỉ</span>
}
</div>
}
</div>
@* ═══ UPCOMING SHIFTS ═══ *@
<div class="staff-table-card">
<div class="staff-table-card__header">
<span class="staff-table-card__title">Thông tin ca làm việc</span>
</div>
<div style="padding:20px;">
<div style="display:flex;flex-direction:column;gap:12px;">
<div style="display:flex;align-items:center;gap:12px;padding:12px 16px;background:var(--admin-bg-interactive);border-radius:var(--admin-radius-md);">
<i data-lucide="clock" style="width:20px;height:20px;color:#22C55E;"></i>
<div>
<div style="font-size:14px;font-weight:600;color:var(--admin-text-primary);">Ca sáng: 08:00 - 12:00</div>
<div style="font-size:12px;color:var(--admin-text-tertiary);">Thu 2 - Thu 6</div>
</div>
</div>
<div style="display:flex;align-items:center;gap:12px;padding:12px 16px;background:var(--admin-bg-interactive);border-radius:var(--admin-radius-md);">
<i data-lucide="clock" style="width:20px;height:20px;color:#3B82F6;"></i>
<div>
<div style="font-size:14px;font-weight:600;color:var(--admin-text-primary);">Ca chiều: 13:00 - 17:00</div>
<div style="font-size:12px;color:var(--admin-text-tertiary);">Thu 2 - Thu 6</div>
</div>
@* ═══ SHIFT DETAILS ═══ *@
<div class="staff-table-card">
<div class="staff-table-card__header">
<span class="staff-table-card__title">Thông tin ca làm việc</span>
</div>
<div style="padding:20px;">
<div style="display:flex;flex-direction:column;gap:12px;">
@foreach (var group in _mySchedules.GroupBy(s => new { s.StartTime, s.EndTime }).OrderBy(g => g.Key.StartTime))
{
var isMorning = group.Key.StartTime?.CompareTo("12:00") < 0;
var isEvening = group.Key.StartTime?.CompareTo("18:00") >= 0;
var color = isEvening ? "#F59E0B" : isMorning ? "#22C55E" : "#3B82F6";
var label = isEvening ? "Ca tối" : isMorning ? "Ca sáng" : "Ca chiều";
var days = string.Join(", ", group.OrderBy(s => s.DayOfWeek == 0 ? 7 : s.DayOfWeek).Select(s => DayLabel(s.DayOfWeek)));
<div style="display:flex;align-items:center;gap:12px;padding:12px 16px;background:var(--admin-bg-interactive);border-radius:var(--admin-radius-md);">
<i data-lucide="clock" style="width:20px;height:20px;color:@color;"></i>
<div>
<div style="font-size:14px;font-weight:600;color:var(--admin-text-primary);">@label: @FormatTime(group.Key.StartTime) - @FormatTime(group.Key.EndTime)</div>
<div style="font-size:12px;color:var(--admin-text-tertiary);">@days</div>
</div>
</div>
}
</div>
</div>
</div>
</div>
}
</div>
@code {
private bool _loading = true;
private List<PosDataService.ScheduleInfo> _mySchedules = new();
private DateTime _weekStart = DateTime.Now.Date.AddDays(-(int)DateTime.Now.DayOfWeek + (int)DayOfWeek.Monday);
protected override async Task OnAfterRenderAsync(bool firstRender)
{
try { await JS.InvokeVoidAsync("lucide.createIcons"); } catch { }
if (firstRender)
{
// EN: Restore staff session and load schedule data
// VI: Khôi phục session nhân viên và tải dữ liệu lịch
if (!AuthState.IsAuthenticated)
{
try { await AuthSvc.TryRestoreSessionAsync("staff"); } catch { }
}
try
{
var profile = await DataService.GetMyStaffProfileAsync();
if (profile?.ShopId != null)
{
var allSchedules = await DataService.GetStaffSchedulesAsync(profile.ShopId.Value);
_mySchedules = allSchedules.Where(s => s.StaffId == profile.StaffId).ToList();
}
}
catch { /* API unavailable */ }
_loading = false;
StateHasChanged();
}
}
/// <summary>
/// EN: Format time string — remove seconds if present (08:00:00 → 08:00).
/// VI: Định dạng giờ — bỏ giây nếu có (08:00:00 → 08:00).
/// </summary>
private static string FormatTime(string? time)
{
if (string.IsNullOrEmpty(time)) return "—";
// Handle "08:00:00" -> "08:00"
var parts = time.Split(':');
return parts.Length >= 2 ? $"{parts[0]}:{parts[1]}" : time;
}
private static string GetDayName(DateTime d) => d.DayOfWeek switch
@@ -83,4 +150,16 @@
DayOfWeek.Sunday => "CN",
_ => ""
};
private static string DayLabel(int dow) => dow switch
{
1 => "Thứ 2",
2 => "Thứ 3",
3 => "Thứ 4",
4 => "Thứ 5",
5 => "Thứ 6",
6 => "Thứ 7",
0 => "CN",
_ => ""
};
}