feat: Display attendance times in local timezone and calculate staff leave statistics from real data, removing stub notifications.

This commit is contained in:
Ho Ngoc Hai
2026-03-13 16:23:00 +07:00
parent 598193e6cb
commit ddd02be26e
6 changed files with 53 additions and 40 deletions

View File

@@ -79,8 +79,8 @@
<tr>
<td style="font-weight:600;">@(r.StaffName ?? r.StaffId.ToString()[..8])</td>
<td>@r.Date.ToString("dd/MM")</td>
<td style="color:#22C55E;">@(r.CheckIn?.ToString("HH:mm") ?? "--")</td>
<td style="color:#F59E0B;">@(r.CheckOut?.ToString("HH:mm") ?? "--")</td>
<td style="color:#22C55E;">@(r.CheckIn?.ToLocalTime().ToString("HH:mm") ?? "--")</td>
<td style="color:#F59E0B;">@(r.CheckOut?.ToLocalTime().ToString("HH:mm") ?? "--")</td>
<td>@(r.HoursWorked.HasValue ? r.HoursWorked.Value.ToString("0.#") + "h" : "--")</td>
<td>
@{

View File

@@ -157,16 +157,17 @@
// VI: Dữ liệu dialog — được điền từ context hiện tại (đơn hàng/sản phẩm đã chọn).
// TODO: Integrate with Order/Catalog/Inventory APIs when DDD Value Object mapping is fixed.
// EN: Stock-in state / VI: Trạng thái nhập kho
private string _productSearch = "Cà phê hạt Arabica";
private string _selectedProduct = "Cà phê hạt Arabica";
private string _selectedSku = "CF-ARA-500";
private int _currentStock = 120;
private int _quantity = 50;
private decimal _unitCost = 185_000;
private string _supplier = "Công ty TNHH Cà phê Đà Lạt";
private string _lotNumber = "LOT-2026-0226";
private DateTime _expiryDate = new(2027, 2, 26);
// EN: Stock-in state — will be loaded from Inventory API. No hardcoded values in production.
// VI: Trạng thái nhập kho — sẽ tải từ Inventory API. Không hardcode giá trị trong production.
private string _productSearch = "";
private string _selectedProduct = "";
private string _selectedSku = "";
private int _currentStock = 0;
private int _quantity = 0;
private decimal _unitCost = 0;
private string _supplier = "";
private string _lotNumber = "";
private DateTime _expiryDate = DateTime.Now.AddMonths(6);
private string _notes = "";
// EN: Suppliers / VI: Nhà cung cấp

View File

@@ -74,8 +74,8 @@
<tr>
<td>@r.Date.ToString("dd/MM/yyyy")</td>
<td>@GetDayOfWeek(r.Date)</td>
<td style="color:#22C55E;">@(r.CheckIn?.ToString("HH:mm") ?? "--")</td>
<td style="color:#F59E0B;">@(r.CheckOut?.ToString("HH:mm") ?? "--")</td>
<td style="color:#22C55E;">@(r.CheckIn?.ToLocalTime().ToString("HH:mm") ?? "--")</td>
<td style="color:#F59E0B;">@(r.CheckOut?.ToLocalTime().ToString("HH:mm") ?? "--")</td>
<td>@(r.HoursWorked.HasValue ? r.HoursWorked.Value.ToString("0.#") + "h" : "--")</td>
<td>
<span class="staff-status @GetStatusCss(r.Status)">

View File

@@ -69,7 +69,7 @@
<i data-lucide="calendar-off" style="color:#F59E0B;"></i>
</div>
<span class="staff-stat-card__value">@_leaveBalance</span>
<span class="staff-stat-card__label">Ngày phép còn lại</span>
<span class="staff-stat-card__label">Ngày phép đã dùng</span>
</div>
<div class="staff-stat-card">
<div class="staff-stat-card__icon" style="background:rgba(139,92,246,0.12);">
@@ -111,8 +111,8 @@
{
<tr>
<td>@r.Date.ToString("dd/MM")</td>
<td>@(r.CheckIn?.ToString("HH:mm") ?? "--")</td>
<td>@(r.CheckOut?.ToString("HH:mm") ?? "--")</td>
<td>@(r.CheckIn?.ToLocalTime().ToString("HH:mm") ?? "--")</td>
<td>@(r.CheckOut?.ToLocalTime().ToString("HH:mm") ?? "--")</td>
<td>@(r.HoursWorked.HasValue ? r.HoursWorked.Value.ToString("0.#") + "h" : "--")</td>
<td>
<span class="staff-status @(r.Status == "Completed" ? "staff-status--success" : r.Status == "Working" ? "staff-status--info" : "staff-status--neutral")">
@@ -141,7 +141,9 @@
private bool _checkedOut = false;
private string _todayHours = "0";
private int _monthDays = 0;
private int _leaveBalance = 12;
// EN: Leave balance — loaded from real leave request data. TODO: load annual allowance from merchant config API.
// VI: Số phép còn lại — tính từ dữ liệu nghỉ phép thực. TODO: tải số phép năm từ API cấu hình merchant.
private int _leaveBalance = 0;
private int _unreadCount = 0;
private string _displayName => _profile?.FirstName ?? AuthState.UserEmail?.Split('@').FirstOrDefault() ?? "Staff";
@@ -177,16 +179,28 @@
_unreadCount = _notifications.Count(n => !n.IsRead);
_monthDays = _recentAttendance.Count(r => r.Status == "Completed");
var today = _recentAttendance.FirstOrDefault(r => r.Date.Date == DateTime.Now.Date);
// EN: Compare UTC dates consistently — backend stores all times in UTC.
// VI: So sánh ngày UTC nhất quán — backend lưu tất cả thời gian theo UTC.
var todayUtc = DateTime.UtcNow.Date;
var today = _recentAttendance.FirstOrDefault(r => r.Date.Date == todayUtc);
if (today != null)
{
_checkedIn = today.CheckIn.HasValue;
_checkedOut = today.CheckOut.HasValue;
if (today.CheckIn.HasValue && !today.CheckOut.HasValue)
_todayHours = ((DateTime.Now - today.CheckIn.Value).TotalHours).ToString("0.#");
_todayHours = ((DateTime.UtcNow - today.CheckIn.Value).TotalHours).ToString("0.#");
else if (today.HoursWorked.HasValue)
_todayHours = today.HoursWorked.Value.ToString("0.#");
}
// EN: Calculate leave days used from approved leave requests (real data).
// VI: Tính số ngày phép đã dùng từ các yêu cầu nghỉ phép đã duyệt (dữ liệu thực).
try
{
var leaveRequests = await DataService.GetMyLeaveRequestsAsync();
_leaveBalance = leaveRequests.Where(r => r.Status == "Approved").Sum(r => (r.EndDate - r.StartDate).Days + 1);
}
catch { }
}
catch { }
finally { _loading = false; }

View File

@@ -26,23 +26,25 @@
</div>
@* ═══ SUMMARY ═══ *@
@* EN: Leave stats from real data. Total annual allowance will come from merchant config API (TODO).
VI: Thống kê phép từ dữ liệu thực. Tổng phép năm sẽ từ API cấu hình merchant (TODO). *@
<div class="staff-stats-grid" style="margin-bottom:20px;">
<div class="staff-stat-card">
<span class="staff-stat-card__value">12</span>
<span class="staff-stat-card__label">Tổng phép năm</span>
</div>
<div class="staff-stat-card">
<span class="staff-stat-card__value">@_usedDays</span>
<span class="staff-stat-card__label">Đã sử dng</span>
</div>
<div class="staff-stat-card">
<span class="staff-stat-card__value">@(12 - _usedDays)</span>
<span class="staff-stat-card__label">Còn lại</span>
<span class="staff-stat-card__label">Ngày phép đã dùng</span>
</div>
<div class="staff-stat-card">
<span class="staff-stat-card__value">@_pendingCount</span>
<span class="staff-stat-card__label">Chờ duyệt</span>
</div>
<div class="staff-stat-card">
<span class="staff-stat-card__value">@_rejectedCount</span>
<span class="staff-stat-card__label">Từ chối</span>
</div>
<div class="staff-stat-card">
<span class="staff-stat-card__value">@_requests.Count</span>
<span class="staff-stat-card__label">Tổng yêu cầu</span>
</div>
</div>
@* ═══ CREATE FORM ═══ *@
@@ -129,6 +131,7 @@
private bool _submitting = false;
private int _usedDays = 0;
private int _pendingCount = 0;
private int _rejectedCount = 0;
// Form fields
private string _leaveType = "Annual";
@@ -158,6 +161,7 @@
_requests = await DataService.GetMyLeaveRequestsAsync();
_usedDays = _requests.Where(r => r.Status == "Approved").Sum(r => (r.EndDate - r.StartDate).Days + 1);
_pendingCount = _requests.Count(r => r.Status == "Pending");
_rejectedCount = _requests.Count(r => r.Status == "Rejected");
}
catch { }
finally { _loading = false; }

