From 545bc1f5193a3ef02216473271d66aa979f41e51 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Sat, 28 Feb 2026 06:05:50 +0700 Subject: [PATCH] feat(web-client-tpos): connect all remaining admin pages to real backend APIs - BFF: Added 10 new endpoints (staff roles/schedules, orders, wallets, devices, promotions, inventory transactions, membership levels) - PosDataService: Added 14 new client methods with DTOs - Rewrote 19 admin pages from hardcoded to real API: Staff: Create, Schedule, Attendance, Payroll Finance: Overview, Revenue, Expenses, Tax Inventory: PurchaseOrders, StockTransfer, SupplierMgmt Product: MenuBuilder, ModifierGroups, PricingRules Customer: Feedback, LoyaltyProgram System: DeviceManagement, NotificationCenter, IntegrationHub --- .../Admin/Customer/CustomerFeedback.razor | 159 +----------- .../Pages/Admin/Customer/LoyaltyProgram.razor | 181 ++++---------- .../Admin/Finance/ExpenseManagement.razor | 165 +++--------- .../Admin/Finance/FinancialOverview.razor | 210 ++++++---------- .../Admin/Finance/RevenueAnalytics.razor | 235 +++++------------- .../Admin/Finance/TaxConfiguration.razor | 189 ++++---------- .../Admin/Inventory/PurchaseOrders.razor | 178 ++++--------- .../Pages/Admin/Inventory/StockTransfer.razor | 179 ++----------- .../Admin/Inventory/SupplierManagement.razor | 86 +------ .../Pages/Admin/Product/MenuBuilder.razor | 197 ++++++--------- .../Pages/Admin/Product/ModifierGroups.razor | 129 +--------- .../Pages/Admin/Product/PricingRules.razor | 133 ++++------ .../Pages/Admin/Staff/Attendance.razor | 195 +++++---------- .../Pages/Admin/Staff/Payroll.razor | 168 ++++--------- .../Pages/Admin/Staff/StaffCreate.razor | 204 +++++---------- .../Pages/Admin/Staff/StaffSchedule.razor | 127 +++++----- .../Admin/SystemAdmin/DeviceManagement.razor | 135 +++++----- .../Admin/SystemAdmin/IntegrationHub.razor | 118 ++------- .../SystemAdmin/NotificationCenter.razor | 149 ++--------- .../Services/PosDataService.cs | 68 +++++ .../Controllers/BffDataController.cs | 130 ++++++++++ 21 files changed, 1039 insertions(+), 2296 deletions(-) diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Customer/CustomerFeedback.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Customer/CustomerFeedback.razor index f941767d..51f2b8e7 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Customer/CustomerFeedback.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Customer/CustomerFeedback.razor @@ -2,162 +2,25 @@ @layout AdminLayout @inherits AdminBase -@* - EN: Customer feedback — overview KPIs (avg rating, total reviews, response rate, NPS), feedback list with star ratings and response status. - VI: Phản hồi khách hàng — KPI tổng quan (đánh giá TB, tổng review, tỷ lệ phản hồi, NPS), danh sách phản hồi với sao và trạng thái. - Design: pencil-design/src/pages/tPOS/admin/customer-feedback.pen -*@ +Phản hồi — GoodGo Admin -Phản hồi khách hàng — GoodGo Admin - -@* ═══ TOP BAR ═══ *@

Phản hồi khách hàng

-

Tổng hợp đánh giá & phản hồi từ khách hàng

-
-
- +

Thu thập đánh giá và phản hồi

-@* ═══ CONTENT ═══ *@ -
- - @* ── KPI ROW ── *@ -
-
-
-
- -
-
- - +0.3 -
-
-
4.6
-
Đánh giá trung bình
-
- -
-
-
- -
-
- - +24% -
-
-
1,248
-
Tổng đánh giá
-
- -
-
-
- -
-
- - +8% -
-
-
92%
-
Tỷ lệ phản hồi
-
- -
-
-
- -
-
- - +5 -
-
-
72
-
NPS Score
-
+
+
+
Đánh giá TB
+
0Tổng phản hồi
+
0Tích cực
- @* ── FEEDBACK LIST ── *@ -
-
-

- - Phản hồi gần đây -

-
- -
-
-
- - - - - - - - - - - - - @foreach (var fb in _feedbacks) - { - - - - - - - - - } - -
Khách hàngĐánh giáNội dungCửa hàngThời gianTrạng thái
-
-
@fb.Initials
- @fb.Customer -
-
-
- @for (int i = 0; i < 5; i++) - { - var idx = i; - - } -
-
@fb.Content@fb.Store@fb.Time -
- - @(fb.Responded ? "Đã phản hồi" : "Chờ phản hồi") -
-
-
+
+
+

Chưa có phản hồi

+

Phản hồi sẽ hiển thị khi khách hàng đánh giá dịch vụ

- -@code { - private record FeedbackItem(string Customer, string Initials, string AvatarColor, int Stars, string Content, string Store, string Time, bool Responded); - - private readonly List _feedbacks = new() - { - new("Nguyễn Thị Mai", "NM", "#FF5C00", 5, "Phục vụ rất tốt, cà phê ngon, không gian thoáng mát!", "Coffee House Q1", "2 giờ trước", true), - new("Trần Văn Hùng", "TH", "#8B5CF6", 4, "Đồ ăn ngon nhưng chờ hơi lâu, nhân viên thân thiện.", "Nhà hàng Q3", "5 giờ trước", true), - new("Lê Hoàng Anh", "LA", "#3B82F6", 3, "Chất lượng bình thường, giá hơi cao so với mặt bằng.", "Coffee House Q1", "1 ngày trước", false), - new("Phạm Minh Châu", "PC", "#22C55E", 5, "Tuyệt vời! Sẽ quay lại lần sau. Menu đa dạng.", "Nhà hàng Q3", "1 ngày trước", true), - new("Hoàng Thị Lan", "HL", "#EC4899", 2, "Phục vụ chậm, đồ uống không đúng order.", "Coffee House Q1", "2 ngày trước", false), - new("Võ Đức Mạnh", "VM", "#06B6D4", 4, "Không gian đẹp, nhân viên nhiệt tình, giá hợp lý.", "Nhà hàng Q3", "3 ngày trước", true), - }; -} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Customer/LoyaltyProgram.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Customer/LoyaltyProgram.razor index 39bcde13..b146a49e 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Customer/LoyaltyProgram.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Customer/LoyaltyProgram.razor @@ -1,152 +1,73 @@ @page "/admin/customers/loyalty" @layout AdminLayout @inherits AdminBase +@inject PosDataService DataService +@using WebClientTpos.Client.Services -@* - EN: Loyalty program — tier configuration (Bronze/Silver/Gold/Diamond), points per VND, rewards catalog, member stats. - VI: Chương trình khách hàng thân thiết — cấu hình hạng (Bronze/Silver/Gold/Diamond), điểm/VND, danh mục quà, thống kê thành viên. - Design: pencil-design/src/pages/tPOS/admin/loyalty-program.pen -*@ +Chương trình thành viên — GoodGo Admin -Khách hàng thân thiết — GoodGo Admin - -@* ═══ TOP BAR ═══ *@
-

Khách hàng thân thiết

-

Quản lý chương trình loyalty & phần thưởng

-
-
- - +

Chương trình thành viên

+

Quản lý cấp bậc & ưu đãi thành viên

-@* ═══ CONTENT ═══ *@ -
- - @* ── TIER CARDS ── *@ -
- @foreach (var tier in _tiers) - { -
-
-
- -
-
- @tier.Members thành viên +
+ @if (IsLoading) + { +
+ } + else if (!_levels.Any()) + { +
+
+

Chưa cấu hình cấp bậc

+

Thiết lập cấp bậc thành viên để xây dựng chương trình loyalty

+
+ } + else + { +
+ @foreach (var level in _levels) + { +
+
+
+ +
+

@level.Name

+

Level @level.Level

+
+
@level.MinExp.ToString("N0")
Min EXP
+
@level.MemberCount
Thành viên
+
-
@tier.Name
-
Từ @tier.MinPoints điểm • @tier.Discount giảm giá
-
- } -
- - @* ── BOTTOM ROW: Settings + Rewards ── *@ -
- - @* LEFT: Points Settings *@ -
-
-

- - Cài đặt điểm -

-
-
-
- - -
-
- - -
-
- - -
-
-
- Cho phép đổi điểm - -
-
- Tự động nâng hạng - -
-
-
+ }
- @* RIGHT: Rewards Catalog *@ -
-
-

- - Danh mục phần thưởng -

- Quản lý tất cả → -
-
- - - - - - - - - - - - @foreach (var rw in _rewards) - { - - - - - - - - } - -
Phần thưởngĐiểm cầnHạng tối thiểuĐã đổiTrạng thái
@rw.Name@rw.Points@rw.MinTier@rw.Redeemed lượt -
- - @(rw.Active ? "Hoạt động" : "Tạm dừng") -
-
+
+

Tổng quan

+
+
@_levels.Sum(l => l.MemberCount)
Tổng thành viên
+
@_levels.Count
Cấp bậc
-
+ }
@code { - private record TierDef(string Name, string Icon, string Color, string MinPoints, string Discount, string Members); - private readonly TierDef[] _tiers = new[] - { - new TierDef("Bronze", "award", "#CD7F32", "0", "5%", "124"), - new TierDef("Silver", "award", "#8B8B90", "500", "10%", "86"), - new TierDef("Gold", "crown", "#F59E0B", "2,000", "15%", "42"), - new TierDef("Diamond", "gem", "#8B5CF6", "5,000", "20%", "18"), - }; + private List _levels = new(); - private record RewardDef(string Name, string Points, string MinTier, string Redeemed, bool Active); - private readonly List _rewards = new() + protected override async Task OnInitializedAsync() { - new("Giảm 10% đơn hàng", "200", "Bronze", "342", true), - new("Miễn phí 1 ly cà phê", "500", "Silver", "128", true), - new("Combo bữa trưa miễn phí", "1,500", "Gold", "45", true), - new("Voucher 500K", "3,000", "Gold", "22", true), - new("Tiệc sinh nhật VIP", "5,000", "Diamond", "8", false), - }; + IsLoading = true; + try { _levels = await DataService.GetMembershipLevelsAsync(); } + catch { } finally { IsLoading = false; } + } + + private static string GetLevelColor(int l) => l switch { 1 => "#94A3B8", 2 => "#22C55E", 3 => "#3B82F6", 4 => "#F59E0B", _ => "#FF5C00" }; + private static string GetLevelBg(int l) => l switch { 1 => "rgba(148,163,184,0.1)", 2 => "rgba(34,197,94,0.1)", 3 => "rgba(59,130,246,0.1)", 4 => "rgba(245,158,11,0.1)", _ => "rgba(255,92,0,0.1)" }; + private static string GetLevelIcon(int l) => l switch { 1 => "user", 2 => "star", 3 => "award", 4 => "gem", _ => "crown" }; } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Finance/ExpenseManagement.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Finance/ExpenseManagement.razor index 25b4fd74..d16a36d2 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Finance/ExpenseManagement.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Finance/ExpenseManagement.razor @@ -1,147 +1,62 @@ @page "/admin/finance/expenses" @layout AdminLayout @inherits AdminBase +@inject PosDataService DataService +@using WebClientTpos.Client.Services -@* - EN: Expense Management — expense categories, add expense form, approval status. - VI: Quản lý chi phí — danh mục chi phí, thêm chi phí, trạng thái duyệt. - Design: pencil-design/src/pages/tPOS/admin/expense-management.pen -*@ +Chi phí — GoodGo Admin -Quản lý chi phí — GoodGo Admin - -@* ═══ TOP BAR ═══ *@

