refactor(web-client-tpos): move business pages to shop-scoped sidebar
- AdminLayout: removed Sản phẩm, Kho hàng, Tài chính, Nhân sự, Khách hàng from admin sidebar - ShopSidebarConfig: added Finance to all 4 verticals (Café, Restaurant, Karaoke, Spa) - ShopPage: rewritten with real API data for menu/inventory/finance/staff/customers sections - Each section filtered by shopId, loads only required data
This commit is contained in:
@@ -83,29 +83,9 @@
|
||||
<i data-lucide="store"></i>
|
||||
<span>Cửa hàng</span>
|
||||
</NavLink>
|
||||
<NavLink href="/admin/products" class="admin-nav-item" ActiveClass="admin-nav-item--active">
|
||||
<i data-lucide="package"></i>
|
||||
<span>Sản phẩm</span>
|
||||
</NavLink>
|
||||
<NavLink href="/admin/inventory" class="admin-nav-item" ActiveClass="admin-nav-item--active">
|
||||
<i data-lucide="warehouse"></i>
|
||||
<span>Kho hàng</span>
|
||||
</NavLink>
|
||||
<NavLink href="/admin/finance" class="admin-nav-item" ActiveClass="admin-nav-item--active">
|
||||
<i data-lucide="trending-up"></i>
|
||||
<span>Tài chính</span>
|
||||
</NavLink>
|
||||
|
||||
@* ── NHÂN SỰ & KHÁCH HÀNG ── *@
|
||||
<span class="admin-nav-label">Nhân sự & Khách hàng</span>
|
||||
<NavLink href="/admin/staff" class="admin-nav-item" ActiveClass="admin-nav-item--active">
|
||||
<i data-lucide="users"></i>
|
||||
<span>Nhân sự</span>
|
||||
</NavLink>
|
||||
<NavLink href="/admin/customers" class="admin-nav-item" ActiveClass="admin-nav-item--active">
|
||||
<i data-lucide="heart"></i>
|
||||
<span>Khách hàng</span>
|
||||
</NavLink>
|
||||
@* ── QUẢN TRỊ ── *@
|
||||
<span class="admin-nav-label">Quản trị</span>
|
||||
<NavLink href="/admin/roles" class="admin-nav-item" ActiveClass="admin-nav-item--active">
|
||||
<i data-lucide="shield"></i>
|
||||
<span>Phân quyền</span>
|
||||
|
||||
@@ -5,10 +5,8 @@
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
@*
|
||||
EN: Catch-all for shop sub-pages (menu, inventory, staff, customers, etc).
|
||||
Each section either shows real content or a "coming soon" placeholder.
|
||||
VI: Catch-all cho các trang con cửa hàng (menu, kho, nhân sự, khách hàng...).
|
||||
Mỗi section hiển thị nội dung thật hoặc placeholder "sắp ra mắt".
|
||||
EN: Shop-scoped page — renders different content per section with real data.
|
||||
VI: Trang theo cửa hàng — hiển thị nội dung theo section với dữ liệu thật.
|
||||
*@
|
||||
|
||||
<PageTitle>@_sectionTitle — @(_shopName ?? "Cửa hàng") — GoodGo Admin</PageTitle>
|
||||
@@ -19,18 +17,6 @@
|
||||
<h1 class="admin-topbar__title">@_sectionTitle</h1>
|
||||
<p class="admin-topbar__subtitle">@(_shopName ?? "Cửa hàng") • @_verticalLabel</p>
|
||||
</div>
|
||||
<div class="admin-topbar__right">
|
||||
@if (_sectionActions.Count > 0)
|
||||
{
|
||||
@foreach (var act in _sectionActions)
|
||||
{
|
||||
<button class="admin-btn-primary">
|
||||
<i data-lucide="@act.Icon"></i>
|
||||
<span>@act.Label</span>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ CONTENT ═══ *@
|
||||
@@ -45,33 +31,201 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
@* ── Section-specific content placeholder ── *@
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__body" style="text-align:center;padding:60px 20px;">
|
||||
<div style="width:80px;height:80px;border-radius:24px;background:rgba(255,92,0,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 20px;">
|
||||
<i data-lucide="@_sectionIcon" style="width:36px;height:36px;color:var(--admin-orange-primary);"></i>
|
||||
@switch (_section)
|
||||
{
|
||||
// ═══ OVERVIEW ═══
|
||||
case "overview":
|
||||
<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="package" style="color:#22C55E;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_products.Count</span><span class="admin-stat-card__label">Sản phẩm</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="warehouse" style="color:#3B82F6;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_inventory.Count</span><span class="admin-stat-card__label">Tồn kho</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="receipt" style="color:#FF5C00;"></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(139,92,246,0.1);"><i data-lucide="users" style="color:#8B5CF6;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_staff.Count</span><span class="admin-stat-card__label">Nhân viên</span></div></div>
|
||||
</div>
|
||||
<h2 style="font-size:22px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFFFFF);">@_sectionTitle</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0 0 24px;max-width:400px;margin-left:auto;margin-right:auto;">
|
||||
@_sectionDescription
|
||||
</p>
|
||||
@if (_hasQuickStats)
|
||||
break;
|
||||
|
||||
// ═══ MENU / PRODUCTS ═══
|
||||
case "menu":
|
||||
case "products":
|
||||
@if (!_products.Any())
|
||||
{
|
||||
<div style="display:flex;gap:16px;justify-content:center;margin-bottom:24px;">
|
||||
@foreach (var stat in _quickStats)
|
||||
@RenderEmpty("coffee", "#F59E0B", "Chưa có sản phẩm", "Thêm sản phẩm để bắt đầu bán hàng")
|
||||
}
|
||||
else
|
||||
{
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;">
|
||||
@foreach (var p in _products)
|
||||
{
|
||||
<div style="background:var(--admin-surface);border:1px solid var(--admin-border, #1F1F23);border-radius:12px;padding:16px 24px;min-width:120px;">
|
||||
<div style="font-size:24px;font-weight:700;color:var(--admin-orange-primary);">@stat.Value</div>
|
||||
<div style="font-size:12px;color:var(--admin-text-tertiary);margin-top:4px;">@stat.Label</div>
|
||||
<div class="admin-panel" style="cursor:pointer;">
|
||||
<div class="admin-panel__body" style="padding:16px;text-align:center;">
|
||||
<div style="width:48px;height:48px;border-radius:12px;background:rgba(255,92,0,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 12px;"><i data-lucide="package" style="color:#FF5C00;width:24px;height:24px;"></i></div>
|
||||
<div style="font-weight:600;font-size:14px;margin-bottom:4px;">@p.Name</div>
|
||||
<div style="font-size:13px;color:var(--admin-text-tertiary);">@(p.CategoryName ?? "—")</div>
|
||||
<div style="font-weight:700;color:var(--admin-orange-primary);margin-top:8px;">@p.Price.ToString("N0")₫</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<p style="font-size:13px;color:var(--admin-text-quaternary, #6B6B6F);margin:0;">
|
||||
Tính năng này sẽ được kích hoạt khi có dữ liệu từ hệ thống
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
|
||||
// ═══ INVENTORY ═══
|
||||
case "inventory":
|
||||
@if (!_inventory.Any())
|
||||
{
|
||||
@RenderEmpty("warehouse", "#3B82F6", "Chưa có tồn kho", "Tồn kho sẽ hiển thị khi có sản phẩm")
|
||||
}
|
||||
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="check-circle" style="color:#22C55E;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_inventory.Count(i => i.Quantity > 10)</span><span class="admin-stat-card__label">Còn hàng</span></div></div>
|
||||
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(245,158,11,0.1);"><i data-lucide="alert-triangle" style="color:#F59E0B;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_inventory.Count(i => i.Quantity > 0 && i.Quantity <= 10)</span><span class="admin-stat-card__label">Sắp hết</span></div></div>
|
||||
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(239,68,68,0.1);"><i data-lucide="x-circle" style="color:#EF4444;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_inventory.Count(i => i.Quantity <= 0)</span><span class="admin-stat-card__label">Hết hàng</span></div></div>
|
||||
</div>
|
||||
<div class="admin-panel">
|
||||
<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);">Sản phẩm</th>
|
||||
<th style="padding:12px 16px;text-align:right;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Số lượng</th>
|
||||
<th style="padding:12px 16px;text-align:right;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Mức nhập lại</th>
|
||||
</tr></thead><tbody>
|
||||
@foreach (var item in _inventory)
|
||||
{
|
||||
<tr style="border-top:1px solid var(--admin-border-subtle);">
|
||||
<td style="padding:12px 16px;font-weight:600;">@(item.ProductName ?? item.ProductId.ToString()[..8])</td>
|
||||
<td style="padding:12px 16px;text-align:right;font-weight:600;color:@(item.Quantity <= 0 ? "#EF4444" : item.Quantity <= 10 ? "#F59E0B" : "#22C55E");">@item.Quantity</td>
|
||||
<td style="padding:12px 16px;text-align:right;font-size:13px;color:var(--admin-text-tertiary);">@item.ReorderLevel</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
break;
|
||||
|
||||
// ═══ FINANCE ═══
|
||||
case "finance":
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,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>
|
||||
@if (!_orders.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")
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="admin-panel">
|
||||
<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>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">ID</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: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);">Ngày</th>
|
||||
</tr></thead><tbody>
|
||||
@foreach (var o in _orders.Take(20))
|
||||
{
|
||||
<tr style="border-top:1px solid var(--admin-border-subtle);">
|
||||
<td style="padding:12px 16px;font-size:12px;font-family:monospace;color:var(--admin-text-tertiary);">@o.Id.ToString()[..8]</td>
|
||||
<td style="padding:12px 16px;text-align:right;font-weight:600;">@FormatVND(o.TotalAmount)</td>
|
||||
<td style="padding:12px 16px;text-align:center;"><span class="admin-status-badge admin-status-badge--online" style="font-size:11px;padding:2px 10px;">@(o.Status ?? "—")</span></td>
|
||||
<td style="padding:12px 16px;font-size:13px;color:var(--admin-text-tertiary);">@o.CreatedAt.ToString("dd/MM HH:mm")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
break;
|
||||
|
||||
// ═══ STAFF ═══
|
||||
case "staff":
|
||||
@if (!_staff.Any())
|
||||
{
|
||||
@RenderEmpty("users", "#8B5CF6", "Chưa có nhân viên", "Thêm nhân viên để quản lý cửa hàng")
|
||||
}
|
||||
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="user-check" style="color:#22C55E;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_staff.Count(s => s.Status == "Active")</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(59,130,246,0.1);"><i data-lucide="users" style="color:#3B82F6;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_staff.Count</span><span class="admin-stat-card__label">Tổng nhân viên</span></div></div>
|
||||
</div>
|
||||
<div class="admin-panel">
|
||||
<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ã NV</th>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Vai 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);">Cửa hàng</th>
|
||||
</tr></thead><tbody>
|
||||
@foreach (var s in _staff)
|
||||
{
|
||||
<tr style="border-top:1px solid var(--admin-border-subtle);">
|
||||
<td style="padding:12px 16px;font-weight:600;">@(s.EmployeeCode ?? s.Id.ToString()[..6])</td>
|
||||
<td style="padding:12px 16px;">@(s.Role ?? "—")</td>
|
||||
<td style="padding:12px 16px;text-align:center;"><span class="admin-status-badge @(s.Status == "Active" ? "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>@(s.Status ?? "—")</span></td>
|
||||
<td style="padding:12px 16px;font-size:13px;color:var(--admin-text-tertiary);">@(s.ShopName ?? "—")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
break;
|
||||
|
||||
// ═══ CUSTOMERS ═══
|
||||
case "customers":
|
||||
@if (!_members.Any())
|
||||
{
|
||||
@RenderEmpty("heart", "#EF4444", "Chưa có khách hàng", "Khách hàng sẽ hiển thị khi có giao dịch")
|
||||
}
|
||||
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>
|
||||
<div class="admin-panel">
|
||||
<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>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Cấp bậc</th>
|
||||
<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)
|
||||
{
|
||||
<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;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>
|
||||
}
|
||||
</tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
break;
|
||||
|
||||
// ═══ PLACEHOLDER SECTIONS (POS, Tables, Kitchen, Rooms, Appointments, Services, Reports) ═══
|
||||
default:
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__body" style="text-align:center;padding:60px 20px;">
|
||||
<div style="width:80px;height:80px;border-radius:24px;background:rgba(255,92,0,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 20px;">
|
||||
<i data-lucide="@_sectionIcon" style="width:36px;height:36px;color:var(--admin-orange-primary);"></i>
|
||||
</div>
|
||||
<h2 style="font-size:22px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFFFFF);">@_sectionTitle</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0 0 24px;max-width:400px;margin-left:auto;margin-right:auto;">
|
||||
@_sectionDescription
|
||||
</p>
|
||||
<p style="font-size:13px;color:var(--admin-text-quaternary, #6B6B6F);margin:0;">
|
||||
Tính năng này sẽ được kích hoạt khi có dữ liệu từ hệ thống
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -82,21 +236,40 @@
|
||||
|
||||
private string _shopName = "";
|
||||
private string _verticalLabel = "";
|
||||
private string _section = "";
|
||||
private string _sectionTitle = "";
|
||||
private string _sectionIcon = "layout-dashboard";
|
||||
private string _sectionDescription = "";
|
||||
private bool _hasQuickStats = false;
|
||||
private List<(string Value, string Label)> _quickStats = new();
|
||||
private List<(string Icon, string Label)> _sectionActions = new();
|
||||
private Guid? _shopGuid;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
// ═══ DATA ═══
|
||||
private List<PosDataService.AdminProductInfo> _products = new();
|
||||
private List<PosDataService.InventoryItemInfo> _inventory = new();
|
||||
private List<PosDataService.OrderInfo> _orders = new();
|
||||
private List<PosDataService.StaffInfo> _staff = new();
|
||||
private List<PosDataService.MemberInfo> _members = new();
|
||||
|
||||
protected override async Task OnInitializedAsync() => await LoadData();
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
// EN: Called when URL params change / VI: Gọi khi params URL thay đổi
|
||||
if (_section != (Section?.ToLowerInvariant() ?? ""))
|
||||
await LoadData();
|
||||
}
|
||||
|
||||
private async Task LoadData()
|
||||
{
|
||||
IsLoading = true;
|
||||
_section = Section?.ToLowerInvariant() ?? "";
|
||||
ConfigureSection();
|
||||
|
||||
try
|
||||
{
|
||||
if (Guid.TryParse(ShopId, out var id))
|
||||
_shopGuid = Guid.TryParse(ShopId, out var id) ? id : null;
|
||||
if (_shopGuid.HasValue)
|
||||
{
|
||||
var shop = await DataService.GetShopByIdAsync(id);
|
||||
var shop = await DataService.GetShopByIdAsync(_shopGuid.Value);
|
||||
if (shop != null)
|
||||
{
|
||||
_shopName = shop.Name ?? "Cửa hàng";
|
||||
@@ -104,110 +277,78 @@
|
||||
Layout?.SetShopContext(ShopId, _shopName, shop.Category);
|
||||
}
|
||||
}
|
||||
ConfigureSection();
|
||||
|
||||
// EN: Load only data needed for current section / VI: Chỉ tải data cần cho section hiện tại
|
||||
switch (_section)
|
||||
{
|
||||
case "overview":
|
||||
_products = await DataService.GetAllProductsAsync(_shopGuid);
|
||||
_inventory = await DataService.GetInventoryAsync(_shopGuid);
|
||||
_orders = await DataService.GetOrdersAsync(_shopGuid);
|
||||
_staff = await DataService.GetStaffAsync();
|
||||
break;
|
||||
case "menu":
|
||||
case "products":
|
||||
_products = await DataService.GetAllProductsAsync(_shopGuid);
|
||||
break;
|
||||
case "inventory":
|
||||
_inventory = await DataService.GetInventoryAsync(_shopGuid);
|
||||
break;
|
||||
case "finance":
|
||||
_orders = await DataService.GetOrdersAsync(_shopGuid);
|
||||
break;
|
||||
case "staff":
|
||||
_staff = await DataService.GetStaffAsync();
|
||||
break;
|
||||
case "customers":
|
||||
_members = await DataService.GetMembersAsync();
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
finally { IsLoading = false; }
|
||||
}
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
ConfigureSection();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Configure section-specific title, icon, description, and quick stats.
|
||||
/// VI: Cấu hình tiêu đề, icon, mô tả, thống kê nhanh theo section.
|
||||
/// </summary>
|
||||
private void ConfigureSection()
|
||||
{
|
||||
var sec = Section?.ToLowerInvariant() ?? "";
|
||||
|
||||
// EN: Reset defaults
|
||||
// VI: Đặt lại giá trị mặc định
|
||||
_quickStats = new();
|
||||
_sectionActions = new();
|
||||
|
||||
switch (sec)
|
||||
switch (_section)
|
||||
{
|
||||
case "pos":
|
||||
_sectionTitle = "POS Bán hàng";
|
||||
_sectionIcon = "monitor";
|
||||
_sectionDescription = "Mở giao diện bán hàng tại điểm để phục vụ khách hàng nhanh chóng.";
|
||||
_sectionActions = new() { ("monitor", "Mở POS") };
|
||||
break;
|
||||
case "menu":
|
||||
_sectionTitle = "Quản lý Menu";
|
||||
_sectionIcon = "coffee";
|
||||
_sectionDescription = "Quản lý danh mục, món/sản phẩm, giá, tùy chọn thêm cho cửa hàng.";
|
||||
_quickStats = new() { ("0", "Danh mục"), ("0", "Sản phẩm"), ("0", "Topping") };
|
||||
_sectionActions = new() { ("plus", "Thêm sản phẩm") };
|
||||
break;
|
||||
case "tables":
|
||||
_sectionTitle = "Quản lý Bàn";
|
||||
_sectionIcon = "grid-3x3";
|
||||
_sectionDescription = "Thiết lập sơ đồ bàn, khu vực phục vụ cho nhà hàng.";
|
||||
_quickStats = new() { ("0", "Bàn"), ("0", "Khu vực") };
|
||||
_sectionActions = new() { ("plus", "Thêm bàn") };
|
||||
break;
|
||||
case "kitchen":
|
||||
_sectionTitle = "Bếp (Kitchen Display)";
|
||||
_sectionIcon = "flame";
|
||||
_sectionDescription = "Màn hình hiển thị đơn cho bếp, quản lý tiến độ chế biến.";
|
||||
break;
|
||||
case "rooms":
|
||||
_sectionTitle = "Quản lý Phòng";
|
||||
_sectionIcon = "door-open";
|
||||
_sectionDescription = "Thiết lập loại phòng, giá theo giờ, trạng thái phòng karaoke.";
|
||||
_quickStats = new() { ("0", "Phòng"), ("0", "Loại phòng") };
|
||||
_sectionActions = new() { ("plus", "Thêm phòng") };
|
||||
break;
|
||||
case "appointments":
|
||||
_sectionTitle = "Lịch hẹn";
|
||||
_sectionIcon = "calendar";
|
||||
_sectionDescription = "Quản lý lịch hẹn khách hàng, phân công nhân viên phục vụ.";
|
||||
_quickStats = new() { ("0", "Hôm nay"), ("0", "Tuần này") };
|
||||
_sectionActions = new() { ("plus", "Tạo lịch hẹn") };
|
||||
break;
|
||||
case "services":
|
||||
_sectionTitle = "Dịch vụ";
|
||||
_sectionIcon = "sparkles";
|
||||
_sectionDescription = "Quản lý danh mục dịch vụ, giá, thời gian thực hiện.";
|
||||
_quickStats = new() { ("0", "Dịch vụ"), ("0", "Gói combo") };
|
||||
_sectionActions = new() { ("plus", "Thêm dịch vụ") };
|
||||
break;
|
||||
case "inventory":
|
||||
_sectionTitle = "Tồn kho";
|
||||
_sectionIcon = "warehouse";
|
||||
_sectionDescription = "Theo dõi nguyên liệu, hàng tồn kho, cảnh báo hết hàng.";
|
||||
_quickStats = new() { ("0", "Nguyên liệu"), ("0", "Cần nhập") };
|
||||
break;
|
||||
case "staff":
|
||||
_sectionTitle = "Nhân sự";
|
||||
_sectionIcon = "users";
|
||||
_sectionDescription = "Quản lý nhân viên cửa hàng, ca làm việc, phân công.";
|
||||
_quickStats = new() { ("0", "Nhân viên"), ("0", "Ca hôm nay") };
|
||||
_sectionActions = new() { ("plus", "Thêm nhân viên") };
|
||||
break;
|
||||
case "customers":
|
||||
_sectionTitle = "Khách hàng";
|
||||
_sectionIcon = "heart";
|
||||
_sectionDescription = "Danh sách khách hàng, lịch sử mua hàng, tích điểm.";
|
||||
_quickStats = new() { ("0", "Khách hàng"), ("0", "Thành viên") };
|
||||
break;
|
||||
case "reports":
|
||||
_sectionTitle = "Báo cáo";
|
||||
_sectionIcon = "bar-chart-2";
|
||||
_sectionDescription = "Doanh thu, đơn hàng, sản phẩm bán chạy, hiệu suất nhân viên.";
|
||||
_sectionActions = new() { ("download", "Xuất báo cáo") };
|
||||
break;
|
||||
default:
|
||||
_sectionTitle = Section ?? "Trang";
|
||||
_sectionIcon = "layout-dashboard";
|
||||
_sectionDescription = "Trang này đang được phát triển.";
|
||||
break;
|
||||
case "overview": _sectionTitle = "Tổng quan"; _sectionIcon = "layout-dashboard"; _sectionDescription = "Tổng quan hoạt động cửa hàng."; break;
|
||||
case "menu": _sectionTitle = "Menu / Sản phẩm"; _sectionIcon = "coffee"; _sectionDescription = "Quản lý danh mục, sản phẩm, giá."; break;
|
||||
case "products": _sectionTitle = "Sản phẩm"; _sectionIcon = "package"; _sectionDescription = "Quản lý sản phẩm."; break;
|
||||
case "inventory": _sectionTitle = "Tồn kho"; _sectionIcon = "warehouse"; _sectionDescription = "Theo dõi tồn kho, cảnh báo hết hàng."; break;
|
||||
case "finance": _sectionTitle = "Tài chính"; _sectionIcon = "trending-up"; _sectionDescription = "Doanh thu, đơn hàng, chi phí."; break;
|
||||
case "staff": _sectionTitle = "Nhân sự"; _sectionIcon = "users"; _sectionDescription = "Quản lý nhân viên cửa hàng."; break;
|
||||
case "customers": _sectionTitle = "Khách hàng"; _sectionIcon = "heart"; _sectionDescription = "Khách hàng, thành viên."; break;
|
||||
case "pos": _sectionTitle = "POS Bán hàng"; _sectionIcon = "monitor"; _sectionDescription = "Mở giao diện bán hàng tại điểm."; break;
|
||||
case "tables": _sectionTitle = "Quản lý Bàn"; _sectionIcon = "grid-3x3"; _sectionDescription = "Sơ đồ bàn, khu vực phục vụ."; break;
|
||||
case "kitchen": _sectionTitle = "Bếp (Kitchen)"; _sectionIcon = "flame"; _sectionDescription = "Màn hình hiển thị đơn cho bếp."; break;
|
||||
case "rooms": _sectionTitle = "Phòng"; _sectionIcon = "door-open"; _sectionDescription = "Quản lý phòng karaoke."; break;
|
||||
case "appointments": _sectionTitle = "Lịch hẹn"; _sectionIcon = "calendar"; _sectionDescription = "Quản lý lịch hẹn khách hàng."; break;
|
||||
case "services": _sectionTitle = "Dịch vụ"; _sectionIcon = "sparkles"; _sectionDescription = "Quản lý danh mục dịch vụ."; break;
|
||||
case "reports": _sectionTitle = "Báo cáo"; _sectionIcon = "bar-chart-2"; _sectionDescription = "Doanh thu, sản phẩm bán chạy."; break;
|
||||
default: _sectionTitle = Section ?? "Trang"; _sectionIcon = "layout-dashboard"; _sectionDescription = "Trang đang phát triển."; break;
|
||||
}
|
||||
}
|
||||
|
||||
_hasQuickStats = _quickStats.Any();
|
||||
private static string FormatVND(decimal val) => val.ToString("N0") + "₫";
|
||||
|
||||
// EN: Reusable empty state renderer / VI: Renderer trạng thái trống tái sử dụng
|
||||
private RenderFragment RenderEmpty(string icon, string color, string title, string desc) => __builder =>
|
||||
{
|
||||
<div style="text-align:center;padding:60px 20px;">
|
||||
<div style="width:80px;height:80px;border-radius:24px;background:@($"rgba({HexToRgb(color)},0.1)");display:flex;align-items:center;justify-content:center;margin:0 auto 20px;">
|
||||
<i data-lucide="@icon" style="width:36px;height:36px;color:@color;"></i>
|
||||
</div>
|
||||
<h2 style="font-size:20px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFF);">@title</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0;">@desc</p>
|
||||
</div>
|
||||
};
|
||||
|
||||
private static string HexToRgb(string hex)
|
||||
{
|
||||
hex = hex.TrimStart('#');
|
||||
if (hex.Length != 6) return "0,0,0";
|
||||
return $"{Convert.ToInt32(hex[..2], 16)},{Convert.ToInt32(hex[2..4], 16)},{Convert.ToInt32(hex[4..], 16)}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,13 @@ namespace WebClientTpos.Client.Services;
|
||||
/// <summary>
|
||||
/// EN: Static config for shop-level sidebar menus per vertical type.
|
||||
/// VI: Cấu hình tĩnh cho menu sidebar cấp cửa hàng theo loại ngành hàng.
|
||||
///
|
||||
/// Mỗi ngành hàng có menu khác nhau:
|
||||
/// - Café: Menu đồ uống, tồn kho nguyên liệu
|
||||
/// - Restaurant: Menu món ăn + Bàn + Bếp
|
||||
/// - Karaoke: Phòng + Menu bar
|
||||
/// - Spa: Lịch hẹn + Dịch vụ
|
||||
/// Tất cả đều có: Tài chính, Nhân sự, Khách hàng, Báo cáo
|
||||
/// </summary>
|
||||
public static class ShopSidebarConfig
|
||||
{
|
||||
@@ -17,14 +24,7 @@ public static class ShopSidebarConfig
|
||||
/// </summary>
|
||||
public static List<MenuItem> GetMenuItems(string? category)
|
||||
{
|
||||
var vertical = (category ?? "").ToLowerInvariant() switch
|
||||
{
|
||||
"cafe" or "café" or "coffee" or "foodbeverage" => "cafe",
|
||||
"restaurant" or "nhà hàng" => "restaurant",
|
||||
"karaoke" or "entertainment" => "karaoke",
|
||||
"spa" or "beauty" => "spa",
|
||||
_ => "cafe" // EN: Default to café / VI: Mặc định là cafe
|
||||
};
|
||||
var vertical = NormalizeVertical(category);
|
||||
|
||||
return vertical switch
|
||||
{
|
||||
@@ -32,9 +32,10 @@ public static class ShopSidebarConfig
|
||||
{
|
||||
new("Tổng quan", "layout-dashboard", "overview"),
|
||||
new("POS Bán hàng", "monitor", "pos"),
|
||||
new("Menu & Đồ uống", "coffee", "menu", true),
|
||||
new("Tồn kho", "warehouse", "inventory", true),
|
||||
new("Nhân sự", "users", "staff", true),
|
||||
new("Menu & Đồ uống", "coffee", "menu"),
|
||||
new("Tồn kho", "warehouse", "inventory"),
|
||||
new("Tài chính", "trending-up", "finance"),
|
||||
new("Nhân sự", "users", "staff"),
|
||||
new("Khách hàng", "heart", "customers"),
|
||||
new("Báo cáo", "bar-chart-2", "reports"),
|
||||
},
|
||||
@@ -42,11 +43,12 @@ public static class ShopSidebarConfig
|
||||
{
|
||||
new("Tổng quan", "layout-dashboard", "overview"),
|
||||
new("POS Bán hàng", "monitor", "pos"),
|
||||
new("Menu & Món ăn", "utensils", "menu", true),
|
||||
new("Bàn / Table", "grid-3x3", "tables", true),
|
||||
new("Bếp (Kitchen)", "flame", "kitchen", true),
|
||||
new("Tồn kho", "warehouse", "inventory", true),
|
||||
new("Nhân sự", "users", "staff", true),
|
||||
new("Menu & Món ăn", "utensils", "menu"),
|
||||
new("Bàn / Table", "grid-3x3", "tables"),
|
||||
new("Bếp (Kitchen)", "flame", "kitchen"),
|
||||
new("Tồn kho", "warehouse", "inventory"),
|
||||
new("Tài chính", "trending-up", "finance"),
|
||||
new("Nhân sự", "users", "staff"),
|
||||
new("Khách hàng", "heart", "customers"),
|
||||
new("Báo cáo", "bar-chart-2", "reports"),
|
||||
},
|
||||
@@ -54,10 +56,11 @@ public static class ShopSidebarConfig
|
||||
{
|
||||
new("Tổng quan", "layout-dashboard", "overview"),
|
||||
new("POS Bán hàng", "monitor", "pos"),
|
||||
new("Phòng", "door-open", "rooms", true),
|
||||
new("Menu / Bar", "wine", "menu", true),
|
||||
new("Tồn kho", "warehouse", "inventory", true),
|
||||
new("Nhân sự", "users", "staff", true),
|
||||
new("Phòng", "door-open", "rooms"),
|
||||
new("Menu / Bar", "wine", "menu"),
|
||||
new("Tồn kho", "warehouse", "inventory"),
|
||||
new("Tài chính", "trending-up", "finance"),
|
||||
new("Nhân sự", "users", "staff"),
|
||||
new("Khách hàng", "heart", "customers"),
|
||||
new("Báo cáo", "bar-chart-2", "reports"),
|
||||
},
|
||||
@@ -65,9 +68,11 @@ public static class ShopSidebarConfig
|
||||
{
|
||||
new("Tổng quan", "layout-dashboard", "overview"),
|
||||
new("POS Bán hàng", "monitor", "pos"),
|
||||
new("Lịch hẹn", "calendar", "appointments", true),
|
||||
new("Dịch vụ", "sparkles", "services", true),
|
||||
new("Nhân sự", "users", "staff", true),
|
||||
new("Lịch hẹn", "calendar", "appointments"),
|
||||
new("Dịch vụ", "sparkles", "services"),
|
||||
new("Sản phẩm", "package", "products"),
|
||||
new("Tài chính", "trending-up", "finance"),
|
||||
new("Nhân sự", "users", "staff"),
|
||||
new("Khách hàng", "heart", "customers"),
|
||||
new("Báo cáo", "bar-chart-2", "reports"),
|
||||
},
|
||||
@@ -75,23 +80,39 @@ public static class ShopSidebarConfig
|
||||
{
|
||||
new("Tổng quan", "layout-dashboard", "overview"),
|
||||
new("POS Bán hàng", "monitor", "pos"),
|
||||
new("Sản phẩm", "package", "products", true),
|
||||
new("Nhân sự", "users", "staff", true),
|
||||
new("Sản phẩm", "package", "menu"),
|
||||
new("Tồn kho", "warehouse", "inventory"),
|
||||
new("Tài chính", "trending-up", "finance"),
|
||||
new("Nhân sự", "users", "staff"),
|
||||
new("Khách hàng", "heart", "customers"),
|
||||
new("Báo cáo", "bar-chart-2", "reports"),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Normalize category string to internal vertical key.
|
||||
/// VI: Chuẩn hóa chuỗi category thành key nội bộ.
|
||||
/// </summary>
|
||||
private static string NormalizeVertical(string? category) => (category ?? "").ToLowerInvariant() switch
|
||||
{
|
||||
"cafe" or "café" or "coffee" or "foodbeverage" => "cafe",
|
||||
"restaurant" or "nhà hàng" or "bar" => "restaurant",
|
||||
"karaoke" or "entertainment" => "karaoke",
|
||||
"spa" or "beauty" or "salon" => "spa",
|
||||
_ => "default"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get vertical display name.
|
||||
/// VI: Lấy tên hiển thị của ngành hàng.
|
||||
/// </summary>
|
||||
public static string GetVerticalLabel(string? category) => (category ?? "").ToLowerInvariant() switch
|
||||
public static string GetVerticalLabel(string? category) => NormalizeVertical(category) switch
|
||||
{
|
||||
"cafe" or "café" or "coffee" or "foodbeverage" => "Café",
|
||||
"restaurant" or "nhà hàng" => "Nhà hàng",
|
||||
"karaoke" or "entertainment" => "Karaoke",
|
||||
"spa" or "beauty" => "Spa",
|
||||
"cafe" => "Café",
|
||||
"restaurant" => "Nhà hàng / Bar",
|
||||
"karaoke" => "Karaoke",
|
||||
"spa" => "Spa / Thẩm mỹ",
|
||||
_ => "Cửa hàng"
|
||||
};
|
||||
|
||||
@@ -99,12 +120,12 @@ public static class ShopSidebarConfig
|
||||
/// EN: Get vertical icon.
|
||||
/// VI: Lấy icon ngành hàng.
|
||||
/// </summary>
|
||||
public static string GetVerticalIcon(string? category) => (category ?? "").ToLowerInvariant() switch
|
||||
public static string GetVerticalIcon(string? category) => NormalizeVertical(category) switch
|
||||
{
|
||||
"cafe" or "café" or "coffee" or "foodbeverage" => "coffee",
|
||||
"restaurant" or "nhà hàng" => "utensils",
|
||||
"karaoke" or "entertainment" => "mic",
|
||||
"spa" or "beauty" => "sparkles",
|
||||
"cafe" => "coffee",
|
||||
"restaurant" => "utensils",
|
||||
"karaoke" => "mic",
|
||||
"spa" => "sparkles",
|
||||
_ => "store"
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user