View File

@@ -367,21 +367,15 @@ public class StaffController : ControllerBase
}
/// <summary>
/// EN: Get notifications for current staff.
/// VI: Lấy thông báo của nhân viên hiện tại.
/// EN: Get notifications for current staff. Returns empty list until notification service is implemented.
/// VI: Lấy thông báo của nhân viên hiện tại. Trả về danh sách trống cho đến khi notification service được triển khai.
/// </summary>
[HttpGet("staff/me/notifications")]
public IActionResult GetMyNotifications()
{
// EN: Stub — returns sample notifications
// VI: Stub — trả về thông báo mẫu
var notifications = new List<object>
{
new { id = Guid.NewGuid(), title = "Chào mừng!", message = "Bạn đã đăng nhập thành công vào hệ thống GoodGo Staff.", type = "info", isRead = false, createdAt = DateTime.UtcNow.AddHours(-1).ToString("o") },
new { id = Guid.NewGuid(), title = "Lịch làm việc", message = "Lịch làm việc tuần này đã được cập nhật.", type = "schedule", isRead = false, createdAt = DateTime.UtcNow.AddDays(-1).ToString("o") }
};
return Ok(new { success = true, data = new { items = notifications } });
// EN: No notification service yet — return empty list (no fake data in production).
// VI: Chưa có notification service — trả về danh sách trống (không fake data trong production).
return Ok(new { success = true, data = new { items = Array.Empty<object>() } });
}
/// <summary>