diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Retail/RetailDesktop.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Retail/RetailDesktop.razor new file mode 100644 index 00000000..d14dcb50 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Retail/RetailDesktop.razor @@ -0,0 +1,164 @@ +@* + EN: Retail POS Desktop — Left panel: category tabs + product grid with barcode input. Right panel: cart/bill. + VI: POS Bán lẻ Desktop — Panel trái: tab danh mục + lưới sản phẩm với quét mã vạch. Panel phải: giỏ hàng. +*@ +@page "/pos/retail" +@layout PosLayout +@inherits PosBase + +@* ═══ PRODUCT PANEL ═══ *@ +
+ @* EN: Barcode input / VI: Ô nhập mã vạch *@ +
+
+ + + +
+
+ + @* 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) + { +
+
+ +
+ @product.Name +
+ @FormatPrice(product.Price) + Kho: @product.Stock +
+ @product.Sku +
+ } +
+
+ +@* ═══ CART PANEL ═══ *@ +
+
+ Giỏ hàng + @_cartItems.Count sản phẩm +
+ +
+ @foreach (var item in _cartItems) + { +
+
+ @item.Name + @item.Sku + @FormatPrice(item.Price) +
+
+ + @item.Qty + +
+
+ } +
+ + +
+ +@code { + // EN: Categories / VI: Danh mục + private readonly string[] _categories = { "Tất cả", "Thời trang", "Phụ kiện", "Điện tử", "Gia dụng", "Mỹ phẩm" }; + private string _selectedCategory = "Tất cả"; + private string _barcodeInput = ""; + + // EN: Retail product list / VI: Danh sách sản phẩm bán lẻ + private readonly List _products = new() + { + new("Áo thun nam basic", "SKU-TT001", 199_000, 45, "Thời trang", "shirt"), + new("Quần jean nữ slim", "SKU-TT002", 450_000, 22, "Thời trang", "shirt"), + new("Áo khoác gió unisex", "SKU-TT003", 350_000, 18, "Thời trang", "shirt"), + new("Váy liền công sở", "SKU-TT004", 520_000, 12, "Thời trang", "shirt"), + new("Túi xách da nữ", "SKU-PK001", 890_000, 8, "Phụ kiện", "shopping-bag"), + new("Ví da nam", "SKU-PK002", 350_000, 30, "Phụ kiện", "wallet"), + new("Kính mát thời trang", "SKU-PK003", 280_000, 25, "Phụ kiện", "glasses"), + new("Tai nghe Bluetooth", "SKU-DT001", 650_000, 15, "Điện tử", "headphones"), + new("Sạc dự phòng 10000mAh", "SKU-DT002", 380_000, 40, "Điện tử", "battery-charging"), + new("Chuột không dây", "SKU-DT003", 250_000, 35, "Điện tử", "mouse"), + new("Nồi cơm điện 1.8L", "SKU-GD001", 890_000, 10, "Gia dụng", "cooking-pot"), + new("Bình giữ nhiệt 500ml", "SKU-GD002", 180_000, 50, "Gia dụng", "cup-soda"), + new("Son môi cao cấp", "SKU-MP001", 320_000, 28, "Mỹ phẩm", "sparkles"), + new("Kem chống nắng SPF50", "SKU-MP002", 280_000, 35, "Mỹ phẩm", "sun"), + new("Nước hoa mini 30ml", "SKU-MP003", 450_000, 20, "Mỹ phẩm", "droplets"), + }; + + // 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); + private decimal CartTotal => _cartItems.Sum(i => i.Price * i.Qty); + + private void AddToCart(Product product) + { + var existing = _cartItems.FirstOrDefault(i => i.Sku == product.Sku); + if (existing != null) existing.Qty++; + else _cartItems.Add(new CartItem(product.Name, product.Sku, product.Price)); + } + + private void ChangeQty(CartItem item, int delta) + { + item.Qty += delta; + if (item.Qty <= 0) _cartItems.Remove(item); + } + + private void SearchBarcode() + { + var found = _products.FirstOrDefault(p => p.Sku.Equals(_barcodeInput, StringComparison.OrdinalIgnoreCase)); + if (found != null) AddToCart(found); + _barcodeInput = ""; + } + + private void Checkout() { } + + // EN: Models / VI: Mô hình dữ liệu + private record Product(string Name, string Sku, decimal Price, int Stock, string Category, string Icon); + private class CartItem(string name, string sku, decimal price) + { + public string Name { get; set; } = name; + public string Sku { get; set; } = sku; + public decimal Price { get; set; } = price; + public int Qty { get; set; } = 1; + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Retail/RetailMobile.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Retail/RetailMobile.razor new file mode 100644 index 00000000..bed440f7 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Retail/RetailMobile.razor @@ -0,0 +1,153 @@ +@* + EN: Retail POS Mobile — Single column, floating cart button, bottom sheet cart. + VI: POS Bán lẻ Mobile — Một cột, nút giỏ hàng nổi, giỏ hàng dạng sheet dưới. +*@ +@page "/pos/retail/mobile" +@layout PosLayout +@inherits PosBase + +
+ @* EN: Barcode input / VI: Ô nhập mã vạch *@ +
+
+ + +
+
+ + @* 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) + { +
+
+ +
+ @product.Name + @FormatPrice(product.Price) + Kho: @product.Stock +
+ } +
+ + @* EN: Floating cart button / VI: Nút giỏ hàng nổi *@ + @if (_cartItems.Any()) + { + + } + + @* EN: Bottom sheet cart / VI: Giỏ hàng dạng sheet dưới *@ + @if (_showCart) + { +
+
+ @* EN: Handle bar / VI: Thanh kéo *@ +
+
+
+ +
+ Giỏ hàng + +
+ +
+ @foreach (var item in _cartItems) + { +
+
+ @item.Name + @FormatPrice(item.Price) +
+
+ + @item.Qty + +
+
+ } +
+ + +
+
+ } +
+ +@code { + private readonly string[] _categories = { "Tất cả", "Thời trang", "Phụ kiện", "Điện tử", "Gia dụng", "Mỹ phẩm" }; + private string _selectedCategory = "Tất cả"; + private string _barcodeInput = ""; + private bool _showCart; + + private readonly List _products = new() + { + new("Áo thun nam basic", "SKU-TT001", 199_000, 45, "Thời trang", "shirt"), + new("Quần jean nữ slim", "SKU-TT002", 450_000, 22, "Thời trang", "shirt"), + new("Áo khoác gió unisex", "SKU-TT003", 350_000, 18, "Thời trang", "shirt"), + new("Túi xách da nữ", "SKU-PK001", 890_000, 8, "Phụ kiện", "shopping-bag"), + new("Ví da nam", "SKU-PK002", 350_000, 30, "Phụ kiện", "wallet"), + new("Tai nghe Bluetooth", "SKU-DT001", 650_000, 15, "Điện tử", "headphones"), + new("Sạc dự phòng 10000mAh", "SKU-DT002", 380_000, 40, "Điện tử", "battery-charging"), + new("Nồi cơm điện 1.8L", "SKU-GD001", 890_000, 10, "Gia dụng", "cooking-pot"), + new("Son môi cao cấp", "SKU-MP001", 320_000, 28, "Mỹ phẩm", "sparkles"), + new("Kem chống nắng SPF50", "SKU-MP002", 280_000, 35, "Mỹ phẩm", "sun"), + }; + + private readonly List _cartItems = new(); + private IEnumerable FilteredProducts => + _selectedCategory == "Tất cả" ? _products : _products.Where(p => p.Category == _selectedCategory); + private decimal CartTotal => _cartItems.Sum(i => i.Price * i.Qty); + + private void AddToCart(Product product) + { + var existing = _cartItems.FirstOrDefault(i => i.Sku == product.Sku); + if (existing != null) existing.Qty++; + else _cartItems.Add(new CartItem(product.Name, product.Sku, product.Price)); + } + + private void ChangeQty(CartItem item, int delta) + { + item.Qty += delta; + if (item.Qty <= 0) _cartItems.Remove(item); + } + + private void Checkout() { } + + private record Product(string Name, string Sku, decimal Price, int Stock, string Category, string Icon); + private class CartItem(string name, string sku, decimal price) + { + public string Name { get; set; } = name; + public string Sku { get; set; } = sku; + public decimal Price { get; set; } = price; + public int Qty { get; set; } = 1; + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Retail/RetailTablet.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Retail/RetailTablet.razor new file mode 100644 index 00000000..9a1df4c2 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Retail/RetailTablet.razor @@ -0,0 +1,146 @@ +@* + EN: Retail POS Tablet — Touch-optimized 2-column: products + cart sidebar (340px), larger elements. + VI: POS Bán lẻ Tablet — 2 cột tối ưu cảm ứng: sản phẩm + giỏ hàng bên (340px), phần tử lớn hơn. +*@ +@page "/pos/retail/tablet" +@layout PosLayout +@inherits PosBase + +@* ═══ PRODUCT PANEL ═══ *@ +
+ @* EN: Barcode input / VI: Ô nhập mã vạch *@ +
+
+ + +
+
+ + @* EN: Category tabs / VI: Tab danh mục *@ +
+ @foreach (var cat in _categories) + { + + } +
+ + @* EN: Product grid (larger for tablet) / VI: Lưới sản phẩm (lớn hơn cho tablet) *@ +
+ @foreach (var product in FilteredProducts) + { +
+
+ +
+ @product.Name + @FormatPrice(product.Price) + Kho: @product.Stock · @product.Sku +
+ } +
+
+ +@* ═══ CART SIDEBAR ═══ *@ +
+
+ Giỏ hàng + +
+ +
+ @foreach (var item in _cartItems) + { +
+
+ @item.Name + @item.Sku + @FormatPrice(item.Price) +
+
+ + @item.Qty + +
+
+ } +
+ + +
+ +@code { + private readonly string[] _categories = { "Tất cả", "Thời trang", "Phụ kiện", "Điện tử", "Gia dụng", "Mỹ phẩm" }; + private string _selectedCategory = "Tất cả"; + private string _barcodeInput = ""; + + private readonly List _products = new() + { + new("Áo thun nam basic", "SKU-TT001", 199_000, 45, "Thời trang", "shirt"), + new("Quần jean nữ slim", "SKU-TT002", 450_000, 22, "Thời trang", "shirt"), + new("Áo khoác gió unisex", "SKU-TT003", 350_000, 18, "Thời trang", "shirt"), + new("Túi xách da nữ", "SKU-PK001", 890_000, 8, "Phụ kiện", "shopping-bag"), + new("Ví da nam", "SKU-PK002", 350_000, 30, "Phụ kiện", "wallet"), + new("Kính mát thời trang", "SKU-PK003", 280_000, 25, "Phụ kiện", "glasses"), + new("Tai nghe Bluetooth", "SKU-DT001", 650_000, 15, "Điện tử", "headphones"), + new("Sạc dự phòng 10000mAh", "SKU-DT002", 380_000, 40, "Điện tử", "battery-charging"), + new("Nồi cơm điện 1.8L", "SKU-GD001", 890_000, 10, "Gia dụng", "cooking-pot"), + new("Son môi cao cấp", "SKU-MP001", 320_000, 28, "Mỹ phẩm", "sparkles"), + new("Kem chống nắng SPF50", "SKU-MP002", 280_000, 35, "Mỹ phẩm", "sun"), + }; + + private readonly List _cartItems = new(); + private IEnumerable FilteredProducts => + _selectedCategory == "Tất cả" ? _products : _products.Where(p => p.Category == _selectedCategory); + private decimal CartTotal => _cartItems.Sum(i => i.Price * i.Qty); + + private void AddToCart(Product product) + { + var existing = _cartItems.FirstOrDefault(i => i.Sku == product.Sku); + if (existing != null) existing.Qty++; + else _cartItems.Add(new CartItem(product.Name, product.Sku, product.Price)); + } + + private void ChangeQty(CartItem item, int delta) + { + item.Qty += delta; + if (item.Qty <= 0) _cartItems.Remove(item); + } + + private void Checkout() { } + + private record Product(string Name, string Sku, decimal Price, int Stock, string Category, string Icon); + private class CartItem(string name, string sku, decimal price) + { + public string Name { get; set; } = name; + public string Sku { get; set; } = sku; + public decimal Price { get; set; } = price; + public int Qty { get; set; } = 1; + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Retail/Workflow/ProductSearch.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Retail/Workflow/ProductSearch.razor new file mode 100644 index 00000000..174c96eb --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Retail/Workflow/ProductSearch.razor @@ -0,0 +1,178 @@ +@* + EN: Product Search — Search by name, SKU, barcode with filters. Add to cart from results. + VI: Tìm kiếm sản phẩm — Tìm theo tên, SKU, mã vạch với bộ lọc. Thêm vào giỏ từ kết quả. +*@ +@page "/pos/retail/product-search" +@layout PosLayout +@inherits PosBase + +
+ @* ═══ HEADER ═══ *@ +
+ + Tìm kiếm sản phẩm +
+ + @* ═══ SEARCH BAR / THANH TÌM KIẾM ═══ *@ +
+
+ + + @if (!string.IsNullOrEmpty(_searchQuery)) + { + + } +
+
+ + @* ═══ FILTERS / BỘ LỌC ═══ *@ +
+ @* EN: Category filter / VI: Lọc danh mục *@ +
+ @foreach (var cat in _categories) + { + + } +
+ + @* EN: Price range / VI: Khoảng giá *@ + +
+ + @* ═══ SEARCH RESULTS / KẾT QUẢ TÌM KIẾM ═══ *@ +
+
+ @FilteredResults.Count() kết quả @(!string.IsNullOrEmpty(_searchQuery) ? $"cho \"{_searchQuery}\"" : "") +
+ + @foreach (var product in FilteredResults) + { +
+ @* EN: Product image placeholder / VI: Ảnh sản phẩm (placeholder) *@ +
+ +
+ + @* EN: Product info / VI: Thông tin sản phẩm *@ +
+
@product.Name
+
@product.Sku · @product.Category
+
+ @FormatPrice(product.Price) + + Kho: @product.Stock + +
+
+ + @* EN: Add to cart / VI: Thêm vào giỏ *@ + +
+ } +
+ + @* ═══ CART SUMMARY BAR / THANH TÓM TẮT GIỎ ═══ *@ + @if (_cartItems.Any()) + { +
+
+ Giỏ hàng: @_cartItems.Sum(i => i.Qty) sản phẩm + + @FormatPrice(_cartItems.Sum(i => i.Price * i.Qty)) + +
+ +
+ } +
+ +@code { + private string _searchQuery = "ao"; + private string _filterCategory = "Tất cả"; + private string _priceRange = "all"; + private readonly string[] _categories = { "Tất cả", "Thời trang", "Phụ kiện", "Điện tử", "Gia dụng", "Mỹ phẩm" }; + + // EN: All products / VI: Tất cả sản phẩm + private readonly List _products = new() + { + new("Áo thun nam basic", "SKU-TT001", 199_000, 45, "Thời trang", "shirt"), + new("Áo khoác gió unisex", "SKU-TT003", 350_000, 18, "Thời trang", "shirt"), + new("Áo polo nam", "SKU-TT005", 280_000, 32, "Thời trang", "shirt"), + new("Áo sơ mi nữ", "SKU-TT006", 320_000, 14, "Thời trang", "shirt"), + new("Quần jean nữ slim", "SKU-TT002", 450_000, 22, "Thời trang", "shirt"), + new("Váy liền công sở", "SKU-TT004", 520_000, 12, "Thời trang", "shirt"), + new("Túi xách da nữ", "SKU-PK001", 890_000, 8, "Phụ kiện", "shopping-bag"), + new("Ví da nam", "SKU-PK002", 350_000, 30, "Phụ kiện", "wallet"), + new("Kính mát thời trang", "SKU-PK003", 280_000, 25, "Phụ kiện", "glasses"), + new("Tai nghe Bluetooth", "SKU-DT001", 650_000, 15, "Điện tử", "headphones"), + new("Sạc dự phòng 10000mAh", "SKU-DT002", 380_000, 40, "Điện tử", "battery-charging"), + new("Chuột không dây", "SKU-DT003", 250_000, 35, "Điện tử", "mouse"), + new("Nồi cơm điện 1.8L", "SKU-GD001", 890_000, 10, "Gia dụng", "cooking-pot"), + new("Bình giữ nhiệt 500ml", "SKU-GD002", 180_000, 50, "Gia dụng", "cup-soda"), + new("Son môi cao cấp", "SKU-MP001", 320_000, 28, "Mỹ phẩm", "sparkles"), + }; + + private readonly List _cartItems = new(); + + private IEnumerable FilteredResults + { + get + { + var result = _products.AsEnumerable(); + if (!string.IsNullOrWhiteSpace(_searchQuery)) + result = result.Where(p => p.Name.Contains(_searchQuery, StringComparison.OrdinalIgnoreCase) + || p.Sku.Contains(_searchQuery, StringComparison.OrdinalIgnoreCase)); + if (_filterCategory != "Tất cả") + result = result.Where(p => p.Category == _filterCategory); + result = _priceRange switch + { + "under200" => result.Where(p => p.Price < 200_000), + "200to500" => result.Where(p => p.Price >= 200_000 && p.Price <= 500_000), + "over500" => result.Where(p => p.Price > 500_000), + _ => result + }; + return result; + } + } + + private void AddToCart(Product product) + { + var existing = _cartItems.FirstOrDefault(i => i.Sku == product.Sku); + if (existing != null) existing.Qty++; + else _cartItems.Add(new CartItem(product.Name, product.Sku, product.Price)); + } + + private record Product(string Name, string Sku, decimal Price, int Stock, string Category, string Icon); + private class CartItem(string name, string sku, decimal price) + { + public string Name { get; set; } = name; + public string Sku { get; set; } = sku; + public decimal Price { get; set; } = price; + public int Qty { get; set; } = 1; + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Retail/Workflow/ReturnExchange.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Retail/Workflow/ReturnExchange.razor new file mode 100644 index 00000000..81d37441 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Retail/Workflow/ReturnExchange.razor @@ -0,0 +1,192 @@ +@* + EN: Returns & Exchanges — Receipt lookup, original order items, select returns, refund method, confirm. + VI: Đổi trả hàng — Tra cứu hóa đơn, món gốc, chọn trả, phương thức hoàn tiền, xác nhận. +*@ +@page "/pos/retail/return-exchange" +@layout PosLayout +@inherits PosBase + +
+ @* ═══ HEADER ═══ *@ +
+ + Đổi trả hàng +
+ +
+ @* ═══ LEFT: RECEIPT LOOKUP + ITEMS / TRÁI: TRA CỨU HÓA ĐƠN + DANH SÁCH MÓN ═══ *@ +
+ @* EN: Receipt lookup / VI: Tra cứu hóa đơn *@ +
+
Tra cứu hóa đơn
+
+
+ + +
+ +
+
+ + @if (_receiptFound) + { + @* EN: Original order info / VI: Thông tin đơn gốc *@ +
+
+
+
Hóa đơn #@_receipt.Id
+
@_receipt.Date · @_receipt.Staff
+
+ @FormatPrice(_receipt.Total) +
+ + @* EN: Select items to return / VI: Chọn sản phẩm trả lại *@ +
Chọn sản phẩm đổi trả
+ @foreach (var item in _receipt.Items) + { +
+ +
+
@item.Name
+
@item.Sku · SL: @item.Qty
+
+ @FormatPrice(item.Price * item.Qty) +
+ } +
+ } +
+ + @* ═══ RIGHT: RETURN DETAILS / PHẢI: CHI TIẾT ĐỔI TRẢ ═══ *@ +
+
+ Chi tiết đổi trả +
+ +
+ @if (_receiptFound && SelectedItems.Any()) + { + @* EN: Selected return items / VI: Sản phẩm đã chọn trả *@ +
+
Sản phẩm trả (@SelectedItems.Count())
+ @foreach (var item in SelectedItems) + { +
+ @item.Name x@item.Qty + -@FormatPrice(item.Price * item.Qty) +
+ } +
+ + @* EN: Return reason / VI: Lý do đổi trả *@ +
+
Lý do
+ +
+ + @* EN: Refund method / VI: Phương thức hoàn tiền *@ +
+
Phương thức hoàn tiền
+
+ @foreach (var method in _refundMethods) + { + + } +
+
+ + @* EN: Refund amount / VI: Số tiền hoàn *@ +
+
Số tiền hoàn trả
+
+ @FormatPrice(RefundAmount) +
+
+ } + else + { +
+ Tra cứu hóa đơn và chọn sản phẩm để đổi trả +
+ } +
+ + @if (_receiptFound && SelectedItems.Any()) + { + + } +
+
+
+ +@code { + private string _receiptInput = "R2024001234"; + private bool _receiptFound = true; + private string _returnReason = ""; + private string _selectedRefundMethod = "original"; + + // EN: Refund methods / VI: Phương thức hoàn tiền + private readonly List _refundMethods = new() + { + new("original", "Hoàn về phương thức gốc", "Hoàn tiền về thẻ/TK ban đầu"), + new("cash", "Tiền mặt", "Hoàn tiền mặt tại quầy"), + new("credit", "Tín dụng cửa hàng", "Cộng vào tài khoản khách hàng"), + }; + + // EN: Demo receipt / VI: Hóa đơn mẫu + private readonly Receipt _receipt = new("R2024001234", "15/01/2024 · 14:30", "Trần Thị B", 1_619_000, new() + { + new("Áo thun nam basic", "SKU-TT001", 199_000, 2, true), + new("Túi xách da nữ", "SKU-PK001", 890_000, 1, false), + new("Kính mát thời trang", "SKU-PK003", 280_000, 1, false), + new("Bình giữ nhiệt 500ml", "SKU-GD002", 180_000, 1, false), + }); + + private IEnumerable SelectedItems => _receipt.Items.Where(i => i.Selected); + private decimal RefundAmount => SelectedItems.Sum(i => i.Price * i.Qty); + + private void LookupReceipt() => _receiptFound = true; + private void ConfirmReturn() { } + + private record RefundMethod(string Key, string Label, string Description); + private record Receipt(string Id, string Date, string Staff, decimal Total, List Items); + private class ReceiptItem(string name, string sku, decimal price, int qty, bool selected) + { + public string Name { get; set; } = name; + public string Sku { get; set; } = sku; + public decimal Price { get; set; } = price; + public int Qty { get; set; } = qty; + public bool Selected { get; set; } = selected; + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Retail/Workflow/StockCheck.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Retail/Workflow/StockCheck.razor new file mode 100644 index 00000000..d318f667 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Retail/Workflow/StockCheck.razor @@ -0,0 +1,185 @@ +@* + EN: Stock Check — Product search/scan, stock across branches, recent movements, reorder suggestion. + VI: Kiểm kho — Tìm/quét sản phẩm, tồn kho theo chi nhánh, biến động gần đây, gợi ý đặt hàng. +*@ +@page "/pos/retail/stock-check" +@layout PosLayout +@inherits PosBase + +
+ @* ═══ HEADER ═══ *@ +
+ + Kiểm kho nhanh +
+ + @* ═══ SEARCH BAR / THANH TÌM KIẾM ═══ *@ +
+
+
+ + +
+ +
+
+ +
+ @* ═══ PRODUCT DETAILS / CHI TIẾT SẢN PHẨM ═══ *@ +
+
+ +
+
+
@_product.Name
+
@_product.Sku · @_product.Category
+
+ @FormatPrice(_product.Price) + + Tổng: @_product.TotalStock SP + +
+
+
+ + @* ═══ STOCK BY BRANCH / TỒN KHO THEO CHI NHÁNH ═══ *@ +
+
+ + Tồn kho theo chi nhánh +
+ + @* EN: Branch stock table / VI: Bảng tồn kho chi nhánh *@ +
+
+ Chi nhánh + Tồn kho + Đã đặt + Trạng thái +
+ @foreach (var branch in _branchStock) + { +
+
+
@branch.Name
+
@branch.Address
+
+ @branch.Stock + @branch.Reserved +
+ + @GetStockLabel(branch.Stock) + +
+
+ } +
+
+ + @* ═══ RECENT STOCK MOVEMENTS / BIẾN ĐỘNG KHO GẦN ĐÂY ═══ *@ +
+
+ + Biến động gần đây +
+ + @foreach (var movement in _movements) + { +
+
+ +
+
+
@movement.Description
+
@movement.Date · @movement.Branch
+
+ + @(movement.Qty > 0 ? "+" : "")@movement.Qty + +
+ } +
+ + @* ═══ REORDER SUGGESTION / GỢI Ý ĐẶT HÀNG ═══ *@ +
+
+ +
+
+
Gợi ý đặt hàng
+
+ Chi nhánh Quận 3 sắp hết hàng (còn 5 SP). Đề xuất nhập thêm 30 SP dựa trên tốc độ bán trung bình 15 SP/tuần. +
+
+ +
+
+
+ +@code { + private string _searchQuery = "Áo thun nam"; + + // EN: Demo product / VI: Sản phẩm mẫu + private readonly StockProduct _product = new("Áo thun nam basic", "SKU-TT001", "Thời trang", 199_000, 72); + + // EN: Branch stock data / VI: Dữ liệu tồn kho chi nhánh + private readonly List _branchStock = new() + { + new("Chi nhánh Quận 1", "123 Nguyễn Huệ, Q.1", 32, 5), + new("Chi nhánh Quận 3", "45 Võ Văn Tần, Q.3", 5, 2), + new("Chi nhánh Quận 7", "789 Nguyễn Thị Thập, Q.7", 35, 8), + }; + + // EN: Recent movements / VI: Biến động gần đây + private readonly List _movements = new() + { + new("Bán hàng — Đơn #DH089", "25/01/2024", "Chi nhánh Q.1", -3), + new("Nhập kho từ NCC", "24/01/2024", "Chi nhánh Q.7", 20), + new("Bán hàng — Đơn #DH085", "24/01/2024", "Chi nhánh Q.3", -2), + new("Chuyển kho Q.1 → Q.3", "23/01/2024", "Chi nhánh Q.3", 10), + new("Chuyển kho Q.1 → Q.3", "23/01/2024", "Chi nhánh Q.1", -10), + new("Bán hàng — Đơn #DH080", "22/01/2024", "Chi nhánh Q.1", -5), + }; + + private static string GetStockColor(int stock) => stock switch + { + <= 10 => "var(--pos-danger)", + <= 20 => "var(--pos-warning)", + _ => "var(--pos-success)" + }; + + private static string GetStockBg(int stock) => stock switch + { + <= 10 => "rgba(239,68,68,.15)", + <= 20 => "rgba(245,158,11,.15)", + _ => "rgba(34,197,94,.15)" + }; + + private static string GetStockLabel(int stock) => stock switch + { + <= 10 => "Sắp hết", + <= 20 => "Thấp", + _ => "Đủ" + }; + + private record StockProduct(string Name, string Sku, string Category, decimal Price, int TotalStock); + private record BranchStock(string Name, string Address, int Stock, int Reserved); + private record StockMovement(string Description, string Date, string Branch, int Qty); +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Shared/Operations/CashDrawer.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Shared/Operations/CashDrawer.razor new file mode 100644 index 00000000..47eb0e5e --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Shared/Operations/CashDrawer.razor @@ -0,0 +1,144 @@ +@* + EN: Cash Drawer — Denomination breakdown, quick count with +/- buttons, expected vs actual totals, variance. + VI: Quản lý ngăn kéo tiền — Bảng mệnh giá, đếm nhanh +/-, so sánh dự kiến/thực tế, chênh lệch. +*@ +@page "/pos/operations/cash-drawer" +@layout PosLayout +@inherits PosBase + +
+ @* ═══ HEADER ═══ *@ +
+ + Kiểm tiền ngăn kéo + + + @(_drawerOpen ? "Đang mở" : "Đã đóng") + +
+ +
+ @* ═══ DENOMINATION BREAKDOWN (LEFT) / BẢNG MỆNH GIÁ (TRÁI) ═══ *@ +
+
Bảng mệnh giá
+ + @foreach (var denom in _denominations) + { +
+ @* EN: Denomination label / VI: Nhãn mệnh giá *@ +
+
@FormatPrice(denom.Value)
+
@denom.Label
+
+ + @* EN: Quick count controls / VI: Điều khiển đếm nhanh *@ +
+ + @denom.Count + +
+ + @* EN: Subtotal / VI: Thành tiền *@ +
+ @FormatPrice(denom.Value * denom.Count) +
+
+ } +
+ + @* ═══ SUMMARY PANEL (RIGHT) / PANEL TỔNG KẾT (PHẢI) ═══ *@ +
+ @* EN: Expected vs actual / VI: Dự kiến so với thực tế *@ +
+
Tổng kết
+ +
+ Dự kiến + @FormatPrice(_expectedCash) +
+
+ Thực đếm + @FormatPrice(ActualTotal) +
+
+ Chênh lệch + + @(Variance >= 0 ? "+" : "")@FormatPrice(Variance) + +
+
+ + @* EN: Variance status / VI: Trạng thái chênh lệch *@ +
+ +
+ @(Math.Abs(Variance) < 1000 ? "Cân bằng" : "Có chênh lệch") +
+
+ + @* EN: Drawer toggle / VI: Nút mở/đóng ngăn kéo *@ + + + @* EN: Save count button / VI: Nút lưu kiểm đếm *@ + +
+
+
+ +@code { + // EN: Drawer state / VI: Trạng thái ngăn kéo + private bool _drawerOpen = true; + private readonly decimal _expectedCash = 2_450_000; + + // EN: Denomination data / VI: Dữ liệu mệnh giá + private readonly List _denominations = new() + { + new(500_000, "Tờ 500K", 2), + new(200_000, "Tờ 200K", 3), + new(100_000, "Tờ 100K", 5), + new(50_000, "Tờ 50K", 4), + new(20_000, "Tờ 20K", 3), + new(10_000, "Tờ 10K", 5), + new(5_000, "Tờ 5K", 2), + new(2_000, "Tờ 2K", 3), + new(1_000, "Tờ 1K", 4), + new(500, "Xu 500", 6), + }; + + private decimal ActualTotal => _denominations.Sum(d => d.Value * d.Count); + private decimal Variance => ActualTotal - _expectedCash; + + private void ChangeDenomCount(Denomination denom, int delta) + { + denom.Count = Math.Max(0, denom.Count + delta); + } + + private void SaveCount() { } + + private class Denomination(decimal value, string label, int count) + { + public decimal Value { get; set; } = value; + public string Label { get; set; } = label; + public int Count { get; set; } = count; + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Shared/Operations/ClockInOut.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Shared/Operations/ClockInOut.razor new file mode 100644 index 00000000..00e73432 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Shared/Operations/ClockInOut.razor @@ -0,0 +1,110 @@ +@* + EN: Staff Clock In/Out — Real-time clock, staff info, toggle clock button, today's log. + VI: Chấm công nhân viên — Đồng hồ thời gian thực, thông tin NV, nút chấm công, nhật ký hôm nay. +*@ +@page "/pos/operations/clock-in-out" +@layout PosLayout +@inherits PosBase + +
+ @* ═══ HEADER ═══ *@ +
+ + Chấm công +
+ +
+ @* ═══ CURRENT TIME DISPLAY / ĐỒNG HỒ HIỆN TẠI ═══ *@ +
+
+ @_currentTime.ToString("HH:mm:ss") +
+
+ @_currentTime.ToString("dddd, dd/MM/yyyy") +
+
+ + @* ═══ STAFF INFO / THÔNG TIN NHÂN VIÊN ═══ *@ +
+
+ +
+
@_staffName
+
@_staffRole
+
+ @(_isClockedIn ? "Đang làm việc" : "Chưa chấm công") +
+
+ + @* ═══ CLOCK IN/OUT BUTTON / NÚT CHẤM CÔNG ═══ *@ + + + @* ═══ TODAY'S LOG / NHẬT KÝ HÔM NAY ═══ *@ +
+
+ Nhật ký hôm nay + + Tổng: @_totalHours.ToString("F1") giờ + +
+ + @foreach (var entry in _clockEntries) + { +
+
+ +
+
+
@(entry.Type == "in" ? "Vào ca" : "Ra ca")
+
@entry.Time
+
+ + @(entry.Type == "in" ? "IN" : "OUT") + +
+ } +
+
+
+ +@code { + // EN: Staff info / VI: Thông tin nhân viên + private readonly string _staffName = "Nguyễn Văn A"; + private readonly string _staffRole = "Thu ngân — Cashier"; + private bool _isClockedIn = true; + private DateTime _currentTime = DateTime.Now; + private double _totalHours = 5.5; + + // EN: Today's clock entries / VI: Bản ghi chấm công hôm nay + private readonly List _clockEntries = new() + { + new("08:00", "in"), + new("12:00", "out"), + new("13:30", "in"), + }; + + private void ToggleClock() + { + _isClockedIn = !_isClockedIn; + _clockEntries.Add(new(DateTime.Now.ToString("HH:mm"), _isClockedIn ? "in" : "out")); + } + + private record ClockEntry(string Time, string Type); +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Shared/Operations/PendingOrders.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Shared/Operations/PendingOrders.razor new file mode 100644 index 00000000..b1abb6a8 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Shared/Operations/PendingOrders.razor @@ -0,0 +1,181 @@ +@* + EN: Pending Orders — Active orders list with status filters, table/customer, items, elapsed time, quick actions. + VI: Đơn hàng đang chờ — Danh sách đơn đang xử lý với bộ lọc trạng thái, bàn/khách, món, thời gian, thao tác nhanh. +*@ +@page "/pos/operations/pending-orders" +@layout PosLayout +@inherits PosBase + +
+ @* ═══ HEADER ═══ *@ +
+ + Đơn hàng đang chờ + + @FilteredOrders.Count() đơn +
+ + @* ═══ STATUS FILTER TABS / TAB LỌC TRẠNG THÁI ═══ *@ +
+
+ @foreach (var tab in _statusTabs) + { + + } +
+
+ + @* ═══ ORDER LIST / DANH SÁCH ĐƠN HÀNG ═══ *@ +
+ @foreach (var order in FilteredOrders) + { +
+ @* EN: Order header / VI: Tiêu đề đơn *@ +
+
+ +
+
+
+ #@order.Id + @if (!string.IsNullOrEmpty(order.Table)) + { + — @order.Table + } +
+
@order.Customer
+
+
+
@FormatPrice(order.Total)
+
@order.Elapsed phút trước
+
+ + @GetStatusLabel(order.Status) + +
+ + @* EN: Order items / VI: Danh sách món *@ +
+ @foreach (var item in order.Items) + { + + @item + + } +
+ + @* EN: Quick actions / VI: Thao tác nhanh *@ +
+ + @if (order.Status != "ready") + { + + } + +
+
+ } +
+
+ +@code { + // EN: Status filter / VI: Bộ lọc trạng thái + private string _activeStatus = "all"; + private readonly List _statusTabs = new() + { + new("all", "Tất cả"), + new("pending", "Chờ xử lý"), + new("preparing", "Đang làm"), + new("ready", "Sẵn sàng"), + }; + + // EN: Demo pending orders / VI: Đơn hàng mẫu đang chờ + private readonly List _orders = new() + { + new("PO001", "Bàn 3", "Trần Văn B", new[] { "Phở bò x2", "Gỏi cuốn x1" }, 285_000, "pending", 3), + new("PO002", "Bàn 7", "Lê Thị C", new[] { "Cơm tấm x1", "Trà đá x2" }, 85_000, "preparing", 8), + new("PO003", "", "Nguyễn Văn D (Mang đi)", new[] { "Bún bò Huế x1", "Chả giò x2" }, 160_000, "pending", 5), + new("PO004", "Bàn 11", "Phạm Thị E", new[] { "Lẩu thái x1", "Nước mía x3" }, 295_000, "preparing", 15), + new("PO005", "Bàn 2", "Hoàng Văn F", new[] { "Cà phê sữa x3", "Bánh mì x2" }, 155_000, "ready", 20), + new("PO006", "Bàn 9", "Đỗ Minh G", new[] { "Gà nướng x1", "Cơm trắng x2", "Canh chua x1" }, 320_000, "pending", 1), + }; + + private IEnumerable FilteredOrders => + _activeStatus == "all" ? _orders : _orders.Where(o => o.Status == _activeStatus); + + private void ViewOrder(PendingOrder order) { } + + private void UpdateStatus(PendingOrder order) + { + order.Status = order.Status switch + { + "pending" => "preparing", + "preparing" => "ready", + _ => order.Status + }; + } + + private void CancelOrder(PendingOrder order) => _orders.Remove(order); + + private static string GetStatusColor(string status) => status switch + { + "pending" => "#F59E0B", + "preparing" => "#3B82F6", + "ready" => "#22C55E", + _ => "var(--pos-text-tertiary)" + }; + + private static string GetStatusBg(string status) => status switch + { + "pending" => "rgba(245,158,11,.15)", + "preparing" => "rgba(59,130,246,.15)", + "ready" => "rgba(34,197,94,.15)", + _ => "var(--pos-bg-interactive)" + }; + + private static string GetStatusLabel(string status) => status switch + { + "pending" => "Chờ xử lý", + "preparing" => "Đang làm", + "ready" => "Sẵn sàng", + _ => status + }; + + private record StatusTab(string Key, string Label); + private class PendingOrder(string id, string table, string customer, string[] items, decimal total, string status, int elapsed) + { + public string Id { get; set; } = id; + public string Table { get; set; } = table; + public string Customer { get; set; } = customer; + public string[] Items { get; set; } = items; + public decimal Total { get; set; } = total; + public string Status { get; set; } = status; + public int Elapsed { get; set; } = elapsed; + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Shared/Operations/QuickSale.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Shared/Operations/QuickSale.razor new file mode 100644 index 00000000..fceab6bc --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Shared/Operations/QuickSale.razor @@ -0,0 +1,150 @@ +@* + EN: Quick Sale — Numpad amount entry, category quick buttons, note field, pay button. + VI: Bán nhanh — Bàn phím số nhập tiền, nút danh mục nhanh, ghi chú, nút thanh toán. +*@ +@page "/pos/operations/quick-sale" +@layout PosLayout +@inherits PosBase + +
+ @* ═══ HEADER ═══ *@ +
+ + Bán nhanh +
+ +
+ @* ═══ NUMPAD PANEL (LEFT) / BẢNG SỐ (TRÁI) ═══ *@ +
+ @* EN: Amount display / VI: Hiển thị số tiền *@ +
+
Số tiền
+
+ @FormatPrice(decimal.Parse(_amountStr == "" ? "0" : _amountStr)) +
+
+ + @* EN: Numpad grid / VI: Lưới bàn phím số *@ +
+ @foreach (var key in _numpadKeys) + { + + } +
+
+ + @* ═══ RIGHT PANEL / PANEL PHẢI ═══ *@ +
+
+ Thông tin +
+ +
+ @* EN: Category quick buttons / VI: Nút danh mục nhanh *@ +
+
Danh mục
+
+ @foreach (var cat in _quickCategories) + { + + } +
+
+ + @* EN: Note input / VI: Ghi chú *@ +
+
Ghi chú
+ +
+
+ + @* ═══ ACTIONS PANEL (RIGHT) / PANEL HÀNH ĐỘNG (PHẢI) ═══ *@ +
+
+ Trạng thái +
+ +
+ @* EN: Status indicator / VI: Chỉ báo trạng thái *@ +
+
+ +
+
Đang thực hiện
+
Phòng 3 • Giường 2
+
+ + @* EN: Session details / VI: Chi tiết phiên *@ +
+
+ Bắt đầu + 14:00 +
+
+ Dự kiến kết thúc + 15:00 +
+
+ Đã gia hạn + @_extendedMinutes phút +
+
+ Dịch vụ tiếp theo + Facial collagen +
+
+
+ + +
+
+ +@code { + private string _remainingTime = "45:00"; + private int _totalDuration = 60; + private double _progress = 0.25; + private int _extendedMinutes = 0; + private string _notes = "Khách yêu cầu lực massage vừa phải. Chú ý vùng vai bị đau."; + + private readonly int[] _extendOptions = { 15, 30, 45 }; + + private void ExtendTime(int minutes) + { + _extendedMinutes += minutes; + _totalDuration += minutes; + } + + private void CompleteTreatment() => NavigateTo("spa/spa-journey"); +}