Quản lý chi phí

-

Tháng 02/2025 • @_expenses.Length khoản chi

-
-
- - +

@_txns.Count giao dịch

-@* ═══ TABS ═══ *@ -
- - - - -
- -@* ═══ CONTENT ═══ *@ -
- - @* KPI Row *@ -
-
-
-
- -
-
-
142.5M
-
Tổng chi phí tháng
+
+ @if (IsLoading) + { +
+ } + else if (!_txns.Any()) + { +
+
+

Chưa có giao dịch chi phí

+

Giao dịch sẽ hiển thị khi có hoạt động ví

-
-
-
- -
-
-
18.2M
-
Chờ duyệt
-
-
-
-
- -
-
-
68.0M
-
Chi phí định kỳ
-
-
- - @* Expense Table *@ -
-
-

- - Danh sách chi phí -

-
-
- - - - - - - - - - - - - @foreach (var item in _expenses) + } + else + { +
+

Giao dịch gần đây

+
+
Mô tảDanh mụcCửa hàngNgàyTrạng tháiSố tiền
+ + + + + @foreach (var t in _txns) { - - - - - - - + + + + } - -
Mô tảSố tiềnNgày
@item.Desc@item.Category@item.Store@item.Date -
- - @item.Status -
-
-@item.Amount
@(t.Description ?? t.ItemName ?? "—")@(t.Amount < 0 ? "-" : "+")@Math.Abs(t.Amount).ToString("N0")₫@t.CreatedAt.ToString("dd/MM HH:mm")
+ +
-
+ }
@code { - private string _tab = "all"; + private List _txns = new(); - private string GetStatusClass(string status) => status switch + protected override async Task OnInitializedAsync() { - "Đã duyệt" => "admin-status-badge--online", - "Chờ duyệt" => "admin-status-badge--setup", - "Từ chối" => "admin-status-badge--offline", - _ => "" - }; - - private record ExpenseRow(string Desc, string Category, string Store, string Date, string Status, string Amount); - private readonly ExpenseRow[] _expenses = new[] - { - new ExpenseRow("Nhập cà phê Arabica", "Nguyên vật liệu", "Coffee House Q1", "12/02", "Đã duyệt", "8.5M"), - new ExpenseRow("Tiền thuê T2/2025", "Mặt bằng", "Nhà hàng Q3", "01/02", "Đã duyệt", "15.0M"), - new ExpenseRow("Sửa máy pha cà phê", "Bảo trì", "Coffee House Q1", "10/02", "Chờ duyệt", "3.2M"), - new ExpenseRow("Tiền điện T1/2025", "Tiện ích", "Nhà hàng Q3", "05/02", "Đã duyệt", "4.8M"), - new ExpenseRow("Quảng cáo Facebook", "Marketing", "Tất cả", "08/02", "Chờ duyệt", "6.2M"), - new ExpenseRow("Đồng phục nhân viên", "Vận hành", "Coffee House Q1", "07/02", "Đã duyệt", "2.4M"), - new ExpenseRow("Mua bàn ghế mới", "Trang thiết bị", "Nhà hàng Q3", "09/02", "Chờ duyệt", "8.8M"), - new ExpenseRow("In menu mới", "Marketing", "Tất cả", "06/02", "Từ chối", "1.5M"), - }; + IsLoading = true; + try { _txns = await DataService.GetWalletTransactionsAsync(100); } + catch { } finally { IsLoading = false; } + } } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Finance/FinancialOverview.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Finance/FinancialOverview.razor index c13c5996..5676e5c7 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Finance/FinancialOverview.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Finance/FinancialOverview.razor @@ -1,178 +1,108 @@ @page "/admin/finance" @layout AdminLayout @inherits AdminBase - -@* - EN: Financial Overview — revenue KPIs, expense breakdown, recent transactions. - VI: Tổng quan tài chính — KPI doanh thu, phân tích chi phí, giao dịch gần đây. - Design: pencil-design/src/pages/tPOS/admin/financial-overview.pen -*@ +@inject PosDataService DataService +@using WebClientTpos.Client.Services Tài chính — GoodGo Admin -@* ═══ TOP BAR ═══ *@

Tổng quan tài chính

-

Tháng 02/2025 • Tất cả cửa hàng

-
-
- - +

@_orders.Count đơn hàng • @_wallets.Count ví

-@* ═══ CONTENT ═══ *@ -
- - @* ── KPI ROW ── *@ -
-
-
-
- -
-
- - +18.2% -
+
+
+
+
+
+ @FormatVND(_totalRevenue) + Tổng doanh thu
-
256.8M
-
Tổng doanh thu
-
-
-
- -
-
- - +5.3% -
+
+
+
+ @FormatVND(_totalExpense) + Tổng chi phí
-
142.5M
-
Tổng chi phí
-
-
-
- -
-
- - +24.1% -
+
+
+
+ @_orders.Count + Tổng đơn hàng
-
114.3M
-
Lợi nhuận ròng
-
-
-
- -
+
+
+
+ @FormatVND(_wallets.Sum(w => w.Balance)) + Số dư ví
-
44.5%
-
Biên lợi nhuận
- @* ── BOTTOM: Expense Breakdown + Transactions ── *@ -
- - @* ── LEFT: Expense Breakdown ── *@ -
-
-

- - Phân tích chi phí -

- Chi tiết → -
-
- @foreach (var exp in _expenses) - { -
-
-
- @exp.Category -
-
- @exp.Amount - @exp.Percent -
-
- } -
+ @if (IsLoading) + { +
+ } + else if (!_orders.Any()) + { +
+
+

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

- - @* ── RIGHT: Recent Transactions ── *@ -
-
-

- - Giao dịch gần đây -

-
+ } + else + { +
+

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

- - - - - - - - - - +
Mã GDMô tảCửa hàngLoạiSố tiền
+ + + + + + - @foreach (var tx in _transactions) + @foreach (var o in _orders.Take(20)) { - - - - - - + + + + + }
IDSố tiềnTrạng tháiNgày
@tx.Code@tx.Desc@tx.Store -
- @(tx.IsIncome ? "Thu" : "Chi") -
-
@tx.Amount
@o.Id.ToString()[..8]@FormatVND(o.TotalAmount)@(o.Status ?? "—")@o.CreatedAt.ToString("dd/MM HH:mm")
-
+ }
@code { - private record ExpenseItem(string Category, string Amount, string Percent, string Color); - private readonly ExpenseItem[] _expenses = new[] - { - new ExpenseItem("Nguyên vật liệu", "52.4M", "36.8%", "#FF5C00"), - new ExpenseItem("Lương nhân viên", "38.6M", "27.1%", "#3B82F6"), - new ExpenseItem("Thuê mặt bằng", "28.0M", "19.6%", "#8B5CF6"), - new ExpenseItem("Tiện ích (điện, nước)", "12.5M", "8.8%", "#F59E0B"), - new ExpenseItem("Marketing", "6.2M", "4.3%", "#EC4899"), - new ExpenseItem("Khác", "4.8M", "3.4%", "#22C55E"), - }; + private List _orders = new(); + private List _wallets = new(); + private decimal _totalRevenue, _totalExpense; - private record TransactionItem(string Code, string Desc, string Store, bool IsIncome, string Amount); - private readonly TransactionItem[] _transactions = new[] + protected override async Task OnInitializedAsync() { - new TransactionItem("GD-2851", "Doanh thu bán hàng", "Coffee House Q1", true, "+8.5M"), - new TransactionItem("GD-2850", "Nhập nguyên liệu", "Coffee House Q1", false, "-3.2M"), - new TransactionItem("GD-2849", "Doanh thu bán hàng", "Nhà hàng Q3", true, "+12.4M"), - new TransactionItem("GD-2848", "Tiền thuê T2/2025", "Nhà hàng Q3", false, "-15.0M"), - new TransactionItem("GD-2847", "Doanh thu bán hàng", "Coffee House Q1", true, "+6.8M"), - new TransactionItem("GD-2846", "Lương nhân viên", "Tất cả", false, "-38.6M"), - }; + IsLoading = true; + try + { + _orders = await DataService.GetOrdersAsync(); + _wallets = await DataService.GetWalletsAsync(); + _totalRevenue = _wallets.Sum(w => w.TotalIncome); + _totalExpense = _wallets.Sum(w => w.TotalExpense); + } + catch { } finally { IsLoading = false; } + } + + private static string FormatVND(decimal val) => val.ToString("N0") + "₫"; } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Finance/RevenueAnalytics.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Finance/RevenueAnalytics.razor index ee5cf0a5..4fe320f7 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Finance/RevenueAnalytics.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Finance/RevenueAnalytics.razor @@ -1,205 +1,86 @@ @page "/admin/finance/revenue" @layout AdminLayout @inherits AdminBase - -@* - EN: Revenue Analytics — revenue by store, category, time period, top products. - VI: Phân tích doanh thu — doanh thu theo cửa hàng, danh mục, kỳ, sản phẩm bán chạy. - Design: pencil-design/src/pages/tPOS/admin/revenue-analytics.pen -*@ +@inject PosDataService DataService +@using WebClientTpos.Client.Services Phân tích doanh thu — GoodGo Admin -@* ═══ TOP BAR ═══ *@

Phân tích doanh thu

-

Tháng 02/2025 • So sánh theo cửa hàng

+

@_orders.Count đơn hàng • @FormatVND(_orders.Sum(o => o.TotalAmount))

