Files
pos-system/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Dashboard.razor
Cursor Agent c1bb68859e fix(admin): dashboard loads shops from BFF API, shows onboarding when empty
- Inject PosDataService and load shops in OnInitializedAsync
- Show 'Welcome! Tạo cửa hàng đầu tiên' with onboarding link when no shops
- Render dynamic shop cards from DB data when shops exist
- Keep existing KPI cards and alerts/activity panels unchanged

Co-authored-by: Velik <hongochai10@users.noreply.github.com>
2026-02-27 07:55:29 +00:00

276 lines
13 KiB
Plaintext

@page "/admin"
@layout AdminLayout
@inherits AdminBase
@inject PosDataService DataService
@using WebClientTpos.Client.Services
@*
EN: Admin Dashboard — overview of business metrics, stores, alerts, and recent activity.
VI: Dashboard Admin — tổng quan chỉ số kinh doanh, cửa hàng, cảnh báo, hoạt động gần đây.
Design: pencil-design/src/pages/tPOS/admin/admin-dashboard.pen
*@
<PageTitle>Dashboard — GoodGo Admin</PageTitle>
@* ═══ TOP BAR ═══ *@
<div class="admin-topbar">
<div class="admin-topbar__left">
<h1 class="admin-topbar__title">Dashboard</h1>
<p class="admin-topbar__subtitle">Tổng quan kinh doanh • @GetTodayFormatted()</p>
</div>
<div class="admin-topbar__right">
<div class="admin-search">
<i data-lucide="search"></i>
<input type="text" placeholder="Tìm kiếm..." @bind="SearchQuery" />
</div>
<button class="admin-icon-btn" title="Thông báo">
<i data-lucide="bell"></i>
<span class="admin-icon-btn__dot"></span>
</button>
<button class="admin-btn-primary" @onclick="@(() => NavigateTo("stores/create"))">
<i data-lucide="plus"></i>
<span>Tạo cửa hàng</span>
</button>
</div>
</div>
@* ═══ CONTENT ═══ *@
<div class="admin-content" style="display:flex;flex-direction:column;gap:24px;">
@* ── KPI ROW ── *@
<div class="admin-kpi-row">
@* KPI 1: Tổng doanh thu *@
<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="trending-up" style="color:#22C55E;"></i>
</div>
<div class="admin-kpi-card__badge admin-kpi-card__badge--up">
<i data-lucide="arrow-up"></i>
<span>+18.2%</span>
</div>
</div>
<div class="admin-kpi-card__value">128.5M</div>
<div class="admin-kpi-card__label">Tổng doanh thu</div>
</div>
@* KPI 2: Tổng đơn hàng *@
<div class="admin-kpi-card">
<div class="admin-kpi-card__header">
<div class="admin-kpi-card__icon" style="background-color:rgba(59,130,246,0.125);">
<i data-lucide="shopping-bag" style="color:#3B82F6;"></i>
</div>
<div class="admin-kpi-card__badge admin-kpi-card__badge--up">
<i data-lucide="arrow-up"></i>
<span>+12.4%</span>
</div>
</div>
<div class="admin-kpi-card__value">1,247</div>
<div class="admin-kpi-card__label">Tổng đơn hàng</div>
</div>
@* KPI 3: Cửa hàng hoạt độ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 class="admin-kpi-card__badge admin-kpi-card__badge--up">
<span>3 online</span>
</div>
</div>
<div class="admin-kpi-card__value">3</div>
<div class="admin-kpi-card__label">Cửa hàng hoạt động</div>
</div>
@* KPI 4: Nhân viên online *@
<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="users" style="color:#EC4899;"></i>
</div>
<div class="admin-kpi-card__badge admin-kpi-card__badge--up">
<i data-lucide="arrow-up"></i>
<span>+22.7%</span>
</div>
</div>
<div class="admin-kpi-card__value">12</div>
<div class="admin-kpi-card__label">Nhân viên online</div>
</div>
</div>
@* ── BOTTOM ROW: Store Overview + Right Column ── *@
<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">Quản lý tất cả →</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;">Welcome! Tạo cửa hàng đầu tiên</h3>
<p style="font-size:14px;color:var(--pos-text-tertiary, #ADADB0);margin:0 0 20px;">Bắt đầu bằng việc tạo cửa hàng để quản lý kinh doanh của bạn.</p>
<a href="/admin/onboarding/store" class="admin-btn-primary" style="display:inline-flex;align-items:center;gap:8px;">
<i data-lucide="plus" style="width:16px;height:16px;"></i>
Tạo cửa hàng ngay
</a>
</div>
}
else
{
@foreach (var shop in _shops)
{
<div class="admin-store-card">
<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="@GetShopIcon(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">@(shop.Category ?? "Shop") • @(shop.Description ?? 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" ? "Đang mở" : "Thiết lập")
</div>
</div>
</div>
}
}
</div>
</div>
@* ── RIGHT COLUMN: Alerts + Activity ── *@
<div style="width:380px;display:flex;flex-direction:column;gap:20px;">
@* Alerts Panel *@
<div class="admin-panel">
<div class="admin-panel__header">
<h3 class="admin-panel__title">
<i data-lucide="alert-triangle" style="color:#F59E0B;"></i>
Cảnh báo
<span class="admin-badge-count admin-badge-count--danger">4</span>
</h3>
</div>
<div class="admin-panel__body">
<div class="admin-alert-list">
<div class="admin-alert-item admin-alert-item--danger">
<i data-lucide="package-x" style="color:#EF4444;"></i>
<div class="admin-alert-item__text">
<div class="admin-alert-item__title">5 sản phẩm sắp hết hàng</div>
<div class="admin-alert-item__sub">Coffee House Q1</div>
</div>
</div>
<div class="admin-alert-item admin-alert-item--warning">
<i data-lucide="clock" style="color:#F59E0B;"></i>
<div class="admin-alert-item__text">
<div class="admin-alert-item__title">Ca tối thiếu 1 nhân viên</div>
<div class="admin-alert-item__sub">Nhà hàng Q3 • Ngày mai</div>
</div>
</div>
<div class="admin-alert-item admin-alert-item--info">
<i data-lucide="printer" style="color:#3B82F6;"></i>
<div class="admin-alert-item__text">
<div class="admin-alert-item__title">Máy in mất kết nối</div>
<div class="admin-alert-item__sub">Coffee House Q1 • Kitchen</div>
</div>
</div>
</div>
</div>
</div>
@* Recent Activity 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>
Hoạt động gần đây
</h3>
</div>
<div class="admin-panel__body">
<div class="admin-activity-list">
<div class="admin-activity-item">
<span class="admin-activity-dot" style="background-color:#22C55E;"></span>
<div class="admin-activity-item__text">
<div class="admin-activity-item__title">Đơn #2847 hoàn thành</div>
<div class="admin-activity-item__time">Coffee House Q1 • 2 phút trước</div>
</div>
</div>
<div class="admin-activity-item">
<span class="admin-activity-dot" style="background-color:#3B82F6;"></span>
<div class="admin-activity-item__text">
<div class="admin-activity-item__title">Nguyễn Văn A clock-in</div>
<div class="admin-activity-item__time">Nhà hàng Q3 • 5 phút trước</div>
</div>
</div>
<div class="admin-activity-item">
<span class="admin-activity-dot" style="background-color:#F59E0B;"></span>
<div class="admin-activity-item__text">
<div class="admin-activity-item__title">Nhập kho 15 sản phẩm</div>
<div class="admin-activity-item__time">Coffee House Q1 • 12 phút trước</div>
</div>
</div>
<div class="admin-activity-item">
<span class="admin-activity-dot" style="background-color:#8B5CF6;"></span>
<div class="admin-activity-item__text">
<div class="admin-activity-item__title">Cập nhật menu buổi tối</div>
<div class="admin-activity-item__time">Nhà hàng Q3 • 28 phút trước</div>
</div>
</div>
<div class="admin-activity-item">
<span class="admin-activity-dot" style="background-color:#EC4899;"></span>
<div class="admin-activity-item__text">
<div class="admin-activity-item__title">Khách VIP mới: Trần Thị B</div>
<div class="admin-activity-item__time">Hệ thống • 45 phút trước</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@code {
private List<PosDataService.ShopInfo> _shops = new();
protected override async Task OnInitializedAsync()
{
IsLoading = true;
try
{
_shops = await DataService.GetShopsAsync();
}
catch
{
_shops = new();
}
finally
{
IsLoading = false;
}
}
private static string GetShopIcon(string? category) => category?.ToLowerInvariant() switch
{
"cafe" or "café" or "coffee" => "coffee",
"restaurant" or "nhà hàng" => "utensils",
"karaoke" => "mic",
"spa" => "sparkles",
"retail" => "shopping-bag",
_ => "store"
};
}