feat(karaoke): add 4 missing Blazor Razor workflow files

- KaraokeJourney.razor: 6-step end-to-end session workflow tracker
- PeakWarning.razor: Peak hours pricing warning with cost estimator
- RoomExtend.razor: Room extension dialog with time options and preview
- RoomReset.razor: Room cleanup/reset checklist with progress tracking

All files follow existing Karaoke patterns (PosLayout, PosBase, FormatPrice,
NavigateTo, bilingual comments, section markers, CSS vars, Lucide icons,
Vietnamese demo data).

Co-authored-by: Velik <hongochai10@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-02-26 18:33:03 +00:00
parent 8a1e5eafad
commit 8953a6c1d9
4 changed files with 794 additions and 0 deletions

View File

@@ -0,0 +1,287 @@
@*
EN: Karaoke Journey — End-to-end session workflow tracker with 6 steps from reception to payment.
VI: Hành trình Karaoke — Theo dõi quy trình phiên từ đón khách đến thanh toán qua 6 bước.
*@
@page "/pos/karaoke/karaoke-journey"
@layout PosLayout
@inherits PosBase
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
@* ═══ HEADER / TIÊU ĐỀ ═══ *@
<div style="display:flex;align-items:center;gap:12px;padding:12px 16px;border-bottom:1px solid var(--pos-border-subtle);flex-shrink:0;">
<button style="background:var(--pos-bg-interactive);border:none;color:var(--pos-text-primary);
padding:8px 12px;border-radius:8px;cursor:pointer;font-size:13px;"
@onclick="@(() => NavigateTo("karaoke"))">
<i data-lucide="arrow-left" style="width:16px;height:16px;display:inline;"></i>
</button>
<span style="font-size:16px;font-weight:700;">Hành trình Karaoke</span>
<span style="font-size:12px;color:var(--pos-text-tertiary);">Bước @(_activeStep + 1)/6</span>
</div>
@* ═══ STEP INDICATOR / CHỈ BÁO BƯỚC ═══ *@
<div style="display:flex;align-items:center;padding:16px;gap:4px;flex-shrink:0;overflow-x:auto;">
@for (var i = 0; i < _steps.Length; i++)
{
var idx = i;
<div @onclick="() => _activeStep = idx"
style="flex:1;display:flex;flex-direction:column;align-items:center;gap:6px;cursor:pointer;min-width:90px;">
<div style="width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;
background:@(idx == _activeStep ? "var(--pos-orange-primary)" : idx < _activeStep ? "rgba(34,197,94,.15)" : "var(--pos-bg-interactive)");
color:@(idx == _activeStep ? "#FFF" : idx < _activeStep ? "#22C55E" : "var(--pos-text-tertiary)");
transition:all .2s ease;">
<i data-lucide="@_steps[idx].Icon" style="width:18px;height:18px;"></i>
</div>
<span style="font-size:11px;font-weight:@(idx == _activeStep ? "700" : "500");text-align:center;
color:@(idx == _activeStep ? "var(--pos-orange-primary)" : idx < _activeStep ? "#22C55E" : "var(--pos-text-tertiary)");">
@_steps[idx].Label
</span>
</div>
@if (i < _steps.Length - 1)
{
<div style="flex:0 0 20px;height:2px;background:@(i < _activeStep ? "#22C55E" : "var(--pos-border-subtle)");
margin-top:-18px;"></div>
}
}
</div>
@* ═══ STEP CONTENT / NỘI DUNG BƯỚC ═══ *@
<div style="flex:1;overflow-y:auto;padding:16px;">
@switch (_activeStep)
{
@* EN: Step 1 — Guest reception / VI: Bước 1 — Đón khách *@
case 0:
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:20px;margin-bottom:16px;">
<div style="font-size:14px;font-weight:600;margin-bottom:12px;">
<i data-lucide="users" style="width:16px;height:16px;display:inline;"></i> Đón khách
</div>
<div style="display:flex;gap:16px;margin-bottom:16px;">
<div style="flex:1;">
<div style="font-size:12px;color:var(--pos-text-tertiary);margin-bottom:6px;">Số khách</div>
<div style="display:flex;align-items:center;gap:12px;">
<button style="width:36px;height:36px;border-radius:8px;border:1px solid var(--pos-border-default);
background:transparent;color:var(--pos-text-primary);cursor:pointer;font-size:16px;"
@onclick="() => _guestCount = Math.Max(1, _guestCount - 1)"></button>
<span style="font-size:24px;font-weight:700;min-width:30px;text-align:center;">@_guestCount</span>
<button style="width:36px;height:36px;border-radius:8px;border:1px solid var(--pos-border-default);
background:transparent;color:var(--pos-text-primary);cursor:pointer;font-size:16px;"
@onclick="() => _guestCount++">+</button>
</div>
</div>
<div style="flex:1;">
<div style="font-size:12px;color:var(--pos-text-tertiary);margin-bottom:6px;">Thẻ thành viên</div>
<div style="display:flex;gap:8px;">
<div style="flex:1;display:flex;align-items:center;gap:8px;background:var(--pos-bg-interactive);
border-radius:8px;padding:0 12px;border:1px solid var(--pos-border-default);">
<i data-lucide="search" style="width:14px;height:14px;color:var(--pos-text-tertiary);"></i>
<input type="text" placeholder="SĐT hoặc mã thẻ..." @bind="_memberSearch"
style="flex:1;background:transparent;border:none;color:var(--pos-text-primary);
font-size:13px;padding:10px 0;outline:none;" />
</div>
</div>
@if (!string.IsNullOrEmpty(_memberSearch))
{
<div style="font-size:12px;color:#22C55E;margin-top:6px;">
<i data-lucide="check-circle" style="width:12px;height:12px;display:inline;"></i>
Nguyễn Văn Minh — Gold • 2,450 điểm
</div>
}
</div>
</div>
</div>
break;
@* EN: Step 2 — Room selection / VI: Bước 2 — Chọn phòng *@
case 1:
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:20px;margin-bottom:16px;">
<div style="font-size:14px;font-weight:600;margin-bottom:12px;">
<i data-lucide="door-open" style="width:16px;height:16px;display:inline;"></i> Chọn phòng
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;">
<div style="text-align:center;padding:16px;background:var(--pos-bg-interactive);border-radius:var(--pos-radius);">
<div style="font-size:11px;color:var(--pos-text-tertiary);margin-bottom:4px;">Phòng</div>
<div style="font-size:18px;font-weight:700;">VIP 2</div>
</div>
<div style="text-align:center;padding:16px;background:var(--pos-bg-interactive);border-radius:var(--pos-radius);">
<div style="font-size:11px;color:var(--pos-text-tertiary);margin-bottom:4px;">Sức chứa</div>
<div style="font-size:18px;font-weight:700;">20 người</div>
</div>
<div style="text-align:center;padding:16px;background:var(--pos-bg-interactive);border-radius:var(--pos-radius);">
<div style="font-size:11px;color:var(--pos-text-tertiary);margin-bottom:4px;">Loại</div>
<div style="font-size:18px;font-weight:700;">Deluxe</div>
</div>
</div>
<div style="font-size:13px;color:var(--pos-text-secondary);margin-top:12px;text-align:center;">
Tầng 3 • Khu Deluxe • @FormatPrice(200_000)/giờ
</div>
</div>
break;
@* EN: Step 3 — Open room / VI: Bước 3 — Mở phòng *@
case 2:
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:20px;margin-bottom:16px;">
<div style="font-size:14px;font-weight:600;margin-bottom:12px;">
<i data-lucide="play" style="width:16px;height:16px;display:inline;"></i> Mở phòng
</div>
<div style="display:flex;flex-direction:column;gap:10px;">
<div style="display:flex;justify-content:space-between;font-size:13px;">
<span style="color:var(--pos-text-secondary);">Giờ bắt đầu</span>
<span style="font-weight:600;">19:30</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:13px;">
<span style="color:var(--pos-text-secondary);">Thời lượng đặt</span>
<span style="font-weight:600;">2.5 giờ</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:13px;">
<span style="color:var(--pos-text-secondary);">Giá/giờ</span>
<span style="font-weight:600;">@FormatPrice(200_000)</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:13px;">
<span style="color:var(--pos-text-secondary);">Giờ kết thúc dự kiến</span>
<span style="font-weight:600;">22:00</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:14px;font-weight:600;
padding-top:10px;border-top:1px solid var(--pos-border-subtle);">
<span>Tạm tính phòng</span>
<span style="color:var(--pos-orange-primary);">@FormatPrice(500_000)</span>
</div>
</div>
</div>
break;
@* EN: Step 4 — In room / VI: Bước 4 — Trong phòng *@
case 3:
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:24px;
text-align:center;margin-bottom:16px;">
<div style="font-size:12px;color:var(--pos-text-tertiary);font-weight:600;margin-bottom:8px;">
THỜI GIAN SỬ DỤNG
</div>
<div style="font-size:48px;font-weight:700;color:var(--pos-orange-primary);font-variant-numeric:tabular-nums;">
02:15:00
</div>
<div style="font-size:13px;color:var(--pos-text-secondary);margin-top:8px;">
Bắt đầu: <b>19:30</b> • Dự kiến: <b>22:00</b>
</div>
</div>
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:16px;margin-bottom:16px;">
<div style="font-size:13px;font-weight:600;margin-bottom:10px;">Đơn F&B hiện tại</div>
<div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px;">
<span style="color:var(--pos-text-secondary);">Số món</span>
<span>6 món</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:14px;font-weight:600;
padding-top:8px;border-top:1px solid var(--pos-border-subtle);">
<span>Tổng F&B</span>
<span style="color:var(--pos-orange-primary);">@FormatPrice(830_000)</span>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<button style="padding:14px;border-radius:var(--pos-radius);background:rgba(59,130,246,.15);
border:none;color:#3B82F6;cursor:pointer;font-size:13px;font-weight:500;">
<i data-lucide="utensils" style="width:16px;height:16px;display:block;margin:0 auto 4px;"></i>
Gọi thêm F&B
</button>
<button style="padding:14px;border-radius:var(--pos-radius);background:rgba(255,92,0,.15);
border:none;color:var(--pos-orange-primary);cursor:pointer;font-size:13px;font-weight:500;">
<i data-lucide="clock" style="width:16px;height:16px;display:block;margin:0 auto 4px;"></i>
Gia hạn
</button>
</div>
break;
@* EN: Step 5 — Close room / VI: Bước 5 — Đóng phòng *@
case 4:
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:20px;margin-bottom:16px;">
<div style="font-size:14px;font-weight:600;margin-bottom:12px;">
<i data-lucide="lock" style="width:16px;height:16px;display:inline;"></i> Kết thúc phiên
</div>
<div style="display:flex;flex-direction:column;gap:8px;">
<div style="display:flex;justify-content:space-between;font-size:13px;">
<span style="color:var(--pos-text-secondary);">Thời gian sử dụng</span>
<span style="font-weight:600;">2 giờ 30 phút</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:13px;">
<span style="color:var(--pos-text-secondary);">Tiền phòng</span>
<span>@FormatPrice(500_000)</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:13px;">
<span style="color:var(--pos-text-secondary);">Tiền F&B</span>
<span>@FormatPrice(830_000)</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:15px;font-weight:700;
padding-top:10px;border-top:1px solid var(--pos-border-subtle);">
<span>Tổng cộng</span>
<span style="color:var(--pos-orange-primary);">@FormatPrice(1_330_000)</span>
</div>
</div>
</div>
break;
@* EN: Step 6 — Payment / VI: Bước 6 — Thanh toán *@
case 5:
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:24px;
text-align:center;margin-bottom:16px;">
<div style="font-size:12px;color:var(--pos-text-tertiary);font-weight:600;margin-bottom:8px;
text-transform:uppercase;letter-spacing:1px;">
TỔNG THANH TOÁN
</div>
<div style="font-size:40px;font-weight:700;color:var(--pos-orange-primary);">
@FormatPrice(1_330_000)
</div>
<div style="font-size:13px;color:var(--pos-text-secondary);margin-top:8px;">
Phòng VIP 2 • 2h30 • 6 món F&B
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:16px;">
<button style="padding:16px;border-radius:var(--pos-radius);background:rgba(34,197,94,.15);
border:none;color:#22C55E;cursor:pointer;font-size:14px;font-weight:600;">
<i data-lucide="banknote" style="width:18px;height:18px;display:block;margin:0 auto 4px;"></i>
Tiền mặt
</button>
<button style="padding:16px;border-radius:var(--pos-radius);background:rgba(59,130,246,.15);
border:none;color:#3B82F6;cursor:pointer;font-size:14px;font-weight:600;">
<i data-lucide="credit-card" style="width:18px;height:18px;display:block;margin:0 auto 4px;"></i>
Thẻ/Chuyển khoản
</button>
</div>
break;
}
</div>
@* ═══ NAVIGATION BUTTONS / NÚT ĐIỀU HƯỚNG ═══ *@
<div style="display:flex;gap:12px;padding:12px 16px;border-top:1px solid var(--pos-border-subtle);flex-shrink:0;">
<button style="flex:1;padding:14px;border-radius:var(--pos-radius);background:var(--pos-bg-interactive);
border:none;color:var(--pos-text-primary);cursor:pointer;font-size:14px;font-weight:600;
opacity:@(_activeStep == 0 ? "0.4" : "1");"
disabled="@(_activeStep == 0)"
@onclick="() => _activeStep = Math.Max(0, _activeStep - 1)">
<i data-lucide="arrow-left" style="width:16px;height:16px;display:inline;"></i> Quay lại
</button>
<button style="flex:1;padding:14px;border-radius:var(--pos-radius);
background:@(_activeStep == _steps.Length - 1 ? "var(--pos-success)" : "var(--pos-orange-primary)");
border:none;color:#FFF;cursor:pointer;font-size:14px;font-weight:600;"
@onclick="() => _activeStep = Math.Min(_steps.Length - 1, _activeStep + 1)">
@(_activeStep == _steps.Length - 1 ? "Hoàn tất" : "Tiếp")
<i data-lucide="arrow-right" style="width:16px;height:16px;display:inline;"></i>
</button>
</div>
</div>
@code {
// EN: Active step index / VI: Chỉ số bước hiện tại
private int _activeStep;
private int _guestCount = 8;
private string _memberSearch = "0901234567";
// EN: Journey steps / VI: Các bước hành trình
private readonly StepInfo[] _steps =
{
new("Đón khách", "users"),
new("Chọn phòng", "door-open"),
new("Mở phòng", "play"),
new("Trong phòng", "music"),
new("Đóng phòng", "lock"),
new("Thanh toán", "credit-card"),
};
private record StepInfo(string Label, string Icon);
}

