diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopPage.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopPage.razor
index 82c37915..d0760a59 100644
--- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopPage.razor
+++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopPage.razor
@@ -214,7 +214,225 @@
}
break;
- // ═══ PLACEHOLDER SECTIONS (POS, Tables, Kitchen, Rooms, Appointments, Services, Reports) ═══
+ // ═══ TABLES (Restaurant) ═══
+ case "tables":
+ @if (!_tables.Any())
+ {
+ @RenderEmpty("grid-3x3", "#F59E0B", "Chưa có bàn nào", "Thêm bàn để quản lý sơ đồ phục vụ", "plus-circle", "Thêm bàn")
+ }
+ else
+ {
+
+
+ Trống: @_tables.Count(t => t.Status == "available")
+ Đang dùng: @_tables.Count(t => t.Status == "occupied")
+ Đã đặt: @_tables.Count(t => t.Status == "reserved")
+
+
+
+ @foreach (var table in _tables)
+ {
+ var bgColor = table.Status switch { "available" => "rgba(34,197,94,0.08)", "occupied" => "rgba(239,68,68,0.08)", "reserved" => "rgba(245,158,11,0.08)", _ => "rgba(107,107,111,0.08)" };
+ var borderColor = table.Status switch { "available" => "rgba(34,197,94,0.3)", "occupied" => "rgba(239,68,68,0.3)", "reserved" => "rgba(245,158,11,0.3)", _ => "rgba(107,107,111,0.3)" };
+ var statusColor = table.Status switch { "available" => "#22C55E", "occupied" => "#EF4444", "reserved" => "#F59E0B", _ => "#6B6B6F" };
+ var statusText = table.Status switch { "available" => "Trống", "occupied" => "Đang dùng", "reserved" => "Đã đặt", "cleaning" => "Dọn dẹp", _ => table.Status };
+
+
@table.TableNumber
+
@(table.Zone ?? "Chung") • @table.Capacity chỗ
+
+
+ @statusText
+
+ @if (table.SessionId.HasValue)
+ {
+
+ @table.GuestCount khách • @(table.StartedAt?.ToString("HH:mm") ?? "—")
+
+ }
+
+ }
+
+ }
+ break;
+
+ // ═══ ROOMS (Karaoke — reuse tables structure) ═══
+ case "rooms":
+ @if (!_tables.Any())
+ {
+ @RenderEmpty("door-open", "#8B5CF6", "Chưa có phòng nào", "Thêm phòng để quản lý Karaoke", "plus-circle", "Thêm phòng")
+ }
+ else
+ {
+
+
+ Trống: @_tables.Count(t => t.Status == "available")
+ Đang hát: @_tables.Count(t => t.Status == "occupied")
+ Đã đặt: @_tables.Count(t => t.Status == "reserved")
+
+
+
+ @foreach (var room in _tables)
+ {
+ var bgColor = room.Status switch { "available" => "rgba(139,92,246,0.08)", "occupied" => "rgba(239,68,68,0.08)", "reserved" => "rgba(245,158,11,0.08)", _ => "rgba(107,107,111,0.08)" };
+ var borderColor = room.Status switch { "available" => "rgba(139,92,246,0.3)", "occupied" => "rgba(239,68,68,0.3)", "reserved" => "rgba(245,158,11,0.3)", _ => "rgba(107,107,111,0.3)" };
+ var statusColor = room.Status switch { "available" => "#8B5CF6", "occupied" => "#EF4444", "reserved" => "#F59E0B", _ => "#6B6B6F" };
+ var statusText = room.Status switch { "available" => "Trống", "occupied" => "Đang hát", "reserved" => "Đã đặt", "cleaning" => "Dọn dẹp", _ => room.Status };
+
+
+
+ Phòng @room.TableNumber
+
+
@(room.Zone ?? "Chung") • @room.Capacity chỗ
+
+
+ @statusText
+
+ @if (room.SessionId.HasValue)
+ {
+
+ @room.GuestCount khách • Bắt đầu @(room.StartedAt?.ToString("HH:mm") ?? "—")
+
+ }
+
+ }
+
+ }
+ break;
+
+ // ═══ APPOINTMENTS (Spa / Thẩm mỹ) ═══
+ case "appointments":
+ @if (!_appointments.Any())
+ {
+ @RenderEmpty("calendar", "#EC4899", "Chưa có lịch hẹn", "Lịch hẹn sẽ hiển thị khi khách đặt dịch vụ", "plus-circle", "Tạo lịch hẹn", $"/admin/shop/{ShopId}/appointments")
+ }
+ else
+ {
+
+
@_appointments.CountTổng lịch hẹn
+
@_appointments.Count(a => a.Status == "Confirmed")Đã xác nhận
+
@_appointments.Count(a => a.Status == "Pending")Chờ xác nhận
+
+
+
+
+ | Thời gian |
+ Phòng / Tài nguyên |
+ Trạng thái |
+
+ @foreach (var a in _appointments.OrderBy(a => a.StartTime))
+ {
+ var statusColor = a.Status switch { "Confirmed" => "#22C55E", "Pending" => "#F59E0B", "Completed" => "#3B82F6", "Cancelled" => "#EF4444", _ => "#6B6B6F" };
+
+ @a.StartTime.ToString("dd/MM/yyyy") @a.StartTime.ToString("HH:mm") — @a.EndTime.ToString("HH:mm") |
+ @(a.ResourceName ?? "—") |
+ @a.Status |
+
+ }
+
+
+
+ }
+ break;
+
+ // ═══ SERVICES (Spa — products with type=Service) ═══
+ case "services":
+ @if (!_products.Any(p => p.Type == "Service"))
+ {
+ @RenderEmpty("sparkles", "#EC4899", "Chưa có dịch vụ", "Thêm dịch vụ để khách có thể đặt lịch", "plus-circle", "Thêm dịch vụ", $"/admin/shop/{ShopId}/menu")
+ }
+ else
+ {
+
+
+
+ | Dịch vụ |
+ Giá |
+ Trạng thái |
+
+ @foreach (var s in _products.Where(p => p.Type == "Service"))
+ {
+
+ @s.Name @(s.Description ?? "—") |
+ @FormatVND(s.Price) |
+ @(s.IsActive ? "Hoạt động" : "Tạm ngưng") |
+
+ }
+
+
+
+ }
+ break;
+
+ // ═══ POS — Redirect prompt ═══
+ case "pos":
+
+
+
+
+
+
POS Bán hàng
+
Mở giao diện bán hàng tại điểm để tạo đơn, thanh toán và in hóa đơn.
+
+
+ Mở POS
+
+
+
+ break;
+
+ // ═══ REPORTS — Aggregate stats ═══
+ case "reports":
+
+
@FormatVND(_reportOrders.Sum(o => o.TotalAmount))Tổng doanh thu
+
@_reportOrders.CountTổng đơn hàng
+
@FormatVND(_reportOrders.Any() ? _reportOrders.Average(o => o.TotalAmount) : 0)Giá trị TB / đơn
+
@_reportProducts.CountSản phẩm
+
+ @if (_reportOrders.Any())
+ {
+
+
+
+
+ | Mã đơn |
+ Giá trị |
+ Trạng thái |
+ Ngày tạo |
+
+ @foreach (var o in _reportOrders.Take(20))
+ {
+
+ | @o.Id.ToString()[..8] |
+ @FormatVND(o.TotalAmount) |
+ @(o.Status ?? "—") |
+ @o.CreatedAt.ToString("dd/MM/yyyy HH:mm") |
+
+ }
+
+
+
+ }
+ else
+ {
+ @RenderEmpty("bar-chart-2", "#22C55E", "Chưa có dữ liệu báo cáo", "Dữ liệu sẽ hiển thị khi có đơn hàng và hoạt động kinh doanh")
+ }
+ break;
+
+ // ═══ KITCHEN — Placeholder (needs KDS) ═══
+ case "kitchen":
+
+
+
+
+
+
Kitchen Display System
+
Màn hình hiển thị đơn cho bếp (KDS) — tự động nhận đơn từ POS, phân luồng theo trạm bếp.
+
Tính năng KDS sẽ hoạt động khi kết nối với F&B Engine
+
+
+ break;
+
+ // ═══ UNKNOWN SECTIONS ═══
default:
@@ -222,12 +440,7 @@
@_sectionTitle
-
- @_sectionDescription
-
-
- Tính năng này sẽ được kích hoạt khi có dữ liệu từ hệ thống
-
+
@_sectionDescription
break;
@@ -255,6 +468,11 @@
private List _orders = new();
private List _staff = new();
private List _members = new();
+ private List _tables = new();
+ private List _appointments = new();
+ // Reports data (separate from section-specific _orders)
+ private List _reportOrders = new();
+ private List _reportProducts = new();
protected override async Task OnInitializedAsync() => await LoadData();
@@ -305,6 +523,22 @@
case "customers":
_members = await DataService.GetMembersAsync();
break;
+ case "tables":
+ case "rooms":
+ if (_shopGuid.HasValue)
+ _tables = await DataService.GetTablesAsync(_shopGuid.Value);
+ break;
+ case "appointments":
+ if (_shopGuid.HasValue)
+ _appointments = await DataService.GetAppointmentsAsync(_shopGuid.Value);
+ break;
+ case "services":
+ _products = await DataService.GetAllProductsAsync(_shopGuid);
+ break;
+ case "reports":
+ _reportOrders = await DataService.GetOrdersAsync(_shopGuid);
+ _reportProducts = await DataService.GetAllProductsAsync(_shopGuid);
+ break;
}
}
catch (Exception ex)