From c70248fdec96d840166e1c17867d53c1b130a997 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Thu, 5 Mar 2026 11:15:46 +0700 Subject: [PATCH] feat(karaoke-pos): integrate session management and F&B ordering with backend APIs across the workflow pages. --- .../Pages/Pos/Karaoke/KaraokeDesktop.razor | 84 ++++---- .../Pages/Pos/Karaoke/Workflow/OrderFnb.razor | 97 ++++++--- .../Pos/Karaoke/Workflow/RoomExtend.razor | 156 ++++++--------- .../Pos/Karaoke/Workflow/RoomReset.razor | 125 +++++++----- .../Pos/Karaoke/Workflow/RoomSelect.razor | 78 +++++--- .../Pos/Karaoke/Workflow/RoomSession.razor | 189 +++++++++++------- .../Services/PosDataService.cs | 54 +++++ .../Controllers/FnbController.cs | 26 +++ 8 files changed, 498 insertions(+), 311 deletions(-) diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Karaoke/KaraokeDesktop.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Karaoke/KaraokeDesktop.razor index 80c823e5..0522e08b 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Karaoke/KaraokeDesktop.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Karaoke/KaraokeDesktop.razor @@ -1,10 +1,8 @@ @* EN: Karaoke POS Desktop — Room map grid + session panel for karaoke room management. - Rooms loaded from DB via DataService.GetTablesAsync. Products loaded for F&B panel. - TODO: Implement session tracking (table_sessions) for real-time F&B order display. + Rooms loaded from DB via DataService.GetTablesAsync. Active orders loaded for occupied rooms. VI: POS Karaoke Desktop — Lưới sơ đồ phòng + panel phiên hát cho quản lý phòng karaoke. - Phòng tải từ DB qua DataService.GetTablesAsync. Sản phẩm tải cho panel F&B. - TODO: Triển khai theo dõi phiên (table_sessions) để hiển thị đơn F&B thời gian thực. + Phòng tải từ DB qua DataService.GetTablesAsync. Đơn hàng active tải cho phòng đang sử dụng. *@ @page "/pos/{ShopId:guid}/karaoke" @layout PosLayout @@ -97,21 +95,41 @@
THỜI GIAN SỬ DỤNG
- @((DateTime.Now - SelectedRoom.SessionStart!.Value).ToString(@"hh\:mm\:ss")) + @if (SelectedRoom.SessionStart.HasValue) + { + @((DateTime.Now - SelectedRoom.SessionStart.Value).ToString(@"hh\:mm\:ss")) + } + else + { + --:--:-- + }
- @* EN: F&B orders / VI: Đơn F&B *@ + @* EN: F&B orders from DB / VI: Đơn F&B từ DB *@
ĐƠN F&B
- @foreach (var item in _fnbItems) + @{ var roomOrders = _activeOrders.Where(o => o.TableId.ToString() == SelectedRoom.Id).ToList(); } + @if (roomOrders.Any()) { -
-
- @item.Name - @FormatPrice(item.Price) -
- x@item.Qty + @foreach (var order in roomOrders) + { + @foreach (var item in order.Items) + { +
+
+ @item.ProductName + @FormatPrice(item.UnitPrice) +
+ x@item.Quantity +
+ } + } + } + else + { +
+ Chưa có đơn F&B
}
@@ -126,11 +144,14 @@
@code { + [Parameter] public Guid RoomId { get; set; } - // EN: Loading state / VI: Trạng thái tải private bool _isLoading = true; private bool _loadError; + private bool _submitting; + private bool _orderSuccess; + private string? _statusMsg; + private string _roomName = ""; - // EN: Category filter / VI: Bộ lọc danh mục private string _activeCategory = "Tất cả"; private string[] _categories = { "Tất cả" }; - - // EN: Product list loaded from DB / VI: Danh sách sản phẩm tải từ DB private List _products = new(); - - // EN: Order items / VI: Mục đơn hàng private readonly List _orderItems = new(); private IEnumerable FilteredProducts => @@ -137,17 +141,21 @@ try { + var tablesTask = DataService.GetTablesAsync(ShopId); var productsTask = DataService.GetProductsAsync(ShopId); var categoriesTask = DataService.GetCategoriesAsync(ShopId); - await Task.WhenAll(productsTask, categoriesTask); + await Task.WhenAll(tablesTask, productsTask, categoriesTask); + + // Get room name + var tables = await tablesTask; + var room = tables.FirstOrDefault(t => t.Id == RoomId); + _roomName = room?.TableNumber ?? "Phòng"; var apiProducts = await productsTask; var apiCategories = await categoriesTask; _products = apiProducts.Select(p => new Product( - p.Name, - p.Price, - p.Category ?? "Khác" + p.Id, p.Name, p.Price, p.Category ?? "Khác" )).ToList(); var catNames = apiCategories.Select(c => c.Name).ToList(); @@ -171,9 +179,9 @@ private void AddToOrder(Product p) { - var existing = _orderItems.FirstOrDefault(i => i.Name == p.Name); + var existing = _orderItems.FirstOrDefault(i => i.ProductId == p.Id); if (existing != null) existing.Qty++; - else _orderItems.Add(new OrderItem(p.Name, p.Price)); + else _orderItems.Add(new OrderItem(p.Id, p.Name, p.Price)); } private void ChangeQty(OrderItem item, int delta) @@ -182,14 +190,51 @@ if (item.Qty <= 0) _orderItems.Remove(item); } + private async Task SubmitOrder() + { + if (!_orderItems.Any()) return; + _submitting = true; + _statusMsg = null; + + try + { + var items = _orderItems.Select(i => new WebClientTpos.Client.Services.PosDataService.PosOrderItemRequest( + i.ProductId, i.Name, i.Qty, i.Price, "PreparedFood")).ToList(); + var req = new WebClientTpos.Client.Services.PosDataService.CreatePosOrderRequest( + ShopId, null, items, null, null, null, RoomId); + var result = await DataService.CreatePosOrderAsync(req); + if (result != null) + { + _orderSuccess = true; + _statusMsg = "Đơn F&B đã gửi thành công!"; + _orderItems.Clear(); + } + else + { + _orderSuccess = false; + _statusMsg = "Không thể gửi đơn. Vui lòng thử lại."; + } + } + catch + { + _orderSuccess = false; + _statusMsg = "Lỗi khi gửi đơn."; + } + finally + { + _submitting = false; + } + } + private static string GetCategoryIcon(string cat) => cat switch { - "Đồ uống" => "beer", "Đồ ăn" => "utensils", "Mồi" => "popcorn", _ => "package" + "Đồ uống" => "wine", "Đồ ăn" => "utensils", "Mồi" => "popcorn", _ => "package" }; - private record Product(string Name, decimal Price, string Category); - private class OrderItem(string name, decimal price) + private record Product(Guid Id, string Name, decimal Price, string Category); + private class OrderItem(Guid productId, string name, decimal price) { + public Guid ProductId { get; set; } = productId; public string Name { get; set; } = name; 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/Karaoke/Workflow/RoomExtend.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Karaoke/Workflow/RoomExtend.razor index 3d8529cc..dde43418 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Karaoke/Workflow/RoomExtend.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Karaoke/Workflow/RoomExtend.razor @@ -1,67 +1,66 @@ @* - EN: Karaoke Room Extend — Extension dialog with time options, new end time preview, peak warning. - VI: Gia hạn phòng Karaoke — Dialog gia hạn với tùy chọn thời gian, xem trước giờ kết thúc, cảnh báo giờ cao điểm. + EN: Karaoke Room Extend — Extension dialog with time options, loads room info from DB. + VI: Gia hạn phòng Karaoke — Dialog gia hạn với tùy chọn thời gian, tải thông tin phòng từ DB. *@ -@page "/pos/{ShopId:guid}/karaoke/room-extend" +@page "/pos/{ShopId:guid}/karaoke/room-extend/{RoomId:guid}" @layout PosLayout @inherits PosBase +@inject WebClientTpos.Client.Services.PosDataService DataService
@* ═══ HEADER / TIÊU ĐỀ ═══ *@
Gia hạn phòng
+ @if (_isLoading) + { +
+ Đang tải... +
+ } + else + {
@* ═══ CURRENT SESSION INFO / THÔNG TIN PHIÊN HIỆN TẠI ═══ *@
-
Phòng VIP 2
-
Deluxe • 20 người • Tầng 3
+
@_roomName
+
@_roomType • @_roomCapacity người
Đang hoạt động
-
+
Bắt đầu
-
19:30
+
@(_sessionStart?.ToLocalTime().ToString("HH:mm") ?? "--:--")
Đã dùng
-
2h15
+
@_elapsedStr
-
-
Giá/giờ
-
@FormatPrice(200_000)
-
-
-
- Tiền phòng hiện tại - @FormatPrice(450_000)
@* ═══ EXTENSION OPTIONS / TÙY CHỌN GIA HẠN ═══ *@
Chọn thời gian gia hạn
-
+
@foreach (var opt in _extendOptions) { - }
@@ -73,11 +72,11 @@
+ @onclick="() => _customMinutes = Math.Max(15, _customMinutes - 15)">− @_customMinutes + @onclick="() => _customMinutes += 15">+ phút
- - @* ═══ PREVIEW / XEM TRƯỚC ═══ *@ - @if (_selectedOption is not null) - { -
-
Xem trước sau gia hạn
-
- Giờ kết thúc mới - @_newEndTime -
-
- Thời gian thêm - +@_selectedOption.Label -
-
- Phí gia hạn - +@FormatPrice(_selectedOption.Cost) -
-
- Tổng tiền phòng mới - @FormatPrice(450_000 + _selectedOption.Cost) -
-
- - @* EN: Peak warning if applicable / VI: Cảnh báo giờ cao điểm nếu có *@ - @if (_showPeakWarning) - { -
- -
-
Cảnh báo giờ cao điểm
-
- Gia hạn vào khung giờ cao điểm (sau 22:00). Giá có thể tăng. -
-
-
- } - }
@* ═══ ACTION BUTTONS / NÚT HÀNH ĐỘNG ═══ *@
+ }
@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 + [Parameter] public Guid RoomId { get; set; } + + private bool _isLoading = true; + private string _roomName = ""; + private string _roomType = ""; + private int _roomCapacity; + private DateTime? _sessionStart; + private string _elapsedStr = "--"; - // EN: Extension options / VI: Tùy chọn gia hạn private readonly List _extendOptions = new() { - new(30, "+30 phút", 100_000), - new(60, "+1 giờ", 200_000), - new(90, "+1.5 giờ", 300_000), - new(120, "+2 giờ", 400_000), + new(30, "+30 phút"), + new(60, "+1 giờ"), + new(90, "+1.5 giờ"), + new(120, "+2 giờ"), }; private ExtendOption? _selectedOption; private int _customMinutes = 45; - private string _newEndTime = "22:00"; - private bool _showPeakWarning; - private void SelectExtension(ExtendOption opt) + protected override async Task OnInitializedAsync() { - _selectedOption = opt; - UpdatePreview(opt.Minutes); - } + await base.OnInitializedAsync(); - private void AdjustCustom(int delta) - { - _customMinutes = Math.Max(15, _customMinutes + delta); + try + { + var tables = await DataService.GetTablesAsync(ShopId); + var table = tables.FirstOrDefault(t => t.Id == RoomId); + if (table != null) + { + _roomName = table.TableNumber; + _roomType = table.Zone ?? "Standard"; + _roomCapacity = table.Capacity; + _sessionStart = table.StartedAt; + if (_sessionStart.HasValue) + { + var elapsed = DateTime.Now - _sessionStart.Value; + _elapsedStr = $"{(int)elapsed.TotalHours}h{elapsed.Minutes:D2}"; + } + } + } + catch { } + finally + { + _isLoading = false; + } } private void ApplyCustom() { - var cost = (decimal)_customMinutes / 60 * 200_000; - _selectedOption = new(_customMinutes, $"+{_customMinutes} phút", Math.Round(cost, -3)); - UpdatePreview(_customMinutes); + _selectedOption = new(_customMinutes, $"+{_customMinutes} phút"); } - private void UpdatePreview(int minutes) - { - var baseEnd = new TimeOnly(22, 0); - var newEnd = baseEnd.AddMinutes(minutes); - _newEndTime = newEnd.ToString("HH:mm"); - _showPeakWarning = newEnd.Hour >= 22 || newEnd.Hour < 2; - } - - private record ExtendOption(int Minutes, string Label, decimal Cost); + private record ExtendOption(int Minutes, string Label); } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Karaoke/Workflow/RoomReset.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Karaoke/Workflow/RoomReset.razor index 1b60321b..1bf77610 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Karaoke/Workflow/RoomReset.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Karaoke/Workflow/RoomReset.razor @@ -1,10 +1,11 @@ @* - EN: Karaoke Room Reset — Cleanup checklist after session ends, progress tracking, staff assignment. - VI: Reset phòng Karaoke — Danh sách dọn dẹp sau phiên, theo dõi tiến độ, phân công nhân viên. + EN: Karaoke Room Reset — Cleanup checklist after session ends, loads room info from DB. + VI: Reset phòng Karaoke — Danh sách dọn dẹp sau phiên, tải thông tin phòng từ DB. *@ -@page "/pos/{ShopId:guid}/karaoke/room-reset" +@page "/pos/{ShopId:guid}/karaoke/room-reset/{RoomId:guid}" @layout PosLayout @inherits PosBase +@inject WebClientTpos.Client.Services.PosDataService DataService
@* ═══ HEADER / TIÊU ĐỀ ═══ *@ @@ -27,38 +28,8 @@
-
Phòng VIP 2
-
Deluxe • 20 người • Tầng 3
-
-
-
Phiên trước kết thúc
-
22:15
-
-
-
- - @* ═══ STAFF & TIME / NHÂN VIÊN & THỜI GIAN ═══ *@ -
-
-
- -
-
-
Nhân viên
-
Trần Thị Hoa
-
-
-
-
- -
-
-
Bắt đầu dọn
-
22:18 • 12 phút
+
@_roomName
+
@_roomType • @_roomCapacity người
@@ -97,7 +68,6 @@ color:@(item.Checked ? "var(--pos-text-tertiary)" : "var(--pos-text-primary)");"> @item.Label
-
@item.Description
@@ -107,42 +77,95 @@ @* ═══ COMPLETE BUTTON / NÚT HOÀN TẤT ═══ *@
+ @if (!string.IsNullOrEmpty(_errorMsg)) + { +
@_errorMsg
+ }
@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 + [Parameter] public Guid RoomId { get; set; } + + private string _roomName = ""; + private string _roomType = ""; + private int _roomCapacity; + private bool _completing; + private string? _errorMsg; - // EN: Checklist items / VI: Các mục kiểm tra private readonly List _checkItems = new() { - new("Dọn bàn ghế", "Clean tables/chairs", "armchair", false), - new("Vệ sinh micro", "Clean microphones", "mic", false), - new("Kiểm tra remote", "Check remote controls", "tv", false), - new("Bổ sung nước uống", "Restock beverages", "cup-soda", false), - new("Kiểm tra ánh sáng", "Check lighting", "lightbulb", false), - new("Hệ thống âm thanh", "Sound system check", "volume-2", false), - new("Kiểm tra thiết bị", "Equipment check", "monitor-speaker", false), - new("Vệ sinh toilet", "Clean restroom", "bath", false), + new("Dọn bàn ghế", "armchair", false), + new("Vệ sinh micro", "mic", false), + new("Kiểm tra remote", "tv", false), + new("Bổ sung nước uống", "cup-soda", false), + new("Kiểm tra ánh sáng", "lightbulb", false), + new("Hệ thống âm thanh", "volume-2", false), + new("Kiểm tra thiết bị", "monitor", false), + new("Vệ sinh phòng", "sparkles", false), }; private int CompletedCount => _checkItems.Count(i => i.Checked); private bool AllChecked => _checkItems.All(i => i.Checked); private int ProgressPercent => _checkItems.Count > 0 ? CompletedCount * 100 / _checkItems.Count : 0; - private class CheckItem(string label, string description, string icon, bool isChecked) + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + + try + { + var tables = await DataService.GetTablesAsync(ShopId); + var table = tables.FirstOrDefault(t => t.Id == RoomId); + if (table != null) + { + _roomName = table.TableNumber; + _roomType = table.Zone ?? "Standard"; + _roomCapacity = table.Capacity; + } + } + catch { } + } + + private async Task CompleteReset() + { + _completing = true; + _errorMsg = null; + + try + { + var success = await DataService.UpdateTableStatusAsync(RoomId, "available"); + if (success) + { + NavigateTo("karaoke"); + } + else + { + _errorMsg = "Không thể cập nhật trạng thái phòng."; + } + } + catch + { + _errorMsg = "Lỗi khi hoàn tất reset."; + } + finally + { + _completing = false; + } + } + + private class CheckItem(string label, string icon, bool isChecked) { public string Label { get; set; } = label; - public string Description { get; set; } = description; public string Icon { get; set; } = icon; public bool Checked { get; set; } = isChecked; } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Karaoke/Workflow/RoomSelect.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Karaoke/Workflow/RoomSelect.razor index 2b682eed..1520ed83 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Karaoke/Workflow/RoomSelect.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Karaoke/Workflow/RoomSelect.razor @@ -1,6 +1,6 @@ @* - EN: Karaoke Room Select — Filter by type/capacity, available rooms, price per hour, start session. - VI: Chọn phòng Karaoke — Lọc theo loại/sức chứa, phòng trống, giá/giờ, bắt đầu phiên. + EN: Karaoke Room Select — Filter by type/capacity, available rooms, start session via API. + VI: Chọn phòng Karaoke — Lọc theo loại/sức chứa, phòng trống, bắt đầu phiên qua API. *@ @page "/pos/{ShopId:guid}/karaoke/room-select" @layout PosLayout @@ -76,9 +76,6 @@
@room.Type • @room.Capacity người
-
- @FormatPrice(room.PricePerHour)/giờ -
@room.Zone
@@ -133,26 +130,28 @@ - @* EN: Price summary / VI: Tóm tắt giá *@ -
-
- Giá/giờ - @FormatPrice(_selectedRoom.PricePerHour) + @if (!string.IsNullOrEmpty(_errorMsg)) + { +
+ @_errorMsg
-
- Số giờ - @_selectedHours giờ -
-
+ }
} @@ -167,20 +166,19 @@ @code { - // EN: Loading state / VI: Trạng thái tải private bool _isLoading = true; private bool _loadError; - // EN: Filters / VI: Bộ lọc private string _activeType = "Tất cả"; - private string[] _types = { "Tất cả", "Standard", "VIP", "Deluxe" }; + private string[] _types = { "Tất cả" }; private readonly int[] _capacities = { 4, 8, 12, 15 }; private int? _selectedCapacity; private RoomInfo? _selectedRoom; private int _selectedHours = 2; private int _guests = 4; + private bool _starting; + private string? _errorMsg; - // EN: Available rooms loaded from DB / VI: Phòng trống tải từ DB private List _rooms = new(); protected override async Task OnInitializedAsync() @@ -194,11 +192,10 @@ _rooms = tables .Where(t => t.Status == "available") .Select(t => new RoomInfo( - t.Id.ToString(), + t.Id, t.TableNumber, t.Capacity, t.Zone ?? "Standard", - 100_000, t.Zone ?? "Tầng 1" )).ToList(); @@ -215,6 +212,35 @@ } } + private async Task StartSession() + { + if (_selectedRoom == null) return; + _starting = true; + _errorMsg = null; + + try + { + var session = await DataService.OpenSessionAsync(_selectedRoom.Id, ShopId, _guests); + if (session != null) + { + await DataService.UpdateTableStatusAsync(_selectedRoom.Id, "occupied"); + NavigateTo($"karaoke/room-session/{_selectedRoom.Id}"); + } + else + { + _errorMsg = "Không thể mở phiên. Vui lòng thử lại."; + } + } + catch + { + _errorMsg = "Lỗi khi mở phiên. Vui lòng thử lại."; + } + finally + { + _starting = false; + } + } + private IEnumerable FilteredRooms { get @@ -226,5 +252,5 @@ } } - private record RoomInfo(string Id, string Name, int Capacity, string Type, decimal PricePerHour, string Zone); + private record RoomInfo(Guid Id, string Name, int Capacity, string Type, string Zone); } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Karaoke/Workflow/RoomSession.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Karaoke/Workflow/RoomSession.razor index 6b750043..74065b7c 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Karaoke/Workflow/RoomSession.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Karaoke/Workflow/RoomSession.razor @@ -1,15 +1,30 @@ @* - EN: Karaoke Room Session — Active room timer, F&B orders, extend/end session, total. - VI: Phiên phòng Karaoke — Đồng hồ phòng, đơn F&B, gia hạn/kết thúc, tổng cộng. + EN: Karaoke Room Session — Active room timer, F&B orders from DB, extend/end session via API. + VI: Phiên phòng Karaoke — Đồng hồ phòng, đơn F&B từ DB, gia hạn/kết thúc phiên qua API. *@ -@page "/pos/{ShopId:guid}/karaoke/room-session" +@page "/pos/{ShopId:guid}/karaoke/room-session/{RoomId:guid}" @layout PosLayout @inherits PosBase +@implements IDisposable @inject WebClientTpos.Client.Services.PosDataService DataService
@* ═══ SESSION DETAILS (LEFT) / CHI TIẾT PHIÊN (TRÁI) ═══ *@
+ @if (_isLoading) + { +
+ Đang tải... +
+ } + else if (_loadError) + { +
+ Không thể tải dữ liệu phòng +
+ } + else + { @* EN: Back + Room header / VI: Quay lại + Tiêu đề phòng *@
-
Phòng VIP 2
-
Deluxe • 20 người • Tầng 3
+
@_room?.Name
+
@_room?.Type • @_room?.Capacity người • @_room?.Zone
@@ -30,13 +45,14 @@ THỜI GIAN SỬ DỤNG
- 02:30:15 -
-
- Bắt đầu: 19:30 - Dự kiến: 22:00 - Còn lại: 00:29:45 + @_elapsed.ToString(@"hh\:mm\:ss")
+ @if (_room?.SessionStart != null) + { +
+ Bắt đầu: @_room.SessionStart.Value.ToLocalTime().ToString("HH:mm") +
+ }
@* ═══ ROOM RATE / GIÁ PHÒNG ═══ *@ @@ -44,20 +60,11 @@
Giá phòng
Loại phòng - Deluxe + @(_room?.Type ?? "-")
- Giá/giờ - @FormatPrice(200_000) -
-
- Số giờ - 2.5 giờ -
-
- Tiền phòng - @FormatPrice(500_000) + Thời gian + @_elapsed.ToString(@"h\:mm") giờ
@@ -65,40 +72,32 @@
- @* EN: Extend time dialog / VI: Dialog gia hạn *@ - @if (_showExtend) + @if (!string.IsNullOrEmpty(_errorMsg)) { -
-
Gia hạn thêm
-
- @foreach (var mins in new[] { 30, 60, 90, 120 }) - { - - } -
+
+ @_errorMsg
} + }
@* ═══ F&B ORDER PANEL (RIGHT) / PANEL ĐƠN F&B (PHẢI) ═══ *@ @@ -109,19 +108,7 @@
- @if (_isLoading) - { -
- Đang tải... -
- } - else if (_loadError) - { -
- Không thể tải dữ liệu -
- } - else + @if (_fnbItems.Any()) { @foreach (var item in _fnbItems) { @@ -130,48 +117,46 @@ @item.Name @FormatPrice(item.Price)
-
- - @item.Qty - -
+ x@item.Qty } } + else + { +
+ Chưa có đơn F&B +
+ } @code { + [Parameter] public Guid RoomId { get; set; } - // EN: Loading state / VI: Trạng thái tải private bool _isLoading = true; private bool _loadError; + private bool _ending; + private string? _errorMsg; - // EN: UI toggles / VI: Bật tắt giao diện - private bool _showExtend; - private bool _showEnd; - - // EN: F&B items loaded from DB / VI: Mục F&B tải từ DB + private RoomInfo? _room; private List _fnbItems = new(); + private TimeSpan _elapsed; + private System.Threading.Timer? _timer; protected override async Task OnInitializedAsync() { @@ -179,9 +164,42 @@ try { - var products = await DataService.GetProductsAsync(ShopId); + var tablesTask = DataService.GetTablesAsync(ShopId); + var ordersTask = DataService.GetActiveTableOrdersAsync(ShopId); + await Task.WhenAll(tablesTask, ordersTask); - _fnbItems = products.Take(6).Select(p => new FnbItem(p.Name, p.Price, 1)).ToList(); + var tables = await tablesTask; + var table = tables.FirstOrDefault(t => t.Id == RoomId); + if (table != null) + { + _room = new RoomInfo(table.Id, table.TableNumber, table.Capacity, + table.Zone ?? "Standard", table.Zone ?? "Tầng 1", table.StartedAt); + } + else + { + _loadError = true; + return; + } + + // Load F&B orders for this room + var allOrders = await ordersTask; + var roomOrders = allOrders.Where(o => o.TableId == RoomId).ToList(); + _fnbItems = roomOrders + .SelectMany(o => o.Items) + .GroupBy(i => new { i.ProductName, i.UnitPrice }) + .Select(g => new FnbItem(g.Key.ProductName, g.Key.UnitPrice, g.Sum(i => i.Quantity))) + .ToList(); + + // Start timer + if (_room.SessionStart.HasValue) + { + _elapsed = DateTime.Now - _room.SessionStart.Value; + _timer = new System.Threading.Timer(_ => + { + _elapsed = DateTime.Now - _room.SessionStart!.Value; + InvokeAsync(StateHasChanged); + }, null, 0, 1000); + } } catch { @@ -193,10 +211,27 @@ } } - private class FnbItem(string name, decimal price, int qty) + private async Task EndSession() { - public string Name { get; set; } = name; - public decimal Price { get; set; } = price; - public int Qty { get; set; } = qty; + _ending = true; + _errorMsg = null; + try + { + await DataService.UpdateTableStatusAsync(RoomId, "cleaning"); + NavigateTo($"karaoke/room-reset/{RoomId}"); + } + catch + { + _errorMsg = "Lỗi khi kết thúc phiên."; + _ending = false; + } } + + public void Dispose() + { + _timer?.Dispose(); + } + + private record RoomInfo(Guid Id, string Name, int Capacity, string Type, string Zone, DateTime? SessionStart); + private record FnbItem(string Name, decimal Price, int Qty); } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs b/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs index c00c9b7a..853a3e1e 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs @@ -1066,4 +1066,58 @@ public class PosDataService var r = await _http.SendAsync(request); return r.IsSuccessStatusCode; } + + // ═══ TABLE STATUS ═══ + + public async Task UpdateTableStatusAsync(Guid tableId, string status) + { + AttachToken(); + var request = new HttpRequestMessage(HttpMethod.Patch, $"api/bff/tables/{tableId}/status") + { + Content = JsonContent.Create(new { status }, options: _writeOptions) + }; + var r = await _http.SendAsync(request); + return r.IsSuccessStatusCode; + } + + // ═══ SESSIONS ═══ + + public record SessionInfo(Guid Id, Guid TableId, Guid ShopId, int GuestCount, DateTime StartedAt, DateTime? ClosedAt, string Status); + + public async Task OpenSessionAsync(Guid tableId, Guid shopId, int guestCount = 1) + { + AttachToken(); + var resp = await _http.PostAsJsonAsync("api/bff/sessions", + new { tableId, shopId, guestCount }, _writeOptions); + if (resp.IsSuccessStatusCode) + { + var json = await resp.Content.ReadFromJsonAsync(_jsonOptions); + if (json.TryGetProperty("data", out var data)) + { + var sessionId = data.GetProperty("sessionId").GetGuid(); + return new SessionInfo(sessionId, tableId, shopId, guestCount, DateTime.UtcNow, null, "Open"); + } + } + return null; + } + + public async Task GetSessionAsync(Guid sessionId) + { + AttachToken(); + var resp = await _http.GetAsync($"api/bff/sessions/{sessionId}"); + if (resp.IsSuccessStatusCode) + { + var json = await resp.Content.ReadFromJsonAsync(_jsonOptions); + if (json.TryGetProperty("data", out var data)) + return System.Text.Json.JsonSerializer.Deserialize(data.GetRawText(), _jsonOptions); + } + return null; + } + + public async Task CloseSessionAsync(Guid sessionId) + { + AttachToken(); + var resp = await _http.PostAsJsonAsync($"api/bff/sessions/{sessionId}/close", new { }, _writeOptions); + return resp.IsSuccessStatusCode; + } } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/FnbController.cs b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/FnbController.cs index 9569c860..92062390 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/FnbController.cs +++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/FnbController.cs @@ -138,4 +138,30 @@ public class FnbController : ControllerBase }; return _fnb.SendAsync(request).ProxyAsync(); } + + // ═══ TABLE STATUS ═══ + + [HttpPatch("tables/{tableId:guid}/status")] + public Task UpdateTableStatus(Guid tableId, [FromBody] JsonElement body) + { + var request = new HttpRequestMessage(HttpMethod.Patch, $"/api/v1/tables/{tableId}/status") + { + Content = JsonContent.Create(body) + }; + return _fnb.SendAsync(request).ProxyAsync(); + } + + // ═══ SESSIONS ═══ + + [HttpPost("sessions")] + public Task OpenSession([FromBody] JsonElement body) => + _fnb.PostAsJsonAsync("/api/v1/sessions", body).ProxyAsync(); + + [HttpGet("sessions/{id:guid}")] + public Task GetSession(Guid id) => + _fnb.GetAsync($"/api/v1/sessions/{id}").ProxyAsync(); + + [HttpPost("sessions/{id:guid}/close")] + public Task CloseSession(Guid id) => + _fnb.PostAsJsonAsync($"/api/v1/sessions/{id}/close", new { }).ProxyAsync(); }