Files
pos-system/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Karaoke/KaraokeTablet.razor

210 lines
9.0 KiB
Plaintext

@*
EN: Karaoke POS Tablet — Touch-optimized room map + session sidebar.
VI: POS Karaoke Tablet — Sơ đồ phòng tối ưu cảm ứng + thanh phiên bên cạnh.
*@
@page "/pos/{ShopId:guid}/karaoke/tablet"
@layout PosLayout
@inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
@* ═══ ROOM MAP (LEFT) / SƠ ĐỒ PHÒNG (TRÁI) ═══ *@
<div class="pos-product-panel" style="padding:16px;overflow-y:auto;">
@if (_isLoading)
{
<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--pos-text-tertiary);">
Đang tải...
</div>
}
else if (_loadError)
{
<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--pos-text-tertiary);">
Không thể tải dữ liệu
</div>
}
else
{
@* EN: Zone filter tabs / VI: Tab lọc khu vực *@
<div class="pos-category-tabs">
@foreach (var zone in _zones)
{
<button class="pos-category-tab @(zone == _activeZone ? "pos-category-tab--active" : "")"
@onclick="() => _activeZone = zone"
style="padding:12px 20px;font-size:14px;">
@zone
</button>
}
</div>
@* ═══ ROOM GRID (TOUCH) / LƯỚI PHÒNG (CẢM ỨNG) ═══ *@
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:16px;padding:12px 0;">
@foreach (var room in FilteredRooms)
{
<div @onclick="() => _selectedRoom = room"
style="background:@GetStatusBg(room.Status);border-radius:var(--pos-radius);
padding:20px;text-align:center;cursor:pointer;min-height:120px;
display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;
border:2px solid @(_selectedRoom?.Id == room.Id ? "var(--pos-orange-primary)" : "transparent");
transition:all .2s ease;">
<div style="font-size:11px;font-weight:600;color:rgba(255,255,255,.5);text-transform:uppercase;">@room.Type</div>
<div style="font-size:22px;font-weight:700;">@room.Name</div>
<div style="font-size:13px;color:rgba(255,255,255,.65);">@room.Capacity người</div>
<div style="font-size:12px;font-weight:600;margin-top:4px;">@GetStatusLabel(room.Status)</div>
@if (room.Status == "occupied" && room.SessionStart.HasValue)
{
<div style="font-size:12px;color:var(--pos-orange-primary);font-weight:600;">
@((DateTime.Now - room.SessionStart.Value).ToString(@"h\:mm"))
</div>
}
</div>
}
</div>
}
</div>
@* ═══ SESSION SIDEBAR / THANH PHIÊN BÊN ═══ *@
<div class="pos-cart-panel" style="width:340px;min-width:340px;">
@if (_selectedRoom is not null)
{
<div class="pos-cart-header">
<span class="pos-cart-header__title">@_selectedRoom.Name</span>
<span style="font-size:12px;color:var(--pos-text-tertiary);">@_selectedRoom.Type</span>
</div>
<div class="pos-cart-items" style="padding:12px;">
@if (_selectedRoom.Status == "occupied")
{
@* EN: Timer / VI: Đồng hồ *@
<div style="text-align:center;padding:12px 0 16px;">
<div style="font-size:11px;color:var(--pos-text-tertiary);margin-bottom:4px;">THỜI GIAN</div>
<div style="font-size:28px;font-weight:700;color:var(--pos-orange-primary);">
@((DateTime.Now - _selectedRoom.SessionStart!.Value).ToString(@"h\:mm\:ss"))
</div>
</div>
@* EN: Quick actions / VI: Thao tác nhanh *@
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px;">
<button style="padding:12px;border-radius:var(--pos-radius);background:var(--pos-bg-interactive);
border:none;color:var(--pos-text-primary);cursor:pointer;font-size:13px;font-weight:500;"
@onclick="@(() => NavigateTo("karaoke/order-fnb"))">
<i data-lucide="utensils" style="width:16px;height:16px;display:block;margin:0 auto 4px;"></i>
Gọi F&B
</button>
<button style="padding:12px;border-radius:var(--pos-radius);background:var(--pos-bg-interactive);
border:none;color:var(--pos-text-primary);cursor:pointer;font-size:13px;font-weight:500;"
@onclick="@(() => NavigateTo("karaoke/room-session"))">
<i data-lucide="timer" style="width:16px;height:16px;display:block;margin:0 auto 4px;"></i>
Gia hạn
</button>
</div>
@* EN: Current F&B / VI: F&B hiện tại *@
<div style="font-size:11px;color:var(--pos-text-tertiary);font-weight:600;padding:8px 0;">ĐƠN F&B</div>
@foreach (var item in _demoFnb)
{
<div class="pos-cart-item">
<div class="pos-cart-item__info">
<span class="pos-cart-item__name">@item.Name</span>
<span class="pos-cart-item__price">@FormatPrice(item.Price)</span>
</div>
<span style="font-size:13px;font-weight:600;">x@item.Qty</span>
</div>
}
}
else
{
<div style="text-align:center;padding:40px 16px;color:var(--pos-text-tertiary);font-size:14px;">
@GetStatusLabel(_selectedRoom.Status)
</div>
}
</div>
<div class="pos-cart-footer">
<div class="pos-cart-total">
<span class="pos-cart-total__label">Tổng cộng</span>
<span class="pos-cart-total__value">@FormatPrice(_selectedRoom.Status == "occupied" ? 520_000 : 0)</span>
</div>
<button class="pos-btn-checkout" style="height:52px;font-size:16px;"
@onclick="@(() => NavigateTo("karaoke/room-session"))">
<i data-lucide="receipt" style="width:18px;height:18px;"></i>
@(_selectedRoom.Status == "available" ? "Mở phiên" : "Chi tiết")
</button>
</div>
}
else
{
<div style="flex:1;display:flex;align-items:center;justify-content:center;color:var(--pos-text-tertiary);font-size:15px;">
Chọn phòng để bắt đầu
</div>
}
</div>
@code {
// EN: Loading state / VI: Trạng thái tải
private bool _isLoading = true;
private bool _loadError;
// EN: Zone filter / VI: Bộ lọc khu vực
private string _activeZone = "Tất cả";
private string[] _zones = { "Tất cả" };
private RoomInfo? _selectedRoom;
// EN: Room data loaded from DB / VI: Dữ liệu phòng tải từ DB
private List<RoomInfo> _rooms = new();
// EN: Demo F&B / VI: F&B mẫu
private readonly List<FnbItem> _demoFnb = new()
{
new("Bia Heineken", 40_000, 4), new("Trái cây dĩa", 120_000, 1),
new("Đậu phộng", 30_000, 2),
};
private IEnumerable<RoomInfo> FilteredRooms =>
_activeZone == "Tất cả" ? _rooms : _rooms.Where(r => r.Zone == _activeZone);
protected override async Task OnInitializedAsync()
{
try
{
var tables = await DataService.GetTablesAsync(ShopId);
_rooms = tables.Select(t => new RoomInfo(
t.Id.ToString(),
t.TableNumber,
t.Capacity,
t.Zone ?? "Standard",
t.Status,
t.Zone ?? "Tầng 1",
t.StartedAt
)).ToList();
var zoneNames = _rooms.Select(r => r.Zone).Distinct().ToList();
_zones = new[] { "Tất cả" }.Concat(zoneNames).ToArray();
}
catch
{
_loadError = true;
}
finally
{
_isLoading = false;
}
}
private static string GetStatusBg(string s) => s switch
{
"available" => "rgba(34,197,94,.15)", "occupied" => "rgba(255,92,0,.18)",
"reserved" => "rgba(59,130,246,.18)", "cleaning" => "rgba(245,158,11,.18)",
_ => "var(--pos-bg-interactive)"
};
private static string GetStatusLabel(string s) => s switch
{
"available" => "Trống", "occupied" => "Đang hát",
"reserved" => "Đã đặt", "cleaning" => "Đang dọn", _ => s
};
private record RoomInfo(string Id, string Name, int Capacity, string Type, string Status, string Zone, DateTime? SessionStart);
private record FnbItem(string Name, decimal Price, int Qty);
}