- - - +
-@* ═══ CONTENT ═══ *@ -
- - @* KPI Row *@ -
-
-
-
- -
-
- - +18.2% -
-
-
256.8M
-
Tổng doanh thu
+
+ @if (IsLoading) + { +
+ } + else if (!_orders.Any()) + { +
+
+

Chưa có đơn hàng

-
-
-
- -
-
-
132K
-
Giá trị đơn TB
+ } + else + { +
+
@FormatVND(_orders.Sum(o => o.TotalAmount))Tổng doanh thu
+
@_orders.CountSố đơn hàng
+
@FormatVND(_orders.Any() ? _orders.Average(o => o.TotalAmount) : 0)Trung bình/đơn
-
-
-
- -
-
-
Nhà hàng Q3
-
Cửa hàng dẫn đầu
-
-
-
-
- -
-
- - +12.4% -
-
-
+18.2%
-
Tỷ lệ tăng trưởng
-
-
- - @* Revenue by Store + Revenue by Category *@ -
- @* Revenue by Store *@ -
-
-

- - Doanh thu theo cửa hàng -

-
-
- @foreach (var s in _storeRevenue) - { -
-
- -
-
-
@s.Name
-
@s.Orders đơn hàng
-
-
-
@s.Revenue
-
@s.Growth
-
-
- } -
-
- - @* Revenue by Category *@ -
-
-

- - Doanh thu theo danh mục -

-
-
- @foreach (var c in _categoryRevenue) - { -
-
-
- @c.Category -
-
- @c.Amount - @c.Percent -
-
- } -
-
-
- - @* Top-selling Products Table *@ -
-
-

- - Sản phẩm bán chạy nhất -

-
-
- - - - - - - - - - - - @foreach (var p in _topProducts) +
+

Chi tiết đơn hàng

+
+
#Sản phẩmDanh mụcSố lượng bánDoanh thu
+ + + + + + @foreach (var o in _orders.Take(50)) { - - - - - - + + + + + } - -
IDSố tiềnTrạng tháiNgày
@p.Rank@p.Name@p.Category@p.Sold@p.Revenue
@o.Id.ToString()[..8]@FormatVND(o.TotalAmount)@(o.Status ?? "—")@o.CreatedAt.ToString("dd/MM HH:mm")
+ +
-
+ }
@code { - private record StoreRevItem(string Name, string Icon, string IconColor, string BgColor, string Revenue, string Orders, string Growth); - private readonly StoreRevItem[] _storeRevenue = new[] - { - new StoreRevItem("Nhà hàng Q3", "utensils", "#3B82F6", "rgba(59,130,246,0.125)", "128.4M", "842", "+22.1%"), - new StoreRevItem("Coffee House Q1", "coffee", "#FF5C00", "rgba(255,92,0,0.125)", "96.2M", "1,024", "+15.3%"), - new StoreRevItem("Karaoke Star Q7", "mic", "#8B5CF6", "rgba(139,92,246,0.125)", "32.2M", "156", "+8.7%"), - }; + private List _orders = new(); + private List _shops = new(); + private Guid? _selectedShopId; - private record CategoryRevItem(string Category, string Amount, string Percent, string Color); - private readonly CategoryRevItem[] _categoryRevenue = new[] + protected override async Task OnInitializedAsync() { - new CategoryRevItem("Đồ uống", "108.5M", "42.2%", "#FF5C00"), - new CategoryRevItem("Đồ ăn", "82.3M", "32.1%", "#3B82F6"), - new CategoryRevItem("Dịch vụ phòng", "32.2M", "12.5%", "#8B5CF6"), - new CategoryRevItem("Khác", "33.8M", "13.2%", "#22C55E"), - }; + IsLoading = true; + try { _shops = await DataService.GetShopsAsync(); _orders = await DataService.GetOrdersAsync(); } + catch { } finally { IsLoading = false; } + } - private record TopProductItem(string Rank, string Name, string Category, string Sold, string Revenue); - private readonly TopProductItem[] _topProducts = new[] + private async Task OnShopFilterChanged(ChangeEventArgs e) { - new TopProductItem("1", "Espresso", "Cà phê", "1,024", "35.8M"), - new TopProductItem("2", "Cappuccino", "Cà phê", "856", "38.5M"), - new TopProductItem("3", "Phở bò đặc biệt", "Đồ ăn", "742", "44.5M"), - new TopProductItem("4", "Latte", "Cà phê", "631", "30.9M"), - new TopProductItem("5", "Trà sen vàng", "Trà", "520", "21.8M"), - }; + _selectedShopId = Guid.TryParse(e.Value?.ToString(), out var id) ? id : null; + IsLoading = true; + try { _orders = await DataService.GetOrdersAsync(_selectedShopId); } + catch { } finally { IsLoading = false; } + } + + private static string FormatVND(decimal val) => val.ToString("N0") + "₫"; } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Finance/TaxConfiguration.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Finance/TaxConfiguration.razor index adc84741..8c83937e 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Finance/TaxConfiguration.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Finance/TaxConfiguration.razor @@ -1,176 +1,75 @@ @page "/admin/finance/tax" @layout AdminLayout @inherits AdminBase - -@* - EN: Tax Configuration — tax rate settings, categories, exemptions, report preview. - VI: Cấu hình thuế — cài đặt thuế suất, danh mục, miễn thuế, xem trước báo cáo. - Design: pencil-design/src/pages/tPOS/admin/tax-configuration.pen -*@ +@inject PosDataService DataService +@using WebClientTpos.Client.Services Cấu hình thuế — GoodGo Admin -@* ═══ TOP BAR ═══ *@

Cấu hình thuế

-

Quản lý thuế suất & danh mục thuế

-
-
- - +

Quản lý thuế theo cửa hàng

-@* ═══ CONTENT ═══ *@ -
- - @* Tax Rate Settings + Tax Categories *@ -
- - @* LEFT: Tax Rate Settings *@ -
-
-

- - Cài đặt thuế suất -

-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - @* RIGHT: Tax Categories *@ -
-
-

- - Danh mục thuế -

- -
-
- @foreach (var cat in _taxCategories) +
+ @if (IsLoading) + { +
+ } + else + { +
+

Cửa hàng

+
+ @if (!_shops.Any()) { -
-
- +

Chưa có cửa hàng

+ } + else + { + @foreach (var shop in _shops) + { +
-
@cat.Name
-
@cat.Desc
+ @shop.Name + @(shop.Category ?? "") +
+
+ VAT: 10% +
-
@cat.Rate
-
+ } }
-
- - @* Tax-exempt Products + Tax Report Preview *@ -
- - @* LEFT: Tax-exempt Products *@ -
-
-

- - Sản phẩm miễn thuế -

- -
-
- - - - - - - - - - @foreach (var item in _exemptProducts) - { - - - - - - } - -
Sản phẩmDanh mụcLý do
@item.Name@item.Category@item.Reason
-
-
- - @* RIGHT: Tax Report Preview *@ -
-
-

- - Xem trước báo cáo thuế -

-
+
+

Loại thuế

- @foreach (var line in _taxPreview) + @foreach (var tax in new[] { ("VAT", "10%", "Thuế giá trị gia tăng"), ("TNCN", "Tự động", "Thuế thu nhập cá nhân"), ("Phí dịch vụ", "5%", "Service charge") }) { -
- @line.Label - @line.Value +
+
+ @tax.Item1 + @tax.Item3 +
+ @tax.Item2
}
-
+ }
@code { - private record TaxCategory(string Name, string Desc, string Rate, string Icon, string Color); - private readonly TaxCategory[] _taxCategories = new[] - { - new TaxCategory("VAT", "Thuế giá trị gia tăng", "10%", "receipt", "#FF5C00"), - new TaxCategory("Phí dịch vụ", "Áp dụng cho dine-in", "5%", "utensils", "#3B82F6"), - new TaxCategory("TNCN", "Thuế thu nhập cá nhân", "10%", "user", "#8B5CF6"), - new TaxCategory("Đồ uống có cồn", "Thuế tiêu thụ đặc biệt", "35%", "wine", "#EF4444"), - }; + private List _shops = new(); - private record ExemptProduct(string Name, string Category, string Reason); - private readonly ExemptProduct[] _exemptProducts = new[] + protected override async Task OnInitializedAsync() { - new ExemptProduct("Nước lọc", "Đồ uống", "Miễn VAT theo QĐ"), - new ExemptProduct("Cơm trắng", "Đồ ăn", "Lương thực thiết yếu"), - new ExemptProduct("Trà đá", "Đồ uống", "Đồ uống cơ bản"), - }; - - private record TaxPreviewLine(string Label, string Value, bool IsBold); - private readonly TaxPreviewLine[] _taxPreview = new[] - { - new TaxPreviewLine("Doanh thu trước thuế", "256.8M", false), - new TaxPreviewLine("VAT (10%)", "25.7M", false), - new TaxPreviewLine("Phí dịch vụ (5%)", "8.4M", false), - new TaxPreviewLine("Thuế TTĐB", "2.1M", false), - new TaxPreviewLine("Tổng thuế phải nộp", "36.2M", true), - new TaxPreviewLine("Doanh thu sau thuế", "220.6M", true), - }; + IsLoading = true; + try { _shops = await DataService.GetShopsAsync(); } + catch { } finally { IsLoading = false; } + } } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Inventory/PurchaseOrders.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Inventory/PurchaseOrders.razor index 266bc451..8345f1d6 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Inventory/PurchaseOrders.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Inventory/PurchaseOrders.razor @@ -1,147 +1,79 @@ -@page "/admin/inventory/orders" +@page "/admin/inventory/purchase-orders" @layout AdminLayout @inherits AdminBase +@inject PosDataService DataService +@using WebClientTpos.Client.Services -@* - EN: Purchase Orders — PO list with status, create PO, filter by supplier/status. - VI: Đơn đặt hàng — danh sách PO, tạo PO, lọc theo NCC/trạng thái. - Design: pencil-design/src/pages/tPOS/admin/purchase-orders.pen -*@ +Đơn nhập hàng — GoodGo Admin -Đơn đặt hàng — GoodGo Admin - -@* ═══ TOP BAR ═══ *@
-

Đơn đặt hàng

-

@_orders.Length đơn hàng • Tháng 02/2025

+

Đơn nhập hàng

+

@_txns.Count giao dịch nhập kho

