feat(web-client): implement all vertical-specific sections in ShopPage
- Tables (Restaurant): grid UI with status colors and session info - Rooms (Karaoke): reuses tables structure with purple theme - Appointments (Spa): KPI cards + calendar list with status colors - Services (Spa): products filtered by type=Service - POS: redirect prompt to /pos page - Reports: aggregate KPIs (revenue, orders, avg value, products) - Kitchen: KDS placeholder with description
This commit is contained in:
@@ -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
|
||||
{
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
|
||||
<div style="display:flex;gap:8px;">
|
||||
<span class="admin-status-badge admin-status-badge--online" style="font-size:11px;"><span class="admin-status-badge__dot"></span>Trống: @_tables.Count(t => t.Status == "available")</span>
|
||||
<span class="admin-status-badge admin-status-badge--paused" style="font-size:11px;"><span class="admin-status-badge__dot"></span>Đang dùng: @_tables.Count(t => t.Status == "occupied")</span>
|
||||
<span class="admin-status-badge admin-status-badge--setup" style="font-size:11px;"><span class="admin-status-badge__dot"></span>Đã đặt: @_tables.Count(t => t.Status == "reserved")</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:16px;">
|
||||
@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 };
|
||||
<div style="background:@bgColor;border:1px solid @borderColor;border-radius:14px;padding:20px;text-align:center;">
|
||||
<div style="font-size:22px;font-weight:700;margin-bottom:4px;">@table.TableNumber</div>
|
||||
<div style="font-size:12px;color:var(--admin-text-tertiary);margin-bottom:8px;">@(table.Zone ?? "Chung") • @table.Capacity chỗ</div>
|
||||
<div style="display:inline-flex;align-items:center;gap:4px;font-size:11px;font-weight:600;color:@statusColor;">
|
||||
<span style="width:6px;height:6px;border-radius:50%;background:@statusColor;"></span>
|
||||
@statusText
|
||||
</div>
|
||||
@if (table.SessionId.HasValue)
|
||||
{
|
||||
<div style="margin-top:8px;padding-top:8px;border-top:1px solid @borderColor;font-size:11px;color:var(--admin-text-tertiary);">
|
||||
@table.GuestCount khách • @(table.StartedAt?.ToString("HH:mm") ?? "—")
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
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
|
||||
{
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
|
||||
<div style="display:flex;gap:8px;">
|
||||
<span class="admin-status-badge admin-status-badge--online" style="font-size:11px;"><span class="admin-status-badge__dot"></span>Trống: @_tables.Count(t => t.Status == "available")</span>
|
||||
<span class="admin-status-badge admin-status-badge--paused" style="font-size:11px;"><span class="admin-status-badge__dot"></span>Đang hát: @_tables.Count(t => t.Status == "occupied")</span>
|
||||
<span class="admin-status-badge admin-status-badge--setup" style="font-size:11px;"><span class="admin-status-badge__dot"></span>Đã đặt: @_tables.Count(t => t.Status == "reserved")</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;">
|
||||
@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 };
|
||||
<div style="background:@bgColor;border:1px solid @borderColor;border-radius:14px;padding:20px;">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px;">
|
||||
<i data-lucide="door-open" style="width:20px;height:20px;color:@statusColor;"></i>
|
||||
<span style="font-size:18px;font-weight:700;">Phòng @room.TableNumber</span>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--admin-text-tertiary);margin-bottom:8px;">@(room.Zone ?? "Chung") • @room.Capacity chỗ</div>
|
||||
<div style="display:inline-flex;align-items:center;gap:4px;font-size:11px;font-weight:600;color:@statusColor;">
|
||||
<span style="width:6px;height:6px;border-radius:50%;background:@statusColor;"></span>
|
||||
@statusText
|
||||
</div>
|
||||
@if (room.SessionId.HasValue)
|
||||
{
|
||||
<div style="margin-top:8px;padding-top:8px;border-top:1px solid @borderColor;font-size:11px;color:var(--admin-text-tertiary);">
|
||||
@room.GuestCount khách • Bắt đầu @(room.StartedAt?.ToString("HH:mm") ?? "—")
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
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
|
||||
{
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;margin-bottom:16px;">
|
||||
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(236,72,153,0.1);"><i data-lucide="calendar" style="color:#EC4899;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_appointments.Count</span><span class="admin-stat-card__label">Tổng lịch hẹn</span></div></div>
|
||||
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(34,197,94,0.1);"><i data-lucide="check-circle" style="color:#22C55E;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_appointments.Count(a => a.Status == "Confirmed")</span><span class="admin-stat-card__label">Đã xác nhận</span></div></div>
|
||||
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(245,158,11,0.1);"><i data-lucide="clock" style="color:#F59E0B;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_appointments.Count(a => a.Status == "Pending")</span><span class="admin-stat-card__label">Chờ xác nhận</span></div></div>
|
||||
</div>
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__body" style="padding:0;">
|
||||
<table class="admin-table" style="width:100%;"><thead><tr>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Thời gian</th>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Phòng / Tài nguyên</th>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Trạng thái</th>
|
||||
</tr></thead><tbody>
|
||||
@foreach (var a in _appointments.OrderBy(a => a.StartTime))
|
||||
{
|
||||
var statusColor = a.Status switch { "Confirmed" => "#22C55E", "Pending" => "#F59E0B", "Completed" => "#3B82F6", "Cancelled" => "#EF4444", _ => "#6B6B6F" };
|
||||
<tr style="border-top:1px solid var(--admin-border-subtle);">
|
||||
<td style="padding:12px 16px;"><div style="font-weight:600;font-size:13px;">@a.StartTime.ToString("dd/MM/yyyy")</div><div style="font-size:11px;color:var(--admin-text-tertiary);">@a.StartTime.ToString("HH:mm") — @a.EndTime.ToString("HH:mm")</div></td>
|
||||
<td style="padding:12px 16px;font-size:13px;">@(a.ResourceName ?? "—")</td>
|
||||
<td style="padding:12px 16px;"><span style="display:inline-flex;align-items:center;gap:4px;font-size:11px;font-weight:600;color:@statusColor;"><span style="width:6px;height:6px;border-radius:50%;background:@statusColor;"></span>@a.Status</span></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
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
|
||||
{
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__body" style="padding:0;">
|
||||
<table class="admin-table" style="width:100%;"><thead><tr>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Dịch vụ</th>
|
||||
<th style="padding:12px 16px;text-align:right;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Giá</th>
|
||||
<th style="padding:12px 16px;text-align:center;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Trạng thái</th>
|
||||
</tr></thead><tbody>
|
||||
@foreach (var s in _products.Where(p => p.Type == "Service"))
|
||||
{
|
||||
<tr style="border-top:1px solid var(--admin-border-subtle);">
|
||||
<td style="padding:12px 16px;"><div style="font-weight:600;">@s.Name</div><div style="font-size:12px;color:var(--admin-text-tertiary);">@(s.Description ?? "—")</div></td>
|
||||
<td style="padding:12px 16px;text-align:right;font-weight:600;color:var(--admin-orange-primary);">@FormatVND(s.Price)</td>
|
||||
<td style="padding:12px 16px;text-align:center;"><span class="admin-status-badge @(s.IsActive ? "admin-status-badge--online" : "admin-status-badge--paused")" style="font-size:10px;"><span class="admin-status-badge__dot"></span>@(s.IsActive ? "Hoạt động" : "Tạm ngưng")</span></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
break;
|
||||
|
||||
// ═══ POS — Redirect prompt ═══
|
||||
case "pos":
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__body" style="text-align:center;padding:60px 20px;">
|
||||
<div style="width:80px;height:80px;border-radius:24px;background:rgba(255,92,0,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 20px;">
|
||||
<i data-lucide="monitor" style="width:36px;height:36px;color:var(--admin-orange-primary);"></i>
|
||||
</div>
|
||||
<h2 style="font-size:22px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFFFFF);">POS Bán hàng</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0 0 24px;max-width:400px;margin-left:auto;margin-right:auto;">Mở giao diện bán hàng tại điểm để tạo đơn, thanh toán và in hóa đơn.</p>
|
||||
<a href="/pos" class="admin-btn-primary" style="display:inline-flex;align-items:center;gap:8px;text-decoration:none;font-size:16px;padding:12px 28px;">
|
||||
<i data-lucide="monitor" style="width:18px;height:18px;"></i>
|
||||
Mở POS
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
|
||||
// ═══ REPORTS — Aggregate stats ═══
|
||||
case "reports":
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;margin-bottom:20px;">
|
||||
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(34,197,94,0.1);"><i data-lucide="trending-up" style="color:#22C55E;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@FormatVND(_reportOrders.Sum(o => o.TotalAmount))</span><span class="admin-stat-card__label">Tổng doanh thu</span></div></div>
|
||||
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(59,130,246,0.1);"><i data-lucide="shopping-bag" style="color:#3B82F6;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_reportOrders.Count</span><span class="admin-stat-card__label">Tổng đơn hàng</span></div></div>
|
||||
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(139,92,246,0.1);"><i data-lucide="banknote" style="color:#8B5CF6;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@FormatVND(_reportOrders.Any() ? _reportOrders.Average(o => o.TotalAmount) : 0)</span><span class="admin-stat-card__label">Giá trị TB / đơn</span></div></div>
|
||||
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(236,72,153,0.1);"><i data-lucide="package" style="color:#EC4899;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_reportProducts.Count</span><span class="admin-stat-card__label">Sản phẩm</span></div></div>
|
||||
</div>
|
||||
@if (_reportOrders.Any())
|
||||
{
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header"><h3 style="font-size:14px;font-weight:700;margin:0;">Đơn hàng gần nhất</h3></div>
|
||||
<div class="admin-panel__body" style="padding:0;">
|
||||
<table class="admin-table" style="width:100%;"><thead><tr>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Mã đơn</th>
|
||||
<th style="padding:12px 16px;text-align:right;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Giá trị</th>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Trạng thái</th>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Ngày tạo</th>
|
||||
</tr></thead><tbody>
|
||||
@foreach (var o in _reportOrders.Take(20))
|
||||
{
|
||||
<tr style="border-top:1px solid var(--admin-border-subtle);">
|
||||
<td style="padding:12px 16px;font-family:monospace;font-size:12px;font-weight:600;">@o.Id.ToString()[..8]</td>
|
||||
<td style="padding:12px 16px;text-align:right;font-weight:600;color:var(--admin-orange-primary);">@FormatVND(o.TotalAmount)</td>
|
||||
<td style="padding:12px 16px;"><span class="admin-status-badge admin-status-badge--online" style="font-size:10px;"><span class="admin-status-badge__dot"></span>@(o.Status ?? "—")</span></td>
|
||||
<td style="padding:12px 16px;font-size:13px;color:var(--admin-text-tertiary);">@o.CreatedAt.ToString("dd/MM/yyyy HH:mm")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
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":
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__body" style="text-align:center;padding:60px 20px;">
|
||||
<div style="width:80px;height:80px;border-radius:24px;background:rgba(249,115,22,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 20px;">
|
||||
<i data-lucide="flame" style="width:36px;height:36px;color:#F97316;"></i>
|
||||
</div>
|
||||
<h2 style="font-size:22px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFFFFF);">Kitchen Display System</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0 0 12px;max-width:420px;margin-left:auto;margin-right:auto;">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.</p>
|
||||
<p style="font-size:13px;color:var(--admin-text-quaternary, #6B6B6F);margin:0;">Tính năng KDS sẽ hoạt động khi kết nối với F&B Engine</p>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
|
||||
// ═══ UNKNOWN SECTIONS ═══
|
||||
default:
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__body" style="text-align:center;padding:60px 20px;">
|
||||
@@ -222,12 +440,7 @@
|
||||
<i data-lucide="@_sectionIcon" style="width:36px;height:36px;color:var(--admin-orange-primary);"></i>
|
||||
</div>
|
||||
<h2 style="font-size:22px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFFFFF);">@_sectionTitle</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0 0 24px;max-width:400px;margin-left:auto;margin-right:auto;">
|
||||
@_sectionDescription
|
||||
</p>
|
||||
<p style="font-size:13px;color:var(--admin-text-quaternary, #6B6B6F);margin:0;">
|
||||
Tính năng này sẽ được kích hoạt khi có dữ liệu từ hệ thống
|
||||
</p>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0;">@_sectionDescription</p>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
@@ -255,6 +468,11 @@
|
||||
private List<PosDataService.OrderInfo> _orders = new();
|
||||
private List<PosDataService.StaffInfo> _staff = new();
|
||||
private List<PosDataService.MemberInfo> _members = new();
|
||||
private List<PosDataService.TableInfo> _tables = new();
|
||||
private List<PosDataService.AppointmentInfo> _appointments = new();
|
||||
// Reports data (separate from section-specific _orders)
|
||||
private List<PosDataService.OrderInfo> _reportOrders = new();
|
||||
private List<PosDataService.AdminProductInfo> _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)
|
||||
|
||||
Reference in New Issue
Block a user