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:
Ho Ngoc Hai
2026-02-28 06:19:41 +07:00
parent 545bc1f519
commit f7e431fd01
3 changed files with 336 additions and 194 deletions

View File

@@ -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>

View File

@@ -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)}";
}
}

View File

@@ -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"
};
}