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 @@
_showExtend = !_showExtend">
+ @onclick="@(() => NavigateTo($"karaoke/room-extend/{RoomId}"))">
Gia hạn
NavigateTo("karaoke/order-fnb"))">
+ @onclick="@(() => NavigateTo($"karaoke/order-fnb/{RoomId}"))">
Gọi F&B
_showEnd = !_showEnd">
+ disabled="@_ending"
+ @onclick="EndSession">
- Kết thúc
+ @(_ending ? "Đang..." : "Kết thúc")
- @* 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 })
- {
-
- @(mins)p
-
- }
-
+
+ @_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 = Math.Max(0, item.Qty - 1)">−
- @item.Qty
- 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();
}