From d7031090962f35569359bdf9874c9eb0b112aebf Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Sat, 28 Feb 2026 06:36:41 +0700 Subject: [PATCH] =?UTF-8?q?refactor(web-client):=20audit=20shop=20sidebar?= =?UTF-8?q?=20=E2=80=94=20fix=20bugs,=20DRY=20code,=20enhance=20UX?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1 Bug Fixes: - Fix Finance stat spacing (0₫ → 0 ₫) - Replace 3 empty catch blocks with error state UI + logging - Make POS button functional (navigate to /pos) - Add @implements IDisposable to AdminLayout P2 Code Quality: - Create ShopVerticalHelper.cs (DRY icon/label/normalize) - Remove dead overview case from ShopPage.razor - Delete 15 orphaned admin pages (moved to shop-scope) - ShopSidebarConfig delegates to ShopVerticalHelper P3 UX Enhancement: - Add CTA buttons to all empty states - Real KPIs in ShopOverview (orders, products, revenue) - User role from auth token (Admin/Khách) - Responsive CSS for stat cards, tables, mobile sidebar --- .../Layout/AdminLayout.razor | 4 +- .../Admin/Customer/CustomerFeedback.razor | 26 ----- .../Pages/Admin/Customer/LoyaltyProgram.razor | 73 ------------ .../Pages/Admin/Dashboard.razor | 14 +-- .../Admin/Finance/ExpenseManagement.razor | 62 ---------- .../Admin/Finance/FinancialOverview.razor | 108 ------------------ .../Admin/Finance/RevenueAnalytics.razor | 86 -------------- .../Admin/Finance/TaxConfiguration.razor | 75 ------------ .../Admin/Inventory/PurchaseOrders.razor | 79 ------------- .../Pages/Admin/Inventory/StockTransfer.razor | 49 -------- .../Admin/Inventory/SupplierManagement.razor | 26 ----- .../Pages/Admin/Product/ModifierGroups.razor | 21 ---- .../Pages/Admin/Product/PricingRules.razor | 71 ------------ .../Pages/Admin/Shop/ShopOverview.razor | 42 ++++--- .../Pages/Admin/Shop/ShopPage.razor | 67 ++++++----- .../Pages/Admin/Staff/Attendance.razor | 83 -------------- .../Pages/Admin/Staff/Payroll.razor | 73 ------------ .../Pages/Admin/Staff/StaffSchedule.razor | 84 -------------- .../SystemAdmin/NotificationCenter.razor | 42 ------- .../Services/ShopSidebarConfig.cs | 41 ++----- .../Services/ShopVerticalHelper.cs | 57 +++++++++ .../wwwroot/css/admin.css | 39 +++++++ 22 files changed, 172 insertions(+), 1050 deletions(-) delete mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Customer/CustomerFeedback.razor delete mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Customer/LoyaltyProgram.razor delete mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Finance/ExpenseManagement.razor delete mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Finance/FinancialOverview.razor delete mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Finance/RevenueAnalytics.razor delete mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Finance/TaxConfiguration.razor delete mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Inventory/PurchaseOrders.razor delete mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Inventory/StockTransfer.razor delete mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Inventory/SupplierManagement.razor delete mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Product/ModifierGroups.razor delete mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Product/PricingRules.razor delete mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Staff/Attendance.razor delete mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Staff/Payroll.razor delete mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Staff/StaffSchedule.razor delete mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/SystemAdmin/NotificationCenter.razor create mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Services/ShopVerticalHelper.cs 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 030985d4..5c9fd982 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 @@ -6,6 +6,7 @@ Shop Level: Detected via URL /admin/shop/{shopId}/**, shows vertical-specific menu *@ @inherits LayoutComponentBase +@implements IDisposable @inject NavigationManager NavigationManager @inject IJSRuntime JS @inject WebClientTpos.Client.Services.AuthStateService AuthState @@ -117,7 +118,7 @@
@_userInitials
@_userName - Owner + @_userRole
+ @@ -62,7 +62,7 @@
- +
@_shop.Name
@@ -97,7 +97,7 @@
-
--
+
@FormatVND(_orders.Sum(o => o.TotalAmount))
Doanh thu tháng
@@ -106,7 +106,7 @@
-
--
+
@_orders.Count
Đơn hàng tháng
@@ -115,17 +115,17 @@
-
--
+
@FormatVND(_orders.Any() ? _orders.Average(o => o.TotalAmount) : 0)
Giá trị TB / đơn
- +
-
--
-
Đánh giá TB
+
@_products.Count
+
Sản phẩm
@@ -199,6 +199,8 @@ [Parameter] public string ShopId { get; set; } = ""; private PosDataService.ShopInfo? _shop; + private List _orders = new(); + private List _products = new(); // EN: Cascade layout reference to set shop context for sidebar switching. // VI: Cascade layout để set shop context cho sidebar chuyển đổi. @@ -216,12 +218,23 @@ { Layout?.SetShopContext(ShopId, _shop.Name ?? "Cửa hàng", _shop.Category); } + // EN: Load KPI data in parallel / VI: Tải dữ liệu KPI song song + var ordersTask = DataService.GetOrdersAsync(id); + var productsTask = DataService.GetAllProductsAsync(id); + _orders = await ordersTask; + _products = await productsTask; } } - catch { _shop = null; } + catch (Exception ex) + { + _shop = null; + Console.Error.WriteLine($"[ShopOverview] Error loading shop {ShopId}: {ex}"); + } finally { IsLoading = false; } } + private static string FormatVND(decimal val) => val.ToString("N0") + " ₫"; + private static string GetStatusBadgeClass(string? status) => status?.ToLowerInvariant() switch { "published" or "active" => "online", @@ -239,13 +252,4 @@ _ => status ?? "—" }; - private static string GetShopIcon(string? category) => category?.ToLowerInvariant() switch - { - "foodbeverage" or "café" or "cafe" or "coffee" => "coffee", - "restaurant" or "nhà hàng" => "utensils", - "entertainment" or "karaoke" => "mic", - "beauty" or "spa" => "sparkles", - "retail" => "shopping-bag", - _ => "store" - }; } 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 5b65a54a..e2ea8f0e 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 @@ -22,7 +22,23 @@ @* ═══ CONTENT ═══ *@
- @if (IsLoading) + @if (!string.IsNullOrEmpty(_errorMessage)) + { +
+
+
+ +
+

Lỗi tải dữ liệu

+

@_errorMessage

+ +
+
+ } + else if (IsLoading) {
@@ -33,22 +49,12 @@ { @switch (_section) { - // ═══ OVERVIEW ═══ - case "overview": -
-
@_products.CountSản phẩm
-
@_inventory.CountTồn kho
-
@_orders.CountĐơn hàng
-
@_staff.CountNhân viên
-
- break; - // ═══ MENU / PRODUCTS ═══ case "menu": case "products": @if (!_products.Any()) { - @RenderEmpty("coffee", "#F59E0B", "Chưa có sản phẩm", "Thêm sản phẩm để bắt đầu bán hàng") + @RenderEmpty("coffee", "#F59E0B", "Chưa có sản phẩm", "Thêm sản phẩm để bắt đầu bán hàng", "plus-circle", "Thêm sản phẩm") } else { @@ -72,7 +78,7 @@ 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") + @RenderEmpty("warehouse", "#3B82F6", "Chưa có tồn kho", "Tồn kho sẽ hiển thị khi có sản phẩm", "package", "Thêm sản phẩm trước") } else { @@ -111,7 +117,7 @@
@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") + @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", "monitor", "Mở POS bán hàng") } else { @@ -143,7 +149,7 @@ 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") + @RenderEmpty("users", "#8B5CF6", "Chưa có nhân viên", "Thêm nhân viên để quản lý cửa hàng", "user-plus", "Thêm nhân viên") } else { @@ -178,7 +184,7 @@ 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") + @RenderEmpty("heart", "#EF4444", "Chưa có khách hàng", "Khách hàng sẽ hiển thị khi có giao dịch", "monitor", "Mở POS bán hàng") } else { @@ -240,6 +246,7 @@ private string _sectionTitle = ""; private string _sectionIcon = "layout-dashboard"; private string _sectionDescription = ""; + private string? _errorMessage; private Guid? _shopGuid; // ═══ DATA ═══ @@ -261,6 +268,7 @@ private async Task LoadData() { IsLoading = true; + _errorMessage = null; _section = Section?.ToLowerInvariant() ?? ""; ConfigureSection(); @@ -281,12 +289,6 @@ // 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); @@ -305,7 +307,11 @@ break; } } - catch { } + catch (Exception ex) + { + _errorMessage = $"Không thể tải dữ liệu: {ex.Message}"; + Console.Error.WriteLine($"[ShopPage] Error loading {_section}: {ex}"); + } finally { IsLoading = false; } } @@ -331,17 +337,24 @@ } } - private static string FormatVND(decimal val) => val.ToString("N0") + "₫"; + 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 => + // EN: Reusable empty state renderer with optional CTA / VI: Renderer trạng thái trống tái sử dụng với CTA tùy chọn + private RenderFragment RenderEmpty(string icon, string color, string title, string desc, string? ctaIcon = null, string? ctaLabel = null) => __builder => {

@title

-

@desc

+

@desc

+ @if (ctaIcon != null && ctaLabel != null) + { + + + @ctaLabel + + }
}; diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Staff/Attendance.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Staff/Attendance.razor deleted file mode 100644 index b1fe21d1..00000000 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Staff/Attendance.razor +++ /dev/null @@ -1,83 +0,0 @@ -@page "/admin/staff/attendance" -@layout AdminLayout -@inherits AdminBase -@inject PosDataService DataService -@using WebClientTpos.Client.Services - -Chấm công — GoodGo Admin - -
-
-

Chấm công

-

Theo dõi giờ làm việc nhân viên

-
-
- -
-
-
-
-
@_staff.Count(s => s.Status == "Active")Đang hoạt động
-
-
-
-
@_staff.CountTổng nhân viên
-
-
-
-
@_schedules.CountTổng ca hôm nay
-
-
- - @if (IsLoading) - { -
- } - else - { -
-

Nhân viên hôm nay

-
- - - - - - - - - @foreach (var s in _staff) - { - - - - - - - } - -
Nhân viênVai tròTrạng tháiCửa hàng
@(s.EmployeeCode ?? s.Id.ToString()[..6])@(s.Role ?? "—") - - @(s.Status ?? "—") - - @(s.ShopName ?? "—")
-
-
- } -
- -@code { - private List _staff = new(); - private List _schedules = new(); - - protected override async Task OnInitializedAsync() - { - IsLoading = true; - try - { - _staff = await DataService.GetStaffAsync(); - _schedules = await DataService.GetStaffSchedulesAsync(); - } - catch { } finally { IsLoading = false; } - } -} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Staff/Payroll.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Staff/Payroll.razor deleted file mode 100644 index a4e3dc07..00000000 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Staff/Payroll.razor +++ /dev/null @@ -1,73 +0,0 @@ -@page "/admin/staff/payroll" -@layout AdminLayout -@inherits AdminBase -@inject PosDataService DataService -@using WebClientTpos.Client.Services - -Bảng lương — GoodGo Admin - -
-
-

Bảng lương

-

@_staff.Count nhân viên • @DateTime.Now.ToString("MM/yyyy")

-
-
- -
- @if (IsLoading) - { -
- } - else if (!_staff.Any()) - { -
-
- -
-

Chưa có dữ liệu lương

-

Thêm nhân viên để quản lý bảng lương

-
- } - else - { -
-

Danh sách nhân viên

-
- - - - - - - - - @foreach (var s in _staff) - { - - - - - - - } - -
Mã NVVai tròNgày vàoTrạng thái
@(s.EmployeeCode ?? "—")@(s.Role ?? "—")@(s.JoinedAt?.ToString("dd/MM/yyyy") ?? "—") - - @(s.Status ?? "—") - -
-
-
- } -
- -@code { - private List _staff = new(); - - protected override async Task OnInitializedAsync() - { - IsLoading = true; - try { _staff = await DataService.GetStaffAsync(); } - catch { } finally { IsLoading = false; } - } -} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Staff/StaffSchedule.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Staff/StaffSchedule.razor deleted file mode 100644 index c90adf01..00000000 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Staff/StaffSchedule.razor +++ /dev/null @@ -1,84 +0,0 @@ -@page "/admin/staff/schedule" -@layout AdminLayout -@inherits AdminBase -@inject PosDataService DataService -@using WebClientTpos.Client.Services - -Lịch làm việc — GoodGo Admin - -
-
-

Lịch làm việc

-

@_schedules.Count ca làm việc

-
-
- -
-
- -
- @if (IsLoading) - { -
-

Đang tải lịch...

- } - else if (!_schedules.Any()) - { -
-
- -
-

Chưa có lịch làm việc

-

Thêm lịch cho nhân viên để quản lý ca làm việc

-
- } - else - { - @foreach (var dayGroup in _schedules.GroupBy(s => s.DayOfWeek).OrderBy(g => g.Key)) - { -
-

@DayName(dayGroup.Key)

-
- @foreach (var s in dayGroup) - { -
-
- @(s.EmployeeCode ?? s.StaffId.ToString()[..6]) - @(s.Role ?? "") -
- @s.StartTime — @s.EndTime -
- } -
-
- } - } -
- -@code { - private List _schedules = new(); - private List _shops = new(); - private Guid? _selectedShopId; - - protected override async Task OnInitializedAsync() - { - IsLoading = true; - try { _shops = await DataService.GetShopsAsync(); _schedules = await DataService.GetStaffSchedulesAsync(); } - catch { } finally { IsLoading = false; } - } - - private async Task OnShopFilterChanged(ChangeEventArgs e) - { - _selectedShopId = Guid.TryParse(e.Value?.ToString(), out var id) ? id : null; - IsLoading = true; - try { _schedules = await DataService.GetStaffSchedulesAsync(_selectedShopId); } - catch { } finally { IsLoading = false; } - } - - private static string DayName(int day) => day switch { - 0 => "Chủ nhật", 1 => "Thứ 2", 2 => "Thứ 3", 3 => "Thứ 4", 4 => "Thứ 5", 5 => "Thứ 6", 6 => "Thứ 7", _ => $"Ngày {day}" - }; -} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/SystemAdmin/NotificationCenter.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/SystemAdmin/NotificationCenter.razor deleted file mode 100644 index f1f2f3b3..00000000 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/SystemAdmin/NotificationCenter.razor +++ /dev/null @@ -1,42 +0,0 @@ -@page "/admin/system/notifications" -@layout AdminLayout -@inherits AdminBase - -Thông báo — GoodGo Admin - -
-
-

Trung tâm thông báo

-

Quản lý thông báo đẩy

-
-
- -
-
-

Mẫu thông báo

-
- @foreach (var tmpl in _templates) - { -
-
-
@tmpl.name
-
@tmpl.desc
-
- @(tmpl.active ? "Bật" : "Tắt") -
- } -
-
-
- -@code { - private readonly (string name, string desc, bool active)[] _templates = new[] - { - ("Đơn hàng mới", "Thông báo khi có đơn hàng mới", true), - ("Hết hàng", "Cảnh báo khi sản phẩm hết hàng", true), - ("Khuyến mãi", "Thông báo chương trình khuyến mãi", false), - ("Đặt lịch", "Nhắc nhở lịch hẹn", true), - ("Chấm công", "Thông báo chấm công", false), - ("Hệ thống", "Thông báo bảo trì hệ thống", true), - }; -} 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 39eb92ef..17662252 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 @@ -24,7 +24,7 @@ public static class ShopSidebarConfig /// public static List GetMenuItems(string? category) { - var vertical = NormalizeVertical(category); + var vertical = ShopVerticalHelper.NormalizeVertical(category); return vertical switch { @@ -91,41 +91,14 @@ public static class ShopSidebarConfig } /// - /// EN: Normalize category string to internal vertical key. - /// VI: Chuẩn hóa chuỗi category thành key nội bộ. + /// EN: Get vertical display name (delegates to ShopVerticalHelper). + /// VI: Lấy tên hiển thị của ngành hàng (ủy quyền cho ShopVerticalHelper). /// - 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" - }; + public static string GetVerticalLabel(string? category) => ShopVerticalHelper.GetLabel(category); /// - /// EN: Get vertical display name. - /// VI: Lấy tên hiển thị của ngành hàng. + /// EN: Get vertical icon (delegates to ShopVerticalHelper). + /// VI: Lấy icon ngành hàng (ủy quyền cho ShopVerticalHelper). /// - public static string GetVerticalLabel(string? category) => NormalizeVertical(category) switch - { - "cafe" => "Café", - "restaurant" => "Nhà hàng / Bar", - "karaoke" => "Karaoke", - "spa" => "Spa / Thẩm mỹ", - _ => "Cửa hàng" - }; - - /// - /// EN: Get vertical icon. - /// VI: Lấy icon ngành hàng. - /// - public static string GetVerticalIcon(string? category) => NormalizeVertical(category) switch - { - "cafe" => "coffee", - "restaurant" => "utensils", - "karaoke" => "mic", - "spa" => "sparkles", - _ => "store" - }; + public static string GetVerticalIcon(string? category) => ShopVerticalHelper.GetIcon(category); } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/ShopVerticalHelper.cs b/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/ShopVerticalHelper.cs new file mode 100644 index 00000000..fa77f69f --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/ShopVerticalHelper.cs @@ -0,0 +1,57 @@ +// EN: Centralized helper for shop vertical/category utilities. +// VI: Helper tập trung cho các tiện ích ngành hàng cửa hàng. +// +// Consolidates duplicate GetShopIcon, GetVerticalLabel, NormalizeVertical +// logic that was scattered across Dashboard.razor, ShopOverview.razor, +// and ShopSidebarConfig.cs. + +namespace WebClientTpos.Client.Services; + +/// +/// EN: Centralized utilities for shop vertical display — icon, label, normalize. +/// VI: Tiện ích tập trung cho hiển thị ngành hàng — icon, label, chuẩn hóa. +/// +public static class ShopVerticalHelper +{ + /// + /// EN: Normalize category string to internal vertical key. + /// VI: Chuẩn hóa chuỗi category thành key nội bộ. + /// + public 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", + "retail" => "retail", + _ => "default" + }; + + /// + /// EN: Get Lucide icon name for a shop vertical/category. + /// VI: Lấy tên icon Lucide cho ngành hàng. + /// + public static string GetIcon(string? category) => NormalizeVertical(category) switch + { + "cafe" => "coffee", + "restaurant" => "utensils", + "karaoke" => "mic", + "spa" => "sparkles", + "retail" => "shopping-bag", + _ => "store" + }; + + /// + /// EN: Get display label for a vertical. + /// VI: Lấy tên hiển thị cho ngành hàng. + /// + public static string GetLabel(string? category) => NormalizeVertical(category) switch + { + "cafe" => "Café", + "restaurant" => "Nhà hàng / Bar", + "karaoke" => "Karaoke", + "spa" => "Spa / Thẩm mỹ", + "retail" => "Bán lẻ", + _ => "Cửa hàng" + }; +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/css/admin.css b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/css/admin.css index 5c11a233..7e3a0393 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/css/admin.css +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/css/admin.css @@ -1003,6 +1003,20 @@ a.admin-store-card:hover { left: 0; } + /* EN: Mobile sidebar overlay / VI: Overlay sidebar trên mobile */ + .admin-sidebar-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 199; + backdrop-filter: blur(2px); + } + + /* EN: Mobile hamburger button / VI: Nút hamburger trên mobile */ + .admin-topbar__menu-btn { + display: flex !important; + } + .admin-topbar { padding: 12px 20px; } @@ -1018,6 +1032,18 @@ a.admin-store-card:hover { .admin-kpi-card { min-width: calc(50% - 10px); } + + /* EN: Stat card grid responsive / VI: Lưới stat card responsive */ + .admin-stat-card { + min-width: calc(50% - 8px); + } + + /* EN: Tables scroll horizontally / VI: Bảng scroll ngang */ + .admin-table { + display: block; + overflow-x: auto; + white-space: nowrap; + } } @media (max-width: 640px) { @@ -1025,6 +1051,10 @@ a.admin-store-card:hover { min-width: 100%; } + .admin-stat-card { + min-width: 100%; + } + .admin-content { padding: 16px; } @@ -1035,6 +1065,10 @@ a.admin-store-card:hover { gap: 12px; } + .admin-topbar__title { + font-size: 18px; + } + .admin-search { width: 100%; } @@ -1046,6 +1080,11 @@ a.admin-store-card:hover { .admin-store-stat { min-width: calc(50% - 8px); } + + .admin-btn-primary { + width: 100%; + justify-content: center; + } } /* ═════════════════════════════════════════════════════════════════════════