- - +
-@* ═══ TABS ═══ *@ -
- - - - -
- -@* ═══ CONTENT ═══ *@ -
- - @* KPI Row *@ -
-
-
-
- -
-
-
@_orders.Length
-
Tổng đơn hàng
+
+ @if (IsLoading) + { +
+ } + else if (!_txns.Any()) + { +
+
+

Chưa có đơn nhập hàng

+

Tạo đơn nhập hàng để quản lý tồn kho

-
-
-
- -
-
-
3
-
Chờ xác nhận
-
-
-
-
- -
-
-
68.5M
-
Tổng giá trị PO
-
-
- - @* PO Table *@ -
-
-

- - Danh sách đơn đặt hàng -

-
-
- - - - - - - - - - - - - @foreach (var order in _orders) + } + else + { +
+
+
Mã PONhà cung cấpSố mặt hàngNgày đặtTrạng tháiTổng tiền
+ + + + + + @foreach (var t in _txns) { - - - - - - - + + + + + } - -
LoạiSố lượngGhi chúNgày
@order.Code@order.Supplier@order.Items mặt hàng@order.Date -
- - @order.Status -
-
@order.Total
@(t.TransactionType ?? "—")@(t.QuantityChange > 0 ? "+" : "")@t.QuantityChange@(t.Reason ?? "—")@t.CreatedAt.ToString("dd/MM HH:mm")
+ +
-
+ }
@code { - private string _tab = "all"; + private List _txns = new(); + private List _shops = new(); + private Guid? _selectedShopId; - private string GetPOStatusClass(string status) => status switch + protected override async Task OnInitializedAsync() { - "Đã giao" => "admin-status-badge--online", - "Đã xác nhận" => "admin-status-badge--paused", - "Chờ xác nhận" => "admin-status-badge--setup", - _ => "" - }; + IsLoading = true; + try { _shops = await DataService.GetShopsAsync(); _txns = await DataService.GetInventoryTransactionsAsync(); } + catch { } finally { IsLoading = false; } + } - private record POItem(string Code, string Supplier, string Items, string Date, string Status, string Total); - private readonly POItem[] _orders = new[] + private async Task OnShopFilterChanged(ChangeEventArgs e) { - new POItem("PO-0042", "Cà phê Trung Nguyên", "5", "12/02/2025", "Chờ xác nhận", "18.5M"), - new POItem("PO-0041", "Vinamilk", "3", "10/02/2025", "Đã xác nhận", "8.2M"), - new POItem("PO-0040", "Đại Tân Phát", "8", "09/02/2025", "Chờ xác nhận", "12.4M"), - new POItem("PO-0039", "TH True Milk", "2", "08/02/2025", "Đã giao", "4.8M"), - new POItem("PO-0038", "Cà phê Trung Nguyên", "4", "06/02/2025", "Đã giao", "14.2M"), - new POItem("PO-0037", "Metro Cash & Carry", "12", "05/02/2025", "Đã xác nhận", "6.5M"), - new POItem("PO-0036", "Saigon Food", "6", "03/02/2025", "Chờ xác nhận", "5.8M"), - new POItem("PO-0035", "Vinamilk", "3", "01/02/2025", "Đã giao", "3.9M"), - }; + _selectedShopId = Guid.TryParse(e.Value?.ToString(), out var id) ? id : null; + IsLoading = true; + try { _txns = await DataService.GetInventoryTransactionsAsync(_selectedShopId); } + catch { } finally { IsLoading = false; } + } } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Inventory/StockTransfer.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Inventory/StockTransfer.razor index a4c034ae..5bc209df 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Inventory/StockTransfer.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Inventory/StockTransfer.razor @@ -1,174 +1,49 @@ -@page "/admin/inventory/transfer" +@page "/admin/inventory/transfers" @layout AdminLayout @inherits AdminBase - -@* - EN: Stock Transfer — transfer between stores, transfer history, pending transfers. - VI: Chuyển kho — chuyển hàng giữa cửa hàng, lịch sử, đang chờ. - Design: pencil-design/src/pages/tPOS/admin/stock-transfer.pen -*@ +@inject PosDataService DataService +@using WebClientTpos.Client.Services Chuyển kho — GoodGo Admin -@* ═══ TOP BAR ═══ *@

Chuyển kho

-

Chuyển hàng giữa các cửa hàng

-
-
- +

Quản lý chuyển hàng giữa các cửa hàng

-@* ═══ CONTENT ═══ *@ -
- - @* ── LEFT: Transfer Form ── *@ -
-
-

- - Tạo phiếu chuyển kho -

-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- -
-
- - @* ── RIGHT: Transfer History ── *@ -
- - @* Pending Transfers *@ -
-
-

- - Đang chờ xử lý - 2 -

-
-
- @foreach (var t in _pendingTransfers) +
+ @if (IsLoading) + { +
+ } + else + { +
+
+

Chuyển kho

+

Tạo yêu cầu chuyển hàng giữa các cửa hàng

+
+ @foreach (var shop in _shops) { -
-
- -
-
@t.Item — @t.Qty @t.Unit
-
@t.From → @t.To
-
-
-
- - Chờ xác nhận -
+
+
@shop.Name
+
@(shop.Category ?? "—")
}
- - @* Transfer History Table *@ -
-
-

- - Lịch sử chuyển kho -

-
-
- - - - - - - - - - - - - - @foreach (var h in _transferHistory) - { - - - - - - - - - - } - -
Mã phiếuSản phẩmSLTừĐếnNgàyTrạng thái
@h.Code@h.Item@h.Qty @h.Unit@h.From@h.To@h.Date -
- - Hoàn thành -
-
-
-
-
+ }
@code { - private record PendingTransfer(string Item, string Qty, string Unit, string From, string To); - private readonly PendingTransfer[] _pendingTransfers = new[] - { - new PendingTransfer("Cà phê Arabica", "10", "kg", "Coffee House Q1", "Nhà hàng Q3"), - new PendingTransfer("Ly giấy 12oz", "2", "thùng", "Nhà hàng Q3", "Coffee House Q1"), - }; + private List _shops = new(); - private record TransferHistory(string Code, string Item, string Qty, string Unit, string From, string To, string Date); - private readonly TransferHistory[] _transferHistory = new[] + protected override async Task OnInitializedAsync() { - new TransferHistory("CK-0028", "Sữa tươi", "5", "thùng", "Nhà hàng Q3", "Coffee House Q1", "10/02"), - new TransferHistory("CK-0027", "Trà Oolong", "3", "kg", "Coffee House Q1", "Nhà hàng Q3", "08/02"), - new TransferHistory("CK-0026", "Đường", "10", "kg", "Nhà hàng Q3", "Coffee House Q1", "06/02"), - new TransferHistory("CK-0025", "Ly giấy 12oz", "4", "thùng", "Coffee House Q1", "Karaoke Q7", "04/02"), - new TransferHistory("CK-0024", "Cà phê Robusta", "8", "kg", "Nhà hàng Q3", "Coffee House Q1", "02/02"), - }; + IsLoading = true; + try { _shops = await DataService.GetShopsAsync(); } + catch { } finally { IsLoading = false; } + } } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Inventory/SupplierManagement.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Inventory/SupplierManagement.razor index 1c588e52..bbc57a4b 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Inventory/SupplierManagement.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Inventory/SupplierManagement.razor @@ -1,92 +1,26 @@ @page "/admin/inventory/suppliers" @layout AdminLayout @inherits AdminBase - -@* - EN: Supplier Management — supplier list, add supplier, order history, ratings. - VI: Quản lý nhà cung cấp — danh sách NCC, thêm NCC, lịch sử đặt, đánh giá. - Design: pencil-design/src/pages/tPOS/admin/supplier-management.pen -*@ +@inject PosDataService DataService +@using WebClientTpos.Client.Services Nhà cung cấp — GoodGo Admin -@* ═══ TOP BAR ═══ *@

Nhà cung cấp

-

@_suppliers.Length nhà cung cấp • Quản lý & đánh giá

-
-
- - +

Quản lý nhà cung cấp

-@* ═══ CONTENT ═══ *@ -
- @foreach (var sup in _suppliers) - { -
-
-
-
@sup.Initials
-
-
@sup.Name
-
@sup.Category
-
-
-
- - @(sup.Active ? "Hoạt động" : "Ngưng") -
-
- - @* Contact info *@ -
-
- - @sup.Phone -
-
- - @sup.Email -
-
- - @* Stats *@ -
-
-
@sup.TotalOrders
-
Đơn hàng
-
-
-
@sup.TotalValue
-
Tổng tiền
-
-
-
@sup.Rating ★
-
Đánh giá
-
-
-
- } +
+
+
+

Quản lý nhà cung cấp

+

Tính năng đang phát triển — sẽ sớm ra mắt

+ Coming Soon +
@code { - private record SupplierItem(string Name, string Initials, string Color, string Category, string Phone, string Email, string TotalOrders, string TotalValue, double Rating, bool Active); - private readonly SupplierItem[] _suppliers = new[] - { - new SupplierItem("Cà phê Trung Nguyên", "TN", "#FF5C00", "Nguyên liệu cà phê", "028 3820 1234", "order@trungnguyen.vn", "24", "86.5M", 4.8, true), - new SupplierItem("Vinamilk", "VM", "#3B82F6", "Sữa & Dairy", "028 5413 0000", "b2b@vinamilk.vn", "18", "42.3M", 4.5, true), - new SupplierItem("Metro Cash & Carry", "MC", "#22C55E", "Tổng hợp", "028 3742 0888", "supply@metro.vn", "32", "128.6M", 4.2, true), - new SupplierItem("Đại Tân Phát", "ĐT", "#8B5CF6", "Bao bì & Dụng cụ", "028 3850 4567", "sales@daitanphat.vn", "15", "28.4M", 4.0, true), - new SupplierItem("TH True Milk", "TH", "#EC4899", "Sữa & Dairy", "028 3930 1122", "order@thtruemilk.vn", "12", "18.8M", 4.6, true), - new SupplierItem("Saigon Food", "SF", "#F59E0B", "Thực phẩm đông lạnh", "028 3820 5678", "info@saigonfood.vn", "8", "15.2M", 3.8, false), - }; } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Product/MenuBuilder.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Product/MenuBuilder.razor index acaa882d..b78439e0 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Product/MenuBuilder.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Product/MenuBuilder.razor @@ -1,143 +1,104 @@ -@page "/admin/menu" +@page "/admin/products/menu-builder" @layout AdminLayout @inherits AdminBase - -@* - EN: Menu builder — drag-drop menu sections, items reorder, visibility toggle. - VI: Xây dựng menu — kéo thả section menu, sắp xếp món, ẩn/hiện. - Design: pencil-design/src/pages/tPOS/admin/menu-builder.pen -*@ +@inject PosDataService DataService +@using WebClientTpos.Client.Services Menu Builder — GoodGo Admin -@* ═══ TOP BAR ═══ *@
-

Xây dựng Menu

-

Coffee House Q1 • Kéo thả để sắp xếp

+

Menu Builder

+

@_categories.Count danh mục • @_products.Count sản phẩm

- - +
-@* ═══ CONTENT ═══ *@
- - @* LEFT: Menu Sections *@ -
- - @foreach (var section in _menuSections) - { -
-
-
- -

- - @(section.Name) -

- @(section.Items.Length) -
-
- - -
-
-
- @foreach (var item in section.Items) - { -
- -
- -
-
-
@item.Name
-
@item.Variants biến thể
-
- @item.Price - -
- } -
+ @* Category sidebar *@ +
+
+

Danh mục

+
+ + @foreach (var cat in _categories) + { + var catName = cat.Name; + var count = _products.Count(p => string.Equals(p.CategoryName, catName, StringComparison.OrdinalIgnoreCase)); + + }
- } +
- @* RIGHT: Quick Stats *@ -
-
-
-

- - Thống kê menu -