View File

@@ -0,0 +1,169 @@
@*
EN: Karaoke Peak Warning — Peak hours pricing comparison, room type multipliers, cost estimator.
VI: Cảnh báo giờ cao điểm Karaoke — So sánh giá giờ cao điểm, hệ số phòng, ước tính chi phí.
*@
@page "/pos/karaoke/peak-warning"
@layout PosLayout
@inherits PosBase
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
@* ═══ HEADER / TIÊU ĐỀ ═══ *@
<div style="display:flex;align-items:center;gap:12px;padding:12px 16px;border-bottom:1px solid var(--pos-border-subtle);flex-shrink:0;">
<button style="background:var(--pos-bg-interactive);border:none;color:var(--pos-text-primary);
padding:8px 12px;border-radius:8px;cursor:pointer;font-size:13px;"
@onclick="@(() => NavigateTo("karaoke"))">
<i data-lucide="arrow-left" style="width:16px;height:16px;display:inline;"></i>
</button>
<span style="font-size:16px;font-weight:700;">
<i data-lucide="alert-triangle" style="width:18px;height:18px;display:inline;color:var(--pos-warning);"></i>
Giờ cao điểm
</span>
</div>
<div style="flex:1;overflow-y:auto;padding:16px;">
@* ═══ CURRENT TIME / THỜI GIAN HIỆN TẠI ═══ *@
<div style="background:linear-gradient(135deg,rgba(245,158,11,.2),rgba(239,68,68,.15));
border-radius:var(--pos-radius);padding:24px;margin-bottom:16px;text-align:center;
border:1px solid rgba(245,158,11,.3);">
<div style="font-size:12px;font-weight:600;color:var(--pos-warning);text-transform:uppercase;letter-spacing:1px;">
KHUNG GIỜ HIỆN TẠI
</div>
<div style="font-size:36px;font-weight:700;color:var(--pos-warning);margin:8px 0;">
20:30 — Thứ 7
</div>
<div style="font-size:13px;color:var(--pos-text-secondary);">
Đang áp dụng giá <b style="color:var(--pos-warning);">cuối tuần</b>
</div>
</div>
@* ═══ PRICING TABLE / BẢNG GIÁ ═══ *@
<div style="margin-bottom:20px;">
<div style="font-size:14px;font-weight:600;margin-bottom:12px;">Bảng giá theo khung giờ (Standard)</div>
<div style="display:flex;flex-direction:column;gap:8px;">
@foreach (var rate in _pricingRates)
{
<div style="display:flex;align-items:center;gap:12px;padding:14px 16px;
background:@(rate.IsActive ? "rgba(245,158,11,.12)" : "var(--pos-bg-elevated)");
border-radius:var(--pos-radius);
border:1px solid @(rate.IsActive ? "var(--pos-warning)" : "var(--pos-border-subtle)");">
<div style="flex:1;">
<div style="font-size:13px;font-weight:600;color:@(rate.IsActive ? "var(--pos-warning)" : "var(--pos-text-primary)");">
@rate.Label
</div>
<div style="font-size:11px;color:var(--pos-text-tertiary);margin-top:2px;">@rate.TimeRange</div>
</div>
<div style="text-align:right;">
<div style="font-size:14px;font-weight:700;color:@(rate.IsActive ? "var(--pos-warning)" : "var(--pos-text-primary)");">
@FormatPrice(rate.Price)/giờ
</div>
<div style="font-size:11px;color:var(--pos-text-tertiary);">x@rate.Multiplier.ToString("0.0")</div>
</div>
@if (rate.IsActive)
{
<span style="font-size:11px;padding:3px 8px;border-radius:6px;font-weight:600;
background:rgba(245,158,11,.2);color:var(--pos-warning);">Hiện tại</span>
}
</div>
}
</div>
</div>
@* ═══ ROOM TYPE SELECTOR / CHỌN LOẠI PHÒNG ═══ *@
<div style="margin-bottom:20px;">
<div style="font-size:14px;font-weight:600;margin-bottom:12px;">Loại phòng</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;">
@foreach (var room in _roomTypes)
{
<button @onclick="() => _selectedRoomType = room"
style="padding:16px;border-radius:var(--pos-radius);text-align:center;cursor:pointer;
background:@(_selectedRoomType.Name == room.Name ? "var(--pos-orange-primary)" : "var(--pos-bg-elevated)");
color:@(_selectedRoomType.Name == room.Name ? "#FFF" : "var(--pos-text-primary)");
border:1px solid @(_selectedRoomType.Name == room.Name ? "var(--pos-orange-primary)" : "var(--pos-border-subtle)");">
<div style="font-size:14px;font-weight:700;">@room.Name</div>
<div style="font-size:12px;margin-top:4px;opacity:0.7;">x@room.Multiplier.ToString("0.0")</div>
</button>
}
</div>
</div>
@* ═══ COST ESTIMATOR / ƯỚC TÍNH CHI PHÍ ═══ *@
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:20px;">
<div style="font-size:14px;font-weight:600;margin-bottom:12px;">Ước tính chi phí</div>
<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;">
<span style="font-size:13px;color:var(--pos-text-secondary);">Số giờ:</span>
<button style="width:36px;height:36px;border-radius:8px;border:1px solid var(--pos-border-default);
background:transparent;color:var(--pos-text-primary);cursor:pointer;font-size:16px;"
@onclick="() => _estimateHours = Math.Max(1, _estimateHours - 1)"></button>
<span style="font-size:20px;font-weight:700;min-width:30px;text-align:center;">@_estimateHours</span>
<button style="width:36px;height:36px;border-radius:8px;border:1px solid var(--pos-border-default);
background:transparent;color:var(--pos-text-primary);cursor:pointer;font-size:16px;"
@onclick="() => _estimateHours++">+</button>
<span style="font-size:13px;color:var(--pos-text-tertiary);">giờ</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px;">
<span style="color:var(--pos-text-secondary);">Giá hiện tại (@_selectedRoomType.Name)</span>
<span>@FormatPrice(CurrentRatePrice)/giờ</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px;">
<span style="color:var(--pos-text-secondary);">Số giờ</span>
<span>@_estimateHours giờ</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:16px;font-weight:700;
padding-top:10px;border-top:1px solid var(--pos-border-subtle);">
<span>Tổng ước tính</span>
<span style="color:var(--pos-orange-primary);">@FormatPrice(CurrentRatePrice * _estimateHours)</span>
</div>
</div>
</div>
@* ═══ CONFIRM BUTTON / NÚT XÁC NHẬN ═══ *@
<div style="padding:12px 16px;border-top:1px solid var(--pos-border-subtle);flex-shrink:0;">
<button style="width:100%;padding:16px;border-radius:var(--pos-radius);background:var(--pos-orange-primary);
border:none;color:#FFF;cursor:pointer;font-size:15px;font-weight:700;"
@onclick="@(() => NavigateTo("karaoke/room-select"))">
<i data-lucide="check" style="width:18px;height:18px;display:inline;"></i> Xác nhận giá
</button>
</div>
</div>
@code {
// EN: Estimate hours / VI: Số giờ ước tính
private int _estimateHours = 2;
// EN: Pricing rates / VI: Bảng giá
private readonly List<PricingRate> _pricingRates = new()
{
new("Giờ thường", "T2T5, 10:0017:00", 100_000, 1.0m, false),
new("Giờ cao điểm", "T2T5, 17:0023:00", 150_000, 1.5m, false),
new("Cuối tuần", "T6CN", 180_000, 1.8m, true),
new("Lễ/Tết", "Ngày lễ, Tết", 250_000, 2.5m, false),
};
// EN: Room types / VI: Loại phòng
private readonly RoomType[] _roomTypes =
{
new("Standard", 1.0m),
new("Deluxe", 1.5m),
new("VIP", 2.0m),
};
private RoomType _selectedRoomType = null!;
protected override void OnInitialized()
{
_selectedRoomType = _roomTypes[0];
}
// EN: Current active rate price adjusted for room type / VI: Giá hiện tại theo loại phòng
private decimal CurrentRatePrice
{
get
{
var activeRate = _pricingRates.FirstOrDefault(r => r.IsActive) ?? _pricingRates[0];
return activeRate.Price * _selectedRoomType.Multiplier;
}
}
private record PricingRate(string Label, string TimeRange, decimal Price, decimal Multiplier, bool IsActive);
private record RoomType(string Name, decimal Multiplier);
}

