Files
pos-system/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Dashboard.razor
Ho Ngoc Hai 4a1094b080 fix(web-client-tpos): restore JWT from localStorage before admin API calls
Root cause: AdminBase had no auth initialization. AuthStateService is
in-memory Singleton — token is null after page refresh. Admin pages
called PosDataService without token → BFF forwarded requests without
Authorization header → microservices returned 401.

Fix:
- AdminBase.cs: inject AuthService, call TryRestoreSessionAsync()
- 9 admin pages: add await base.OnInitializedAsync() calls
- BffHttpClient.cs: add debug logging for auth forwarding
2026-03-04 10:27:56 +07:00

225 lines
11 KiB
Plaintext

@page "/admin"
@layout AdminLayout
@inherits AdminBase
@inject PosDataService DataService
@inject Microsoft.Extensions.Localization.IStringLocalizer<Dashboard> L
@using WebClientTpos.Client.Services
@*
EN: Admin Dashboard — overview with real data from shops list.
VI: Dashboard Admin — tổng quan với dữ liệu thực từ danh sách shops.
*@
<PageTitle>Dashboard — GoodGo Admin</PageTitle>
@* ═══ TOP BAR ═══ *@
<div class="admin-topbar">
<div class="admin-topbar__left">
<h1 class="admin-topbar__title">@L["Dashboard_Title"]</h1>
<p class="admin-topbar__subtitle">@L["Dashboard_Subtitle"] • @GetTodayFormatted()</p>
</div>
<div class="admin-topbar__right">
<div class="admin-search">
<i data-lucide="search"></i>
<input type="text" placeholder="@L["Dashboard_Search"]" @bind="SearchQuery" />
</div>
<button class="admin-icon-btn" title="Thông báo">
<i data-lucide="bell"></i>
</button>
<button class="admin-btn-primary" @onclick="@(() => NavigateTo("stores/create"))">
<i data-lucide="plus"></i>
<span>@L["Dashboard_CreateStore"]</span>
</button>
</div>
</div>
@* ═══ CONTENT ═══ *@
<div class="admin-content" style="display:flex;flex-direction:column;gap:24px;">
@* ── KPI ROW (data-driven) ── *@
<div class="admin-kpi-row">
@* KPI 1: Cửa hàng *@
<div class="admin-kpi-card">
<div class="admin-kpi-card__header">
<div class="admin-kpi-card__icon" style="background-color:rgba(139,92,246,0.125);">
<i data-lucide="store" style="color:#8B5CF6;"></i>
</div>
</div>
<div class="admin-kpi-card__value">@_shops.Count</div>
<div class="admin-kpi-card__label">@L["Dashboard_KPI_Stores"]</div>
</div>
@* KPI 2: Đang hoạt động *@
<div class="admin-kpi-card">
<div class="admin-kpi-card__header">
<div class="admin-kpi-card__icon" style="background-color:rgba(34,197,94,0.125);">
<i data-lucide="check-circle" style="color:#22C55E;"></i>
</div>
</div>
<div class="admin-kpi-card__value">@_shops.Count(s => s.Status == "active")</div>
<div class="admin-kpi-card__label">@L["Dashboard_KPI_Active"]</div>
</div>
@* KPI 3: Đang thiết lập *@
<div class="admin-kpi-card">
<div class="admin-kpi-card__header">
<div class="admin-kpi-card__icon" style="background-color:rgba(245,158,11,0.125);">
<i data-lucide="settings" style="color:#F59E0B;"></i>
</div>
</div>
<div class="admin-kpi-card__value">@_shops.Count(s => s.Status != "active")</div>
<div class="admin-kpi-card__label">@L["Dashboard_KPI_Setup"]</div>
</div>
@* KPI 4: Ngành hàng *@
<div class="admin-kpi-card">
<div class="admin-kpi-card__header">
<div class="admin-kpi-card__icon" style="background-color:rgba(236,72,153,0.125);">
<i data-lucide="layers" style="color:#EC4899;"></i>
</div>
</div>
<div class="admin-kpi-card__value">@_shops.Select(s => s.Category).Distinct().Count()</div>
<div class="admin-kpi-card__label">@L["Dashboard_KPI_Verticals"]</div>
</div>
</div>
@* ── BOTTOM ROW ── *@
<div style="display:flex;gap:24px;flex:1;min-height:0;">
@* ── LEFT: Store Overview ── *@
<div class="admin-panel" style="flex:1;">
<div class="admin-panel__header">
<h3 class="admin-panel__title">
<i data-lucide="store" style="color:var(--admin-orange-primary);"></i>
Cửa hàng của bạn
</h3>
@if (_shops.Count > 0)
{
<a href="/admin/stores" class="admin-panel__action">@L["Dashboard_ManageAll"] →</a>
}
</div>
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:12px;">
@if (_shops.Count == 0)
{
<div style="text-align:center;padding:40px 20px;">
<i data-lucide="store" style="width:48px;height:48px;color:var(--admin-orange-primary);margin-bottom:16px;"></i>
<h3 style="font-size:18px;font-weight:700;color:var(--pos-text-primary, #FFFFFF);margin:0 0 8px;">@L["Dashboard_WelcomeTitle"]</h3>
<p style="font-size:14px;color:var(--pos-text-tertiary, #ADADB0);margin:0 0 20px;">@L["Dashboard_WelcomeSubtitle"]</p>
<a href="/admin/stores/create" class="admin-btn-primary" style="display:inline-flex;align-items:center;gap:8px;">
<i data-lucide="plus" style="width:16px;height:16px;"></i>
@L["Dashboard_CreateStoreNow"]
</a>
</div>
}
else
{
@foreach (var shop in _shops)
{
<a href="/admin/shop/@shop.Id/overview" class="admin-store-card" style="text-decoration:none;color:inherit;cursor:pointer;">
<div class="admin-store-card__top">
<div class="admin-store-card__info">
<div class="admin-store-card__avatar" style="background-color:rgba(255,92,0,0.125);">
<i data-lucide="@ShopVerticalHelper.GetIcon(shop.Category)" style="color:var(--admin-orange-primary);"></i>
</div>
<div>
<div class="admin-store-card__name">@shop.Name</div>
<div class="admin-store-card__type">@ShopSidebarConfig.GetVerticalLabel(shop.Category) • @(shop.Slug)</div>
</div>
</div>
<div class="admin-status-badge admin-status-badge--@(shop.Status == "active" ? "online" : "setup")">
<span class="admin-status-badge__dot"></span>
@(shop.Status == "active" ? L["Dashboard_Status_Open"] : L["Dashboard_Status_Setup"])
</div>
</div>
</a>
}
}
</div>
</div>
@* ── RIGHT COLUMN ── *@
<div style="width:380px;display:flex;flex-direction:column;gap:20px;">
@* Quick Actions Panel *@
<div class="admin-panel">
<div class="admin-panel__header">
<h3 class="admin-panel__title">
<i data-lucide="zap" style="color:var(--admin-orange-primary);"></i>
@L["Dashboard_QuickActions"]
</h3>
</div>
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:8px;">
<a href="/admin/stores/create" class="admin-quick-action">
<i data-lucide="plus-circle" style="color:#22C55E;width:18px;height:18px;"></i>
<span>@L["Dashboard_QA_CreateStore"]</span>
<i data-lucide="chevron-right" style="margin-left:auto;width:16px;height:16px;color:var(--admin-text-tertiary);"></i>
</a>
<a href="/admin/system/audit" class="admin-quick-action">
<i data-lucide="settings" style="color:#3B82F6;width:18px;height:18px;"></i>
<span>@L["Dashboard_QA_SystemSettings"]</span>
<i data-lucide="chevron-right" style="margin-left:auto;width:16px;height:16px;color:var(--admin-text-tertiary);"></i>
</a>
<a href="/admin/roles" class="admin-quick-action">
<i data-lucide="shield" style="color:#8B5CF6;width:18px;height:18px;"></i>
<span>@L["Dashboard_QA_Permissions"]</span>
<i data-lucide="chevron-right" style="margin-left:auto;width:16px;height:16px;color:var(--admin-text-tertiary);"></i>
</a>
</div>
</div>
@* Status Panel *@
<div class="admin-panel" style="flex:1;">
<div class="admin-panel__header">
<h3 class="admin-panel__title">
<i data-lucide="activity" style="color:#22C55E;"></i>
@L["Dashboard_SystemStatus"]
</h3>
</div>
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:12px;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<span style="color:var(--admin-text-tertiary);font-size:14px;">API Gateway</span>
<span style="font-size:13px;color:#22C55E;display:flex;align-items:center;gap:4px;">
<span style="width:6px;height:6px;border-radius:50%;background:#22C55E;display:inline-block;"></span> Online
</span>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;">
<span style="color:var(--admin-text-tertiary);font-size:14px;">IAM Service</span>
<span style="font-size:13px;color:#22C55E;display:flex;align-items:center;gap:4px;">
<span style="width:6px;height:6px;border-radius:50%;background:#22C55E;display:inline-block;"></span> Online
</span>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;">
<span style="color:var(--admin-text-tertiary);font-size:14px;">Merchant Service</span>
<span style="font-size:13px;color:#22C55E;display:flex;align-items:center;gap:4px;">
<span style="width:6px;height:6px;border-radius:50%;background:#22C55E;display:inline-block;"></span> Online
</span>
</div>
</div>
</div>
</div>
</div>
</div>
@code {
private List<PosDataService.ShopInfo> _shops = new();
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
IsLoading = true;
try
{
_shops = await DataService.GetShopsAsync();
}
catch (Exception ex)
{
_shops = new();
Console.Error.WriteLine($"[Dashboard] Error loading shops: {ex}");
}
finally
{
IsLoading = false;
}
}
}