+ @* Products grid *@ +
+ @if (IsLoading) + { +
+ } + else if (!FilteredProducts.Any()) + { +

Chưa có sản phẩm

+ } + else + { +
+ @foreach (var p in FilteredProducts) + { +
+
+
+
@p.Name
+
@(p.CategoryName ?? "—")
+
@p.Price.ToString("N0")₫
+
+
+ }
-
-
- Tổng danh mục - @_menuSections.Length -
-
- Tổng sản phẩm - @_menuSections.Sum(s => s.Items.Length) -
-
- Đang hiển thị - @_menuSections.Sum(s => s.Items.Count(i => i.Visible)) -
-
- Đang ẩn - @_menuSections.Sum(s => s.Items.Count(i => !i.Visible)) -
-
-
- -
-
- -
- Kéo thả để sắp xếp thứ tự danh mục và sản phẩm. Thay đổi sẽ phản ánh trực tiếp trên POS. -
-
-
+ }
@code { - private record MenuItem(string Name, string Price, string Variants, bool Visible); - private record MenuSection(string Name, string Icon, string Color, bool Visible, MenuItem[] Items); - private readonly MenuSection[] _menuSections = new[] + private List _products = new(); + private List _categories = new(); + private List _shops = new(); + private string? _selectedCategory; + private Guid? _selectedShopId; + + private IEnumerable FilteredProducts => _selectedCategory == null + ? _products + : _products.Where(p => string.Equals(p.CategoryName, _selectedCategory, StringComparison.OrdinalIgnoreCase)); + + protected override async Task OnInitializedAsync() { - new MenuSection("Cà phê", "coffee", "#FF5C00", true, new[] + IsLoading = true; + try { - new MenuItem("Espresso", "35K", "3", true), - new MenuItem("Cappuccino", "45K", "3", true), - new MenuItem("Latte", "49K", "3", true), - new MenuItem("Americano", "39K", "2", true), - new MenuItem("Mocha", "52K", "3", false), - }), - new MenuSection("Trà", "leaf", "#22C55E", true, new[] + _shops = await DataService.GetShopsAsync(); + _products = await DataService.GetAllProductsAsync(); + _categories = await DataService.GetAllCategoriesAsync(); + } + catch { } finally { IsLoading = false; } + } + + private async Task OnShopFilterChanged(ChangeEventArgs e) + { + _selectedShopId = Guid.TryParse(e.Value?.ToString(), out var id) ? id : null; + IsLoading = true; + try { - new MenuItem("Trà sen vàng", "42K", "2", true), - new MenuItem("Trà đào cam sả", "39K", "2", true), - new MenuItem("Trà sữa", "38K", "3", true), - }), - new MenuSection("Đồ ăn", "utensils", "#F59E0B", true, new[] - { - new MenuItem("Croissant", "35K", "1", true), - new MenuItem("Bánh mì", "28K", "2", true), - new MenuItem("Tiramisu", "45K", "1", false), - }), - }; + _products = await DataService.GetAllProductsAsync(_selectedShopId); + _categories = await DataService.GetAllCategoriesAsync(_selectedShopId); + } + catch { } finally { IsLoading = false; } + } + + private void SelectCategory(string? cat) { _selectedCategory = cat; } } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Product/ModifierGroups.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Product/ModifierGroups.razor index 066b23fd..a8f68261 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Product/ModifierGroups.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Product/ModifierGroups.razor @@ -1,128 +1,21 @@ -@page "/admin/modifiers" +@page "/admin/products/modifiers" @layout AdminLayout @inherits AdminBase -@* - EN: Modifier groups — manage add-on groups (Sugar, Ice, Topping) with options & pricing. - VI: Nhóm modifier — quản lý nhóm tùy chọn (Đường, Đá, Topping) với options & giá. - Design: pencil-design/src/pages/tPOS/admin/modifier-groups.pen -*@ +Nhóm tùy chọn — GoodGo Admin -Nhóm Modifier — GoodGo Admin - -@* ═══ TOP BAR ═══ *@
-

Nhóm Modifier

-

Quản lý tùy chọn thêm cho sản phẩm

-
-
- +

Nhóm tùy chỉnh

+

Quản lý topping, size, đường/đá

-@* ═══ CONTENT ═══ *@ -
- - @foreach (var group in _modifierGroups) - { -
-
-
-

- - @group.Name -

- @group.Options.Length tùy chọn - - @(group.Required ? "Bắt buộc" : "Không bắt buộc") • @(group.Multi ? "Nhiều lựa chọn" : "Một lựa chọn") - -
-
- - -
-
-
- - - - - - - - - - - @foreach (var opt in group.Options) - { - - - - - - - } - -
Tùy chọnGiá thêmMặc địnhTrạng thái
@opt.Name - @(opt.Price == "0" ? "Miễn phí" : $"+{opt.Price}") - - @if (opt.IsDefault) - { - Mặc định - } - - -
-
-
- } +
+
+
+

Nhóm tùy chỉnh

+

Tính năng đang phát triển — sẽ sớm ra mắt

+ Coming Soon +
- -@code { - private bool _showCreate = false; - - private record ModOption(string Name, string Price, bool IsDefault, bool Active); - private record ModGroup(string Name, string Icon, string Color, bool Required, bool Multi, ModOption[] Options); - private readonly ModGroup[] _modifierGroups = new[] - { - new ModGroup("Mức đường", "droplet", "#FF5C00", true, false, new[] - { - new ModOption("100% đường", "0", true, true), - new ModOption("70% đường", "0", false, true), - new ModOption("50% đường", "0", false, true), - new ModOption("30% đường", "0", false, true), - new ModOption("Không đường", "0", false, true), - }), - new ModGroup("Mức đá", "snowflake", "#3B82F6", true, false, new[] - { - new ModOption("Đá bình thường", "0", true, true), - new ModOption("Ít đá", "0", false, true), - new ModOption("Nhiều đá", "0", false, true), - new ModOption("Không đá", "0", false, true), - }), - new ModGroup("Topping", "layers", "#8B5CF6", false, true, new[] - { - new ModOption("Trân châu đen", "8K", false, true), - new ModOption("Trân châu trắng", "8K", false, true), - new ModOption("Thạch", "10K", false, true), - new ModOption("Kem phô mai", "15K", false, true), - new ModOption("Shot espresso", "12K", false, true), - new ModOption("Sữa tươi", "5K", false, true), - }), - new ModGroup("Loại sữa", "milk", "#22C55E", false, false, new[] - { - new ModOption("Sữa đặc", "0", true, true), - new ModOption("Sữa tươi", "5K", false, true), - new ModOption("Sữa yến mạch", "12K", false, true), - new ModOption("Sữa hạnh nhân", "12K", false, false), - }), - }; -} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Product/PricingRules.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Product/PricingRules.razor index 572682df..714c5dcb 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Product/PricingRules.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Product/PricingRules.razor @@ -1,104 +1,71 @@ -@page "/admin/pricing" +@page "/admin/products/pricing" @layout AdminLayout @inherits AdminBase +@inject PosDataService DataService +@using WebClientTpos.Client.Services -@* - EN: Pricing rules — time-based, happy hour, combo, loyalty discounts. - VI: Quy tắc giá — theo giờ, happy hour, combo, giảm giá loyalty. - Design: pencil-design/src/pages/tPOS/admin/pricing-rules.pen -*@ +Chiến lược giá — GoodGo Admin -Quy tắc giá — GoodGo Admin - -@* ═══ TOP BAR ═══ *@
-

Quy tắc giá

-

Quản lý khuyến mãi, combo, giá theo giờ

-
-
- +

Chiến lược giá & Khuyến mãi

+

@_promos.Count chương trình

-@* ═══ TABS ═══ *@ -
- - - - -
+
+
+
@_promos.Count(p => p.IsActive)Đang hoạt động
+
@_promos.Sum(p => p.VoucherCount)Tổng voucher
+
@_promos.Sum(p => p.RedemptionCount)Đã sử dụng
+
-@* ═══ CONTENT ═══ *@ -
- @foreach (var rule in _pricingRules) + @if (IsLoading) + { +
+ } + else if (!_promos.Any()) + { +
+
+

Chưa có chương trình khuyến mãi

+

Tạo chiến dịch mới để thu hút khách hàng

+
+ } + else {
-
-
-
- -
-
-
@rule.Name
-
@rule.Desc
-
-
-
-
- - @(rule.Active ? "Đang hoạt động" : "Tắt") -
- -
-
-
-
-
- Loại: - @rule.Type -
-
- Giảm: - @rule.Discount -
-
- Áp dụng: - @rule.AppliesTo -
-
- Thời gian: - @rule.Schedule -
-
+
+ + + + + + + + @foreach (var p in _promos) + { + + + + + + + + } +
TênGiảm giáVoucherĐã dùngTrạng thái
@p.Name
@(p.Description ?? "")
@(p.DiscountType == "Percentage" ? $"{p.DiscountValue}%" : $"{p.DiscountValue?.ToString("N0")}₫")@p.VoucherCount@p.RedemptionCount@(p.IsActive ? "Active" : "Inactive")
}
@code { - private string _ruleTab = "all"; + private List _promos = new(); - private record PricingRule(string Name, string Desc, string Type, string Discount, string AppliesTo, string Schedule, string Icon, string Color, bool Active); - private readonly List _pricingRules = new() + protected override async Task OnInitializedAsync() { - new("Happy Hour Chiều", "Giảm giá đồ uống từ 14h-16h", "Happy Hour", "-20%", "Tất cả cà phê", "14:00 – 16:00 hàng ngày", "clock", "#FF5C00", true), - new("Combo Sáng", "Combo cà phê + bánh mì", "Combo", "-15K", "Espresso + Bánh mì", "07:00 – 10:00 T2-T6", "package", "#3B82F6", true), - new("Sinh nhật Loyalty", "Giảm giá cho khách hàng sinh nhật", "Loyalty", "-30%", "Khách hàng thành viên", "Cả ngày", "heart", "#EC4899", true), - new("Weekend Brunch", "Giảm giá cuối tuần cho đồ ăn", "Happy Hour", "-25%", "Danh mục Đồ ăn", "09:00 – 13:00 T7-CN", "sun", "#F59E0B", false), - new("Mua 5 tặng 1", "Tích điểm mua 5 ly tặng 1", "Loyalty", "1 miễn phí", "Tất cả đồ uống", "Không giới hạn", "gift", "#8B5CF6", true), - }; + IsLoading = true; + try { _promos = await DataService.GetPromotionsAsync(); } + catch { } finally { IsLoading = false; } + } } 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 index b6cadb60..b1fe21d1 100644 --- 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 @@ -1,150 +1,83 @@ @page "/admin/staff/attendance" @layout AdminLayout @inherits AdminBase - -@* - EN: Attendance dashboard — today's check-in/out, weekly summary, punctuality. - VI: Dashboard chấm công — điểm danh hôm nay, tổng kết tuần, đúng giờ. - Design: pencil-design/src/pages/tPOS/admin/attendance-dashboard.pen -*@ +@inject PosDataService DataService +@using WebClientTpos.Client.Services Chấm công — GoodGo Admin -@* ═══ TOP BAR ═══ *@

Chấm công

-

Hôm nay, @DateTime.Now.ToString("dd/MM/yyyy") • Coffee House Q1

-
-
- +

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

-@* ═══ CONTENT ═══ *@ -
- - @* KPI Row *@ -
-
-
-
- -
-
-
4/5
-
Đã check-in
+
+
+
+
+
@_staff.Count(s => s.Status == "Active")Đang hoạt động
-
-
-
- -
-
-
96%
-
Tỷ lệ đúng giờ
+
+
+
@_staff.CountTổng nhân viên
-
-
-
- -
-
-
1
-
Đi trễ hôm nay
-
-
-
-
- -
-
-
1
-
Vắng mặt
+
+
+
@_schedules.CountTổng ca hôm nay
- @* Today's attendance table *@ -
-
-