View File

@@ -0,0 +1,191 @@
@*
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.
*@
@page "/pos/karaoke/room-extend"
@layout PosLayout
@inherits PosBase
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
@* ═══ HEADER / TIÊU ĐỀ ═══ *@
<div style="display:flex;align-items:center;gap:12px;padding:12px 16px;border-bottom:1px solid var(--pos-border-subtle);flex-shrink:0;">
<button style="background:var(--pos-bg-interactive);border:none;color:var(--pos-text-primary);
padding:8px 12px;border-radius:8px;cursor:pointer;font-size:13px;"
@onclick="@(() => NavigateTo("karaoke/room-session"))">
<i data-lucide="arrow-left" style="width:16px;height:16px;display:inline;"></i>
</button>
<span style="font-size:16px;font-weight:700;">Gia hạn phòng</span>
</div>
<div style="flex:1;overflow-y:auto;padding:16px;">
@* ═══ CURRENT SESSION INFO / THÔNG TIN PHIÊN HIỆN TẠI ═══ *@
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:20px;margin-bottom:16px;">
<div style="display:flex;justify-content:space-between;align-items:start;margin-bottom:12px;">
<div>
<div style="font-size:18px;font-weight:700;">Phòng VIP 2</div>
<div style="font-size:12px;color:var(--pos-text-tertiary);">Deluxe • 20 người • Tầng 3</div>
</div>
<span style="font-size:12px;padding:4px 10px;border-radius:6px;font-weight:600;
background:rgba(34,197,94,.15);color:#22C55E;">Đang hoạt động</span>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;">
<div style="text-align:center;padding:12px;background:var(--pos-bg-interactive);border-radius:8px;">
<div style="font-size:11px;color:var(--pos-text-tertiary);margin-bottom:4px;">Bắt đầu</div>
<div style="font-size:16px;font-weight:700;">19:30</div>
</div>
<div style="text-align:center;padding:12px;background:var(--pos-bg-interactive);border-radius:8px;">
<div style="font-size:11px;color:var(--pos-text-tertiary);margin-bottom:4px;">Đã dùng</div>
<div style="font-size:16px;font-weight:700;color:var(--pos-orange-primary);">2h15</div>
</div>
<div style="text-align:center;padding:12px;background:var(--pos-bg-interactive);border-radius:8px;">
<div style="font-size:11px;color:var(--pos-text-tertiary);margin-bottom:4px;">Giá/giờ</div>
<div style="font-size:16px;font-weight:700;">@FormatPrice(200_000)</div>
</div>
</div>
<div style="display:flex;justify-content:space-between;font-size:13px;margin-top:12px;
padding-top:10px;border-top:1px solid var(--pos-border-subtle);">
<span style="color:var(--pos-text-secondary);">Tiền phòng hiện tại</span>
<span style="font-weight:600;">@FormatPrice(450_000)</span>
</div>
</div>
@* ═══ EXTENSION OPTIONS / TÙY CHỌN GIA HẠN ═══ *@
<div style="margin-bottom:16px;">
<div style="font-size:14px;font-weight:600;margin-bottom:12px;">Chọn thời gian gia hạn</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;">
@foreach (var opt in _extendOptions)
{
<button @onclick="() => SelectExtension(opt)"
style="padding:20px 12px;border-radius:var(--pos-radius);text-align:center;cursor:pointer;
background:@(_selectedOption?.Minutes == opt.Minutes ? "var(--pos-orange-primary)" : "var(--pos-bg-elevated)");
color:@(_selectedOption?.Minutes == opt.Minutes ? "#FFF" : "var(--pos-text-primary)");
border:1px solid @(_selectedOption?.Minutes == opt.Minutes ? "var(--pos-orange-primary)" : "var(--pos-border-subtle)");">
<div style="font-size:16px;font-weight:700;">@opt.Label</div>
<div style="font-size:13px;margin-top:4px;opacity:0.8;">+@FormatPrice(opt.Cost)</div>
</button>
}
</div>
</div>
@* EN: Custom input / VI: Nhập tùy chỉnh *@
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:16px;margin-bottom:16px;">
<div style="font-size:13px;font-weight:600;margin-bottom:8px;">Tùy chỉnh (phút)</div>
<div style="display:flex;align-items:center;gap:12px;">
<button style="width:36px;height:36px;border-radius:8px;border:1px solid var(--pos-border-default);
background:transparent;color:var(--pos-text-primary);cursor:pointer;font-size:16px;"
@onclick="() => AdjustCustom(-15)"></button>
<span style="font-size:20px;font-weight:700;min-width:50px;text-align:center;">@_customMinutes</span>
<button style="width:36px;height:36px;border-radius:8px;border:1px solid var(--pos-border-default);
background:transparent;color:var(--pos-text-primary);cursor:pointer;font-size:16px;"
@onclick="() => AdjustCustom(15)">+</button>
<span style="font-size:13px;color:var(--pos-text-tertiary);">phút</span>
<button style="padding:8px 14px;border-radius:8px;background:var(--pos-bg-interactive);
border:none;color:var(--pos-text-primary);cursor:pointer;font-size:12px;font-weight:600;"
@onclick="ApplyCustom">
Áp dụng
</button>
</div>
</div>
@* ═══ PREVIEW / XEM TRƯỚC ═══ *@
@if (_selectedOption is not null)
{
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:16px;margin-bottom:16px;">
<div style="font-size:13px;font-weight:600;margin-bottom:10px;">Xem trước sau gia hạn</div>
<div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px;">
<span style="color:var(--pos-text-secondary);">Giờ kết thúc mới</span>
<span style="font-weight:600;">@_newEndTime</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px;">
<span style="color:var(--pos-text-secondary);">Thời gian thêm</span>
<span style="font-weight:600;">+@_selectedOption.Label</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px;">
<span style="color:var(--pos-text-secondary);">Phí gia hạn</span>
<span style="font-weight:600;">+@FormatPrice(_selectedOption.Cost)</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:15px;font-weight:700;
padding-top:10px;border-top:1px solid var(--pos-border-subtle);">
<span>Tổng tiền phòng mới</span>
<span style="color:var(--pos-orange-primary);">@FormatPrice(450_000 + _selectedOption.Cost)</span>
</div>
</div>
@* EN: Peak warning if applicable / VI: Cảnh báo giờ cao điểm nếu có *@
@if (_showPeakWarning)
{
<div style="background:rgba(245,158,11,.12);border-radius:var(--pos-radius);padding:14px 16px;
margin-bottom:16px;display:flex;align-items:center;gap:10px;
border:1px solid rgba(245,158,11,.3);">
<i data-lucide="alert-triangle" style="width:20px;height:20px;color:var(--pos-warning);flex-shrink:0;"></i>
<div>
<div style="font-size:13px;font-weight:600;color:var(--pos-warning);">Cảnh báo giờ cao điểm</div>
<div style="font-size:12px;color:var(--pos-text-tertiary);margin-top:2px;">
Gia hạn vào khung giờ cao điểm (sau 22:00). Giá có thể tăng.
</div>
</div>
</div>
}
}
</div>
@* ═══ ACTION BUTTONS / NÚT HÀNH ĐỘNG ═══ *@
<div style="display:flex;gap:12px;padding:12px 16px;border-top:1px solid var(--pos-border-subtle);flex-shrink:0;">
<button style="flex:1;padding:14px;border-radius:var(--pos-radius);background:var(--pos-bg-interactive);
border:none;color:var(--pos-text-primary);cursor:pointer;font-size:14px;font-weight:600;"
@onclick="@(() => NavigateTo("karaoke/room-session"))">
Hủy
</button>
<button style="flex:1;padding:14px;border-radius:var(--pos-radius);background:var(--pos-orange-primary);
border:none;color:#FFF;cursor:pointer;font-size:14px;font-weight:600;
opacity:@(_selectedOption is null ? "0.4" : "1");"
disabled="@(_selectedOption is null)"
@onclick="@(() => NavigateTo("karaoke/room-session"))">
<i data-lucide="check" style="width:16px;height:16px;display:inline;"></i> Xác nhận gia hạn
</button>
</div>
</div>
@code {
// EN: Extension options / VI: Tùy chọn gia hạn
private readonly List<ExtendOption> _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),
};
private ExtendOption? _selectedOption;
private int _customMinutes = 45;
private string _newEndTime = "22:00";
private bool _showPeakWarning;
private void SelectExtension(ExtendOption opt)
{
_selectedOption = opt;
UpdatePreview(opt.Minutes);
}
private void AdjustCustom(int delta)
{
_customMinutes = Math.Max(15, _customMinutes + delta);
}
private void ApplyCustom()
{
var cost = (decimal)_customMinutes / 60 * 200_000;
_selectedOption = new(_customMinutes, $"+{_customMinutes} phút", Math.Round(cost, -3));
UpdatePreview(_customMinutes);
}
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);
}

