feat(admin): enhance Customer search + Finance date filter (G6, G8)

- Customers: add search bar (filter by ID, membership level)
- Customers: membership level displays as badge
- Customers: show count header with search input
- Finance: add date range tabs (7 ngày / 30 ngày / Tất cả)
- Finance: revenue/order stats update based on selected period
This commit is contained in:
Ho Ngoc Hai
2026-03-01 04:08:38 +07:00
parent a6e85d0451
commit f045dbf5ed

View File

@@ -214,10 +214,28 @@
// ═══ FINANCE ═══
case "finance":
var finOrders = _financePeriod switch {
"7d" => _orders.Where(o => o.CreatedAt >= DateTime.UtcNow.AddDays(-7)).ToList(),
"30d" => _orders.Where(o => o.CreatedAt >= DateTime.UtcNow.AddDays(-30)).ToList(),
_ => _orders };
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
<h3 style="margin:0;font-size:16px;font-weight:700;">Tài chính</h3>
<div style="display:flex;gap:4px;background:var(--admin-bg-elevated);border-radius:8px;padding:3px;">
@foreach (var (label, val) in new[] { ("7 ngày", "7d"), ("30 ngày", "30d"), ("Tất cả", "all") })
{
<button @onclick="@(() => { _financePeriod = val; StateHasChanged(); })"
style="padding:6px 14px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;border:none;
background:@(_financePeriod == val ? "var(--admin-orange-primary)" : "transparent");
color:@(_financePeriod == val ? "#FFF" : "var(--admin-text-tertiary)");">
@label
</button>
}
</div>
</div>
<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(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(finOrders.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">@finOrders.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(finOrders.Any() ? finOrders.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 (_walletTxns.Any())
@@ -346,16 +364,28 @@
// ═══ CUSTOMERS + MEMBERSHIP LEVELS ═══
case "customers":
@if (!_members.Any())
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
<h3 style="margin:0;font-size:16px;font-weight:700;">@_members.Count khách hàng</h3>
<div style="position:relative;">
<i data-lucide="search" style="position:absolute;left:12px;top:50%;transform:translateY(-50%);width:16px;height:16px;color:var(--admin-text-tertiary);"></i>
<input type="text" placeholder="Tìm theo ID..." @bind="_customerSearch" @bind:event="oninput"
style="padding:8px 12px 8px 36px;border-radius:8px;border:1px solid var(--admin-border-subtle);background:var(--admin-bg-elevated);font-size:13px;color:var(--admin-text-primary);width:200px;" />
</div>
</div>
var filteredMembers = string.IsNullOrWhiteSpace(_customerSearch)
? _members
: _members.Where(m => m.Id.ToString().Contains(_customerSearch, StringComparison.OrdinalIgnoreCase)
|| (m.LevelName ?? "").Contains(_customerSearch, StringComparison.OrdinalIgnoreCase)).ToList();
@if (!filteredMembers.Any())
{
@RenderEmpty("heart", "#EF4444", "Chưa có khách hàng", "Khách hàng sẽ hiển thị khi có giao dịch", "monitor", "Mở POS bán hàng", $"/pos/{ShopId}/{_posVertical}")
}
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(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(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">@filteredMembers.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 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">@(filteredMembers.Any() ? filteredMembers.Max(m => m.TotalExpEarned).ToString("N0") : "0")</span><span class="admin-stat-card__label">EXP cao nhất</span></div></div>
</div>
@if (_memberLevels.Any())
{
@@ -390,11 +420,11 @@
<th style="padding:12px 16px;text-align:right;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">EXP</th>
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Ngày tham gia</th>
</tr></thead><tbody>
@foreach (var m in _members)
@foreach (var m in filteredMembers)
{
<tr style="border-top:1px solid var(--admin-border-subtle);">
<td style="padding:12px 16px;font-weight:600;font-family:monospace;font-size:12px;">@m.Id.ToString()[..8]</td>
<td style="padding:12px 16px;">@(m.LevelName ?? "—")</td>
<td style="padding:12px 16px;"><span class="admin-status-badge admin-status-badge--online" style="font-size:11px;padding:2px 10px;">@(m.LevelName ?? "—")</span></td>
<td style="padding:12px 16px;text-align:right;font-weight:600;color:var(--admin-orange-primary);">@m.TotalExpEarned.ToString("N0")</td>
<td style="padding:12px 16px;font-size:13px;color:var(--admin-text-tertiary);">@m.CreatedAt.ToString("dd/MM/yyyy")</td>
</tr>
@@ -889,6 +919,10 @@
private List<PosDataService.ScheduleInfo> _staffSchedules = new();
private List<PosDataService.InventoryTxnInfo> _invTxns = new();
private List<PosDataService.ResourceInfo> _resources = new();
// Customer filter state
private string _customerSearch = "";
// Finance date range filter state
private string _financePeriod = "all"; // 7d, 30d, all
protected override async Task OnInitializedAsync() => await LoadData();