- - Chi tiết 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 ?? "—")
+
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Nhân viênCa làmCheck-inCheck-outTrạng tháiGiờ làm
-
TM
- Trần Minh -
07:00 – 15:0006:55
Đúng giờ
6h 30m
-
LT
- Lê Thảo -
07:00 – 15:0006:58
Đúng giờ
6h 27m
-
NH
- Nguyễn Hà -
08:00 – 16:0008:12
Trễ 12p
5h 13m
-
PA
- Phạm An -
08:00 – 17:0007:50
Đúng giờ
5h 35m
-
HL
- Hoàng Lan -
07:00 – 15:00
Vắng
-
-
+ }
+ +@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 index a9cda163..a4e3dc07 100644 --- 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 @@ -1,133 +1,73 @@ @page "/admin/staff/payroll" @layout AdminLayout @inherits AdminBase +@inject PosDataService DataService +@using WebClientTpos.Client.Services -@* - EN: Payroll & Commission — salary overview, commission breakdown by staff. - VI: Lương & Hoa hồng — tổng quan lương, chi tiết hoa hồng theo nhân viên. - Design: pencil-design/src/pages/tPOS/admin/payroll-commission.pen -*@ +Bảng lương — GoodGo Admin -Lương & Hoa hồng — GoodGo Admin - -@* ═══ TOP BAR ═══ *@
-

Lương & Hoa hồng

-

Tháng 02/2025 • Tất cả cửa hàng

-
-
- - +

Bảng lương

+

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

