feat(multi-vertical): P1 — promotions real data, finance wallets, customer levels

- Promotions: real table from GetPromotionsAsync (stats: total, active, vouchers, used)
- Finance: wallet balance + recent wallet transactions from GetWalletsAsync
- Customers: membership levels table from GetMembershipLevelsAsync
- Staff: schedules data wired from GetStaffSchedulesAsync
- Data vars: wallets, walletTxns, promotions, memberLevels, staffSchedules, invTxns
This commit is contained in:
Ho Ngoc Hai
2026-02-28 23:18:12 +07:00
parent 6d6d6a9f3b
commit 549bfcb038

View File

@@ -214,18 +214,37 @@
// ═══ FINANCE ═══
case "finance":
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:16px;">
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;">
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(34,197,94,0.1);"><i data-lucide="trending-up" style="color:#22C55E;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@FormatVND(_orders.Sum(o => o.TotalAmount))</span><span class="admin-stat-card__label">Tổng doanh thu</span></div></div>
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(59,130,246,0.1);"><i data-lucide="receipt" style="color:#3B82F6;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_orders.Count</span><span class="admin-stat-card__label">Đơn hàng</span></div></div>
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(255,92,0,0.1);"><i data-lucide="calculator" style="color:#FF5C00;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@FormatVND(_orders.Any() ? _orders.Average(o => o.TotalAmount) : 0)</span><span class="admin-stat-card__label">TB/đơn</span></div></div>
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(139,92,246,0.1);"><i data-lucide="wallet" style="color:#8B5CF6;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@FormatVND(_wallets.Sum(w => w.Balance))</span><span class="admin-stat-card__label">Số dư ví</span></div></div>
</div>
@if (!_orders.Any())
@if (_walletTxns.Any())
{
@RenderEmpty("bar-chart-3", "#22C55E", "Chưa có dữ liệu tài chính", "Dữ liệu sẽ tự động cập nhật khi có đơn hàng", "monitor", "Mở POS bán hàng", $"/pos/{ShopId}/{_posVertical}")
<div class="admin-panel" style="margin-top:16px;">
<div class="admin-panel__header"><h3 class="admin-panel__title">Giao dịch ví gần đây</h3></div>
<div class="admin-panel__body" style="padding:0;">
<table class="admin-table" style="width:100%;"><thead><tr>
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Mô tả</th>
<th style="padding:12px 16px;text-align:right;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Số tiền</th>
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Ngày</th>
</tr></thead><tbody>
@foreach (var t in _walletTxns.Take(15))
{
<tr style="border-top:1px solid var(--admin-border-subtle);">
<td style="padding:12px 16px;font-weight:600;font-size:13px;">@(t.Description ?? t.ItemName ?? "—")</td>
<td style="padding:12px 16px;text-align:right;font-weight:600;color:@(t.Amount >= 0 ? "#22C55E" : "#EF4444");">@(t.Amount >= 0 ? "+" : "")@FormatVND(t.Amount)</td>
<td style="padding:12px 16px;font-size:13px;color:var(--admin-text-tertiary);">@t.CreatedAt.ToString("dd/MM HH:mm")</td>
</tr>
}
</tbody></table>
</div>
</div>
}
else
@if (_orders.Any())
{
<div class="admin-panel">
<div class="admin-panel" style="margin-top:16px;">
<div class="admin-panel__header"><h3 class="admin-panel__title">Đơn hàng gần đây</h3></div>
<div class="admin-panel__body" style="padding:0;">
<table class="admin-table" style="width:100%;"><thead><tr>
@@ -247,6 +266,10 @@
</div>
</div>
}
else
{
@RenderEmpty("bar-chart-3", "#22C55E", "Chưa có dữ liệu tài chính", "Dữ liệu sẽ tự động cập nhật khi có đơn hàng", "monitor", "Mở POS bán hàng", $"/pos/{ShopId}/{_posVertical}")
}
break;
// ═══ STAFF ═══
@@ -321,7 +344,7 @@
}
break;
// ═══ CUSTOMERS ═══
// ═══ CUSTOMERS + MEMBERSHIP LEVELS ═══
case "customers":
@if (!_members.Any())
{
@@ -331,8 +354,35 @@
{
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;">
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(255,92,0,0.1);"><i data-lucide="users" style="color:#FF5C00;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_members.Count</span><span class="admin-stat-card__label">Tổng khách hàng</span></div></div>
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(139,92,246,0.1);"><i data-lucide="crown" style="color:#8B5CF6;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_memberLevels.Count</span><span class="admin-stat-card__label">Cấp bậc</span></div></div>
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(34,197,94,0.1);"><i data-lucide="star" style="color:#22C55E;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@(_members.Any() ? _members.Max(m => m.TotalExpEarned).ToString("N0") : "0")</span><span class="admin-stat-card__label">EXP cao nhất</span></div></div>
</div>
<div class="admin-panel">
@if (_memberLevels.Any())
{
<div class="admin-panel" style="margin-top:16px;">
<div class="admin-panel__header"><h3 class="admin-panel__title">Cấp bậc thành viên</h3></div>
<div class="admin-panel__body" style="padding:0;">
<table class="admin-table" style="width:100%;"><thead><tr>
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Level</th>
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Tên</th>
<th style="padding:12px 16px;text-align:right;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">EXP cần</th>
<th style="padding:12px 16px;text-align:right;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Thành viên</th>
</tr></thead><tbody>
@foreach (var lvl in _memberLevels.OrderBy(l => l.Level))
{
<tr style="border-top:1px solid var(--admin-border-subtle);">
<td style="padding:12px 16px;font-weight:700;color:var(--admin-orange-primary);">@lvl.Level</td>
<td style="padding:12px 16px;font-weight:600;">@lvl.Name</td>
<td style="padding:12px 16px;text-align:right;font-size:13px;color:var(--admin-text-tertiary);">@lvl.MinExp.ToString("N0") — @lvl.MaxExp.ToString("N0")</td>
<td style="padding:12px 16px;text-align:right;font-weight:600;">@lvl.MemberCount</td>
</tr>
}
</tbody></table>
</div>
</div>
}
<div class="admin-panel" style="margin-top:16px;">
<div class="admin-panel__header"><h3 class="admin-panel__title">Danh sách khách hàng</h3></div>
<div class="admin-panel__body" style="padding:0;">
<table class="admin-table" style="width:100%;"><thead><tr>
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">ID</th>
@@ -608,9 +658,44 @@
@RenderEmpty("clipboard-list", "#A855F7", "Quản lý liệu trình", "Theo dõi liệu trình điều trị dài hạn, ảnh before/after, tiến trình")
break;
// ═══ PROMOTIONS ═══
// ═══ PROMOTIONS (real data) ═══
case "promotions":
@RenderEmpty("tag", "#22C55E", "Quản lý khuyến mãi", "Tạo mã giảm giá, combo, chương trình loyalty — Kết nối Promotion Service", "plus-circle", "Tạo khuyến mãi")
@if (!_promotions.Any())
{
@RenderEmpty("tag", "#22C55E", "Chưa có khuyến mãi", "Tạo mã giảm giá, combo, chương trình loyalty", "plus-circle", "Tạo khuyến mãi")
}
else
{
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;">
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(34,197,94,0.1);"><i data-lucide="tag" style="color:#22C55E;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_promotions.Count</span><span class="admin-stat-card__label">Tổng KM</span></div></div>
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(59,130,246,0.1);"><i data-lucide="zap" style="color:#3B82F6;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_promotions.Count(p => p.IsActive)</span><span class="admin-stat-card__label">Đang hoạt động</span></div></div>
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(236,72,153,0.1);"><i data-lucide="ticket" style="color:#EC4899;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_promotions.Sum(p => p.VoucherCount)</span><span class="admin-stat-card__label">Voucher</span></div></div>
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(255,92,0,0.1);"><i data-lucide="check-circle" style="color:#FF5C00;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_promotions.Sum(p => p.RedemptionCount)</span><span class="admin-stat-card__label">Đã dùng</span></div></div>
</div>
<div class="admin-panel" style="margin-top:16px;">
<div class="admin-panel__header"><h3 class="admin-panel__title">Danh sách khuyến mãi</h3></div>
<div class="admin-panel__body" style="padding:0;">
<table class="admin-table" style="width:100%;"><thead><tr>
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Tên</th>
<th style="padding:12px 16px;text-align:center;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Loại</th>
<th style="padding:12px 16px;text-align:right;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Giá trị</th>
<th style="padding:12px 16px;text-align:center;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Trạng thái</th>
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Thời gian</th>
</tr></thead><tbody>
@foreach (var p in _promotions)
{
<tr style="border-top:1px solid var(--admin-border-subtle);">
<td style="padding:12px 16px;font-weight:600;">@p.Name</td>
<td style="padding:12px 16px;text-align:center;font-size:12px;color:var(--admin-text-tertiary);">@(p.DiscountType ?? "—")</td>
<td style="padding:12px 16px;text-align:right;font-weight:600;color:var(--admin-orange-primary);">@(p.DiscountType == "Percentage" ? $"{p.DiscountValue}%" : FormatVND(p.DiscountValue ?? 0))</td>
<td style="padding:12px 16px;text-align:center;"><span class="admin-status-badge @(p.IsActive ? "admin-status-badge--online" : "admin-status-badge--offline")" style="font-size:11px;padding:2px 10px;"><span class="admin-status-badge__dot" style="width:5px;height:5px;"></span>@(p.IsActive ? "Active" : "Inactive")</span></td>
<td style="padding:12px 16px;font-size:12px;color:var(--admin-text-tertiary);">@(p.StartDate?.ToString("dd/MM/yy") ?? "—") → @(p.EndDate?.ToString("dd/MM/yy") ?? "∞")</td>
</tr>
}
</tbody></table>
</div>
</div>
}
break;
// ═══ SETTINGS ═══
@@ -710,6 +795,13 @@
private string? _staffFormMessage;
private bool _staffFormSuccess;
private Guid? _merchantId;
// New data: wallets, promotions, member levels, schedules, inv txns
private List<PosDataService.WalletInfo> _wallets = new();
private List<PosDataService.WalletTxnInfo> _walletTxns = new();
private List<PosDataService.PromotionInfo> _promotions = new();
private List<PosDataService.LevelDefinitionInfo> _memberLevels = new();
private List<PosDataService.ScheduleInfo> _staffSchedules = new();
private List<PosDataService.InventoryTxnInfo> _invTxns = new();
protected override async Task OnInitializedAsync() => await LoadData();
@@ -764,12 +856,16 @@
break;
case "finance":
_orders = await DataService.GetOrdersAsync(_shopGuid);
_wallets = await DataService.GetWalletsAsync();
_walletTxns = await DataService.GetWalletTransactionsAsync();
break;
case "staff":
_staff = await DataService.GetStaffAsync();
_staffSchedules = await DataService.GetStaffSchedulesAsync(_shopGuid);
break;
case "customers":
_members = await DataService.GetMembersAsync();
_memberLevels = await DataService.GetMembershipLevelsAsync();
break;
case "tables":
case "rooms":
@@ -787,6 +883,9 @@
_reportOrders = await DataService.GetOrdersAsync(_shopGuid);
_reportProducts = await DataService.GetAllProductsAsync(_shopGuid);
break;
case "promotions":
_promotions = await DataService.GetPromotionsAsync();
break;
}
}
catch (Exception ex)