From f7e431fd013396c3090ee133efb8f6553e787c55 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Sat, 28 Feb 2026 06:19:41 +0700 Subject: [PATCH] refactor(web-client-tpos): move business pages to shop-scoped sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../Layout/AdminLayout.razor | 24 +- .../Pages/Admin/Shop/ShopPage.razor | 415 ++++++++++++------ .../Services/ShopSidebarConfig.cs | 91 ++-- 3 files changed, 336 insertions(+), 194 deletions(-) diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/AdminLayout.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/AdminLayout.razor index bc7a5fb5..030985d4 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/AdminLayout.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/AdminLayout.razor @@ -83,29 +83,9 @@ Cửa hàng - - - Sản phẩm - - - - Kho hàng - - - - Tài chính - - @* ── NHÂN SỰ & KHÁCH HÀNG ── *@ - Nhân sự & Khách hàng - - - Nhân sự - - - - Khách hàng - + @* ── QUẢN TRỊ ── *@ + Quản trị Phân quyền diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopPage.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopPage.razor index da1c53ea..5b65a54a 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopPage.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopPage.razor @@ -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. *@ @_sectionTitle — @(_shopName ?? "Cửa hàng") — GoodGo Admin @@ -19,18 +17,6 @@

@_sectionTitle

@(_shopName ?? "Cửa hàng") • @_verticalLabel

-
- @if (_sectionActions.Count > 0) - { - @foreach (var act in _sectionActions) - { - - } - } -
@* ═══ CONTENT ═══ *@ @@ -45,33 +31,201 @@ } else { - @* ── Section-specific content placeholder ── *@ -
-
-
- + @switch (_section) + { + // ═══ OVERVIEW ═══ + case "overview": +
+
@_products.CountSản phẩm
+
@_inventory.CountTồn kho
+
@_orders.CountĐơn hàng
+
@_staff.CountNhân viên
-

@_sectionTitle

-

- @_sectionDescription -

- @if (_hasQuickStats) + break; + + // ═══ MENU / PRODUCTS ═══ + case "menu": + case "products": + @if (!_products.Any()) { -
- @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 + { +
+ @foreach (var p in _products) { -
-
@stat.Value
-
@stat.Label
+
+
+
+
@p.Name
+
@(p.CategoryName ?? "—")
+
@p.Price.ToString("N0")₫
+
}
} -

- Tính năng này sẽ được kích hoạt khi có dữ liệu từ hệ thống -

-
-
+ 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 + { +
+
@_inventory.Count(i => i.Quantity > 10)Còn hàng
+
@_inventory.Count(i => i.Quantity > 0 && i.Quantity <= 10)Sắp hết
+
@_inventory.Count(i => i.Quantity <= 0)Hết hàng
+
+
+
+ + + + + + @foreach (var item in _inventory) + { + + + + + + } +
Sản phẩmSố lượngMức nhập lại
@(item.ProductName ?? item.ProductId.ToString()[..8])@item.Quantity@item.ReorderLevel
+
+
+ } + break; + + // ═══ FINANCE ═══ + case "finance": +
+
@FormatVND(_orders.Sum(o => o.TotalAmount))Tổng doanh thu
+
@_orders.CountĐơn hàng
+
@FormatVND(_orders.Any() ? _orders.Average(o => o.TotalAmount) : 0)TB/đơn
+
+ @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 + { +
+

Đơn hàng gần đây

+
+ + + + + + + @foreach (var o in _orders.Take(20)) + { + + + + + + + } +
IDSố tiềnTrạng tháiNgày
@o.Id.ToString()[..8]@FormatVND(o.TotalAmount)@(o.Status ?? "—")@o.CreatedAt.ToString("dd/MM HH:mm")
+
+
+ } + 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 + { +
+
@_staff.Count(s => s.Status == "Active")Đang hoạt động
+
@_staff.CountTổng nhân viên
+
+
+
+ + + + + + + @foreach (var s in _staff) + { + + + + + + + } +
Mã NVVai tròTrạng tháiCửa hàng
@(s.EmployeeCode ?? s.Id.ToString()[..6])@(s.Role ?? "—")@(s.Status ?? "—")@(s.ShopName ?? "—")
+
+
+ } + 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 + { +
+
@_members.CountTổng khách hàng
+
+
+
+ + + + + + + @foreach (var m in _members) + { + + + + + + + } +
IDCấp bậcEXPNgày tham gia
@m.Id.ToString()[..8]@(m.LevelName ?? "—")@m.TotalExpEarned.ToString("N0")@m.CreatedAt.ToString("dd/MM/yyyy")
+
+
+ } + break; + + // ═══ PLACEHOLDER SECTIONS (POS, Tables, Kitchen, Rooms, Appointments, Services, Reports) ═══ + default: +
+
+
+ +
+

@_sectionTitle

+

+ @_sectionDescription +

+

+ Tính năng này sẽ được kích hoạt khi có dữ liệu từ hệ thống +

+
+
+ break; + } }
@@ -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 _products = new(); + private List _inventory = new(); + private List _orders = new(); + private List _staff = new(); + private List _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(); - } - - /// - /// 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. - /// 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 => + { +
+
+ +
+

@title

+

@desc

+
+ }; + + 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)}"; } } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/ShopSidebarConfig.cs b/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/ShopSidebarConfig.cs index 348b7fbb..39eb92ef 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/ShopSidebarConfig.cs +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/ShopSidebarConfig.cs @@ -6,6 +6,13 @@ namespace WebClientTpos.Client.Services; /// /// 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 /// public static class ShopSidebarConfig { @@ -17,14 +24,7 @@ public static class ShopSidebarConfig /// public static List 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"), }, }; } + /// + /// EN: Normalize category string to internal vertical key. + /// VI: Chuẩn hóa chuỗi category thành key nội bộ. + /// + 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" + }; + /// /// EN: Get vertical display name. /// VI: Lấy tên hiển thị của ngành hàng. /// - 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. /// - 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" }; }