210 lines
9.0 KiB
Plaintext
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);
|
|
}
|