feat(admin): P3 — Karaoke hourly pricing + product type badges

- K2+K5: Room hourly rate (VIP 200k, Party 350k, Standard 120k/h)
- K2+K5: Active session billing calculator (elapsed hours × rate)
- C3-C4: Product type badge (Đồ uống/Dịch vụ/Vật lý)
- C3-C4: Variant/topping tag parsing from product description
This commit is contained in:
Ho Ngoc Hai
2026-03-01 04:32:13 +07:00
parent 8ea22b22b9
commit 644751be7b

View File

@@ -164,13 +164,25 @@
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;">
@foreach (var p in _products)
{
var typeColor = (p.Type ?? "") switch { "Service" => "#EC4899", "Physical" => "#3B82F6", _ => "#F59E0B" };
var typeLabel = (p.Type ?? "") switch { "Service" => "Dịch vụ", "Physical" => "Vật lý", _ => "Đồ uống" };
<div class="admin-panel" style="position:relative;">
<div class="admin-panel__body" style="padding:16px;text-align:center;">
<button @onclick="@(() => DeleteProduct(p.Id))" style="position:absolute;top:8px;right:8px;background:rgba(239,68,68,0.1);border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;" title="Xóa"><i data-lucide="trash-2" style="color:#EF4444;width:14px;height:14px;"></i></button>
<div style="width:48px;height:48px;border-radius:12px;background:rgba(255,92,0,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 12px;"><i data-lucide="package" style="color:#FF5C00;width:24px;height:24px;"></i></div>
<div style="font-weight:600;font-size:14px;margin-bottom:4px;">@p.Name</div>
<div style="font-size:13px;color:var(--admin-text-tertiary);">@(p.CategoryName ?? "—")</div>
<div style="font-size:13px;color:var(--admin-text-tertiary);margin-bottom:4px;">@(p.CategoryName ?? "—")</div>
<span style="font-size:10px;font-weight:600;padding:2px 8px;border-radius:6px;background:@($"{typeColor}22");color:@typeColor;">@typeLabel</span>
<div style="font-weight:700;color:var(--admin-orange-primary);margin-top:8px;">@p.Price.ToString("N0")₫</div>
@if (p.Description?.Contains("variant:") == true || p.Description?.Contains("topping:") == true)
{
<div style="margin-top:6px;display:flex;gap:4px;justify-content:center;flex-wrap:wrap;">
@foreach (var tag in (p.Description ?? "").Split(',').Where(t => t.Trim().StartsWith("variant:") || t.Trim().StartsWith("topping:")).Take(3))
{
<span style="font-size:9px;padding:1px 6px;border-radius:4px;background:rgba(59,130,246,0.1);color:#3B82F6;">@tag.Trim()</span>
}
</div>
}
</div>
</div>
}
@@ -499,7 +511,7 @@
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 };
var roomType = (room.Zone ?? "").ToLower() switch { var z when z.Contains("vip") => ("VIP", "#F59E0B"), var z when z.Contains("party") => ("Party", "#EC4899"), _ => ("Standard", "#8B5CF6") };
var roomType = (room.Zone ?? "").ToLower() switch { var z when z.Contains("vip") => ("VIP", "#F59E0B", 200000m), var z when z.Contains("party") => ("Party", "#EC4899", 350000m), _ => ("Standard", "#8B5CF6", 120000m) };
<div style="background:@bgColor;border:1px solid @borderColor;border-radius:14px;padding:20px;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
<div style="display:flex;align-items:center;gap:10px;">
@@ -508,15 +520,23 @@
</div>
<span style="font-size:10px;font-weight:700;padding:2px 8px;border-radius:6px;background:@($"{roomType.Item2}22");color:@roomType.Item2;">@roomType.Item1</span>
</div>
<div style="font-size:12px;color:var(--admin-text-tertiary);margin-bottom:8px;">@(room.Zone ?? "Chung") • @room.Capacity chỗ</div>
<div style="font-size:12px;color:var(--admin-text-tertiary);margin-bottom:4px;">@(room.Zone ?? "Chung") • @room.Capacity chỗ</div>
<div style="font-size:12px;font-weight:600;color:var(--admin-orange-primary);margin-bottom:8px;">@FormatVND(roomType.Item3)/giờ</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") ?? "—")
var elapsed = DateTime.UtcNow - (room.StartedAt ?? DateTime.UtcNow);
var hours = Math.Max(1, (int)Math.Ceiling(elapsed.TotalHours));
var bill = hours * roomType.Item3;
<div style="margin-top:8px;padding-top:8px;border-top:1px solid @borderColor;">
<div style="font-size:11px;color:var(--admin-text-tertiary);margin-bottom:4px;">@room.GuestCount khách • Bắt đầu @(room.StartedAt?.ToString("HH:mm") ?? "—")</div>
<div style="display:flex;justify-content:space-between;align-items:center;">
<span style="font-size:11px;color:var(--admin-text-tertiary);"><i data-lucide="clock" style="width:12px;height:12px;vertical-align:middle;"></i> @hours giờ</span>
<span style="font-size:13px;font-weight:700;color:var(--admin-orange-primary);">@FormatVND(bill)</span>
</div>
</div>
}
</div>