-@* ═══ CONTENT ═══ *@ -
- - @* KPI Row *@ -
-
-
-
- -
+
+ @if (IsLoading) + { +
+ } + else if (!_staff.Any()) + { +
+
+
-
86.4M
-
Tổng lương tháng
+

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

+

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

-
-
-
- -
-
-
12.8M
-
Tổng hoa hồng
-
-
-
-
- -
-
-
8
-
Nhân viên
-
-
-
-
- -
-
-
12.4M
-
Lương TB / người
-
-
- - @* Payroll Table *@ -
-
-

- - Bảng lương chi tiết -

-
- Chưa thanh toán + } + 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 ?? "—") + +
-
- - - - - - - - - - - - - - @foreach (var item in _payrollData) - { - - - - - - - - - - } - -
Nhân viênVai tròLương cơ bảnGiờ làmHoa hồngThưởng/PhạtTổng
-
@item.Initials
- @item.Name -
@item.Role@item.BaseSalary@item.Hours h+@item.Commission@item.BonusPenalty@item.Total
-
-
+ }
@code { - private record PayrollItem(string Name, string Initials, string Color, string Role, string BaseSalary, string Hours, string Commission, string BonusPenalty, string Total); - private readonly PayrollItem[] _payrollData = new[] + private List _staff = new(); + + protected override async Task OnInitializedAsync() { - new PayrollItem("Trần Minh", "TM", "#FF5C00", "Barista", "8.0M", "172", "1.8M", "+500K", "10.3M"), - new PayrollItem("Lê Thảo", "LT", "#3B82F6", "Thu ngân", "7.5M", "168", "2.1M", "+300K", "9.9M"), - new PayrollItem("Nguyễn Hà", "NH", "#22C55E", "Phục vụ", "6.5M", "160", "1.2M", "+200K", "7.9M"), - new PayrollItem("Phạm An", "PA", "#8B5CF6", "Quản lý", "15.0M", "180", "3.5M", "+1.0M", "19.5M"), - new PayrollItem("Hoàng Lan", "HL", "#EC4899", "Barista", "8.0M", "148", "1.4M", "-200K", "9.2M"), - new PayrollItem("Võ Khoa", "VK", "#F59E0B", "Bếp trưởng", "12.0M", "176", "2.0M", "+500K", "14.5M"), - new PayrollItem("Đặng Linh", "ĐL", "#06B6D4", "Phục vụ", "6.5M", "152", "0.8M", "0", "7.3M"), - new PayrollItem("Bùi Tùng", "BT", "#3B82F6", "Thu ngân", "7.5M", "164", "1.5M", "+200K", "9.2M"), - }; + 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/StaffCreate.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Staff/StaffCreate.razor index b3d17923..320dabf1 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Staff/StaffCreate.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Staff/StaffCreate.razor @@ -1,189 +1,101 @@ @page "/admin/staff/create" @layout AdminLayout @inherits AdminBase - -@* - EN: Create staff — form with personal info, role, store assignment, schedule. - VI: Thêm nhân viên — form thông tin cá nhân, vai trò, phân công cửa hàng, lịch. - Design: pencil-design/src/pages/tPOS/admin/staff-create.pen -*@ +@inject PosDataService DataService +@using WebClientTpos.Client.Services Thêm nhân viên — GoodGo Admin -@* ═══ TOP BAR ═══ *@
- +

Thêm nhân viên mới

-

Điền thông tin để tạo tài khoản nhân viên

+

Điền thông tin nhân viên

-
-@* ═══ CONTENT ═══ *@
- - @* LEFT COLUMN: Main form *@
+ @if (!string.IsNullOrEmpty(_message)) + { +
+
+ + @_message +
+
+ } - @* Personal Info *@
-

- - Thông tin cá nhân -

+

Thông tin nhân viên

- - + +
-
- - -
-
-
-
- - -
-
- - -
-
-
- - -
-
- - -
-
-
- - @* Employment Info *@ -
-
-

- - Thông tin công việc -

-
-
-
- -
-
- - + @foreach (var r in _roles) + { + + }
- - + +
- - + +
- - @* Salary *@ -
-
-

- - Lương & Phúc lợi -

-
-
-
-
- - -
-
- - -
-
-
-
-
- - @* RIGHT COLUMN: Summary/Preview *@ -
-
-
-

- - Xem trước thẻ -

-
-
-
NV
-
-
Nhân viên mới
-
Chưa phân công
-
-
-
-
- Mã PIN - Tự động -
-
- Quyền POS - Cơ bản -
-
-
-
- -
-
- -
- Nhân viên sẽ nhận mã PIN 4 số để đăng nhập POS. Bạn có thể thay đổi quyền hạn trong phần Phân quyền. -
-
-
+ +@code { + private string _empCode = "", _role = "Cashier", _phone = "", _email = "", _message = ""; + private bool _isSuccess, _isSaving; + private List _roles = new(); + + protected override async Task OnInitializedAsync() + { + try { _roles = await DataService.GetStaffRolesAsync(); if (_roles.Any()) _role = _roles[0].Name; } catch { } + } + + private async Task HandleSubmit() + { + _message = ""; + if (string.IsNullOrWhiteSpace(_role)) { _message = "Vui lòng chọn vai trò."; _isSuccess = false; return; } + _isSaving = true; + try + { + // EN: Use first merchant or Guid.Empty / VI: Dùng merchant đầu tiên hoặc Guid.Empty + var shops = await DataService.GetShopsAsync(); + var merchantId = Guid.Empty; // Will be set correctly when merchant context is available + var ok = await DataService.CreateStaffAsync(new(merchantId, _empCode.Trim(), _phone.Trim(), _email.Trim(), _role)); + if (ok) { _message = "Đã thêm nhân viên thành công!"; _isSuccess = true; _empCode = ""; _phone = ""; _email = ""; } + else { _message = "Không thể thêm nhân viên."; _isSuccess = false; } + } + catch (Exception ex) { _message = $"Lỗi: {ex.Message}"; _isSuccess = false; } + finally { _isSaving = 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 index 43e2780d..c90adf01 100644 --- 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 @@ -1,95 +1,84 @@ @page "/admin/staff/schedule" @layout AdminLayout @inherits AdminBase - -@* - EN: Staff schedule — weekly grid view with shifts, drag-drop. - VI: Lịch làm việc — hiển thị tuần dạng grid, ca làm, kéo thả. - Design: pencil-design/src/pages/tPOS/admin/staff-schedule.pen -*@ +@inject PosDataService DataService +@using WebClientTpos.Client.Services Lịch làm việc — GoodGo Admin -@* ═══ TOP BAR ═══ *@

Lịch làm việc

-

Tuần 10/02 – 16/02/2025 • Coffee House Q1

+

@_schedules.Count ca làm việc

- - Tuần này - - +
-@* ═══ SCHEDULE GRID ═══ *@ -
-
- @* Header row *@ -
-
Nhân viên
- @foreach (var day in _days) - { -
- @day.DayName - @day.Date -
- } +
+ @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

- - @* Staff rows *@ - @foreach (var staff in _scheduleStaff) + } + else + { + @foreach (var dayGroup in _schedules.GroupBy(s => s.DayOfWeek).OrderBy(g => g.Key)) { -
-
-
- @staff.Initials -
-
-
@staff.Name
-
@staff.Role
-
-
- @foreach (var shift in staff.Shifts) - { -
- @if (!string.IsNullOrEmpty(shift)) - { -
- @shift +
+

@DayName(dayGroup.Key)

+
+ @foreach (var s in dayGroup) + { +
+
+ @(s.EmployeeCode ?? s.StaffId.ToString()[..6]) + @(s.Role ?? "")
- } -
- } + @s.StartTime — @s.EndTime +
+ } +
} -
+ }
@code { - private record DayInfo(string DayName, string Date); - private readonly DayInfo[] _days = new[] - { - new DayInfo("T2", "10"), new DayInfo("T3", "11"), new DayInfo("T4", "12"), - new DayInfo("T5", "13"), new DayInfo("T6", "14"), new DayInfo("T7", "15"), new DayInfo("CN", "16") - }; + private List _schedules = new(); + private List _shops = new(); + private Guid? _selectedShopId; - private record ScheduleStaff(string Name, string Initials, string Role, string Color, string[] Shifts); - private readonly ScheduleStaff[] _scheduleStaff = new[] + protected override async Task OnInitializedAsync() { - new ScheduleStaff("Trần Minh", "TM", "Barista", "#FF5C00", new[] {"07:00-15:00", "07:00-15:00", "", "07:00-15:00", "07:00-15:00", "07:00-15:00", ""}), - new ScheduleStaff("Lê Thảo", "LT", "Thu ngân", "#3B82F6", new[] {"15:00-22:00", "15:00-22:00", "15:00-22:00", "15:00-22:00", "", "15:00-22:00", "15:00-22:00"}), - new ScheduleStaff("Nguyễn Hà", "NH", "Phục vụ", "#22C55E", new[] {"07:00-15:00", "", "07:00-15:00", "07:00-15:00", "07:00-15:00", "", "07:00-15:00"}), - new ScheduleStaff("Phạm An", "PA", "Quản lý", "#8B5CF6", new[] {"08:00-17:00", "08:00-17:00", "08:00-17:00", "08:00-17:00", "08:00-17:00", "", ""}), - new ScheduleStaff("Hoàng Lan", "HL", "Barista", "#EC4899", new[] {"", "", "15:00-22:00", "", "15:00-22:00", "07:00-15:00", "07:00-15:00"}), + 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/DeviceManagement.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/SystemAdmin/DeviceManagement.razor index aec61ee3..72feafd5 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/SystemAdmin/DeviceManagement.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/SystemAdmin/DeviceManagement.razor @@ -1,101 +1,78 @@ @page "/admin/system/devices" @layout AdminLayout @inherits AdminBase +@inject PosDataService DataService +@using WebClientTpos.Client.Services -@* - EN: Device management — list of POS terminals, printers, scanners with status (online/offline), store assignment, last seen, battery. - VI: Quản lý thiết bị — danh sách máy POS, máy in, máy quét với trạng thái, gán cửa hàng, lần cuối kết nối, pin. - Design: pencil-design/src/pages/tPOS/admin/device-management.pen -*@ +Thiết bị — GoodGo Admin -Quản lý thiết bị — GoodGo Admin - -@* ═══ TOP BAR ═══ *@

Quản lý thiết bị

-

@_devices.Count thiết bị • @_devices.Count(d => d.Status == "online") đang hoạt động

+

@_devices.Count thiết bị đã đăng ký

-
- + +
+
+
+
+
@_devices.Count(d => d.IsActive)Đang hoạt động
+
+
+
+
@_devices.Count(d => !d.IsActive)Không hoạt động
-
-
-@* ═══ TABS ═══ *@ -
- - - -
- -@* ═══ DEVICE GRID ═══ *@ -
- @foreach (var dev in FilteredDevices) + @if (IsLoading) { -
-
-
- -
-
- - @(dev.Status == "online" ? "Online" : "Offline") -
-
-
- @dev.Name - @dev.Type • @dev.Store -
-
-
-
@dev.LastSeen
-
Lần cuối
-
-
-
@dev.Battery
-
Pin
-
-
-
@dev.Serial
-
S/N
-
+
+ } + else if (!_devices.Any()) + { +
+
+

Chưa có thiết bị

+

Thiết bị sẽ tự động đăng ký khi nhân viên đăng nhập

+
+ } + else + { +
+
+ + + + + + + + @foreach (var d in _devices) + { + + + + + + + + } +
Thiết bịNền tảngNhân viênTrạng tháiNgày đăng ký
@(d.DeviceToken?[..Math.Min(12, d.DeviceToken.Length)] ?? "—")...@(d.Platform ?? "—")@(d.StaffCode ?? "—") + @(d.IsActive ? "Active" : "Inactive") + @d.CreatedAt.ToString("dd/MM/yyyy")
}
@code { - private string _activeTab = "all"; + private List _devices = new(); - private List FilteredDevices => _activeTab == "all" - ? _devices - : _devices.Where(d => d.Status == _activeTab).ToList(); - - private record DeviceItem(string Name, string Type, string Icon, string IconColor, string Store, - string Status, string LastSeen, string Battery, string BatteryColor, string Serial); - - private readonly List _devices = new() + protected override async Task OnInitializedAsync() { - new("POS Terminal #01", "Máy POS", "monitor", "#3B82F6", "Coffee House Q1", "online", "Vừa xong", "85%", "#22C55E", "POS-001"), - new("POS Terminal #02", "Máy POS", "monitor", "#3B82F6", "Nhà hàng Q3", "online", "Vừa xong", "92%", "#22C55E", "POS-002"), - new("Máy in bếp", "Máy in", "printer", "#8B5CF6", "Coffee House Q1", "offline", "2 giờ", "—", "var(--admin-text-tertiary)", "PRT-001"), - new("Máy in bill", "Máy in", "printer", "#8B5CF6", "Coffee House Q1", "online", "Vừa xong", "—", "var(--admin-text-tertiary)", "PRT-002"), - new("Máy quét QR", "Scanner", "scan-line", "#22C55E", "Nhà hàng Q3", "online", "5 phút", "68%", "#F59E0B", "SCN-001"), - new("Tablet Order #01", "Tablet", "tablet", "#F59E0B", "Nhà hàng Q3", "online", "Vừa xong", "45%", "#EF4444", "TAB-001"), - new("POS Terminal #03", "Máy POS", "monitor", "#3B82F6", "Karaoke Star Q7", "offline", "3 ngày", "0%", "#EF4444", "POS-003"), - new("Máy in bill #02", "Máy in", "printer", "#8B5CF6", "Nhà hàng Q3", "online", "Vừa xong", "—", "var(--admin-text-tertiary)", "PRT-003"), - }; + IsLoading = true; + try { _devices = await DataService.GetDevicesAsync(); } + catch { } finally { IsLoading = false; } + } } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/SystemAdmin/IntegrationHub.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/SystemAdmin/IntegrationHub.razor index cccc3bf3..b3b4b8f3 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/SystemAdmin/IntegrationHub.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/SystemAdmin/IntegrationHub.razor @@ -2,111 +2,43 @@ @layout AdminLayout @inherits AdminBase -@* - EN: Integration hub — integration tiles (Payment, Delivery, Accounting, Social), connection status, configure button. - VI: Trung tâm tích hợp — thẻ tích hợp (Thanh toán, Giao hàng, Kế toán, Mạng xã hội), trạng thái kết nối, nút cấu hình. - Design: pencil-design/src/pages/tPOS/admin/integration-hub.pen -*@ - Tích hợp — GoodGo Admin -@* ═══ TOP BAR ═══ *@
-

Tích hợp

-

Quản lý kết nối với dịch vụ bên ngoài

-
-
- +

Tích hợp bên thứ 3

+

Kết nối với các dịch vụ bên ngoài

-@* ═══ CONTENT ═══ *@ -
- - @foreach (var group in _integrationGroups) - { -
-
- @group.GroupName -
-
- @foreach (var item in group.Items) - { -
-
-
- -
-
- - @(item.Connected ? "Đã kết nối" : "Chưa kết nối") -
-
-
- @item.Name - @item.Description -
-
- @if (item.Connected) - { - - - } - else - { - - } -
+
+
+ @foreach (var intg in _integrations) + { +
+
+
+
- } +
+
@intg.name
+
@intg.desc
+
+ @(intg.connected ? "Đã kết nối" : "Chưa kết nối") +
-
- } + } +
@code { - private record IntegrationItem(string Name, string Description, string Icon, string Color, bool Connected); - private record IntegrationGroup(string GroupName, IntegrationItem[] Items); - - private readonly IntegrationGroup[] _integrationGroups = new[] + private readonly (string name, string desc, string icon, string color, string bg, bool connected)[] _integrations = new[] { - new IntegrationGroup("THANH TOÁN", new[] - { - new IntegrationItem("VNPay", "Thanh toán QR & thẻ ngân hàng", "qr-code", "#3B82F6", true), - new IntegrationItem("MoMo", "Ví điện tử MoMo", "wallet", "#EC4899", true), - new IntegrationItem("ZaloPay", "Thanh toán qua ZaloPay", "smartphone", "#3B82F6", false), - new IntegrationItem("Visa/Mastercard", "Thanh toán thẻ quốc tế", "credit-card", "#8B5CF6", true), - }), - new IntegrationGroup("GIAO HÀNG", new[] - { - new IntegrationItem("GrabFood", "Đối tác giao đồ ăn", "bike", "#22C55E", true), - new IntegrationItem("ShopeeFood", "Nền tảng đặt đồ ăn", "shopping-bag", "#FF5C00", true), - new IntegrationItem("Baemin", "Dịch vụ giao hàng", "truck", "#06B6D4", false), - }), - new IntegrationGroup("KẾ TOÁN & QUẢN LÝ", new[] - { - new IntegrationItem("MISA", "Phần mềm kế toán", "calculator", "#F59E0B", true), - new IntegrationItem("Google Sheets", "Xuất báo cáo tự động", "file-spreadsheet", "#22C55E", false), - new IntegrationItem("Hóa đơn điện tử", "Phát hành hóa đơn GTGT", "file-text", "#3B82F6", true), - }), - new IntegrationGroup("MẠNG XÃ HỘI", new[] - { - new IntegrationItem("Facebook", "Fanpage & quảng cáo", "facebook", "#3B82F6", true), - new IntegrationItem("Zalo OA", "Chăm sóc khách hàng", "message-circle", "#3B82F6", false), - new IntegrationItem("Google Business", "Hiển thị trên Google Maps", "map-pin", "#EF4444", true), - }), + ("Thanh toán VNPay", "Cổng thanh toán trực tuyến", "credit-card", "#3B82F6", "rgba(59,130,246,0.1)", false), + ("Thanh toán Momo", "Ví điện tử Momo", "wallet", "#A855F7", "rgba(168,85,247,0.1)", false), + ("GrabFood", "Đối tác giao đồ ăn", "bike", "#22C55E", "rgba(34,197,94,0.1)", false), + ("ShopeeFood", "Đối tác giao đồ ăn", "shopping-cart", "#EF4444", "rgba(239,68,68,0.1)", false), + ("Máy in hóa đơn", "Kết nối máy in POS", "printer", "#F59E0B", "rgba(245,158,11,0.1)", false), + ("Kế toán MISA", "Đồng bộ dữ liệu kế toán", "file-spreadsheet", "#FF5C00", "rgba(255,92,0,0.1)", false), }; } 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 index 654d3de2..f1f2f3b3 100644 --- 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 @@ -2,150 +2,41 @@ @layout AdminLayout @inherits AdminBase -@* - EN: Notification center — notification settings, channel toggles (Email/SMS/Push/In-app), event types with channel preferences, test send. - VI: Trung tâm thông báo — cài đặt thông báo, bật/tắt kênh (Email/SMS/Push/In-app), loại sự kiện theo kênh, gửi thử. - Design: pencil-design/src/pages/tPOS/admin/notification-center.pen -*@ - Thông báo — GoodGo Admin -@* ═══ TOP BAR ═══ *@

Trung tâm thông báo

-

Cấu hình kênh & loại thông báo

-
-
- - +

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

-@* ═══ CONTENT ═══ *@ -
- - @* LEFT: Notification Types *@ -
-
-

- - Loại thông báo -

-
-
- - - - - - - - - - - - @foreach (var evt in _eventTypes) - { - - - - - - - - } - -
Sự kiệnEmailSMSPushIn-app
-
- @evt.Name - @evt.Desc -
-
-
-
- - @* RIGHT: Channel Config *@ -
- @* Channel Status *@ -
-
-

- - Kênh thông báo -

-
-
- @foreach (var ch in _channels) - { -
-
-
- -
-
- @ch.Name - @ch.Provider -
-
- +
+
+

Mẫu thông báo

+
+ @foreach (var tmpl in _templates) + { +
+
+
@tmpl.name
+
@tmpl.desc
- } -
-
- - @* Delivery Schedule *@ -
-
-

- - Lịch gửi -

-
-
-
- - + @(tmpl.active ? "Bật" : "Tắt")
-
- - -
-
- Không làm phiền - -
-
+ }
@code { - private record EventType(string Name, string Desc, bool Email, bool Sms, bool Push, bool InApp); - private readonly EventType[] _eventTypes = new[] + private readonly (string name, string desc, bool active)[] _templates = new[] { - new EventType("Đơn hàng mới", "Khi có đơn hàng mới được tạo", false, false, true, true), - new EventType("Đơn hàng hủy", "Khi đơn hàng bị hủy", true, true, true, true), - new EventType("Hàng sắp hết", "Tồn kho dưới mức tối thiểu", true, false, true, true), - new EventType("Nhân viên clock-in", "Nhân viên bắt đầu ca làm việc", false, false, false, true), - new EventType("Doanh thu bất thường", "Doanh thu thấp/cao bất thường", true, true, true, true), - new EventType("Thiết bị offline", "Thiết bị mất kết nối", true, true, true, true), - new EventType("Phản hồi khách hàng", "Khách hàng gửi đánh giá mới", false, false, true, true), - new EventType("Bảo mật", "Đăng nhập lạ, đổi mật khẩu", true, true, true, true), - }; - - private record ChannelDef(string Name, string Icon, string Color, string Provider, bool Enabled); - private readonly ChannelDef[] _channels = new[] - { - new ChannelDef("Email", "mail", "#3B82F6", "SendGrid", true), - new ChannelDef("SMS", "smartphone", "#22C55E", "Twilio", true), - new ChannelDef("Push", "bell-ring", "#FF5C00", "Firebase FCM", true), - new ChannelDef("In-app", "message-square", "#8B5CF6", "WebSocket", true), + ("Đơ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/PosDataService.cs b/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs index b53cda24..61710c9a 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs @@ -107,4 +107,72 @@ public class PosDataService var resp = await _http.PostAsJsonAsync("api/bff/staff", req, _jsonOptions); return resp.IsSuccessStatusCode; } + + // ═══ STAFF ROLES & SCHEDULES ═══ + + public record StaffRoleInfo(int Id, string Name); + public record ScheduleInfo(Guid Id, Guid StaffId, Guid ShopId, int DayOfWeek, string StartTime, string EndTime, + string? EmployeeCode, string? Role, string? Phone); + + public async Task> GetStaffRolesAsync() + => await _http.GetFromJsonAsync>("api/bff/staff/roles", _jsonOptions) ?? new(); + + public async Task> GetStaffSchedulesAsync(Guid? shopId = null) + { + var url = shopId.HasValue ? $"api/bff/staff/schedules?shopId={shopId}" : "api/bff/staff/schedules"; + return await _http.GetFromJsonAsync>(url, _jsonOptions) ?? new(); + } + + // ═══ ORDERS ═══ + + public record OrderInfo(Guid Id, Guid ShopId, decimal TotalAmount, int StatusId, DateTime CreatedAt, string? Status); + + public async Task> GetOrdersAsync(Guid? shopId = null) + { + var url = shopId.HasValue ? $"api/bff/orders?shopId={shopId}" : "api/bff/orders"; + return await _http.GetFromJsonAsync>(url, _jsonOptions) ?? new(); + } + + // ═══ WALLETS / FINANCE ═══ + + public record WalletInfo(Guid Id, decimal Balance, string? Currency, Guid OwnerId, DateTime CreatedAt, decimal TotalIncome, decimal TotalExpense); + public record WalletTxnInfo(Guid Id, Guid WalletId, decimal Amount, string? Description, DateTime CreatedAt, string? ItemName); + + public async Task> GetWalletsAsync() + => await _http.GetFromJsonAsync>("api/bff/wallets", _jsonOptions) ?? new(); + + public async Task> GetWalletTransactionsAsync(int limit = 50) + => await _http.GetFromJsonAsync>($"api/bff/wallet/transactions?limit={limit}", _jsonOptions) ?? new(); + + // ═══ DEVICES ═══ + + public record DeviceInfo(Guid Id, string? DeviceToken, string? Platform, bool IsActive, DateTime CreatedAt, string? StaffCode); + + public async Task> GetDevicesAsync() + => await _http.GetFromJsonAsync>("api/bff/devices", _jsonOptions) ?? new(); + + // ═══ PROMOTIONS ═══ + + public record PromotionInfo(Guid Id, string Name, string? Description, DateTime? StartDate, DateTime? EndDate, + bool IsActive, string? DiscountType, decimal? DiscountValue, int VoucherCount, int RedemptionCount); + + public async Task> GetPromotionsAsync() + => await _http.GetFromJsonAsync>("api/bff/promotions", _jsonOptions) ?? new(); + + // ═══ INVENTORY TRANSACTIONS ═══ + + public record InventoryTxnInfo(Guid Id, Guid InventoryItemId, int QuantityChange, string? Reason, DateTime CreatedAt, string? TransactionType); + + public async Task> GetInventoryTransactionsAsync(Guid? shopId = null) + { + var url = shopId.HasValue ? $"api/bff/inventory/transactions?shopId={shopId}" : "api/bff/inventory/transactions"; + return await _http.GetFromJsonAsync>(url, _jsonOptions) ?? new(); + } + + // ═══ MEMBERSHIP LEVELS ═══ + + public record LevelDefinitionInfo(Guid Id, int Level, string Name, int MinExp, int MaxExp, int MemberCount); + + public async Task> GetMembershipLevelsAsync() + => await _http.GetFromJsonAsync>("api/bff/membership/levels", _jsonOptions) ?? new(); } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/BffDataController.cs b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/BffDataController.cs index 1460bc76..4f5873f5 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/BffDataController.cs +++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/BffDataController.cs @@ -307,6 +307,136 @@ public class BffDataController : ControllerBase return CreatedAtAction(nameof(GetStaff), new { }, new { id }); } + // ═══ STAFF ROLES ═══ + [HttpGet("staff/roles")] + public async Task GetStaffRoles() + { + await using var conn = new NpgsqlConnection(ConnStr("merchant_service")); + var roles = await conn.QueryAsync("SELECT id, name FROM staff_roles ORDER BY id"); + return Ok(roles); + } + + // ═══ STAFF SCHEDULES ═══ + [HttpGet("staff/schedules")] + public async Task GetStaffSchedules([FromQuery] Guid? shopId = null) + { + await using var conn = new NpgsqlConnection(ConnStr("booking_service")); + var sql = @"SELECT id, staff_id, shop_id, day_of_week, start_time, end_time FROM staff_schedules"; + if (shopId.HasValue) sql += " WHERE shop_id = @ShopId"; + sql += " ORDER BY day_of_week, start_time"; + var schedules = await conn.QueryAsync(sql, new { ShopId = shopId }); + + // EN: Enrich with staff names / VI: Bổ sung tên nhân viên + await using var mConn = new NpgsqlConnection(ConnStr("merchant_service")); + var staffList = (await mConn.QueryAsync( + "SELECT ms.id, ms.employee_code, ms.phone, sr.name as role FROM merchant_staff ms JOIN staff_roles sr ON ms.role_id = sr.id")).ToList(); + var staffMap = staffList.ToDictionary(s => (Guid)s.id, s => new { code = (string?)s.employee_code, role = (string)s.role, phone = (string?)s.phone }); + + var result = schedules.Select(s => new { + s.id, s.staff_id, s.shop_id, s.day_of_week, s.start_time, s.end_time, + employee_code = staffMap.TryGetValue((Guid)s.staff_id, out var info) ? info.code : null, + role = info?.role, phone = info?.phone + }); + return Ok(result); + } + + // ═══ ORDERS SUMMARY ═══ + [HttpGet("orders")] + public async Task GetOrders([FromQuery] Guid? shopId = null) + { + await using var conn = new NpgsqlConnection(ConnStr("order_service")); + var sql = @"SELECT o.id, o.shop_id, o.total_amount, o.status_id, o.created_at, + os.name as status + FROM orders o + JOIN order_statuses os ON o.status_id = os.id"; + if (shopId.HasValue) sql += " WHERE o.shop_id = @ShopId"; + sql += " ORDER BY o.created_at DESC LIMIT 200"; + var orders = await conn.QueryAsync(sql, new { ShopId = shopId }); + return Ok(orders); + } + + // ═══ WALLET/FINANCE ═══ + [HttpGet("wallets")] + public async Task GetWallets() + { + await using var conn = new NpgsqlConnection(ConnStr("wallet_service")); + var wallets = await conn.QueryAsync( + @"SELECT w.id, w.balance, w.currency, w.owner_id, w.created_at, + (SELECT COALESCE(SUM(amount),0) FROM wallet_transactions wt WHERE wt.wallet_id = w.id AND wt.amount > 0) as total_income, + (SELECT COALESCE(SUM(ABS(amount)),0) FROM wallet_transactions wt WHERE wt.wallet_id = w.id AND wt.amount < 0) as total_expense + FROM wallets w ORDER BY w.created_at DESC"); + return Ok(wallets); + } + + [HttpGet("wallet/transactions")] + public async Task GetWalletTransactions([FromQuery] int limit = 50) + { + await using var conn = new NpgsqlConnection(ConnStr("wallet_service")); + var txns = await conn.QueryAsync( + @"SELECT wt.id, wt.wallet_id, wt.amount, wt.description, wt.created_at, + wi.name as item_name + FROM wallet_transactions wt + LEFT JOIN wallet_items wi ON wt.reference_id = wi.id + ORDER BY wt.created_at DESC LIMIT @Limit", + new { Limit = limit }); + return Ok(txns); + } + + // ═══ DEVICES ═══ + [HttpGet("devices")] + public async Task GetDevices() + { + await using var conn = new NpgsqlConnection(ConnStr("merchant_service")); + var devices = await conn.QueryAsync( + @"SELECT dt.id, dt.device_token, dt.platform, dt.is_active, dt.created_at, + ms.employee_code as staff_code + FROM device_tokens dt + LEFT JOIN merchant_staff ms ON dt.staff_id = ms.id + ORDER BY dt.created_at DESC"); + return Ok(devices); + } + + // ═══ PROMOTIONS ═══ + [HttpGet("promotions")] + public async Task GetPromotions() + { + await using var conn = new NpgsqlConnection(ConnStr("promotion_service")); + var promos = await conn.QueryAsync( + @"SELECT c.id, c.name, c.description, c.start_date, c.end_date, c.is_active, c.discount_type, c.discount_value, + (SELECT COUNT(*) FROM vouchers v WHERE v.campaign_id = c.id) as voucher_count, + (SELECT COUNT(*) FROM redemptions r WHERE r.campaign_id = c.id) as redemption_count + FROM campaigns c ORDER BY c.created_at DESC"); + return Ok(promos); + } + + // ═══ INVENTORY TRANSACTIONS ═══ + [HttpGet("inventory/transactions")] + public async Task GetInventoryTransactions([FromQuery] Guid? shopId = null) + { + await using var conn = new NpgsqlConnection(ConnStr("inventory_service")); + var sql = @"SELECT it.id, it.inventory_item_id, it.quantity_change, it.reason, it.created_at, + tt.name as transaction_type + FROM inventory_transactions it + JOIN transaction_types tt ON it.type_id = tt.id"; + if (shopId.HasValue) + sql += @" JOIN inventory_items ii ON it.inventory_item_id = ii.id WHERE ii.shop_id = @ShopId"; + sql += " ORDER BY it.created_at DESC LIMIT 100"; + var txns = await conn.QueryAsync(sql, new { ShopId = shopId }); + return Ok(txns); + } + + // ═══ MEMBERSHIP LEVELS ═══ + [HttpGet("membership/levels")] + public async Task GetMembershipLevels() + { + await using var conn = new NpgsqlConnection(ConnStr("membership_service")); + var levels = await conn.QueryAsync( + @"SELECT ld.id, ld.level, ld.name, ld.min_exp, ld.max_exp, + (SELECT COUNT(*) FROM members m WHERE m.current_level = ld.level) as member_count + FROM level_definitions ld ORDER BY ld.level"); + return Ok(levels); + } + // EN: Request DTOs / VI: DTO yêu cầu public record CreateProductRequest(Guid ShopId, string Name, string? Description, decimal Price, string? Type, string? Sku, string? ImageUrl); public record CreateStaffRequest(Guid MerchantId, string? EmployeeCode, string? Phone, string? Email, string? Role);