From fe6e14ce8590e9ee9fefb98c982cd8d0a7b3fdf6 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Tue, 3 Mar 2026 11:13:27 +0700 Subject: [PATCH] feat(web-client-tpos): unify POS with inline payment and tabs (path fix) --- .../Pages/Pos/Cafe/CafeDesktop.razor | 630 +++++++++++++++--- 1 file changed, 535 insertions(+), 95 deletions(-) diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/CafeDesktop.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/CafeDesktop.razor index 291000b3..5c07166f 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/CafeDesktop.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/CafeDesktop.razor @@ -1,105 +1,391 @@ @* - EN: Café POS Desktop — 3-column layout: categories, product grid, cart. - VI: POS Café Desktop — Bố cục 3 cột: danh mục, lưới sản phẩm, giỏ hàng. + EN: Café POS Desktop — Unified single-page with tabs: Sale, History, Dashboard. + Payment flow is inline (cart panel transforms to payment panel). + VI: POS Café Desktop — Trang đơn với tabs: Bán hàng, Lịch sử, Dashboard. + Thanh toán inline (panel giỏ hàng chuyển thành panel thanh toán). *@ @page "/pos/{ShopId:guid}/cafe" @layout PosLayout @inherits PosBase @inject WebClientTpos.Client.Services.PosDataService DataService -@* ═══ PRODUCT PANEL ═══ *@ -
- @if (_isLoading) +@* ═══════════════ MAIN CONTENT AREA ═══════════════ *@ +
+ @switch (_activeTab) { -
- Đang tải... -
- } - else if (_loadError) - { -
- Không thể tải dữ liệu -
- } - else - { - @* EN: Category tabs / VI: Tab danh mục *@ -
- @foreach (var cat in _categories) - { - - } -
- - @* EN: Product grid / VI: Lưới sản phẩm *@ -
- @foreach (var product in FilteredProducts) - { -
-
- + case PosTab.Sale: + @* ═══ PRODUCT PANEL (LEFT) ═══ *@ +
+ @if (_isLoading) + { +
+ Đang tải...
- @product.Name - @FormatPrice(product.Price) + } + else if (_loadError) + { +
+ Không thể tải dữ liệu +
+ } + else + { +
+ @foreach (var cat in _categories) + { + + } +
+ +
+ @foreach (var product in FilteredProducts) + { +
+
+ +
+ @product.Name + @FormatPrice(product.Price) +
+ } +
+ } +
+ + @* ═══ CART / PAYMENT PANEL (RIGHT) ═══ *@ +
+ @if (_paymentStep == PayStep.None) + { + @* ─── NORMAL CART MODE ─── *@ +
+ Đơn hàng + @_cartItems.Count món +
+
+ @foreach (var item in _cartItems) + { +
+
+ @item.Name + @FormatPrice(item.Price) +
+
+ + @item.Qty + +
+
+ } +
+ + } + else if (_paymentStep == PayStep.MethodSelect) + { + @* ─── PAYMENT: METHOD SELECT ─── *@ +
+
+ + Thanh toán + @FormatPrice(CartTotal) +
+
+ + + + +
+
+ } + else if (_paymentStep == PayStep.AmountInput) + { + @* ─── PAYMENT: CASH AMOUNT INPUT ─── *@ +
+
+ + 💵 Tiền mặt + @FormatPrice(CartTotal) +
+
+
Số tiền nhanh
+
+ @foreach (var qa in GetQuickAmounts()) + { + + } +
+
Nhập số tiền
+ + @if (!string.IsNullOrEmpty(_customAmountInput)) + { + + } +
+
+ Khách đưa + @FormatPrice(_receivedAmount) +
+
+ Tiền thối + + @FormatPrice(ChangeAmount) + +
+
+
+
+ +
+
+ } + else if (_paymentStep == PayStep.Processing) + { + @* ─── PAYMENT: QR/CARD/TRANSFER — Confirm ─── *@ +
+
+ + @GetMethodLabel() +
+
+
@FormatPrice(CartTotal)
+ @if (_selectedMethod == "qr") + { +
+
QR Code
VietQR
+
+
Quét mã bằng app Ngân hàng / MoMo / ZaloPay
+ } + else if (_selectedMethod == "card") + { +
💳
+
Chạm, quẹt hoặc cắm thẻ
+ } + else + { +
🏦
+
Xác nhận đã nhận chuyển khoản
+ } +
+
+ +
+
+ } + else if (_paymentStep == PayStep.Success) + { + @* ─── PAYMENT: SUCCESS ─── *@ +
+
+
+ +
+
+
Thanh toán thành công!
+
@FormatPrice(_lastOrderTotal)
+
Mã: @_lastTransactionId
+
+ + +
+
+ } +
+ break; + + case PosTab.History: + @* ═══ ORDER HISTORY TAB ═══ *@ +
+
+ + @foreach (var f in _historyFilters) + { + + }
- } -
+
+ @foreach (var order in FilteredOrders) + { +
+
+ @order.Id + + @order.Status + +
+
+ @order.Items +
+
@FormatPrice(order.Total)
+
@order.Time
+
@order.Method
+
+
+
+ } + @if (!FilteredOrders.Any()) + { +
+ +
Không tìm thấy đơn hàng
+
+ } +
+
+ break; + + case PosTab.Dashboard: + @* ═══ DASHBOARD TAB ═══ *@ +
+
+
+
Dashboard bán hàng
+
@DateTime.Now.ToString("dd/MM/yyyy") — Hôm nay
+
+
+ +
+ @foreach (var stat in _dashStats) + { +
+
@stat.Label
+
@stat.Value
+
@stat.Sub
+
+ } +
+ +
+ @* Popular items *@ +
+
Món bán chạy
+ @foreach (var item in _dashPopular) + { + + } +
+ + @* Payment breakdown + Hourly chart *@ +
+
Hình thức thanh toán
+ @foreach (var p in _dashPayments) + { +
+
+ @p.Method + @FormatPrice(p.Amount) +
+
+
+
+
+ } + +
Doanh thu theo giờ
+
+ @foreach (var h in _dashHourly) + { +
+
+ @h.Hour +
+ } +
+
+
+
+ break; }
-@* ═══ CART PANEL ═══ *@ -
-
- Đơn hàng - @_cartItems.Count món -
- -
- @foreach (var item in _cartItems) - { -
-
- @item.Name - @FormatPrice(item.Price) -
-
- - @item.Qty - -
-
- } -
- - +@* ═══════════════ BOTTOM NAVIGATION ═══════════════ *@ +
+ + +
@code { + // ═══════════════ TAB SYSTEM ═══════════════ + private enum PosTab { Sale, History, Dashboard } + private PosTab _activeTab = PosTab.Sale; - // EN: Loading state / VI: Trạng thái tải + private void SwitchTab(PosTab tab) + { + if (_paymentStep != PayStep.None && _paymentStep != PayStep.Success) return; // Block tab switch during payment + _activeTab = tab; + } + + // ═══════════════ SALE TAB — Product & Cart ═══════════════ private bool _isLoading = true; private bool _loadError; - - // EN: Categories / VI: Danh mục private string[] _categories = { "Tất cả" }; private string _selectedCategory = "Tất cả"; - - // EN: Product list / VI: Danh sách sản phẩm private List _products = new(); - - // EN: Cart items / VI: Mục giỏ hàng private readonly List _cartItems = new(); private IEnumerable FilteredProducts => _selectedCategory == "Tất cả" ? _products : _products.Where(p => p.Category == _selectedCategory); @@ -116,11 +402,7 @@ var apiProducts = await productsTask; var apiCategories = await categoriesTask; - _products = apiProducts.Select(p => new Product( - p.Name, - p.Price, - p.Category ?? "Khác" - )).ToList(); + _products = apiProducts.Select(p => new Product(p.Name, p.Price, p.Category ?? "Khác")).ToList(); var catNames = apiCategories.Select(c => c.Name).ToList(); if (catNames.Count > 0) @@ -131,18 +413,13 @@ _categories = new[] { "Tất cả" }.Concat(productCats).ToArray(); } } - catch - { - _loadError = true; - } - finally - { - _isLoading = false; - } + catch { _loadError = true; } + finally { _isLoading = false; } } private void AddToCart(Product product) { + if (_paymentStep != PayStep.None) return; var existing = _cartItems.FirstOrDefault(i => i.Name == product.Name); if (existing != null) existing.Qty++; else _cartItems.Add(new CartItem(product.Name, product.Price)); @@ -154,9 +431,166 @@ if (item.Qty <= 0) _cartItems.Remove(item); } - private void Checkout() => NavigateTo("cafe/order-customize"); + // ═══════════════ INLINE PAYMENT ═══════════════ + private enum PayStep { None, MethodSelect, AmountInput, Processing, Success } + private PayStep _paymentStep = PayStep.None; + private string _selectedMethod = ""; + private decimal _receivedAmount; + private string _customAmountInput = ""; + private decimal _lastOrderTotal; + private string _lastTransactionId = ""; + private decimal ChangeAmount => _receivedAmount - CartTotal; - // EN: Models / VI: Mô hình dữ liệu + private void StartPayment() + { + if (!_cartItems.Any()) return; + _paymentStep = PayStep.MethodSelect; + } + + private void CancelPayment() + { + _paymentStep = PayStep.None; + _selectedMethod = ""; + _receivedAmount = 0; + _customAmountInput = ""; + } + + private void SelectPaymentMethod(string method) + { + _selectedMethod = method; + _receivedAmount = 0; + _customAmountInput = ""; + + if (method == "cash") + _paymentStep = PayStep.AmountInput; + else + _paymentStep = PayStep.Processing; + } + + private string GetMethodLabel() => _selectedMethod switch + { + "cash" => "💵 Tiền mặt", + "card" => "💳 Thẻ", + "qr" => "📱 Mã QR", + "transfer" => "🏦 Chuyển khoản", + _ => "Thanh toán" + }; + + private List<(string Label, decimal Value)> GetQuickAmounts() + { + var total = CartTotal; + var amounts = new List<(string, decimal)>(); + var roundUp = Math.Ceiling(total / 50_000) * 50_000; + if (roundUp == total) roundUp += 50_000; + amounts.Add(("Đúng tiền", total)); + amounts.Add((FormatPrice(roundUp), roundUp)); + amounts.Add((FormatPrice(roundUp + 50_000), roundUp + 50_000)); + amounts.Add((FormatPrice(500_000), 500_000)); + amounts.Add((FormatPrice(200_000), 200_000)); + amounts.Add((FormatPrice(1_000_000), 1_000_000)); + return amounts; + } + + private void ApplyCustomAmount() + { + if (decimal.TryParse(_customAmountInput, out var val)) + _receivedAmount = val; + } + + private void ConfirmPayment() + { + _lastOrderTotal = CartTotal; + _lastTransactionId = $"TXN-{DateTime.Now:yyyyMMdd}-{Random.Shared.Next(100, 999)}"; + + // EN: Save to history / VI: Lưu vào lịch sử + var methodLabel = _selectedMethod switch { "cash" => "Tiền mặt", "card" => "Thẻ", "qr" => "QR Code", _ => "Chuyển khoản" }; + _orderHistory.Insert(0, new OrderRecord( + _lastTransactionId, + string.Join(", ", _cartItems.Select(i => $"{i.Name} x{i.Qty}")), + _lastOrderTotal, + DateTime.Now.ToString("HH:mm"), + methodLabel, + "Hoàn thành" + )); + + _paymentStep = PayStep.Success; + } + + private void ResetAfterPayment() + { + _cartItems.Clear(); + _paymentStep = PayStep.None; + _selectedMethod = ""; + _receivedAmount = 0; + _customAmountInput = ""; + } + + // ═══════════════ HISTORY TAB ═══════════════ + private string _historySearch = ""; + private string _historyFilter = "today"; + private OrderRecord? _selectedOrder; + + private readonly List _historyFilters = new() + { + new("today", "Hôm nay"), + new("week", "7 ngày"), + new("month", "30 ngày"), + }; + + // EN: Demo history data + real-time additions / VI: Dữ liệu lịch sử mẫu + thêm real-time + private readonly List _orderHistory = new() + { + new("TXN-20260303-001", "Cà phê sữa đá x2, Bạc xỉu x1", 103_000, "10:15", "Tiền mặt", "Hoàn thành"), + new("TXN-20260303-002", "Cappuccino x1, Croissant bơ x2", 125_000, "10:02", "QR Code", "Hoàn thành"), + new("TXN-20260303-003", "Trà đào cam sả x3, Sinh tố bơ x1", 190_000, "09:48", "Thẻ", "Hoàn thành"), + new("TXN-20260303-004", "Matcha Latte x1", 59_000, "09:30", "Tiền mặt", "Hoàn thành"), + new("TXN-20260303-005", "Espresso x2, Latte x1", 145_000, "09:15", "Chuyển khoản", "Hoàn thành"), + new("TXN-20260303-006", "Cà phê đen đá x4", 116_000, "08:55", "Tiền mặt", "Hoàn thành"), + new("TXN-20260303-007", "Trà sen vàng x2, Bánh mì bơ x1", 133_000, "08:40", "QR Code", "Hoàn thành"), + new("TXN-20260303-008", "Sinh tố bơ x1", 55_000, "08:25", "Tiền mặt", "Hoàn trả"), + }; + + private IEnumerable FilteredOrders => + string.IsNullOrWhiteSpace(_historySearch) + ? _orderHistory + : _orderHistory.Where(o => + o.Id.Contains(_historySearch, StringComparison.OrdinalIgnoreCase) || + o.Items.Contains(_historySearch, StringComparison.OrdinalIgnoreCase)); + + // ═══════════════ DASHBOARD TAB ═══════════════ + private readonly List _dashStats = new() + { + new("Doanh thu", "8,450,000₫", "var(--pos-orange-primary)", "+12% so với hôm qua"), + new("Đơn hàng", "156", "var(--pos-success)", "TB 54,167₫/đơn"), + new("Khách hàng", "132", "var(--pos-info)", "18% khách quay lại"), + new("Món bán ra", "347", "var(--pos-warning)", "2.2 món/đơn"), + }; + + private readonly List _dashPopular = new() + { + new("Cà phê sữa đá", 52, 1_820_000), + new("Bạc xỉu", 38, 1_482_000), + new("Trà đào cam sả", 29, 1_305_000), + new("Cappuccino", 24, 1_320_000), + new("Sinh tố bơ", 18, 990_000), + }; + + private readonly List _dashPayments = new() + { + new("Tiền mặt", 4_650_000, 55, "var(--pos-success)"), + new("Chuyển khoản", 2_535_000, 30, "var(--pos-info)"), + new("Thẻ", 845_000, 10, "var(--pos-warning)"), + new("Ví điện tử", 420_000, 5, "var(--pos-orange-primary)"), + }; + + private readonly List _dashHourly = new() + { + new("7h", 30), new("8h", 65), new("9h", 85), new("10h", 55), + new("11h", 90), new("12h", 100), new("13h", 70), new("14h", 45), + new("15h", 60), new("16h", 75), new("17h", 50), new("18h", 20), + }; + + // ═══════════════ RECORDS ═══════════════ private record Product(string Name, decimal Price, string Category); private class CartItem(string name, decimal price) { @@ -164,4 +598,10 @@ public decimal Price { get; set; } = price; public int Qty { get; set; } = 1; } + private record OrderRecord(string Id, string Items, decimal Total, string Time, string Method, string Status); + private record HistoryFilter(string Key, string Label); + private record DashStat(string Label, string Value, string Color, string Sub); + private record DashPopular(string Name, int Qty, decimal Revenue); + private record DashPayment(string Method, decimal Amount, int Pct, string Color); + private record DashHour(string Hour, int Pct); }