diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/RestaurantMobile.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/RestaurantMobile.razor index 70e849a9..6e7c57a5 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/RestaurantMobile.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/RestaurantMobile.razor @@ -5,98 +5,138 @@ @page "/pos/restaurant/mobile" @layout PosLayout @inherits PosBase +@inject WebClientTpos.Client.Services.PosDataService DataService
- @* ═══ HEADER / TIÊU ĐỀ ═══ *@ -
- Sơ đồ bàn - @_tables.Count bàn -
+ @if (_isLoading) + { +
+ Đang tải... +
+ } + else if (_loadError) + { +
+ Không thể tải dữ liệu +
+ } + else + { + @* ═══ HEADER / TIÊU ĐỀ ═══ *@ +
+ Sơ đồ bàn + @_tables.Count bàn +
- @* ═══ SECTION FILTER / LỌC KHU VỰC ═══ *@ -
- @foreach (var s in _sections) - { - - } -
+ @* ═══ SECTION FILTER / LỌC KHU VỰC ═══ *@ +
+ @foreach (var s in _sections) + { + + } +
- @* ═══ TABLE LIST / DANH SÁCH BÀN ═══ *@ -
- @foreach (var t in FilteredTables) - { -
- @* EN: Table number badge / VI: Badge số bàn *@ -
- @t.Number -
-
-
@t.Name
-
- @t.Seats chỗ · @t.Section + @* ═══ TABLE LIST / DANH SÁCH BÀN ═══ *@ +
+ @foreach (var t in FilteredTables) + { +
+ @* EN: Table number badge / VI: Badge số bàn *@ +
+ @t.Number
-
-
-
- @StatusLabel(t.Status) -
- @if (t.Status == "occupied") - { -
- @FormatPrice(t.Amount) +
+
@t.Name
+
+ @t.Seats chỗ · @t.Section
- } +
+
+
+ @StatusLabel(t.Status) +
+ @if (t.Status == "occupied") + { +
+ @FormatPrice(t.Amount) +
+ } +
+
- -
- } -
+ } +
- @* ═══ BOTTOM STATS / THỐNG KÊ DƯỚI ═══ *@ -
-
-
@_tables.Count(t => t.Status == "available")
-
Trống
+ @* ═══ BOTTOM STATS / THỐNG KÊ DƯỚI ═══ *@ +
+
+
@_tables.Count(t => t.Status == "available")
+
Trống
+
+
+
@_tables.Count(t => t.Status == "occupied")
+
Đang phục vụ
+
+
+
@_tables.Count(t => t.Status == "reserved")
+
Đã đặt
+
-
-
@_tables.Count(t => t.Status == "occupied")
-
Đang phục vụ
-
-
-
@_tables.Count(t => t.Status == "reserved")
-
Đã đặt
-
-
+ }
@code { - private string _activeSection = "Tất cả"; - private readonly string[] _sections = { "Tất cả", "Trong nhà", "Ngoài trời", "VIP" }; + // EN: Restaurant shop ID / VI: ID cửa hàng nhà hàng + private static readonly Guid RestaurantShopId = Guid.Parse("b0000002-0000-0000-0000-000000000002"); - // EN: Demo tables / VI: Bàn mẫu - private readonly List _tables = new() - { - new(1, "Bàn 1", 4, "available", "Trong nhà", 0), - new(2, "Bàn 2", 2, "occupied", "Trong nhà", 195_000), - new(3, "Bàn 3", 6, "occupied", "Trong nhà", 420_000), - new(4, "Bàn 4", 4, "reserved", "VIP", 0), - new(5, "Bàn 5", 8, "available", "VIP", 0), - new(6, "Bàn 6", 4, "occupied", "Ngoài trời", 310_000), - new(7, "Bàn 7", 4, "available", "Ngoài trời", 0), - new(8, "Bàn 8", 10, "reserved", "VIP", 0), - new(9, "Bàn 9", 2, "available", "Trong nhà", 0), - new(10, "Bàn 10", 4, "occupied", "Ngoài trời", 175_000), - }; + // EN: Loading state / VI: Trạng thái tải + private bool _isLoading = true; + private bool _loadError; + + // EN: Active section filter / VI: Bộ lọc khu vực + private string _activeSection = "Tất cả"; + private string[] _sections = { "Tất cả" }; + + // EN: Table data from API / VI: Dữ liệu bàn từ API + private List _tables = new(); private IEnumerable FilteredTables => _activeSection == "Tất cả" ? _tables : _tables.Where(t => t.Section == _activeSection); + protected override async Task OnInitializedAsync() + { + try + { + var apiTables = await DataService.GetTablesAsync(RestaurantShopId); + + _tables = apiTables.Select(t => new MobileTable( + int.TryParse(t.TableNumber, out var num) ? num : 0, + $"Bàn {t.TableNumber}", + t.Capacity, + t.Status ?? "available", + t.Zone ?? "Trong nhà", + 0 + )).ToList(); + + var zones = _tables.Select(t => t.Section).Distinct().ToList(); + _sections = new[] { "Tất cả" }.Concat(zones).ToArray(); + } + catch + { + _loadError = true; + } + finally + { + _isLoading = false; + } + } + private void OpenTable(MobileTable t) => NavigateTo("restaurant/waiter-pad"); private static string StatusColor(string s) => s switch diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/RestaurantTablet.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/RestaurantTablet.razor index e72435e8..6f9df994 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/RestaurantTablet.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/RestaurantTablet.razor @@ -5,34 +5,50 @@ @page "/pos/restaurant/tablet" @layout PosLayout @inherits PosBase +@inject WebClientTpos.Client.Services.PosDataService DataService
- @* ═══ SECTION FILTER / LỌC KHU VỰC ═══ *@ -
- @foreach (var s in _sections) - { - - } -
+ @if (_isLoading) + { +
+ Đang tải... +
+ } + else if (_loadError) + { +
+ Không thể tải dữ liệu +
+ } + else + { + @* ═══ SECTION FILTER / LỌC KHU VỰC ═══ *@ +
+ @foreach (var s in _sections) + { + + } +
- @* ═══ TABLE MAP / SƠ ĐỒ BÀN ═══ *@ -
- @foreach (var t in FilteredTables) - { -
- @t.Name - @t.Seats chỗ - @StatusLabel(t.Status) -
- } -
+ @* ═══ TABLE MAP / SƠ ĐỒ BÀN ═══ *@ +
+ @foreach (var t in FilteredTables) + { +
+ @t.Name + @t.Seats chỗ + @StatusLabel(t.Status) +
+ } +
+ }
@* ═══ ORDER SIDEBAR / SIDEBAR ĐẶT MÓN ═══ *@ @@ -85,28 +101,60 @@
@code { + // EN: Restaurant shop ID / VI: ID cửa hàng nhà hàng + private static readonly Guid RestaurantShopId = Guid.Parse("b0000002-0000-0000-0000-000000000002"); + + // EN: Loading state / VI: Trạng thái tải + private bool _isLoading = true; + private bool _loadError; + + // EN: Active section filter / VI: Bộ lọc khu vực private string _activeSection = "Tất cả"; - private readonly string[] _sections = { "Tất cả", "Trong nhà", "Ngoài trời", "VIP" }; + private string[] _sections = { "Tất cả" }; + + // EN: Selected table reference / VI: Bàn đang chọn private TableInfo? _selected; - private readonly List _tables = new() - { - new("T01","Bàn 1", 4, "available", "Trong nhà"), new("T02","Bàn 2", 2, "occupied", "Trong nhà"), - new("T03","Bàn 3", 6, "occupied", "Trong nhà"), new("T04","Bàn 4", 4, "reserved", "VIP"), - new("T05","Bàn 5", 8, "available", "VIP"), new("T06","Bàn 6", 4, "available", "Ngoài trời"), - new("T07","Bàn 7", 4, "occupied", "Ngoài trời"), new("T08","Bàn 8", 2, "available", "Ngoài trời"), - new("T09","Bàn 9", 10, "reserved", "VIP"), new("T10","Bàn 10", 6, "occupied", "Trong nhà"), - }; + // EN: Table data from API / VI: Dữ liệu bàn từ API + private List _tables = new(); private IEnumerable FilteredTables => _activeSection == "Tất cả" ? _tables : _tables.Where(t => t.Section == _activeSection); + // EN: Demo order items for occupied tables / VI: Món mẫu cho bàn đang phục vụ private readonly List _items = new() { new("Bún bò Huế", 80_000, 1), new("Nem rán", 50_000, 2), new("Cá kho tộ", 120_000, 1), new("Nước mía", 15_000, 2), }; + protected override async Task OnInitializedAsync() + { + try + { + var apiTables = await DataService.GetTablesAsync(RestaurantShopId); + + _tables = apiTables.Select(t => new TableInfo( + t.Id.ToString(), + $"Bàn {t.TableNumber}", + t.Capacity, + t.Status ?? "available", + t.Zone ?? "Trong nhà" + )).ToList(); + + var zones = _tables.Select(t => t.Section).Distinct().ToList(); + _sections = new[] { "Tất cả" }.Concat(zones).ToArray(); + } + catch + { + _loadError = true; + } + finally + { + _isLoading = false; + } + } + private static string StatusBg(string s) => s switch { "available" => "rgba(34,197,94,.15)", "occupied" => "rgba(255,92,0,.18)", diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/KitchenDisplay.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/KitchenDisplay.razor index 8a83e55b..d967fb1b 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/KitchenDisplay.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/KitchenDisplay.razor @@ -5,6 +5,7 @@ @page "/pos/restaurant/kitchen-display" @layout PosLayout @inherits PosBase +@inject WebClientTpos.Client.Services.PosDataService DataService
@* ═══ HEADER / TIÊU ĐỀ ═══ *@ @@ -23,6 +24,20 @@
@* ═══ TICKET COLUMNS / CỘT PHIẾU ═══ *@ + @if (_isLoading) + { +
+ Đang tải... +
+ } + else if (_loadError) + { +
+ Không thể tải dữ liệu +
+ } + else + {
@foreach (var status in _statuses) { @@ -91,15 +106,23 @@
}
+ }
@code { + // EN: Restaurant shop ID / VI: ID cửa hàng nhà hàng + private static readonly Guid RestaurantShopId = Guid.Parse("b0000002-0000-0000-0000-000000000002"); + + // EN: Loading state / VI: Trạng thái tải + private bool _isLoading = true; + private bool _loadError; + private readonly Dictionary _statuses = new() { ["new"] = "Mới", ["cooking"] = "Đang nấu", ["ready"] = "Sẵn sàng" }; - // EN: Demo kitchen tickets / VI: Phiếu bếp mẫu + // EN: Demo kitchen tickets — needs kitchen_tickets API / VI: Phiếu bếp mẫu — cần API kitchen_tickets private readonly List _tickets = new() { new("Bàn 2", "new", 2, new() { new(2, "Gỏi cuốn", ""), new(1, "Phở bò tái", "Ít hành") }), @@ -110,6 +133,24 @@ new("Bàn 10", "ready", 8, new() { new(2, "Cơm tấm sườn", ""), new(1, "Cà phê sữa", "") }), }; + protected override async Task OnInitializedAsync() + { + try + { + // EN: Preload tables for reference — kitchen_tickets API not yet available + // VI: Tải trước bàn để tham chiếu — API kitchen_tickets chưa có + await DataService.GetTablesAsync(RestaurantShopId); + } + catch + { + _loadError = true; + } + finally + { + _isLoading = false; + } + } + private static string ColumnBg(string s) => s switch { "new" => "rgba(239,68,68,.15)", "cooking" => "rgba(245,158,11,.15)", diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/OrderHistory.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/OrderHistory.razor index 068deb0e..10937e34 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/OrderHistory.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/OrderHistory.razor @@ -5,6 +5,7 @@ @page "/pos/restaurant/order-history" @layout PosLayout @inherits PosBase +@inject WebClientTpos.Client.Services.PosDataService DataService
@* ═══ HEADER / TIÊU ĐỀ ═══ *@ @@ -36,6 +37,20 @@ @* ═══ ORDER LIST / DANH SÁCH ĐƠN ═══ *@
+ @if (_isLoading) + { +
+ Đang tải... +
+ } + else if (_loadError) + { +
+ Không thể tải dữ liệu +
+ } + else + { @foreach (var order in FilteredOrders) {
} + }
@* ═══ FOOTER SUMMARY / TỔNG KẾT ═══ *@ @@ -93,12 +109,19 @@
@code { + // EN: Restaurant shop ID / VI: ID cửa hàng nhà hàng + private static readonly Guid RestaurantShopId = Guid.Parse("b0000002-0000-0000-0000-000000000002"); + + // EN: Loading state / VI: Trạng thái tải + private bool _isLoading = true; + private bool _loadError; + private string _searchQuery = string.Empty; private string _activeFilter = "Tất cả"; private string? _expandedId; private readonly string[] _filters = { "Tất cả", "Tiền mặt", "Thẻ", "Chuyển khoản" }; - // EN: Demo order history / VI: Lịch sử đơn mẫu + // EN: Demo order history — needs orders API / VI: Lịch sử đơn mẫu — cần API orders private readonly List _orders = new() { new("DH001", "B3", "10:15", "Nguyễn Văn A", 3, 285_000, "Tiền mặt", @@ -119,6 +142,24 @@ new() { new("Bún bò Huế", 80_000, 2), new("Gỏi cuốn", 45_000, 2), new("Cà phê sữa", 29_000, 2), new("Bánh flan", 25_000, 2) }), }; + protected override async Task OnInitializedAsync() + { + try + { + // EN: Preload tables for reference — orders API not yet available + // VI: Tải trước bàn để tham chiếu — API orders chưa có + await DataService.GetTablesAsync(RestaurantShopId); + } + catch + { + _loadError = true; + } + finally + { + _isLoading = false; + } + } + private IEnumerable FilteredOrders { get diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/RestaurantMenuManagement.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/RestaurantMenuManagement.razor index 369ef120..18399e41 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/RestaurantMenuManagement.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/RestaurantMenuManagement.razor @@ -5,6 +5,7 @@ @page "/pos/restaurant/menu-management" @layout PosLayout @inherits PosBase +@inject WebClientTpos.Client.Services.PosDataService DataService
@* ═══ HEADER / TIÊU ĐỀ ═══ *@ @@ -53,6 +54,20 @@ @* ═══ MENU ITEM GRID / LƯỚI MÓN ═══ *@
+ @if (_isLoading) + { +
+ Đang tải... +
+ } + else if (_loadError) + { +
+ Không thể tải dữ liệu +
+ } + else + {
@foreach (var item in FilteredMenu) { @@ -92,6 +107,7 @@
}
+ }
@* ═══ STATS BAR / THANH THỐNG KÊ ═══ *@ @@ -104,30 +120,52 @@
@code { - private string _activeCategory = "Khai vị"; + // EN: Restaurant shop ID / VI: ID cửa hàng nhà hàng + private static readonly Guid RestaurantShopId = Guid.Parse("b0000002-0000-0000-0000-000000000002"); + + // EN: Loading state / VI: Trạng thái tải + private bool _isLoading = true; + private bool _loadError; + + private string _activeCategory = "Tất cả"; private bool _editMode = false; private string? _selectedItem; - private readonly string[] _categories = { "Khai vị", "Món chính", "Lẩu", "Nước", "Tráng miệng" }; + private string[] _categories = { "Tất cả" }; - // EN: Menu items / VI: Các món trong thực đơn - private readonly List _menuItems = new() - { - new("Gỏi cuốn tôm", 45_000, "Khai vị", "salad", true, ""), - new("Chả giò chiên", 40_000, "Khai vị", "flame", true, ""), - new("Súp cua", 55_000, "Khai vị", "soup", true, "Đặc biệt"), - new("Phở bò tái", 75_000, "Món chính", "beef", true, ""), - new("Cơm tấm sườn", 65_000, "Món chính", "utensils", true, ""), - new("Cá kho tộ", 120_000, "Món chính", "fish", false, "Hết nguyên liệu"), - new("Gà nướng mật ong", 180_000, "Món chính", "drumstick", true, ""), - new("Lẩu thái", 250_000, "Lẩu", "soup", true, "Best seller"), - new("Lẩu nấm", 200_000, "Lẩu", "leaf", true, ""), - new("Trà đá", 10_000, "Nước", "glass-water", true, ""), - new("Cà phê sữa", 29_000, "Nước", "coffee", true, ""), - new("Chè thái", 30_000, "Tráng miệng", "ice-cream-cone", true, ""), - }; + // EN: Menu items from API / VI: Các món từ API + private List _menuItems = new(); private IEnumerable FilteredMenu => - _menuItems.Where(m => m.Category == _activeCategory); + _activeCategory == "Tất cả" ? _menuItems : _menuItems.Where(m => m.Category == _activeCategory); + + protected override async Task OnInitializedAsync() + { + try + { + var products = await DataService.GetProductsAsync(RestaurantShopId); + + _menuItems = products.Select(p => new RestMenuItem( + p.Name, + p.Price, + p.Category ?? "Khác", + "utensils", + true, + "" + )).ToList(); + + var cats = _menuItems.Select(m => m.Category).Distinct().ToList(); + _categories = new[] { "Tất cả" }.Concat(cats).ToArray(); + _activeCategory = _categories.First(); + } + catch + { + _loadError = true; + } + finally + { + _isLoading = false; + } + } private void MarkSoldOut() { diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/TableMap.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/TableMap.razor index 2f16ca59..5aa70075 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/TableMap.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/TableMap.razor @@ -5,6 +5,7 @@ @page "/pos/restaurant/table-map" @layout PosLayout @inherits PosBase +@inject WebClientTpos.Client.Services.PosDataService DataService
@* ═══ TOOLBAR / THANH CÔNG CỤ ═══ *@ @@ -21,6 +22,20 @@
@* ═══ SECTION TABS / TAB KHU VỰC ═══ *@ + @if (_isLoading) + { +
+ Đang tải... +
+ } + else if (_loadError) + { +
+ Không thể tải dữ liệu +
+ } + else + {
@foreach (var s in _sections) { @@ -57,6 +72,8 @@
+ } + @* ═══ FOOTER STATS / THỐNG KÊ ═══ *@
Tổng: @_tables.Count bàn @@ -71,23 +88,51 @@
@code { + // EN: Restaurant shop ID / VI: ID cửa hàng nhà hàng + private static readonly Guid RestaurantShopId = Guid.Parse("b0000002-0000-0000-0000-000000000002"); + + // EN: Loading state / VI: Trạng thái tải + private bool _isLoading = true; + private bool _loadError; + private string _activeSection = "Tất cả"; - private readonly string[] _sections = { "Tất cả", "Trong nhà", "Ngoài trời", "VIP" }; + private string[] _sections = { "Tất cả" }; private readonly HashSet _selectedIds = new(); - private readonly List _tables = new() - { - new("T01","Bàn 1", 4, "available", "Trong nhà", "Vuông"), new("T02","Bàn 2", 2, "occupied", "Trong nhà", "Tròn"), - new("T03","Bàn 3", 6, "occupied", "Trong nhà", "Chữ nhật"),new("T04","Bàn 4", 4, "reserved", "VIP", "Tròn"), - new("T05","Bàn 5", 8, "available", "VIP", "Chữ nhật"), new("T06","Bàn 6", 4, "available", "Ngoài trời", "Vuông"), - new("T07","Bàn 7", 4, "occupied", "Ngoài trời", "Vuông"), new("T08","Bàn 8", 2, "available", "Ngoài trời", "Tròn"), - new("T09","Bàn 9", 10, "reserved", "VIP", "Chữ nhật"), new("T10","Bàn 10", 6, "occupied", "Trong nhà", "Vuông"), - new("T11","Bàn 11", 4, "available", "Trong nhà", "Tròn"), new("T12","Bàn 12", 2, "available", "Ngoài trời", "Tròn"), - }; + // EN: Table data from API / VI: Dữ liệu bàn từ API + private List _tables = new(); private IEnumerable FilteredTables => _activeSection == "Tất cả" ? _tables : _tables.Where(t => t.Section == _activeSection); + protected override async Task OnInitializedAsync() + { + try + { + var apiTables = await DataService.GetTablesAsync(RestaurantShopId); + + _tables = apiTables.Select(t => new MapTable( + t.Id.ToString(), + $"Bàn {t.TableNumber}", + t.Capacity, + t.Status ?? "available", + t.Zone ?? "Trong nhà", + t.Capacity <= 2 ? "Tròn" : t.Capacity <= 6 ? "Vuông" : "Chữ nhật" + )).ToList(); + + var zones = _tables.Select(t => t.Section).Distinct().ToList(); + _sections = new[] { "Tất cả" }.Concat(zones).ToArray(); + } + catch + { + _loadError = true; + } + finally + { + _isLoading = false; + } + } + private void ToggleSelect(MapTable t) { if (!_selectedIds.Add(t.Id)) _selectedIds.Remove(t.Id); diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/TableMergeSplit.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/TableMergeSplit.razor index 86c8fbf4..cb8e7644 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/TableMergeSplit.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/TableMergeSplit.razor @@ -5,6 +5,7 @@ @page "/pos/restaurant/table-merge-split" @layout PosLayout @inherits PosBase +@inject WebClientTpos.Client.Services.PosDataService DataService
@* ═══ HEADER / TIÊU ĐỀ ═══ *@ @@ -30,7 +31,19 @@
- @if (_mode == "merge") + @if (_isLoading) + { +
+ Đang tải... +
+ } + else if (_loadError) + { +
+ Không thể tải dữ liệu +
+ } + else if (_mode == "merge") { @* ═══ MERGE MODE / CHẾ ĐỘ GHÉP ═══ *@
@@ -158,28 +171,53 @@
@code { + // EN: Restaurant shop ID / VI: ID cửa hàng nhà hàng + private static readonly Guid RestaurantShopId = Guid.Parse("b0000002-0000-0000-0000-000000000002"); + + // EN: Loading state / VI: Trạng thái tải + private bool _isLoading = true; + private bool _loadError; + private string _mode = "merge"; private readonly HashSet _mergeSelected = new(); private string? _splitSelected; - // EN: Tables for merge / VI: Bàn để ghép - private readonly List _mergeTables = new() - { - new("T03", "Bàn 3", 4, "available"), - new("T04", "Bàn 4", 6, "available"), - new("T05", "Bàn 5", 4, "occupied"), - new("T06", "Bàn 6", 4, "available"), - new("T08", "Bàn 8", 2, "available"), - new("T11", "Bàn 11", 4, "available"), - }; + // EN: Tables from API / VI: Bàn từ API + private List _mergeTables = new(); + private List _splitTables = new(); - // EN: Tables for split / VI: Bàn để tách - private readonly List _splitTables = new() + protected override async Task OnInitializedAsync() { - new("T07", "Bàn 7", 6, 5), - new("T03", "Bàn 3", 4, 3), - new("T10", "Bàn 10", 8, 7), - }; + try + { + var apiTables = await DataService.GetTablesAsync(RestaurantShopId); + + _mergeTables = apiTables + .Select(t => new MergeTable( + t.Id.ToString(), + $"Bàn {t.TableNumber}", + t.Capacity, + t.Status ?? "available" + )).ToList(); + + _splitTables = apiTables + .Where(t => (t.Status ?? "available") == "occupied") + .Select(t => new SplitTable( + t.Id.ToString(), + $"Bàn {t.TableNumber}", + t.Capacity, + 0 + )).ToList(); + } + catch + { + _loadError = true; + } + finally + { + _isLoading = false; + } + } private void SwitchMode(string mode) { diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/TableSelect.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/TableSelect.razor index 0ee69893..eeb687a3 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/TableSelect.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/TableSelect.razor @@ -5,6 +5,7 @@ @page "/pos/restaurant/table-select" @layout PosLayout @inherits PosBase +@inject WebClientTpos.Client.Services.PosDataService DataService
@* ═══ HEADER / TIÊU ĐỀ ═══ *@ @@ -46,6 +47,20 @@ @* ═══ TABLE GRID / LƯỚI BÀN ═══ *@
+ @if (_isLoading) + { +
+ Đang tải... +
+ } + else if (_loadError) + { +
+ Không thể tải dữ liệu +
+ } + else + {
@foreach (var table in FilteredTables) { @@ -82,6 +97,7 @@
Không tìm thấy bàn trống phù hợp
} + }
@* ═══ SELECTED TABLE INFO / THÔNG TIN BÀN ĐÃ CHỌN ═══ *@ @@ -117,28 +133,52 @@
@code { + // EN: Restaurant shop ID / VI: ID cửa hàng nhà hàng + private static readonly Guid RestaurantShopId = Guid.Parse("b0000002-0000-0000-0000-000000000002"); + + // EN: Loading state / VI: Trạng thái tải + private bool _isLoading = true; + private bool _loadError; + private int _guestCount = 4; private string _activeSection = "Tất cả"; private string? _selectedTable; - private readonly string[] _sections = { "Tất cả", "Tầng 1", "Tầng 2", "Sân vườn", "VIP" }; + private string[] _sections = { "Tất cả" }; - // EN: Available tables / VI: Bàn trống - private readonly List _availableTables = new() - { - new("T01", "Bàn 1", 4, "Tầng 1"), - new("T05", "Bàn 5", 8, "VIP"), - new("T06", "Bàn 6", 4, "Sân vườn"), - new("T08", "Bàn 8", 2, "Sân vườn"), - new("T11", "Bàn 11", 4, "Tầng 1"), - new("T12", "Bàn 12", 2, "Tầng 2"), - new("T13", "Bàn 13", 6, "VIP"), - new("T14", "Bàn 14", 10, "Tầng 2"), - new("T15", "Bàn 15", 4, "Sân vườn"), - }; + // EN: Available tables from API / VI: Bàn trống từ API + private List _availableTables = new(); private IEnumerable FilteredTables => _activeSection == "Tất cả" ? _availableTables : _availableTables.Where(t => t.Section == _activeSection); + protected override async Task OnInitializedAsync() + { + try + { + var apiTables = await DataService.GetTablesAsync(RestaurantShopId); + + _availableTables = apiTables + .Where(t => (t.Status ?? "available") == "available") + .Select(t => new AvailableTable( + t.Id.ToString(), + $"Bàn {t.TableNumber}", + t.Capacity, + t.Zone ?? "Tầng 1" + )).ToList(); + + var zones = _availableTables.Select(t => t.Section).Distinct().ToList(); + _sections = new[] { "Tất cả" }.Concat(zones).ToArray(); + } + catch + { + _loadError = true; + } + finally + { + _isLoading = false; + } + } + private static string CapacityColor(int seats, int guests) => seats >= guests + 2 ? "var(--pos-success)" : seats >= guests ? "#F59E0B" diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/WaiterPad.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/WaiterPad.razor index dc492e37..d4df3525 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/WaiterPad.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Restaurant/Workflow/WaiterPad.razor @@ -5,6 +5,7 @@ @page "/pos/restaurant/waiter-pad" @layout PosLayout @inherits PosBase +@inject WebClientTpos.Client.Services.PosDataService DataService
@* ═══ HEADER / TIÊU ĐỀ ═══ *@ @@ -16,28 +17,43 @@ PV: Nguyễn Văn A
- @* ═══ COURSE TABS / TAB MÓN THEO COURSE ═══ *@ -
- @foreach (var c in _courses) - { - - } -
+ @if (_isLoading) + { +
+ Đang tải... +
+ } + else if (_loadError) + { +
+ Không thể tải dữ liệu +
+ } + else + { + @* ═══ COURSE TABS / TAB MÓN THEO COURSE ═══ *@ +
+ @foreach (var c in _courses) + { + + } +
- @* ═══ MENU ITEMS / DANH SÁCH MÓN ═══ *@ -
- @foreach (var item in FilteredMenu) - { -
-
- + @* ═══ MENU ITEMS / DANH SÁCH MÓN ═══ *@ +
+ @foreach (var item in FilteredMenu) + { +
+
+ +
+ @item.Name + @FormatPrice(item.Price)
- @item.Name - @FormatPrice(item.Price) -
- } -
+ } +
+ }
@* ═══ ORDER PANEL / PANEL ĐƠN GỌI ═══ *@ @@ -88,25 +104,52 @@
@code { - private string _activeCourse = "Khai vị"; + // EN: Restaurant shop ID / VI: ID cửa hàng nhà hàng + private static readonly Guid RestaurantShopId = Guid.Parse("b0000002-0000-0000-0000-000000000002"); + + // EN: Loading state / VI: Trạng thái tải + private bool _isLoading = true; + private bool _loadError; + + private string _activeCourse = "Tất cả"; private string _specialRequest = string.Empty; - private readonly string[] _courses = { "Khai vị", "Món chính", "Tráng miệng", "Đồ uống" }; + private string[] _courses = { "Tất cả" }; - // EN: Menu items by course / VI: Thực đơn theo course - private readonly List _menu = new() + // EN: Menu items from API / VI: Thực đơn từ API + private List _menu = new(); + + private IEnumerable FilteredMenu => + _activeCourse == "Tất cả" ? _menu : _menu.Where(m => m.Course == _activeCourse); + + protected override async Task OnInitializedAsync() { - new("Gỏi cuốn", 45_000, "Khai vị", "salad"), new("Chả giò", 40_000, "Khai vị", "flame"), - new("Súp cua", 55_000, "Khai vị", "soup"), new("Nộm bò bóp thấu", 65_000, "Khai vị", "leaf"), - new("Phở bò tái", 75_000, "Món chính", "beef"), new("Cơm tấm sườn", 65_000, "Món chính", "utensils"), - new("Bún bò Huế", 80_000, "Món chính", "flame"), new("Cá kho tộ", 120_000, "Món chính", "fish"), - new("Lẩu thái", 250_000, "Món chính", "soup"), new("Gà nướng mật ong", 180_000, "Món chính", "drumstick"), - new("Chè thái", 30_000, "Tráng miệng", "ice-cream-cone"), new("Bánh flan", 25_000, "Tráng miệng", "cake"), - new("Trái cây dĩa", 45_000, "Tráng miệng", "apple"), new("Trà đá", 10_000, "Đồ uống", "glass-water"), - new("Cà phê sữa", 29_000, "Đồ uống", "coffee"), new("Nước mía", 15_000, "Đồ uống", "cup-soda"), - new("Bia Sài Gòn", 25_000, "Đồ uống", "beer"), - }; + try + { + var products = await DataService.GetProductsAsync(RestaurantShopId); + var categories = await DataService.GetCategoriesAsync(RestaurantShopId); - private IEnumerable FilteredMenu => _menu.Where(m => m.Course == _activeCourse); + var categoryMap = categories.ToDictionary(c => c.Id, c => c.Name); + + _menu = products.Select(p => new MenuItem( + p.Name, + p.Price, + p.Category ?? categoryMap.GetValueOrDefault(Guid.Empty, "Khác"), + "utensils" + )).ToList(); + + var courseNames = _menu.Select(m => m.Course).Distinct().ToList(); + _courses = new[] { "Tất cả" }.Concat(courseNames).ToArray(); + _activeCourse = _courses.First(); + } + catch + { + _loadError = true; + } + finally + { + _isLoading = false; + } + } // EN: Current order / VI: Đơn gọi hiện tại private readonly List _orderItems = new() 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 index d14dcb50..c97a238f 100644 --- 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 @@ -5,6 +5,7 @@ @page "/pos/retail" @layout PosLayout @inherits PosBase +@inject WebClientTpos.Client.Services.PosDataService DataService @* ═══ PRODUCT PANEL ═══ *@
@@ -20,34 +21,48 @@
- @* EN: Category tabs / VI: Tab danh mục *@ -
- @foreach (var cat in _categories) - { - - } -
+ @if (_isLoading) + { +
+ Đ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) - { -
-
- + @* EN: Product grid / VI: Lưới sản phẩm *@ +
+ @foreach (var product in FilteredProducts) + { +
+
+ +
+ @product.Name +
+ @FormatPrice(product.Price) + @product.Sku +
- @product.Name -
- @FormatPrice(product.Price) - Kho: @product.Stock -
- @product.Sku -
- } -
+ } +
+ }
@* ═══ CART PANEL ═══ *@ @@ -99,30 +114,20 @@ @code { + // EN: Retail shop ID (using cafe shop for now) / VI: ID cửa hàng bán lẻ (dùng tạm shop cafe) + private static readonly Guid RetailShopId = Guid.Parse("b0000001-0000-0000-0000-000000000001"); + + // EN: Loading state / VI: Trạng thái tải + private bool _isLoading = true; + private bool _loadError; + // 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[] _categories = { "Tất cả" }; 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: Product list from API / VI: Danh sách sản phẩm từ API + private List _products = new(); // EN: Cart items / VI: Mục giỏ hàng private readonly List _cartItems = new(); @@ -130,6 +135,33 @@ _selectedCategory == "Tất cả" ? _products : _products.Where(p => p.Category == _selectedCategory); private decimal CartTotal => _cartItems.Sum(i => i.Price * i.Qty); + protected override async Task OnInitializedAsync() + { + try + { + var apiProducts = await DataService.GetProductsAsync(RetailShopId); + + _products = apiProducts.Select(p => new Product( + p.Name, + p.Sku ?? "", + p.Price, + p.Category ?? "Khác", + GetCategoryIcon(p.Category ?? "Khác") + )).ToList(); + + var cats = _products.Select(p => p.Category).Distinct().ToList(); + _categories = new[] { "Tất cả" }.Concat(cats).ToArray(); + } + catch + { + _loadError = true; + } + finally + { + _isLoading = false; + } + } + private void AddToCart(Product product) { var existing = _cartItems.FirstOrDefault(i => i.Sku == product.Sku); @@ -152,8 +184,14 @@ private void Checkout() { } + private static string GetCategoryIcon(string category) => category switch + { + "Thời trang" => "shirt", "Phụ kiện" => "shopping-bag", "Điện tử" => "headphones", + "Gia dụng" => "cooking-pot", "Mỹ phẩm" => "sparkles", _ => "package" + }; + // 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 record Product(string Name, string Sku, decimal Price, string Category, string Icon); private class CartItem(string name, string sku, decimal price) { public string Name { get; set; } = name; 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 index bed440f7..128f6ba4 100644 --- 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 @@ -5,6 +5,7 @@ @page "/pos/retail/mobile" @layout PosLayout @inherits PosBase +@inject WebClientTpos.Client.Services.PosDataService DataService
@* EN: Barcode input / VI: Ô nhập mã vạch *@ @@ -17,6 +18,20 @@
+ @if (_isLoading) + { +
+ Đ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) @@ -39,10 +54,10 @@
@product.Name @FormatPrice(product.Price) - Kho: @product.Stock } + } @* EN: Floating cart button / VI: Nút giỏ hàng nổi *@ @if (_cartItems.Any()) @@ -103,30 +118,54 @@ @code { - private readonly string[] _categories = { "Tất cả", "Thời trang", "Phụ kiện", "Điện tử", "Gia dụng", "Mỹ phẩm" }; + // EN: Retail shop ID (using cafe shop for now) / VI: ID cửa hàng bán lẻ (dùng tạm shop cafe) + private static readonly Guid RetailShopId = Guid.Parse("b0000001-0000-0000-0000-000000000001"); + + // EN: Loading state / VI: Trạng thái tải + 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ả"; 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"), - }; + // EN: Product list from API / VI: Danh sách sản phẩm từ API + private List _products = new(); 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); + protected override async Task OnInitializedAsync() + { + try + { + var apiProducts = await DataService.GetProductsAsync(RetailShopId); + + _products = apiProducts.Select(p => new Product( + p.Name, + p.Sku ?? "", + p.Price, + p.Category ?? "Khác", + GetCategoryIcon(p.Category ?? "Khác") + )).ToList(); + + var cats = _products.Select(p => p.Category).Distinct().ToList(); + _categories = new[] { "Tất cả" }.Concat(cats).ToArray(); + } + catch + { + _loadError = true; + } + finally + { + _isLoading = false; + } + } + private void AddToCart(Product product) { var existing = _cartItems.FirstOrDefault(i => i.Sku == product.Sku); @@ -142,7 +181,14 @@ private void Checkout() { } - private record Product(string Name, string Sku, decimal Price, int Stock, string Category, string Icon); + private static string GetCategoryIcon(string category) => category switch + { + "Thời trang" => "shirt", "Phụ kiện" => "shopping-bag", "Điện tử" => "headphones", + "Gia dụng" => "cooking-pot", "Mỹ phẩm" => "sparkles", _ => "package" + }; + + // EN: Models / VI: Mô hình dữ liệu + private record Product(string Name, string Sku, decimal Price, string Category, string Icon); private class CartItem(string name, string sku, decimal price) { public string Name { get; set; } = name; 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 index 9a1df4c2..8a498d54 100644 --- 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 @@ -5,6 +5,7 @@ @page "/pos/retail/tablet" @layout PosLayout @inherits PosBase +@inject WebClientTpos.Client.Services.PosDataService DataService @* ═══ PRODUCT PANEL ═══ *@
@@ -18,32 +19,47 @@
- @* EN: Category tabs / VI: Tab danh mục *@ -
- @foreach (var cat in _categories) - { - - } -
+ @if (_isLoading) + { +
+ Đ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 (larger for tablet) / VI: Lưới sản phẩm (lớn hơn cho tablet) *@ -
- @foreach (var product in FilteredProducts) - { -
-
- + @* 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) + @product.Sku
- @product.Name - @FormatPrice(product.Price) - Kho: @product.Stock · @product.Sku -
- } -
+ } +
+ }
@* ═══ CART SIDEBAR ═══ *@ @@ -96,30 +112,53 @@ @code { - private readonly string[] _categories = { "Tất cả", "Thời trang", "Phụ kiện", "Điện tử", "Gia dụng", "Mỹ phẩm" }; + // EN: Retail shop ID (using cafe shop for now) / VI: ID cửa hàng bán lẻ (dùng tạm shop cafe) + private static readonly Guid RetailShopId = Guid.Parse("b0000001-0000-0000-0000-000000000001"); + + // EN: Loading state / VI: Trạng thái tải + 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ả"; 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"), - }; + // EN: Product list from API / VI: Danh sách sản phẩm từ API + private List _products = new(); 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); + protected override async Task OnInitializedAsync() + { + try + { + var apiProducts = await DataService.GetProductsAsync(RetailShopId); + + _products = apiProducts.Select(p => new Product( + p.Name, + p.Sku ?? "", + p.Price, + p.Category ?? "Khác", + GetCategoryIcon(p.Category ?? "Khác") + )).ToList(); + + var cats = _products.Select(p => p.Category).Distinct().ToList(); + _categories = new[] { "Tất cả" }.Concat(cats).ToArray(); + } + catch + { + _loadError = true; + } + finally + { + _isLoading = false; + } + } + private void AddToCart(Product product) { var existing = _cartItems.FirstOrDefault(i => i.Sku == product.Sku); @@ -135,7 +174,14 @@ private void Checkout() { } - private record Product(string Name, string Sku, decimal Price, int Stock, string Category, string Icon); + private static string GetCategoryIcon(string category) => category switch + { + "Thời trang" => "shirt", "Phụ kiện" => "shopping-bag", "Điện tử" => "headphones", + "Gia dụng" => "cooking-pot", "Mỹ phẩm" => "sparkles", _ => "package" + }; + + // EN: Models / VI: Mô hình dữ liệu + private record Product(string Name, string Sku, decimal Price, string Category, string Icon); private class CartItem(string name, string sku, decimal price) { public string Name { get; set; } = name; 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 index 174c96eb..7c5cd7ad 100644 --- 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 @@ -5,6 +5,7 @@ @page "/pos/retail/product-search" @layout PosLayout @inherits PosBase +@inject WebClientTpos.Client.Services.PosDataService DataService
@* ═══ HEADER ═══ *@ @@ -56,6 +57,20 @@
@* ═══ SEARCH RESULTS / KẾT QUẢ TÌM KIẾM ═══ *@ + @if (_isLoading) + { +
+ Đang tải... +
+ } + else if (_loadError) + { +
+ Không thể tải dữ liệu +
+ } + else + {
@FilteredResults.Count() kết quả @(!string.IsNullOrEmpty(_searchQuery) ? $"cho \"{_searchQuery}\"" : "") @@ -76,9 +91,6 @@
@product.Sku · @product.Category
@FormatPrice(product.Price) - - Kho: @product.Stock -
@@ -91,6 +103,7 @@
} + } @* ═══ CART SUMMARY BAR / THANH TÓM TẮT GIỎ ═══ *@ @if (_cartItems.Any()) @@ -112,33 +125,50 @@ @code { - private string _searchQuery = "ao"; + // EN: Retail shop ID (using cafe shop for now) / VI: ID cửa hàng bán lẻ (dùng tạm shop cafe) + private static readonly Guid RetailShopId = Guid.Parse("b0000001-0000-0000-0000-000000000001"); + + // EN: Loading state / VI: Trạng thái tải + private bool _isLoading = true; + private bool _loadError; + + private string _searchQuery = ""; 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" }; + private string[] _categories = { "Tất cả" }; - // 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"), - }; + // EN: Product list from API / VI: Danh sách sản phẩm từ API + private List _products = new(); private readonly List _cartItems = new(); + protected override async Task OnInitializedAsync() + { + try + { + var apiProducts = await DataService.GetProductsAsync(RetailShopId); + + _products = apiProducts.Select(p => new Product( + p.Name, + p.Sku ?? "", + p.Price, + p.Category ?? "Khác", + GetCategoryIcon(p.Category ?? "Khác") + )).ToList(); + + var cats = _products.Select(p => p.Category).Distinct().ToList(); + _categories = new[] { "Tất cả" }.Concat(cats).ToArray(); + } + catch + { + _loadError = true; + } + finally + { + _isLoading = false; + } + } + private IEnumerable FilteredResults { get @@ -167,7 +197,14 @@ 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 static string GetCategoryIcon(string category) => category switch + { + "Thời trang" => "shirt", "Phụ kiện" => "shopping-bag", "Điện tử" => "headphones", + "Gia dụng" => "cooking-pot", "Mỹ phẩm" => "sparkles", _ => "package" + }; + + // EN: Models / VI: Mô hình dữ liệu + private record Product(string Name, string Sku, decimal Price, string Category, string Icon); private class CartItem(string name, string sku, decimal price) { public string Name { get; set; } = name; 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 index 81d37441..a15a2a5e 100644 --- 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 @@ -151,6 +151,8 @@ @code { + // EN: Static UI configuration — does not require DB data / VI: Cấu hình UI tĩnh — không cần dữ liệu từ DB + private string _receiptInput = "R2024001234"; private bool _receiptFound = true; private string _returnReason = ""; 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 index d318f667..5034d115 100644 --- 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 @@ -134,9 +134,9 @@ @code { - private string _searchQuery = "Áo thun nam"; + // EN: Static UI configuration — does not require DB data (needs inventory API) / VI: Cấu hình UI tĩnh — không cần dữ liệu từ DB (cần inventory API) - // EN: Demo product / VI: Sản phẩm mẫu + private string _searchQuery = "Áo thun nam"; 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 diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/SpaMobile.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/SpaMobile.razor index bc147fd1..e5287c7e 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/SpaMobile.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/SpaMobile.razor @@ -5,8 +5,23 @@ @page "/pos/spa/mobile" @layout PosLayout @inherits PosBase +@inject WebClientTpos.Client.Services.PosDataService DataService
+ @if (_isLoading) + { +
+ Đ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) @@ -33,6 +48,7 @@
}
+ } @* EN: Floating appointment button / VI: Nút lịch hẹn nổi *@ @if (_appointmentItems.Any()) @@ -97,31 +113,53 @@ @code { - private readonly string[] _categories = { "Tất cả", "Massage", "Facial", "Body", "Nail", "Hair" }; + // EN: Spa shop ID / VI: ID cửa hàng spa + private static readonly Guid SpaShopId = Guid.Parse("b0000004-0000-0000-0000-000000000004"); + + // EN: Loading state / VI: Trạng thái tải + 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ả"; private bool _showSheet; - private readonly List _services = new() - { - new("Massage toàn thân", 500_000, 60, "Massage"), - new("Massage chân", 250_000, 45, "Massage"), - new("Massage đầu vai cổ", 300_000, 30, "Massage"), - new("Facial cơ bản", 350_000, 45, "Facial"), - new("Facial collagen", 600_000, 60, "Facial"), - new("Tắm trắng toàn thân", 800_000, 90, "Body"), - new("Tẩy tế bào chết", 400_000, 45, "Body"), - new("Sơn gel", 150_000, 30, "Nail"), - new("Nail art cao cấp", 300_000, 60, "Nail"), - new("Chăm sóc móng tay", 120_000, 30, "Nail"), - new("Gội đầu dưỡng sinh", 200_000, 40, "Hair"), - new("Ủ tóc phục hồi", 350_000, 45, "Hair"), - }; + // EN: Service list from API / VI: Danh sách dịch vụ từ API + private List _services = new(); + // EN: Appointment items / VI: Mục lịch hẹn private readonly List _appointmentItems = new(); private IEnumerable FilteredServices => _selectedCategory == "Tất cả" ? _services : _services.Where(s => s.Category == _selectedCategory); private decimal AppointmentTotal => _appointmentItems.Sum(i => i.Price); + protected override async Task OnInitializedAsync() + { + try + { + var apiProducts = await DataService.GetProductsAsync(SpaShopId); + + _services = apiProducts.Select(p => new SpaService( + p.Name, + p.Price, + p.DurationMinutes ?? 60, + p.Category ?? "Khác" + )).ToList(); + + var cats = _services.Select(s => s.Category).Distinct().ToList(); + _categories = new[] { "Tất cả" }.Concat(cats).ToArray(); + } + catch + { + _loadError = true; + } + finally + { + _isLoading = false; + } + } + private void AddToAppointment(SpaService svc) { _appointmentItems.Add(new AppointmentItem(svc.Name, svc.Price, svc.Duration)); @@ -135,6 +173,7 @@ "Nail" => "paintbrush", "Hair" => "scissors", _ => "heart" }; + // EN: Models / VI: Mô hình dữ liệu private record SpaService(string Name, decimal Price, int Duration, string Category); private record AppointmentItem(string Name, decimal Price, int Duration); } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/SpaTablet.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/SpaTablet.razor index 0e184fb4..a0b80d04 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/SpaTablet.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/SpaTablet.razor @@ -5,9 +5,24 @@ @page "/pos/spa/tablet" @layout PosLayout @inherits PosBase +@inject WebClientTpos.Client.Services.PosDataService DataService @* ═══ SERVICE PANEL / PANEL DỊCH VỤ ═══ *@
+ @if (_isLoading) + { +
+ Đang tải... +
+ } + else if (_loadError) + { +
+ Không thể tải dữ liệu +
+ } + else + {
@foreach (var cat in _categories) { @@ -34,6 +49,7 @@
}
+ } @* ═══ APPOINTMENT SIDEBAR / SIDEBAR LỊCH HẸN ═══ *@ @@ -85,30 +101,52 @@ @code { - private readonly string[] _categories = { "Tất cả", "Massage", "Facial", "Body", "Nail", "Hair" }; + // EN: Spa shop ID / VI: ID cửa hàng spa + private static readonly Guid SpaShopId = Guid.Parse("b0000004-0000-0000-0000-000000000004"); + + // EN: Loading state / VI: Trạng thái tải + 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ả"; - private readonly List _services = new() - { - new("Massage toàn thân", 500_000, 60, "Massage"), - new("Massage chân", 250_000, 45, "Massage"), - new("Massage đầu vai cổ", 300_000, 30, "Massage"), - new("Facial cơ bản", 350_000, 45, "Facial"), - new("Facial collagen", 600_000, 60, "Facial"), - new("Tắm trắng toàn thân", 800_000, 90, "Body"), - new("Tẩy tế bào chết", 400_000, 45, "Body"), - new("Sơn gel", 150_000, 30, "Nail"), - new("Nail art cao cấp", 300_000, 60, "Nail"), - new("Chăm sóc móng tay", 120_000, 30, "Nail"), - new("Gội đầu dưỡng sinh", 200_000, 40, "Hair"), - new("Ủ tóc phục hồi", 350_000, 45, "Hair"), - }; + // EN: Service list from API / VI: Danh sách dịch vụ từ API + private List _services = new(); + // EN: Appointment items / VI: Mục lịch hẹn private readonly List _appointmentItems = new(); private IEnumerable FilteredServices => _selectedCategory == "Tất cả" ? _services : _services.Where(s => s.Category == _selectedCategory); private decimal AppointmentTotal => _appointmentItems.Sum(i => i.Price); + protected override async Task OnInitializedAsync() + { + try + { + var apiProducts = await DataService.GetProductsAsync(SpaShopId); + + _services = apiProducts.Select(p => new SpaService( + p.Name, + p.Price, + p.DurationMinutes ?? 60, + p.Category ?? "Khác" + )).ToList(); + + var cats = _services.Select(s => s.Category).Distinct().ToList(); + _categories = new[] { "Tất cả" }.Concat(cats).ToArray(); + } + catch + { + _loadError = true; + } + finally + { + _isLoading = false; + } + } + private void AddToAppointment(SpaService svc) { _appointmentItems.Add(new AppointmentItem(svc.Name, svc.Price, svc.Duration)); @@ -122,6 +160,7 @@ "Nail" => "paintbrush", "Hair" => "scissors", _ => "heart" }; + // EN: Models / VI: Mô hình dữ liệu private record SpaService(string Name, decimal Price, int Duration, string Category); private record AppointmentItem(string Name, decimal Price, int Duration); } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/AppointmentBook.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/AppointmentBook.razor index 1cd52d54..891fffce 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/AppointmentBook.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/AppointmentBook.razor @@ -5,6 +5,7 @@ @page "/pos/spa/appointment-book" @layout PosLayout @inherits PosBase +@inject WebClientTpos.Client.Services.PosDataService DataService
@* ═══ SCHEDULE PANEL (LEFT) / PANEL LỊCH (TRÁI) ═══ *@ @@ -19,6 +20,20 @@ Đặt lịch hẹn
+ @if (_isLoading) + { +
+ Đang tải... +
+ } + else if (_loadError) + { +
+ Không thể tải dữ liệu +
+ } + else + { @* ═══ DATE PICKER / CHỌN NGÀY ═══ *@
Chọn ngày
@@ -81,6 +96,7 @@ }
+ } @* ═══ BOOKING SUMMARY (RIGHT) / TÓM TẮT ĐẶT LỊCH (PHẢI) ═══ *@ @@ -143,6 +159,13 @@ @code { + // EN: Spa shop ID / VI: ID cửa hàng spa + private static readonly Guid SpaShopId = Guid.Parse("b0000004-0000-0000-0000-000000000004"); + + // EN: Loading state / VI: Trạng thái tải + private bool _isLoading = true; + private bool _loadError; + private string _selectedDate = "Hôm nay"; private string? _selectedTime = "10:00"; private string _selectedStaff = "Chị Hoa"; @@ -159,25 +182,51 @@ // EN: Staff list / VI: Danh sách nhân viên private readonly string[] _staffList = { "Chị Hoa", "Anh Minh", "Chị Lan", "Chị Trang", "Bất kỳ" }; - // EN: Time slots / VI: Khung giờ - private readonly List _timeSlots = new() - { - new("09:00", "available"), new("09:30", "available"), new("10:00", "available"), - new("10:30", "booked"), new("11:00", "booked"), new("11:30", "available"), - new("12:00", "available"), new("12:30", "available"), new("13:00", "available"), - new("13:30", "booked"), new("14:00", "available"), new("14:30", "available"), - new("15:00", "available"), new("15:30", "booked"), new("16:00", "available"), - new("16:30", "available"), new("17:00", "available"), new("17:30", "available"), - new("18:00", "booked"), new("18:30", "available"), new("19:00", "available"), - new("19:30", "available"), new("20:00", "available"), - }; + // EN: Time slots from API / VI: Khung giờ từ API + private List _timeSlots = new(); - // EN: Demo selected services / VI: Dịch vụ đã chọn mẫu - private readonly List _selectedServices = new() + // EN: Selected services from API / VI: Dịch vụ đã chọn từ API + private List _selectedServices = new(); + + protected override async Task OnInitializedAsync() { - new("Massage toàn thân", 500_000, 60), - new("Facial collagen", 600_000, 60), - }; + try + { + var appointments = await DataService.GetAppointmentsAsync(SpaShopId); + + var bookedTimes = appointments + .Select(a => a.StartTime.ToString("HH:mm")) + .ToHashSet(); + + var slots = new List(); + for (var hour = 9; hour <= 20; hour++) + { + foreach (var min in new[] { 0, 30 }) + { + if (hour == 20 && min == 30) break; + var time = $"{hour:D2}:{min:D2}"; + var status = bookedTimes.Contains(time) ? "booked" : "available"; + slots.Add(new TimeSlot(time, status)); + } + } + _timeSlots = slots; + + var products = await DataService.GetProductsAsync(SpaShopId); + _selectedServices = products.Take(2).Select(p => new ServiceInfo( + p.Name, + p.Price, + p.DurationMinutes ?? 60 + )).ToList(); + } + catch + { + _loadError = true; + } + finally + { + _isLoading = false; + } + } private record DateOption(string Label, string Day, string Value); private record TimeSlot(string Time, string Status); diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/CustomerLookup.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/CustomerLookup.razor index f2b1b4f2..31698c01 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/CustomerLookup.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/CustomerLookup.razor @@ -86,9 +86,9 @@ @code { - private string _searchTerm = "Nguyễn"; + // EN: Static UI configuration — does not require DB data (needs customer API) / VI: Cấu hình UI tĩnh — không cần dữ liệu từ DB (cần customer API) - // EN: Demo customers / VI: Khách hàng mẫu + private string _searchTerm = "Nguyễn"; private readonly List _customers = new() { new("Nguyễn Thị Mai", "0901234567", "Gold", "15/02/2025", 28), diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/CustomerProfile.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/CustomerProfile.razor index 9ea939ef..f4d55e17 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/CustomerProfile.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/CustomerProfile.razor @@ -139,9 +139,9 @@ @code { - private RewardInfo? _selectedReward; + // EN: Static UI configuration — does not require DB data (needs customer API) / VI: Cấu hình UI tĩnh — không cần dữ liệu từ DB (cần customer API) - // EN: Visit history / VI: Lịch sử ghé + private RewardInfo? _selectedReward; private readonly List _visitHistory = new() { new("Massage toàn thân + Facial", "15/02/2025", "Chị Hoa", 850_000, 85), diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/ServiceCombo.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/ServiceCombo.razor index ed724d69..b426ebc8 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/ServiceCombo.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/ServiceCombo.razor @@ -118,9 +118,9 @@ @code { - private string? _selectedCombo; + // EN: Static UI configuration — combo definitions are config, does not require DB data / VI: Cấu hình UI tĩnh — định nghĩa combo là cấu hình, không cần dữ liệu từ DB - // EN: Demo combos / VI: Combo mẫu + private string? _selectedCombo; private readonly List _combos = new() { new("CB1", "Mua 2 tặng 1", "Mua 2 dịch vụ Massage bất kỳ, tặng 1 Massage chân miễn phí", diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/ServicePackage.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/ServicePackage.razor index 01f6dea1..f2d42b36 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/ServicePackage.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/ServicePackage.razor @@ -5,6 +5,7 @@ @page "/pos/spa/service-package" @layout PosLayout @inherits PosBase +@inject WebClientTpos.Client.Services.PosDataService DataService
@* ═══ HEADER / TIÊU ĐỀ ═══ *@ @@ -19,6 +20,20 @@
@* ═══ PACKAGE LIST / DANH SÁCH GÓI ═══ *@ + @if (_isLoading) + { +
+ Đang tải... +
+ } + else if (_loadError) + { +
+ Không thể tải dữ liệu +
+ } + else + {
@foreach (var pkg in _packages) @@ -100,43 +115,72 @@ }
+ } @code { + // EN: Spa shop ID / VI: ID cửa hàng spa + private static readonly Guid SpaShopId = Guid.Parse("b0000004-0000-0000-0000-000000000004"); + + // EN: Loading state / VI: Trạng thái tải + private bool _isLoading = true; + private bool _loadError; + private string? _expandedId = "PKG1"; - // EN: Demo packages / VI: Gói dịch vụ mẫu - private readonly List _packages = new() + // EN: Packages built from DB services / VI: Gói dịch vụ xây dựng từ dữ liệu DB + private List _packages = new(); + + // EN: Static package definitions — service groupings are config, prices come from DB + // VI: Định nghĩa gói tĩnh — nhóm dịch vụ là cấu hình, giá từ DB + private static readonly List PackageConfigs = new() { - new("PKG1", "Gói Thư giãn", 900_000, 1_050_000, 3, 135, true, "leaf", "rgba(34,197,94,.15)", "#22C55E", new() - { - new("Massage toàn thân", 500_000, 60), - new("Facial cơ bản", 350_000, 45), - new("Gội đầu dưỡng sinh", 200_000, 30), - }), - new("PKG2", "Gói VIP", 1_800_000, 2_250_000, 5, 225, true, "crown", "rgba(245,158,11,.15)", "#F59E0B", new() - { - new("Massage toàn thân", 500_000, 60), - new("Facial collagen", 600_000, 60), - new("Tắm trắng toàn thân", 800_000, 90), - new("Sơn gel", 150_000, 30), - new("Gội đầu dưỡng sinh", 200_000, 30), - }), - new("PKG3", "Gói Cặp đôi", 1_500_000, 1_800_000, 4, 180, false, "heart", "rgba(239,68,68,.15)", "#EF4444", new() - { - new("Massage toàn thân x2", 1_000_000, 60), - new("Facial cơ bản x2", 700_000, 45), - new("Trà thảo mộc x2", 100_000, 15), - }), - new("PKG4", "Gói Làm đẹp", 1_200_000, 1_450_000, 4, 165, false, "sparkles", "rgba(139,92,246,.15)", "#8B5CF6", new() - { - new("Facial collagen", 600_000, 60), - new("Tẩy tế bào chết", 400_000, 45), - new("Sơn gel", 150_000, 30), - new("Ủ tóc phục hồi", 300_000, 30), - }), + new("PKG1", "Gói Thư giãn", 0.857m, true, "leaf", "rgba(34,197,94,.15)", "#22C55E", + new() { "Massage toàn thân", "Facial cơ bản", "Gội đầu dưỡng sinh" }), + new("PKG2", "Gói VIP", 0.8m, true, "crown", "rgba(245,158,11,.15)", "#F59E0B", + new() { "Massage toàn thân", "Facial collagen", "Tắm trắng toàn thân", "Sơn gel", "Gội đầu dưỡng sinh" }), + new("PKG3", "Gói Cặp đôi", 0.833m, false, "heart", "rgba(239,68,68,.15)", "#EF4444", + new() { "Massage toàn thân", "Facial cơ bản" }), + new("PKG4", "Gói Làm đẹp", 0.828m, false, "sparkles", "rgba(139,92,246,.15)", "#8B5CF6", + new() { "Facial collagen", "Tẩy tế bào chết", "Sơn gel", "Ủ tóc phục hồi" }), }; + protected override async Task OnInitializedAsync() + { + try + { + var apiProducts = await DataService.GetProductsAsync(SpaShopId); + var productLookup = apiProducts.ToDictionary(p => p.Name, p => p); + + _packages = PackageConfigs.Select(cfg => + { + var services = cfg.ServiceNames.Select(name => + { + if (productLookup.TryGetValue(name, out var p)) + return new PackageService(p.Name, p.Price, p.DurationMinutes ?? 60); + return new PackageService(name, 0, 0); + }).ToList(); + + var originalPrice = services.Sum(s => s.Price); + var packagePrice = Math.Round(originalPrice * cfg.DiscountFactor); + var totalDuration = services.Sum(s => s.Duration); + + return new PackageInfo(cfg.Id, cfg.Name, packagePrice, originalPrice, + services.Count, totalDuration, cfg.Popular, cfg.Icon, cfg.BgColor, cfg.FgColor, services); + }).ToList(); + } + catch + { + _loadError = true; + } + finally + { + _isLoading = false; + } + } + + private record PackageConfig(string Id, string Name, decimal DiscountFactor, bool Popular, + string Icon, string BgColor, string FgColor, List ServiceNames); private record PackageService(string Name, decimal Price, int Duration); private record PackageInfo(string Id, string Name, decimal Price, decimal OriginalPrice, int ServiceCount, int TotalDuration, bool Popular, string Icon, string BgColor, string FgColor, List Services); diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/SpaJourney.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/SpaJourney.razor index e0f8ccc1..334a24e2 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/SpaJourney.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/SpaJourney.razor @@ -269,9 +269,9 @@ @code { - private int _currentStep = 2; + // EN: Static UI configuration — does not require DB data / VI: Cấu hình UI tĩnh — không cần dữ liệu từ DB - // EN: Journey steps / VI: Các bước hành trình + private int _currentStep = 2; private readonly List _steps = new() { new("Check-in", "check-in", "Tiếp tục"), diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/StaffAssign.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/StaffAssign.razor index d0ae57d4..4bfb4e8d 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/StaffAssign.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/StaffAssign.razor @@ -112,6 +112,8 @@ @code { + // EN: Static UI configuration — does not require DB data (needs staff API) / VI: Cấu hình UI tĩnh — không cần dữ liệu từ DB (cần staff API) + private string _activeFilter = "Tất cả"; private string? _selectedStaff = "S01"; private readonly string[] _filters = { "Tất cả", "Rảnh", "Đang bận", "Nghỉ giải lao" }; diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/TherapistSchedule.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/TherapistSchedule.razor index 4ce4f6d0..2e8b52d1 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/TherapistSchedule.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/TherapistSchedule.razor @@ -117,10 +117,10 @@ @code { + // EN: Static UI configuration — does not require DB data (needs schedule API) / VI: Cấu hình UI tĩnh — không cần dữ liệu từ DB (cần schedule API) + // EN: Hours range / VI: Phạm vi giờ private readonly int[] _hours = { 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 }; - - // EN: Demo schedule / VI: Lịch mẫu private readonly List _scheduleData = new() { new("Trần Thị Hoa", "Massage", new() diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/TreatmentTimer.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/TreatmentTimer.razor index e5b2414d..48719f07 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/TreatmentTimer.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/Workflow/TreatmentTimer.razor @@ -158,6 +158,8 @@ @code { + // EN: Static UI configuration — does not require DB data / VI: Cấu hình UI tĩnh — không cần dữ liệu từ DB + private string _remainingTime = "45:00"; private int _totalDuration = 60; private double _progress = 0.25;