View File

@@ -0,0 +1,147 @@
@*
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.
*@
@page "/pos/karaoke/room-reset"
@layout PosLayout
@inherits PosBase
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
@* ═══ HEADER / TIÊU ĐỀ ═══ *@
<div style="display:flex;align-items:center;gap:12px;padding:12px 16px;border-bottom:1px solid var(--pos-border-subtle);flex-shrink:0;">
<button style="background:var(--pos-bg-interactive);border:none;color:var(--pos-text-primary);
padding:8px 12px;border-radius:8px;cursor:pointer;font-size:13px;"
@onclick="@(() => NavigateTo("karaoke"))">
<i data-lucide="arrow-left" style="width:16px;height:16px;display:inline;"></i>
</button>
<span style="font-size:16px;font-weight:700;">Reset phòng</span>
<span style="font-size:12px;padding:3px 10px;border-radius:6px;font-weight:600;
background:@(AllChecked ? "rgba(34,197,94,.15)" : "rgba(245,158,11,.15)");
color:@(AllChecked ? "#22C55E" : "var(--pos-warning)");">
@(AllChecked ? "Sẵn sàng" : "Đang dọn")
</span>
</div>
<div style="flex:1;overflow-y:auto;padding:16px;">
@* ═══ ROOM INFO / THÔNG TIN PHÒNG ═══ *@
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:20px;margin-bottom:16px;">
<div style="display:flex;justify-content:space-between;align-items:start;">
<div>
<div style="font-size:18px;font-weight:700;">Phòng VIP 2</div>
<div style="font-size:12px;color:var(--pos-text-tertiary);margin-top:2px;">Deluxe • 20 người • Tầng 3</div>
</div>
<div style="text-align:right;">
<div style="font-size:12px;color:var(--pos-text-tertiary);">Phiên trước kết thúc</div>
<div style="font-size:16px;font-weight:700;">22:15</div>
</div>
</div>
</div>
@* ═══ STAFF & TIME / NHÂN VIÊN & THỜI GIAN ═══ *@
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px;">
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:14px;
display:flex;align-items:center;gap:10px;">
<div style="width:36px;height:36px;border-radius:10px;background:rgba(59,130,246,.15);
display:flex;align-items:center;justify-content:center;">
<i data-lucide="user" style="width:18px;height:18px;color:#3B82F6;"></i>
</div>
<div>
<div style="font-size:11px;color:var(--pos-text-tertiary);">Nhân viên</div>
<div style="font-size:13px;font-weight:600;">Trần Thị Hoa</div>
</div>
</div>
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:14px;
display:flex;align-items:center;gap:10px;">
<div style="width:36px;height:36px;border-radius:10px;background:rgba(255,92,0,.15);
display:flex;align-items:center;justify-content:center;">
<i data-lucide="clock" style="width:18px;height:18px;color:var(--pos-orange-primary);"></i>
</div>
<div>
<div style="font-size:11px;color:var(--pos-text-tertiary);">Bắt đầu dọn</div>
<div style="font-size:13px;font-weight:600;">22:18 • 12 phút</div>
</div>
</div>
</div>
@* ═══ PROGRESS BAR / THANH TIẾN ĐỘ ═══ *@
<div style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:16px;margin-bottom:16px;">
<div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:8px;">
<span style="font-weight:600;">Tiến độ</span>
<span style="color:var(--pos-text-tertiary);">@CompletedCount/@_checkItems.Count hoàn thành</span>
</div>
<div style="height:8px;background:var(--pos-bg-interactive);border-radius:4px;overflow:hidden;">
<div style="height:100%;width:@(ProgressPercent)%;border-radius:4px;transition:width .3s ease;
background:@(AllChecked ? "var(--pos-success)" : "var(--pos-orange-primary)");"></div>
</div>
</div>
@* ═══ CHECKLIST / DANH SÁCH KIỂM TRA ═══ *@
<div style="display:flex;flex-direction:column;gap:8px;">
@foreach (var item in _checkItems)
{
<div @onclick="() => item.Checked = !item.Checked"
style="display:flex;align-items:center;gap:14px;padding:14px 16px;cursor:pointer;
background:var(--pos-bg-elevated);border-radius:var(--pos-radius);
border:1px solid @(item.Checked ? "rgba(34,197,94,.3)" : "var(--pos-border-subtle)");
opacity:@(item.Checked ? "0.7" : "1");transition:all .2s ease;">
<div style="width:28px;height:28px;border-radius:8px;display:flex;align-items:center;justify-content:center;
flex-shrink:0;border:2px solid @(item.Checked ? "#22C55E" : "var(--pos-border-default)");
background:@(item.Checked ? "rgba(34,197,94,.15)" : "transparent");">
@if (item.Checked)
{
<i data-lucide="check" style="width:16px;height:16px;color:#22C55E;"></i>
}
</div>
<div style="flex:1;">
<div style="font-size:14px;font-weight:600;text-decoration:@(item.Checked ? "line-through" : "none");
color:@(item.Checked ? "var(--pos-text-tertiary)" : "var(--pos-text-primary)");">
@item.Label
</div>
<div style="font-size:12px;color:var(--pos-text-tertiary);margin-top:2px;">@item.Description</div>
</div>
<i data-lucide="@item.Icon" style="width:18px;height:18px;color:var(--pos-text-tertiary);flex-shrink:0;"></i>
</div>
}
</div>
</div>
@* ═══ COMPLETE BUTTON / NÚT HOÀN TẤT ═══ *@
<div style="padding:12px 16px;border-top:1px solid var(--pos-border-subtle);flex-shrink:0;">
<button style="width:100%;padding:16px;border-radius:var(--pos-radius);
background:@(AllChecked ? "var(--pos-success)" : "var(--pos-bg-interactive)");
border:none;color:@(AllChecked ? "#FFF" : "var(--pos-text-tertiary)");
cursor:@(AllChecked ? "pointer" : "not-allowed");font-size:15px;font-weight:700;"
disabled="@(!AllChecked)"
@onclick="@(() => NavigateTo("karaoke"))">
<i data-lucide="@(AllChecked ? "check-circle" : "loader")" style="width:18px;height:18px;display:inline;"></i>
Hoàn tất reset
</button>
</div>
</div>
@code {
// EN: Checklist items / VI: Các mục kiểm tra
private readonly List<CheckItem> _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),
};
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)
{
public string Label { get; set; } = label;
public string Description { get; set; } = description;
public string Icon { get; set; } = icon;
public bool Checked { get; set; } = isChecked;
}
}