refactor: connect 9 Restaurant POS Razor files to PosDataService DB data

- RestaurantTablet.razor: load tables from DB via GetTablesAsync
- RestaurantMobile.razor: load tables from DB via GetTablesAsync
- TableMap.razor: load tables from DB with shape inference
- TableSelect.razor: load available tables from DB
- TableMergeSplit.razor: load tables for merge/split from DB
- KitchenDisplay.razor: inject DataService, keep demo tickets (needs API)
- WaiterPad.razor: load products/categories from DB
- RestaurantMenuManagement.razor: load products from DB
- OrderHistory.razor: inject DataService, keep demo orders (needs API)

All files follow the RestaurantDesktop pattern with loading/error states.

Co-authored-by: Velik <hongochai10@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-02-26 20:49:30 +00:00
parent 620d3812d5
commit c4b4d83db4
26 changed files with 1142 additions and 424 deletions

View File

@@ -5,98 +5,138 @@
@page "/pos/restaurant/mobile"
@layout PosLayout
@inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
@* ═══ HEADER / TIÊU ĐỀ ═══ *@
<div style="padding:12px 16px;border-bottom:1px solid var(--pos-border-subtle);display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:16px;font-weight:700;">Sơ đồ bàn</span>
<span style="font-size:12px;color:var(--pos-text-tertiary);">@_tables.Count bàn</span>
</div>
@if (_isLoading)
{
<div style="flex:1;display:flex;align-items:center;justify-content:center;color:var(--pos-text-tertiary);">
Đang tải...
</div>
}
else if (_loadError)
{
<div style="flex:1;display:flex;align-items:center;justify-content:center;color:var(--pos-text-tertiary);">
Không thể tải dữ liệu
</div>
}
else
{
@* ═══ HEADER / TIÊU ĐỀ ═══ *@
<div style="padding:12px 16px;border-bottom:1px solid var(--pos-border-subtle);display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:16px;font-weight:700;">Sơ đồ bàn</span>
<span style="font-size:12px;color:var(--pos-text-tertiary);">@_tables.Count bàn</span>
</div>
@* ═══ SECTION FILTER / LỌC KHU VỰC ═══ *@
<div class="pos-category-tabs" style="padding:8px 12px;">
@foreach (var s in _sections)
{
<button class="pos-category-tab @(s == _activeSection ? "pos-category-tab--active" : "")"
style="font-size:12px;padding:6px 12px;" @onclick="() => _activeSection = s">@s</button>
}
</div>
@* ═══ SECTION FILTER / LỌC KHU VỰC ═══ *@
<div class="pos-category-tabs" style="padding:8px 12px;">
@foreach (var s in _sections)
{
<button class="pos-category-tab @(s == _activeSection ? "pos-category-tab--active" : "")"
style="font-size:12px;padding:6px 12px;" @onclick="() => _activeSection = s">@s</button>
}
</div>
@* ═══ TABLE LIST / DANH SÁCH BÀN ═══ *@
<div style="flex:1;overflow-y:auto;padding:8px 12px;display:flex;flex-direction:column;gap:8px;">
@foreach (var t in FilteredTables)
{
<div @onclick="() => OpenTable(t)"
style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:14px 16px;
display:flex;align-items:center;gap:12px;cursor:pointer;
border-left:4px solid @StatusColor(t.Status);">
@* EN: Table number badge / VI: Badge số bàn *@
<div style="width:44px;height:44px;border-radius:10px;background:@StatusBg(t.Status);
display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:700;">
@t.Number
</div>
<div style="flex:1;">
<div style="font-size:14px;font-weight:600;">@t.Name</div>
<div style="font-size:12px;color:var(--pos-text-tertiary);margin-top:2px;">
@t.Seats chỗ · @t.Section
@* ═══ TABLE LIST / DANH SÁCH BÀN ═══ *@
<div style="flex:1;overflow-y:auto;padding:8px 12px;display:flex;flex-direction:column;gap:8px;">
@foreach (var t in FilteredTables)
{
<div @onclick="() => OpenTable(t)"
style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:14px 16px;
display:flex;align-items:center;gap:12px;cursor:pointer;
border-left:4px solid @StatusColor(t.Status);">
@* EN: Table number badge / VI: Badge số bàn *@
<div style="width:44px;height:44px;border-radius:10px;background:@StatusBg(t.Status);
display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:700;">
@t.Number
</div>
</div>
<div style="text-align:right;">
<div style="font-size:11px;font-weight:600;color:@StatusColor(t.Status);">
@StatusLabel(t.Status)
</div>
@if (t.Status == "occupied")
{
<div style="font-size:13px;font-weight:700;color:var(--pos-orange-primary);margin-top:2px;">
@FormatPrice(t.Amount)
<div style="flex:1;">
<div style="font-size:14px;font-weight:600;">@t.Name</div>
<div style="font-size:12px;color:var(--pos-text-tertiary);margin-top:2px;">
@t.Seats chỗ · @t.Section
</div>
}
</div>
<div style="text-align:right;">
<div style="font-size:11px;font-weight:600;color:@StatusColor(t.Status);">
@StatusLabel(t.Status)
</div>
@if (t.Status == "occupied")
{
<div style="font-size:13px;font-weight:700;color:var(--pos-orange-primary);margin-top:2px;">
@FormatPrice(t.Amount)
</div>
}
</div>
<i data-lucide="chevron-right" style="color:var(--pos-text-tertiary);width:16px;"></i>
</div>
<i data-lucide="chevron-right" style="color:var(--pos-text-tertiary);width:16px;"></i>
</div>
}
</div>
}
</div>
@* ═══ BOTTOM STATS / THỐNG KÊ DƯỚI ═══ *@
<div style="padding:12px 16px;border-top:1px solid var(--pos-border-subtle);display:flex;justify-content:space-around;
background:var(--pos-bg-elevated);font-size:12px;">
<div style="text-align:center;">
<div style="font-weight:700;font-size:16px;color:var(--pos-success);">@_tables.Count(t => t.Status == "available")</div>
<div style="color:var(--pos-text-tertiary);">Trống</div>
@* ═══ BOTTOM STATS / THỐNG KÊ DƯỚI ═══ *@
<div style="padding:12px 16px;border-top:1px solid var(--pos-border-subtle);display:flex;justify-content:space-around;
background:var(--pos-bg-elevated);font-size:12px;">
<div style="text-align:center;">
<div style="font-weight:700;font-size:16px;color:var(--pos-success);">@_tables.Count(t => t.Status == "available")</div>
<div style="color:var(--pos-text-tertiary);">Trống</div>
</div>
<div style="text-align:center;">
<div style="font-weight:700;font-size:16px;color:var(--pos-orange-primary);">@_tables.Count(t => t.Status == "occupied")</div>
<div style="color:var(--pos-text-tertiary);">Đang phục vụ</div>
</div>
<div style="text-align:center;">
<div style="font-weight:700;font-size:16px;color:var(--pos-info);">@_tables.Count(t => t.Status == "reserved")</div>
<div style="color:var(--pos-text-tertiary);">Đã đặt</div>
</div>
</div>
<div style="text-align:center;">
<div style="font-weight:700;font-size:16px;color:var(--pos-orange-primary);">@_tables.Count(t => t.Status == "occupied")</div>
<div style="color:var(--pos-text-tertiary);">Đang phục vụ</div>
</div>
<div style="text-align:center;">
<div style="font-weight:700;font-size:16px;color:var(--pos-info);">@_tables.Count(t => t.Status == "reserved")</div>
<div style="color:var(--pos-text-tertiary);">Đã đặt</div>
</div>
</div>
}
</div>
@code {
private string _activeSection = "Tất cả";
private readonly string[] _sections = { "Tất cả", "Trong nhà", "Ngoài trời", "VIP" };
// EN: Restaurant shop ID / VI: ID cửa hàng nhà hàng
private static readonly Guid RestaurantShopId = Guid.Parse("b0000002-0000-0000-0000-000000000002");
// EN: Demo tables / VI: Bàn mẫu
private readonly List<MobileTable> _tables = new()
{
new(1, "Bàn 1", 4, "available", "Trong nhà", 0),
new(2, "Bàn 2", 2, "occupied", "Trong nhà", 195_000),
new(3, "Bàn 3", 6, "occupied", "Trong nhà", 420_000),
new(4, "Bàn 4", 4, "reserved", "VIP", 0),
new(5, "Bàn 5", 8, "available", "VIP", 0),
new(6, "Bàn 6", 4, "occupied", "Ngoài trời", 310_000),
new(7, "Bàn 7", 4, "available", "Ngoài trời", 0),
new(8, "Bàn 8", 10, "reserved", "VIP", 0),
new(9, "Bàn 9", 2, "available", "Trong nhà", 0),
new(10, "Bàn 10", 4, "occupied", "Ngoài trời", 175_000),
};
// EN: Loading state / VI: Trạng thái tải
private bool _isLoading = true;
private bool _loadError;
// EN: Active section filter / VI: Bộ lọc khu vực
private string _activeSection = "Tất cả";
private string[] _sections = { "Tất cả" };
// EN: Table data from API / VI: Dữ liệu bàn từ API
private List<MobileTable> _tables = new();
private IEnumerable<MobileTable> FilteredTables =>
_activeSection == "Tất cả" ? _tables : _tables.Where(t => t.Section == _activeSection);
protected override async Task OnInitializedAsync()
{
try
{
var apiTables = await DataService.GetTablesAsync(RestaurantShopId);
_tables = apiTables.Select(t => new MobileTable(
int.TryParse(t.TableNumber, out var num) ? num : 0,
$"Bàn {t.TableNumber}",
t.Capacity,
t.Status ?? "available",
t.Zone ?? "Trong nhà",
0
)).ToList();
var zones = _tables.Select(t => t.Section).Distinct().ToList();
_sections = new[] { "Tất cả" }.Concat(zones).ToArray();
}
catch
{
_loadError = true;
}
finally
{
_isLoading = false;
}
}
private void OpenTable(MobileTable t) => NavigateTo("restaurant/waiter-pad");
private static string StatusColor(string s) => s switch

View File

@@ -5,34 +5,50 @@
@page "/pos/restaurant/tablet"
@layout PosLayout
@inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div class="pos-product-panel" style="padding:16px;overflow-y:auto;">
@* ═══ SECTION FILTER / LỌC KHU VỰC ═══ *@
<div class="pos-category-tabs">
@foreach (var s in _sections)
{
<button class="pos-category-tab @(s == _activeSection ? "pos-category-tab--active" : "")"
style="padding:12px 20px;font-size:15px;"
@onclick="() => _activeSection = s">@s</button>
}
</div>
@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
{
@* ═══ SECTION FILTER / LỌC KHU VỰC ═══ *@
<div class="pos-category-tabs">
@foreach (var s in _sections)
{
<button class="pos-category-tab @(s == _activeSection ? "pos-category-tab--active" : "")"
style="padding:12px 20px;font-size:15px;"
@onclick="() => _activeSection = s">@s</button>
}
</div>
@* ═══ TABLE MAP / SƠ ĐỒ BÀN ═══ *@
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:16px;padding:12px 0;">
@foreach (var t in FilteredTables)
{
<div @onclick="() => _selected = t"
style="background:@StatusBg(t.Status);border-radius:var(--pos-radius);padding:20px;
text-align:center;cursor:pointer;min-height:100px;display:flex;flex-direction:column;
align-items:center;justify-content:center;gap:6px;
border:3px solid @(_selected?.Id == t.Id ? "var(--pos-orange-primary)" : "transparent");
transition:all .2s ease;">
<span style="font-size:22px;font-weight:700;">@t.Name</span>
<span style="font-size:13px;color:rgba(255,255,255,.65);">@t.Seats chỗ</span>
<span style="font-size:12px;font-weight:600;margin-top:4px;">@StatusLabel(t.Status)</span>
</div>
}
</div>
@* ═══ TABLE MAP / SƠ ĐỒ BÀN ═══ *@
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:16px;padding:12px 0;">
@foreach (var t in FilteredTables)
{
<div @onclick="() => _selected = t"
style="background:@StatusBg(t.Status);border-radius:var(--pos-radius);padding:20px;
text-align:center;cursor:pointer;min-height:100px;display:flex;flex-direction:column;
align-items:center;justify-content:center;gap:6px;
border:3px solid @(_selected?.Id == t.Id ? "var(--pos-orange-primary)" : "transparent");
transition:all .2s ease;">
<span style="font-size:22px;font-weight:700;">@t.Name</span>
<span style="font-size:13px;color:rgba(255,255,255,.65);">@t.Seats chỗ</span>
<span style="font-size:12px;font-weight:600;margin-top:4px;">@StatusLabel(t.Status)</span>
</div>
}
</div>
}
</div>
@* ═══ ORDER SIDEBAR / SIDEBAR ĐẶT MÓN ═══ *@
@@ -85,28 +101,60 @@
</div>
@code {
// EN: Restaurant shop ID / VI: ID cửa hàng nhà hàng
private static readonly Guid RestaurantShopId = Guid.Parse("b0000002-0000-0000-0000-000000000002");
// EN: Loading state / VI: Trạng thái tải
private bool _isLoading = true;
private bool _loadError;
// EN: Active section filter / VI: Bộ lọc khu vực
private string _activeSection = "Tất cả";
private readonly string[] _sections = { "Tất cả", "Trong nhà", "Ngoài trời", "VIP" };
private string[] _sections = { "Tất cả" };
// EN: Selected table reference / VI: Bàn đang chọn
private TableInfo? _selected;
private readonly List<TableInfo> _tables = new()
{
new("T01","Bàn 1", 4, "available", "Trong nhà"), new("T02","Bàn 2", 2, "occupied", "Trong nhà"),
new("T03","Bàn 3", 6, "occupied", "Trong nhà"), new("T04","Bàn 4", 4, "reserved", "VIP"),
new("T05","Bàn 5", 8, "available", "VIP"), new("T06","Bàn 6", 4, "available", "Ngoài trời"),
new("T07","Bàn 7", 4, "occupied", "Ngoài trời"), new("T08","Bàn 8", 2, "available", "Ngoài trời"),
new("T09","Bàn 9", 10, "reserved", "VIP"), new("T10","Bàn 10", 6, "occupied", "Trong nhà"),
};
// EN: Table data from API / VI: Dữ liệu bàn từ API
private List<TableInfo> _tables = new();
private IEnumerable<TableInfo> FilteredTables =>
_activeSection == "Tất cả" ? _tables : _tables.Where(t => t.Section == _activeSection);
// EN: Demo order items for occupied tables / VI: Món mẫu cho bàn đang phục vụ
private readonly List<OrderItem> _items = new()
{
new("Bún bò Huế", 80_000, 1), new("Nem rán", 50_000, 2),
new("Cá kho tộ", 120_000, 1), new("Nước mía", 15_000, 2),
};
protected override async Task OnInitializedAsync()
{
try
{
var apiTables = await DataService.GetTablesAsync(RestaurantShopId);
_tables = apiTables.Select(t => new TableInfo(
t.Id.ToString(),
$"Bàn {t.TableNumber}",
t.Capacity,
t.Status ?? "available",
t.Zone ?? "Trong nhà"
)).ToList();
var zones = _tables.Select(t => t.Section).Distinct().ToList();
_sections = new[] { "Tất cả" }.Concat(zones).ToArray();
}
catch
{
_loadError = true;
}
finally
{
_isLoading = false;
}
}
private static string StatusBg(string s) => s switch
{
"available" => "rgba(34,197,94,.15)", "occupied" => "rgba(255,92,0,.18)",

View File

@@ -5,6 +5,7 @@
@page "/pos/restaurant/kitchen-display"
@layout PosLayout
@inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
@* ═══ HEADER / TIÊU ĐỀ ═══ *@
@@ -23,6 +24,20 @@
</div>
@* ═══ TICKET COLUMNS / CỘT PHIẾU ═══ *@
@if (_isLoading)
{
<div style="flex:1;display:flex;align-items:center;justify-content:center;color:var(--pos-text-tertiary);">
Đang tải...
</div>
}
else if (_loadError)
{
<div style="flex:1;display:flex;align-items:center;justify-content:center;color:var(--pos-text-tertiary);">
Không thể tải dữ liệu
</div>
}
else
{
<div style="flex:1;display:flex;gap:16px;padding:16px;overflow-x:auto;">
@foreach (var status in _statuses)
{
@@ -91,15 +106,23 @@
</div>
}
</div>
}
</div>
@code {
// EN: Restaurant shop ID / VI: ID cửa hàng nhà hàng
private static readonly Guid RestaurantShopId = Guid.Parse("b0000002-0000-0000-0000-000000000002");
// EN: Loading state / VI: Trạng thái tải
private bool _isLoading = true;
private bool _loadError;
private readonly Dictionary<string, string> _statuses = new()
{
["new"] = "Mới", ["cooking"] = "Đang nấu", ["ready"] = "Sẵn sàng"
};
// EN: Demo kitchen tickets / VI: Phiếu bếp mẫu
// EN: Demo kitchen tickets — needs kitchen_tickets API / VI: Phiếu bếp mẫu — cần API kitchen_tickets
private readonly List<KitchenTicket> _tickets = new()
{
new("Bàn 2", "new", 2, new() { new(2, "Gỏi cuốn", ""), new(1, "Phở bò tái", "Ít hành") }),
@@ -110,6 +133,24 @@
new("Bàn 10", "ready", 8, new() { new(2, "Cơm tấm sườn", ""), new(1, "Cà phê sữa", "") }),
};
protected override async Task OnInitializedAsync()
{
try
{
// EN: Preload tables for reference — kitchen_tickets API not yet available
// VI: Tải trước bàn để tham chiếu — API kitchen_tickets chưa có
await DataService.GetTablesAsync(RestaurantShopId);
}
catch
{
_loadError = true;
}
finally
{
_isLoading = false;
}
}
private static string ColumnBg(string s) => s switch
{
"new" => "rgba(239,68,68,.15)", "cooking" => "rgba(245,158,11,.15)",

View File

@@ -5,6 +5,7 @@
@page "/pos/restaurant/order-history"
@layout PosLayout
@inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
@* ═══ HEADER / TIÊU ĐỀ ═══ *@
@@ -36,6 +37,20 @@
@* ═══ ORDER LIST / DANH SÁCH ĐƠN ═══ *@
<div style="flex:1;overflow-y:auto;padding:8px 16px;">
@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
{
@foreach (var order in FilteredOrders)
{
<div @onclick="() => _expandedId = _expandedId == order.Id ? null : order.Id"
@@ -82,6 +97,7 @@
}
</div>
}
}
</div>
@* ═══ FOOTER SUMMARY / TỔNG KẾT ═══ *@
@@ -93,12 +109,19 @@
</div>
@code {
// EN: Restaurant shop ID / VI: ID cửa hàng nhà hàng
private static readonly Guid RestaurantShopId = Guid.Parse("b0000002-0000-0000-0000-000000000002");
// EN: Loading state / VI: Trạng thái tải
private bool _isLoading = true;
private bool _loadError;
private string _searchQuery = string.Empty;
private string _activeFilter = "Tất cả";
private string? _expandedId;
private readonly string[] _filters = { "Tất cả", "Tiền mặt", "Thẻ", "Chuyển khoản" };
// EN: Demo order history / VI: Lịch sử đơn mẫu
// EN: Demo order history — needs orders API / VI: Lịch sử đơn mẫu — cần API orders
private readonly List<HistoryOrder> _orders = new()
{
new("DH001", "B3", "10:15", "Nguyễn Văn A", 3, 285_000, "Tiền mặt",
@@ -119,6 +142,24 @@
new() { new("Bún bò Huế", 80_000, 2), new("Gỏi cuốn", 45_000, 2), new("Cà phê sữa", 29_000, 2), new("Bánh flan", 25_000, 2) }),
};
protected override async Task OnInitializedAsync()
{
try
{
// EN: Preload tables for reference — orders API not yet available
// VI: Tải trước bàn để tham chiếu — API orders chưa có
await DataService.GetTablesAsync(RestaurantShopId);
}
catch
{
_loadError = true;
}
finally
{
_isLoading = false;
}
}
private IEnumerable<HistoryOrder> FilteredOrders
{
get

View File

@@ -5,6 +5,7 @@
@page "/pos/restaurant/menu-management"
@layout PosLayout
@inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
@* ═══ HEADER / TIÊU ĐỀ ═══ *@
@@ -53,6 +54,20 @@
@* ═══ MENU ITEM GRID / LƯỚI MÓN ═══ *@
<div style="flex:1;overflow-y:auto;padding:16px;">
@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
{
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px;">
@foreach (var item in FilteredMenu)
{
@@ -92,6 +107,7 @@
</div>
}
</div>
}
</div>
@* ═══ STATS BAR / THANH THỐNG KÊ ═══ *@
@@ -104,30 +120,52 @@
</div>
@code {
private string _activeCategory = "Khai vị";
// EN: Restaurant shop ID / VI: ID cửa hàng nhà hàng
private static readonly Guid RestaurantShopId = Guid.Parse("b0000002-0000-0000-0000-000000000002");
// EN: Loading state / VI: Trạng thái tải
private bool _isLoading = true;
private bool _loadError;
private string _activeCategory = "Tất cả";
private bool _editMode = false;
private string? _selectedItem;
private readonly string[] _categories = { "Khai vị", "Món chính", "Lẩu", "Nước", "Tráng miệng" };
private string[] _categories = { "Tất cả" };
// EN: Menu items / VI: Các món trong thực đơn
private readonly List<RestMenuItem> _menuItems = new()
{
new("Gỏi cuốn tôm", 45_000, "Khai vị", "salad", true, ""),
new("Chả giò chiên", 40_000, "Khai vị", "flame", true, ""),
new("Súp cua", 55_000, "Khai vị", "soup", true, "Đặc biệt"),
new("Phở bò tái", 75_000, "Món chính", "beef", true, ""),
new("Cơm tấm sườn", 65_000, "Món chính", "utensils", true, ""),
new("Cá kho tộ", 120_000, "Món chính", "fish", false, "Hết nguyên liệu"),
new("Gà nướng mật ong", 180_000, "Món chính", "drumstick", true, ""),
new("Lẩu thái", 250_000, "Lẩu", "soup", true, "Best seller"),
new("Lẩu nấm", 200_000, "Lẩu", "leaf", true, ""),
new("Trà đá", 10_000, "Nước", "glass-water", true, ""),
new("Cà phê sữa", 29_000, "Nước", "coffee", true, ""),
new("Chè thái", 30_000, "Tráng miệng", "ice-cream-cone", true, ""),
};
// EN: Menu items from API / VI: Các món từ API
private List<RestMenuItem> _menuItems = new();
private IEnumerable<RestMenuItem> FilteredMenu =>
_menuItems.Where(m => m.Category == _activeCategory);
_activeCategory == "Tất cả" ? _menuItems : _menuItems.Where(m => m.Category == _activeCategory);
protected override async Task OnInitializedAsync()
{
try
{
var products = await DataService.GetProductsAsync(RestaurantShopId);
_menuItems = products.Select(p => new RestMenuItem(
p.Name,
p.Price,
p.Category ?? "Khác",
"utensils",
true,
""
)).ToList();
var cats = _menuItems.Select(m => m.Category).Distinct().ToList();
_categories = new[] { "Tất cả" }.Concat(cats).ToArray();
_activeCategory = _categories.First();
}
catch
{
_loadError = true;
}
finally
{
_isLoading = false;
}
}
private void MarkSoldOut()
{

View File

@@ -5,6 +5,7 @@
@page "/pos/restaurant/table-map"
@layout PosLayout
@inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
@* ═══ TOOLBAR / THANH CÔNG CỤ ═══ *@
@@ -21,6 +22,20 @@
</div>
@* ═══ SECTION TABS / TAB KHU VỰC ═══ *@
@if (_isLoading)
{
<div style="flex:1;display:flex;align-items:center;justify-content:center;color:var(--pos-text-tertiary);">
Đang tải...
</div>
}
else if (_loadError)
{
<div style="flex:1;display:flex;align-items:center;justify-content:center;color:var(--pos-text-tertiary);">
Không thể tải dữ liệu
</div>
}
else
{
<div class="pos-category-tabs">
@foreach (var s in _sections)
{
@@ -57,6 +72,8 @@
</div>
</div>
}
@* ═══ FOOTER STATS / THỐNG KÊ ═══ *@
<div style="padding:10px 16px;border-top:1px solid var(--pos-border-subtle);display:flex;gap:20px;font-size:12px;color:var(--pos-text-secondary);">
<span>Tổng: <b>@_tables.Count</b> bàn</span>
@@ -71,23 +88,51 @@
</div>
@code {
// EN: Restaurant shop ID / VI: ID cửa hàng nhà hàng
private static readonly Guid RestaurantShopId = Guid.Parse("b0000002-0000-0000-0000-000000000002");
// EN: Loading state / VI: Trạng thái tải
private bool _isLoading = true;
private bool _loadError;
private string _activeSection = "Tất cả";
private readonly string[] _sections = { "Tất cả", "Trong nhà", "Ngoài trời", "VIP" };
private string[] _sections = { "Tất cả" };
private readonly HashSet<string> _selectedIds = new();
private readonly List<MapTable> _tables = new()
{
new("T01","Bàn 1", 4, "available", "Trong nhà", "Vuông"), new("T02","Bàn 2", 2, "occupied", "Trong nhà", "Tròn"),
new("T03","Bàn 3", 6, "occupied", "Trong nhà", "Chữ nhật"),new("T04","Bàn 4", 4, "reserved", "VIP", "Tròn"),
new("T05","Bàn 5", 8, "available", "VIP", "Chữ nhật"), new("T06","Bàn 6", 4, "available", "Ngoài trời", "Vuông"),
new("T07","Bàn 7", 4, "occupied", "Ngoài trời", "Vuông"), new("T08","Bàn 8", 2, "available", "Ngoài trời", "Tròn"),
new("T09","Bàn 9", 10, "reserved", "VIP", "Chữ nhật"), new("T10","Bàn 10", 6, "occupied", "Trong nhà", "Vuông"),
new("T11","Bàn 11", 4, "available", "Trong nhà", "Tròn"), new("T12","Bàn 12", 2, "available", "Ngoài trời", "Tròn"),
};
// EN: Table data from API / VI: Dữ liệu bàn từ API
private List<MapTable> _tables = new();
private IEnumerable<MapTable> FilteredTables =>
_activeSection == "Tất cả" ? _tables : _tables.Where(t => t.Section == _activeSection);
protected override async Task OnInitializedAsync()
{
try
{
var apiTables = await DataService.GetTablesAsync(RestaurantShopId);
_tables = apiTables.Select(t => new MapTable(
t.Id.ToString(),
$"Bàn {t.TableNumber}",
t.Capacity,
t.Status ?? "available",
t.Zone ?? "Trong nhà",
t.Capacity <= 2 ? "Tròn" : t.Capacity <= 6 ? "Vuông" : "Chữ nhật"
)).ToList();
var zones = _tables.Select(t => t.Section).Distinct().ToList();
_sections = new[] { "Tất cả" }.Concat(zones).ToArray();
}
catch
{
_loadError = true;
}
finally
{
_isLoading = false;
}
}
private void ToggleSelect(MapTable t)
{
if (!_selectedIds.Add(t.Id)) _selectedIds.Remove(t.Id);

View File

@@ -5,6 +5,7 @@
@page "/pos/restaurant/table-merge-split"
@layout PosLayout
@inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
@* ═══ HEADER / TIÊU ĐỀ ═══ *@
@@ -30,7 +31,19 @@
</div>
<div style="flex:1;overflow-y:auto;padding:16px;">
@if (_mode == "merge")
@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 if (_mode == "merge")
{
@* ═══ MERGE MODE / CHẾ ĐỘ GHÉP ═══ *@
<div style="margin-bottom:20px;">
@@ -158,28 +171,53 @@
</div>
@code {
// EN: Restaurant shop ID / VI: ID cửa hàng nhà hàng
private static readonly Guid RestaurantShopId = Guid.Parse("b0000002-0000-0000-0000-000000000002");
// EN: Loading state / VI: Trạng thái tải
private bool _isLoading = true;
private bool _loadError;
private string _mode = "merge";
private readonly HashSet<string> _mergeSelected = new();
private string? _splitSelected;
// EN: Tables for merge / VI: Bàn để ghép
private readonly List<MergeTable> _mergeTables = new()
{
new("T03", "Bàn 3", 4, "available"),
new("T04", "Bàn 4", 6, "available"),
new("T05", "Bàn 5", 4, "occupied"),
new("T06", "Bàn 6", 4, "available"),
new("T08", "Bàn 8", 2, "available"),
new("T11", "Bàn 11", 4, "available"),
};
// EN: Tables from API / VI: Bàn từ API
private List<MergeTable> _mergeTables = new();
private List<SplitTable> _splitTables = new();
// EN: Tables for split / VI: Bàn để tách
private readonly List<SplitTable> _splitTables = new()
protected override async Task OnInitializedAsync()
{
new("T07", "Bàn 7", 6, 5),
new("T03", "Bàn 3", 4, 3),
new("T10", "Bàn 10", 8, 7),
};
try
{
var apiTables = await DataService.GetTablesAsync(RestaurantShopId);
_mergeTables = apiTables
.Select(t => new MergeTable(
t.Id.ToString(),
$"Bàn {t.TableNumber}",
t.Capacity,
t.Status ?? "available"
)).ToList();
_splitTables = apiTables
.Where(t => (t.Status ?? "available") == "occupied")
.Select(t => new SplitTable(
t.Id.ToString(),
$"Bàn {t.TableNumber}",
t.Capacity,
0
)).ToList();
}
catch
{
_loadError = true;
}
finally
{
_isLoading = false;
}
}
private void SwitchMode(string mode)
{

View File

@@ -5,6 +5,7 @@
@page "/pos/restaurant/table-select"
@layout PosLayout
@inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
@* ═══ HEADER / TIÊU ĐỀ ═══ *@
@@ -46,6 +47,20 @@
@* ═══ TABLE GRID / LƯỚI BÀN ═══ *@
<div style="flex:1;overflow-y:auto;padding:16px;">
@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
{
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px;">
@foreach (var table in FilteredTables)
{
@@ -82,6 +97,7 @@
<div style="font-size:14px;">Không tìm thấy bàn trống phù hợp</div>
</div>
}
}
</div>
@* ═══ SELECTED TABLE INFO / THÔNG TIN BÀN ĐÃ CHỌN ═══ *@
@@ -117,28 +133,52 @@
</div>
@code {
// EN: Restaurant shop ID / VI: ID cửa hàng nhà hàng
private static readonly Guid RestaurantShopId = Guid.Parse("b0000002-0000-0000-0000-000000000002");
// EN: Loading state / VI: Trạng thái tải
private bool _isLoading = true;
private bool _loadError;
private int _guestCount = 4;
private string _activeSection = "Tất cả";
private string? _selectedTable;
private readonly string[] _sections = { "Tất cả", "Tầng 1", "Tầng 2", "Sân vườn", "VIP" };
private string[] _sections = { "Tất cả" };
// EN: Available tables / VI: Bàn trống
private readonly List<AvailableTable> _availableTables = new()
{
new("T01", "Bàn 1", 4, "Tầng 1"),
new("T05", "Bàn 5", 8, "VIP"),
new("T06", "Bàn 6", 4, "Sân vườn"),
new("T08", "Bàn 8", 2, "Sân vườn"),
new("T11", "Bàn 11", 4, "Tầng 1"),
new("T12", "Bàn 12", 2, "Tầng 2"),
new("T13", "Bàn 13", 6, "VIP"),
new("T14", "Bàn 14", 10, "Tầng 2"),
new("T15", "Bàn 15", 4, "Sân vườn"),
};
// EN: Available tables from API / VI: Bàn trống từ API
private List<AvailableTable> _availableTables = new();
private IEnumerable<AvailableTable> FilteredTables =>
_activeSection == "Tất cả" ? _availableTables : _availableTables.Where(t => t.Section == _activeSection);
protected override async Task OnInitializedAsync()
{
try
{
var apiTables = await DataService.GetTablesAsync(RestaurantShopId);
_availableTables = apiTables
.Where(t => (t.Status ?? "available") == "available")
.Select(t => new AvailableTable(
t.Id.ToString(),
$"Bàn {t.TableNumber}",
t.Capacity,
t.Zone ?? "Tầng 1"
)).ToList();
var zones = _availableTables.Select(t => t.Section).Distinct().ToList();
_sections = new[] { "Tất cả" }.Concat(zones).ToArray();
}
catch
{
_loadError = true;
}
finally
{
_isLoading = false;
}
}
private static string CapacityColor(int seats, int guests) =>
seats >= guests + 2 ? "var(--pos-success)"
: seats >= guests ? "#F59E0B"

View File

@@ -5,6 +5,7 @@
@page "/pos/restaurant/waiter-pad"
@layout PosLayout
@inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div class="pos-product-panel" style="display:flex;flex-direction:column;overflow:hidden;">
@* ═══ HEADER / TIÊU ĐỀ ═══ *@
@@ -16,28 +17,43 @@
<span style="font-size:12px;color:var(--pos-text-tertiary);">PV: Nguyễn Văn A</span>
</div>
@* ═══ COURSE TABS / TAB MÓN THEO COURSE ═══ *@
<div class="pos-category-tabs">
@foreach (var c in _courses)
{
<button class="pos-category-tab @(c == _activeCourse ? "pos-category-tab--active" : "")"
@onclick="() => _activeCourse = c">@c</button>
}
</div>
@if (_isLoading)
{
<div style="flex:1;display:flex;align-items:center;justify-content:center;color:var(--pos-text-tertiary);">
Đang tải...
</div>
}
else if (_loadError)
{
<div style="flex:1;display:flex;align-items:center;justify-content:center;color:var(--pos-text-tertiary);">
Không thể tải dữ liệu
</div>
}
else
{
@* ═══ COURSE TABS / TAB MÓN THEO COURSE ═══ *@
<div class="pos-category-tabs">
@foreach (var c in _courses)
{
<button class="pos-category-tab @(c == _activeCourse ? "pos-category-tab--active" : "")"
@onclick="() => _activeCourse = c">@c</button>
}
</div>
@* ═══ MENU ITEMS / DANH SÁCH MÓN ═══ *@
<div class="pos-product-grid" style="grid-template-columns:repeat(auto-fill,minmax(150px,1fr));">
@foreach (var item in FilteredMenu)
{
<div class="pos-product-card" @onclick="() => AddToOrder(item)">
<div class="pos-product-card__image" style="display:flex;align-items:center;justify-content:center;">
<i data-lucide="@item.Icon" style="width:28px;color:var(--pos-text-tertiary);"></i>
@* ═══ MENU ITEMS / DANH SÁCH MÓN ═══ *@
<div class="pos-product-grid" style="grid-template-columns:repeat(auto-fill,minmax(150px,1fr));">
@foreach (var item in FilteredMenu)
{
<div class="pos-product-card" @onclick="() => AddToOrder(item)">
<div class="pos-product-card__image" style="display:flex;align-items:center;justify-content:center;">
<i data-lucide="@item.Icon" style="width:28px;color:var(--pos-text-tertiary);"></i>
</div>
<span class="pos-product-card__name">@item.Name</span>
<span class="pos-product-card__price">@FormatPrice(item.Price)</span>
</div>
<span class="pos-product-card__name">@item.Name</span>
<span class="pos-product-card__price">@FormatPrice(item.Price)</span>
</div>
}
</div>
}
</div>
}
</div>
@* ═══ ORDER PANEL / PANEL ĐƠN GỌI ═══ *@
@@ -88,25 +104,52 @@
</div>
@code {
private string _activeCourse = "Khai vị";
// EN: Restaurant shop ID / VI: ID cửa hàng nhà hàng
private static readonly Guid RestaurantShopId = Guid.Parse("b0000002-0000-0000-0000-000000000002");
// EN: Loading state / VI: Trạng thái tải
private bool _isLoading = true;
private bool _loadError;
private string _activeCourse = "Tất cả";
private string _specialRequest = string.Empty;
private readonly string[] _courses = { "Khai vị", "Món chính", "Tráng miệng", "Đồ uống" };
private string[] _courses = { "Tất cả" };
// EN: Menu items by course / VI: Thực đơn theo course
private readonly List<MenuItem> _menu = new()
// EN: Menu items from API / VI: Thực đơn từ API
private List<MenuItem> _menu = new();
private IEnumerable<MenuItem> FilteredMenu =>
_activeCourse == "Tất cả" ? _menu : _menu.Where(m => m.Course == _activeCourse);
protected override async Task OnInitializedAsync()
{
new("Gỏi cuốn", 45_000, "Khai vị", "salad"), new("Chả giò", 40_000, "Khai vị", "flame"),
new("Súp cua", 55_000, "Khai vị", "soup"), new("Nộm bò bóp thấu", 65_000, "Khai vị", "leaf"),
new("Phở bò tái", 75_000, "Món chính", "beef"), new("Cơm tấm sườn", 65_000, "Món chính", "utensils"),
new("Bún bò Huế", 80_000, "Món chính", "flame"), new("Cá kho tộ", 120_000, "Món chính", "fish"),
new("Lẩu thái", 250_000, "Món chính", "soup"), new("Gà nướng mật ong", 180_000, "Món chính", "drumstick"),
new("Chè thái", 30_000, "Tráng miệng", "ice-cream-cone"), new("Bánh flan", 25_000, "Tráng miệng", "cake"),
new("Trái cây dĩa", 45_000, "Tráng miệng", "apple"), new("Trà đá", 10_000, "Đồ uống", "glass-water"),
new("Cà phê sữa", 29_000, "Đồ uống", "coffee"), new("Nước mía", 15_000, "Đồ uống", "cup-soda"),
new("Bia Sài Gòn", 25_000, "Đồ uống", "beer"),
};
try
{
var products = await DataService.GetProductsAsync(RestaurantShopId);
var categories = await DataService.GetCategoriesAsync(RestaurantShopId);
private IEnumerable<MenuItem> FilteredMenu => _menu.Where(m => m.Course == _activeCourse);
var categoryMap = categories.ToDictionary(c => c.Id, c => c.Name);
_menu = products.Select(p => new MenuItem(
p.Name,
p.Price,
p.Category ?? categoryMap.GetValueOrDefault(Guid.Empty, "Khác"),
"utensils"
)).ToList();
var courseNames = _menu.Select(m => m.Course).Distinct().ToList();
_courses = new[] { "Tất cả" }.Concat(courseNames).ToArray();
_activeCourse = _courses.First();
}
catch
{
_loadError = true;
}
finally
{
_isLoading = false;
}
}
// EN: Current order / VI: Đơn gọi hiện tại
private readonly List<OrderLine> _orderItems = new()

View File

@@ -5,6 +5,7 @@
@page "/pos/retail"
@layout PosLayout
@inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
@* ═══ PRODUCT PANEL ═══ *@
<div class="pos-product-panel">
@@ -20,34 +21,48 @@
</div>
</div>
@* EN: Category tabs / VI: Tab danh mục *@
<div class="pos-category-tabs">
@foreach (var cat in _categories)
{
<button class="pos-category-tab @(cat == _selectedCategory ? "pos-category-tab--active" : "")"
@onclick="() => _selectedCategory = cat">
@cat
</button>
}
</div>
@if (_isLoading)
{
<div style="display:flex;align-items:center;justify-content:center;flex:1;color:var(--pos-text-tertiary);">
Đang tải...
</div>
}
else if (_loadError)
{
<div style="display:flex;align-items:center;justify-content:center;flex:1;color:var(--pos-text-tertiary);">
Không thể tải dữ liệu
</div>
}
else
{
@* EN: Category tabs / VI: Tab danh mục *@
<div class="pos-category-tabs">
@foreach (var cat in _categories)
{
<button class="pos-category-tab @(cat == _selectedCategory ? "pos-category-tab--active" : "")"
@onclick="() => _selectedCategory = cat">
@cat
</button>
}
</div>
@* EN: Product grid / VI: Lưới sản phẩm *@
<div class="pos-product-grid">
@foreach (var product in FilteredProducts)
{
<div class="pos-product-card" @onclick="() => AddToCart(product)">
<div class="pos-product-card__image" style="display:flex;align-items:center;justify-content:center;">
<i data-lucide="@product.Icon" style="width:32px;height:32px;color:var(--pos-text-tertiary);"></i>
@* EN: Product grid / VI: Lưới sản phẩm *@
<div class="pos-product-grid">
@foreach (var product in FilteredProducts)
{
<div class="pos-product-card" @onclick="() => AddToCart(product)">
<div class="pos-product-card__image" style="display:flex;align-items:center;justify-content:center;">
<i data-lucide="@product.Icon" style="width:32px;height:32px;color:var(--pos-text-tertiary);"></i>
</div>
<span class="pos-product-card__name">@product.Name</span>
<div style="display:flex;justify-content:space-between;align-items:center;width:100%;padding:0 4px;">
<span class="pos-product-card__price">@FormatPrice(product.Price)</span>
<span style="font-size:10px;color:var(--pos-text-tertiary);">@product.Sku</span>
</div>
</div>
<span class="pos-product-card__name">@product.Name</span>
<div style="display:flex;justify-content:space-between;align-items:center;width:100%;padding:0 4px;">
<span class="pos-product-card__price">@FormatPrice(product.Price)</span>
<span style="font-size:10px;color:var(--pos-text-tertiary);">Kho: @product.Stock</span>
</div>
<span style="font-size:10px;color:var(--pos-text-tertiary);">@product.Sku</span>
</div>
}
</div>
}
</div>
}
</div>
@* ═══ CART PANEL ═══ *@
@@ -99,30 +114,20 @@
</div>
@code {
// EN: Retail shop ID (using cafe shop for now) / VI: ID cửa hàng bán lẻ (dùng tạm shop cafe)
private static readonly Guid RetailShopId = Guid.Parse("b0000001-0000-0000-0000-000000000001");
// EN: Loading state / VI: Trạng thái tải
private bool _isLoading = true;
private bool _loadError;
// EN: Categories / VI: Danh mục
private readonly string[] _categories = { "Tất cả", "Thời trang", "Phụ kiện", "Điện tử", "Gia dụng", "Mỹ phẩm" };
private string[] _categories = { "Tất cả" };
private string _selectedCategory = "Tất cả";
private string _barcodeInput = "";
// EN: Retail product list / VI: Danh sách sản phẩm bán lẻ
private readonly List<Product> _products = new()
{
new("Áo thun nam basic", "SKU-TT001", 199_000, 45, "Thời trang", "shirt"),
new("Quần jean nữ slim", "SKU-TT002", 450_000, 22, "Thời trang", "shirt"),
new("Áo khoác gió unisex", "SKU-TT003", 350_000, 18, "Thời trang", "shirt"),
new("Váy liền công sở", "SKU-TT004", 520_000, 12, "Thời trang", "shirt"),
new("Túi xách da nữ", "SKU-PK001", 890_000, 8, "Phụ kiện", "shopping-bag"),
new("Ví da nam", "SKU-PK002", 350_000, 30, "Phụ kiện", "wallet"),
new("Kính mát thời trang", "SKU-PK003", 280_000, 25, "Phụ kiện", "glasses"),
new("Tai nghe Bluetooth", "SKU-DT001", 650_000, 15, "Điện tử", "headphones"),
new("Sạc dự phòng 10000mAh", "SKU-DT002", 380_000, 40, "Điện tử", "battery-charging"),
new("Chuột không dây", "SKU-DT003", 250_000, 35, "Điện tử", "mouse"),
new("Nồi cơm điện 1.8L", "SKU-GD001", 890_000, 10, "Gia dụng", "cooking-pot"),
new("Bình giữ nhiệt 500ml", "SKU-GD002", 180_000, 50, "Gia dụng", "cup-soda"),
new("Son môi cao cấp", "SKU-MP001", 320_000, 28, "Mỹ phẩm", "sparkles"),
new("Kem chống nắng SPF50", "SKU-MP002", 280_000, 35, "Mỹ phẩm", "sun"),
new("Nước hoa mini 30ml", "SKU-MP003", 450_000, 20, "Mỹ phẩm", "droplets"),
};
// EN: Product list from API / VI: Danh sách sản phẩm từ API
private List<Product> _products = new();
// EN: Cart items / VI: Mục giỏ hàng
private readonly List<CartItem> _cartItems = new();
@@ -130,6 +135,33 @@
_selectedCategory == "Tất cả" ? _products : _products.Where(p => p.Category == _selectedCategory);
private decimal CartTotal => _cartItems.Sum(i => i.Price * i.Qty);
protected override async Task OnInitializedAsync()
{
try
{
var apiProducts = await DataService.GetProductsAsync(RetailShopId);
_products = apiProducts.Select(p => new Product(
p.Name,
p.Sku ?? "",
p.Price,
p.Category ?? "Khác",
GetCategoryIcon(p.Category ?? "Khác")
)).ToList();
var cats = _products.Select(p => p.Category).Distinct().ToList();
_categories = new[] { "Tất cả" }.Concat(cats).ToArray();
}
catch
{
_loadError = true;
}
finally
{
_isLoading = false;
}
}
private void AddToCart(Product product)
{
var existing = _cartItems.FirstOrDefault(i => i.Sku == product.Sku);
@@ -152,8 +184,14 @@
private void Checkout() { }
private static string GetCategoryIcon(string category) => category switch
{
"Thời trang" => "shirt", "Phụ kiện" => "shopping-bag", "Điện tử" => "headphones",
"Gia dụng" => "cooking-pot", "Mỹ phẩm" => "sparkles", _ => "package"
};
// EN: Models / VI: Mô hình dữ liệu
private record Product(string Name, string Sku, decimal Price, int Stock, string Category, string Icon);
private record Product(string Name, string Sku, decimal Price, string Category, string Icon);
private class CartItem(string name, string sku, decimal price)
{
public string Name { get; set; } = name;

View File

@@ -5,6 +5,7 @@
@page "/pos/retail/mobile"
@layout PosLayout
@inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div style="display:flex;flex-direction:column;width:100%;height:100%;overflow:hidden;">
@* EN: Barcode input / VI: Ô nhập mã vạch *@
@@ -17,6 +18,20 @@
</div>
</div>
@if (_isLoading)
{
<div style="display:flex;align-items:center;justify-content:center;flex:1;color:var(--pos-text-tertiary);">
Đang tải...
</div>
}
else if (_loadError)
{
<div style="display:flex;align-items:center;justify-content:center;flex:1;color:var(--pos-text-tertiary);">
Không thể tải dữ liệu
</div>
}
else
{
@* EN: Category tabs / VI: Tab danh mục *@
<div class="pos-category-tabs" style="padding:8px 12px;">
@foreach (var cat in _categories)
@@ -39,10 +54,10 @@
</div>
<span class="pos-product-card__name" style="font-size:12px;">@product.Name</span>
<span class="pos-product-card__price" style="font-size:13px;">@FormatPrice(product.Price)</span>
<span style="font-size:10px;color:var(--pos-text-tertiary);">Kho: @product.Stock</span>
</div>
}
</div>
}
@* EN: Floating cart button / VI: Nút giỏ hàng nổi *@
@if (_cartItems.Any())
@@ -103,30 +118,54 @@
</div>
@code {
private readonly string[] _categories = { "Tất cả", "Thời trang", "Phụ kiện", "Điện tử", "Gia dụng", "Mỹ phẩm" };
// EN: Retail shop ID (using cafe shop for now) / VI: ID cửa hàng bán lẻ (dùng tạm shop cafe)
private static readonly Guid RetailShopId = Guid.Parse("b0000001-0000-0000-0000-000000000001");
// EN: Loading state / VI: Trạng thái tải
private bool _isLoading = true;
private bool _loadError;
// EN: Categories / VI: Danh mục
private string[] _categories = { "Tất cả" };
private string _selectedCategory = "Tất cả";
private string _barcodeInput = "";
private bool _showCart;
private readonly List<Product> _products = new()
{
new("Áo thun nam basic", "SKU-TT001", 199_000, 45, "Thời trang", "shirt"),
new("Quần jean nữ slim", "SKU-TT002", 450_000, 22, "Thời trang", "shirt"),
new("Áo khoác gió unisex", "SKU-TT003", 350_000, 18, "Thời trang", "shirt"),
new("Túi xách da nữ", "SKU-PK001", 890_000, 8, "Phụ kiện", "shopping-bag"),
new("Ví da nam", "SKU-PK002", 350_000, 30, "Phụ kiện", "wallet"),
new("Tai nghe Bluetooth", "SKU-DT001", 650_000, 15, "Điện tử", "headphones"),
new("Sạc dự phòng 10000mAh", "SKU-DT002", 380_000, 40, "Điện tử", "battery-charging"),
new("Nồi cơm điện 1.8L", "SKU-GD001", 890_000, 10, "Gia dụng", "cooking-pot"),
new("Son môi cao cấp", "SKU-MP001", 320_000, 28, "Mỹ phẩm", "sparkles"),
new("Kem chống nắng SPF50", "SKU-MP002", 280_000, 35, "Mỹ phẩm", "sun"),
};
// EN: Product list from API / VI: Danh sách sản phẩm từ API
private List<Product> _products = new();
private readonly List<CartItem> _cartItems = new();
private IEnumerable<Product> FilteredProducts =>
_selectedCategory == "Tất cả" ? _products : _products.Where(p => p.Category == _selectedCategory);
private decimal CartTotal => _cartItems.Sum(i => i.Price * i.Qty);
protected override async Task OnInitializedAsync()
{
try
{
var apiProducts = await DataService.GetProductsAsync(RetailShopId);
_products = apiProducts.Select(p => new Product(
p.Name,
p.Sku ?? "",
p.Price,
p.Category ?? "Khác",
GetCategoryIcon(p.Category ?? "Khác")
)).ToList();
var cats = _products.Select(p => p.Category).Distinct().ToList();
_categories = new[] { "Tất cả" }.Concat(cats).ToArray();
}
catch
{
_loadError = true;
}
finally
{
_isLoading = false;
}
}
private void AddToCart(Product product)
{
var existing = _cartItems.FirstOrDefault(i => i.Sku == product.Sku);
@@ -142,7 +181,14 @@
private void Checkout() { }
private record Product(string Name, string Sku, decimal Price, int Stock, string Category, string Icon);
private static string GetCategoryIcon(string category) => category switch
{
"Thời trang" => "shirt", "Phụ kiện" => "shopping-bag", "Điện tử" => "headphones",
"Gia dụng" => "cooking-pot", "Mỹ phẩm" => "sparkles", _ => "package"
};
// EN: Models / VI: Mô hình dữ liệu
private record Product(string Name, string Sku, decimal Price, string Category, string Icon);
private class CartItem(string name, string sku, decimal price)
{
public string Name { get; set; } = name;

View File

@@ -5,6 +5,7 @@
@page "/pos/retail/tablet"
@layout PosLayout
@inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
@* ═══ PRODUCT PANEL ═══ *@
<div class="pos-product-panel">
@@ -18,32 +19,47 @@
</div>
</div>
@* EN: Category tabs / VI: Tab danh mục *@
<div class="pos-category-tabs">
@foreach (var cat in _categories)
{
<button class="pos-category-tab @(cat == _selectedCategory ? "pos-category-tab--active" : "")"
style="padding:12px 20px;font-size:15px;"
@onclick="() => _selectedCategory = cat">
@cat
</button>
}
</div>
@if (_isLoading)
{
<div style="display:flex;align-items:center;justify-content:center;flex:1;color:var(--pos-text-tertiary);">
Đang tải...
</div>
}
else if (_loadError)
{
<div style="display:flex;align-items:center;justify-content:center;flex:1;color:var(--pos-text-tertiary);">
Không thể tải dữ liệu
</div>
}
else
{
@* EN: Category tabs / VI: Tab danh mục *@
<div class="pos-category-tabs">
@foreach (var cat in _categories)
{
<button class="pos-category-tab @(cat == _selectedCategory ? "pos-category-tab--active" : "")"
style="padding:12px 20px;font-size:15px;"
@onclick="() => _selectedCategory = cat">
@cat
</button>
}
</div>
@* EN: Product grid (larger for tablet) / VI: Lưới sản phẩm (lớn hơn cho tablet) *@
<div class="pos-product-grid" style="grid-template-columns:repeat(auto-fill, minmax(160px, 1fr));gap:16px;padding:20px;">
@foreach (var product in FilteredProducts)
{
<div class="pos-product-card" style="padding:16px;" @onclick="() => AddToCart(product)">
<div class="pos-product-card__image" style="display:flex;align-items:center;justify-content:center;">
<i data-lucide="@product.Icon" style="width:36px;height:36px;color:var(--pos-text-tertiary);"></i>
@* EN: Product grid (larger for tablet) / VI: Lưới sản phẩm (lớn hơn cho tablet) *@
<div class="pos-product-grid" style="grid-template-columns:repeat(auto-fill, minmax(160px, 1fr));gap:16px;padding:20px;">
@foreach (var product in FilteredProducts)
{
<div class="pos-product-card" style="padding:16px;" @onclick="() => AddToCart(product)">
<div class="pos-product-card__image" style="display:flex;align-items:center;justify-content:center;">
<i data-lucide="@product.Icon" style="width:36px;height:36px;color:var(--pos-text-tertiary);"></i>
</div>
<span class="pos-product-card__name" style="font-size:15px;">@product.Name</span>
<span class="pos-product-card__price" style="font-size:16px;">@FormatPrice(product.Price)</span>
<span style="font-size:11px;color:var(--pos-text-tertiary);">@product.Sku</span>
</div>
<span class="pos-product-card__name" style="font-size:15px;">@product.Name</span>
<span class="pos-product-card__price" style="font-size:16px;">@FormatPrice(product.Price)</span>
<span style="font-size:11px;color:var(--pos-text-tertiary);">Kho: @product.Stock · @product.Sku</span>
</div>
}
</div>
}
</div>
}
</div>
@* ═══ CART SIDEBAR ═══ *@
@@ -96,30 +112,53 @@
</div>
@code {
private readonly string[] _categories = { "Tất cả", "Thời trang", "Phụ kiện", "Điện tử", "Gia dụng", "Mỹ phẩm" };
// EN: Retail shop ID (using cafe shop for now) / VI: ID cửa hàng bán lẻ (dùng tạm shop cafe)
private static readonly Guid RetailShopId = Guid.Parse("b0000001-0000-0000-0000-000000000001");
// EN: Loading state / VI: Trạng thái tải
private bool _isLoading = true;
private bool _loadError;
// EN: Categories / VI: Danh mục
private string[] _categories = { "Tất cả" };
private string _selectedCategory = "Tất cả";
private string _barcodeInput = "";
private readonly List<Product> _products = new()
{
new("Áo thun nam basic", "SKU-TT001", 199_000, 45, "Thời trang", "shirt"),
new("Quần jean nữ slim", "SKU-TT002", 450_000, 22, "Thời trang", "shirt"),
new("Áo khoác gió unisex", "SKU-TT003", 350_000, 18, "Thời trang", "shirt"),
new("Túi xách da nữ", "SKU-PK001", 890_000, 8, "Phụ kiện", "shopping-bag"),
new("Ví da nam", "SKU-PK002", 350_000, 30, "Phụ kiện", "wallet"),
new("Kính mát thời trang", "SKU-PK003", 280_000, 25, "Phụ kiện", "glasses"),
new("Tai nghe Bluetooth", "SKU-DT001", 650_000, 15, "Điện tử", "headphones"),
new("Sạc dự phòng 10000mAh", "SKU-DT002", 380_000, 40, "Điện tử", "battery-charging"),
new("Nồi cơm điện 1.8L", "SKU-GD001", 890_000, 10, "Gia dụng", "cooking-pot"),
new("Son môi cao cấp", "SKU-MP001", 320_000, 28, "Mỹ phẩm", "sparkles"),
new("Kem chống nắng SPF50", "SKU-MP002", 280_000, 35, "Mỹ phẩm", "sun"),
};
// EN: Product list from API / VI: Danh sách sản phẩm từ API
private List<Product> _products = new();
private readonly List<CartItem> _cartItems = new();
private IEnumerable<Product> FilteredProducts =>
_selectedCategory == "Tất cả" ? _products : _products.Where(p => p.Category == _selectedCategory);
private decimal CartTotal => _cartItems.Sum(i => i.Price * i.Qty);
protected override async Task OnInitializedAsync()
{
try
{
var apiProducts = await DataService.GetProductsAsync(RetailShopId);
_products = apiProducts.Select(p => new Product(
p.Name,
p.Sku ?? "",
p.Price,
p.Category ?? "Khác",
GetCategoryIcon(p.Category ?? "Khác")
)).ToList();
var cats = _products.Select(p => p.Category).Distinct().ToList();
_categories = new[] { "Tất cả" }.Concat(cats).ToArray();
}
catch
{
_loadError = true;
}
finally
{
_isLoading = false;
}
}
private void AddToCart(Product product)
{
var existing = _cartItems.FirstOrDefault(i => i.Sku == product.Sku);
@@ -135,7 +174,14 @@
private void Checkout() { }
private record Product(string Name, string Sku, decimal Price, int Stock, string Category, string Icon);
private static string GetCategoryIcon(string category) => category switch
{
"Thời trang" => "shirt", "Phụ kiện" => "shopping-bag", "Điện tử" => "headphones",
"Gia dụng" => "cooking-pot", "Mỹ phẩm" => "sparkles", _ => "package"
};
// EN: Models / VI: Mô hình dữ liệu
private record Product(string Name, string Sku, decimal Price, string Category, string Icon);
private class CartItem(string name, string sku, decimal price)
{
public string Name { get; set; } = name;

View File

@@ -5,6 +5,7 @@
@page "/pos/retail/product-search"
@layout PosLayout
@inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
@* ═══ HEADER ═══ *@
@@ -56,6 +57,20 @@
</div>
@* ═══ SEARCH RESULTS / KẾT QUẢ TÌM KIẾM ═══ *@
@if (_isLoading)
{
<div style="display:flex;align-items:center;justify-content:center;flex:1;color:var(--pos-text-tertiary);">
Đang tải...
</div>
}
else if (_loadError)
{
<div style="display:flex;align-items:center;justify-content:center;flex:1;color:var(--pos-text-tertiary);">
Không thể tải dữ liệu
</div>
}
else
{
<div style="flex:1;overflow-y:auto;padding:12px 16px;">
<div style="font-size:12px;color:var(--pos-text-tertiary);margin-bottom:12px;">
@FilteredResults.Count() kết quả @(!string.IsNullOrEmpty(_searchQuery) ? $"cho \"{_searchQuery}\"" : "")
@@ -76,9 +91,6 @@
<div style="font-size:12px;color:var(--pos-text-tertiary);margin-top:2px;">@product.Sku · @product.Category</div>
<div style="display:flex;align-items:center;gap:12px;margin-top:4px;">
<span style="font-size:15px;font-weight:700;color:var(--pos-orange-primary);">@FormatPrice(product.Price)</span>
<span style="font-size:11px;color:@(product.Stock > 10 ? "var(--pos-success)" : "var(--pos-warning)");">
Kho: @product.Stock
</span>
</div>
</div>
@@ -91,6 +103,7 @@
</div>
}
</div>
}
@* ═══ CART SUMMARY BAR / THANH TÓM TẮT GIỎ ═══ *@
@if (_cartItems.Any())
@@ -112,33 +125,50 @@
</div>
@code {
private string _searchQuery = "ao";
// EN: Retail shop ID (using cafe shop for now) / VI: ID cửa hàng bán lẻ (dùng tạm shop cafe)
private static readonly Guid RetailShopId = Guid.Parse("b0000001-0000-0000-0000-000000000001");
// EN: Loading state / VI: Trạng thái tải
private bool _isLoading = true;
private bool _loadError;
private string _searchQuery = "";
private string _filterCategory = "Tất cả";
private string _priceRange = "all";
private readonly string[] _categories = { "Tất cả", "Thời trang", "Phụ kiện", "Điện tử", "Gia dụng", "Mỹ phẩm" };
private string[] _categories = { "Tất cả" };
// EN: All products / VI: Tất cả sản phẩm
private readonly List<Product> _products = new()
{
new("Áo thun nam basic", "SKU-TT001", 199_000, 45, "Thời trang", "shirt"),
new("Áo khoác gió unisex", "SKU-TT003", 350_000, 18, "Thời trang", "shirt"),
new("Áo polo nam", "SKU-TT005", 280_000, 32, "Thời trang", "shirt"),
new("Áo sơ mi nữ", "SKU-TT006", 320_000, 14, "Thời trang", "shirt"),
new("Quần jean nữ slim", "SKU-TT002", 450_000, 22, "Thời trang", "shirt"),
new("Váy liền công sở", "SKU-TT004", 520_000, 12, "Thời trang", "shirt"),
new("Túi xách da nữ", "SKU-PK001", 890_000, 8, "Phụ kiện", "shopping-bag"),
new("Ví da nam", "SKU-PK002", 350_000, 30, "Phụ kiện", "wallet"),
new("Kính mát thời trang", "SKU-PK003", 280_000, 25, "Phụ kiện", "glasses"),
new("Tai nghe Bluetooth", "SKU-DT001", 650_000, 15, "Điện tử", "headphones"),
new("Sạc dự phòng 10000mAh", "SKU-DT002", 380_000, 40, "Điện tử", "battery-charging"),
new("Chuột không dây", "SKU-DT003", 250_000, 35, "Điện tử", "mouse"),
new("Nồi cơm điện 1.8L", "SKU-GD001", 890_000, 10, "Gia dụng", "cooking-pot"),
new("Bình giữ nhiệt 500ml", "SKU-GD002", 180_000, 50, "Gia dụng", "cup-soda"),
new("Son môi cao cấp", "SKU-MP001", 320_000, 28, "Mỹ phẩm", "sparkles"),
};
// EN: Product list from API / VI: Danh sách sản phẩm từ API
private List<Product> _products = new();
private readonly List<CartItem> _cartItems = new();
protected override async Task OnInitializedAsync()
{
try
{
var apiProducts = await DataService.GetProductsAsync(RetailShopId);
_products = apiProducts.Select(p => new Product(
p.Name,
p.Sku ?? "",
p.Price,
p.Category ?? "Khác",
GetCategoryIcon(p.Category ?? "Khác")
)).ToList();
var cats = _products.Select(p => p.Category).Distinct().ToList();
_categories = new[] { "Tất cả" }.Concat(cats).ToArray();
}
catch
{
_loadError = true;
}
finally
{
_isLoading = false;
}
}
private IEnumerable<Product> FilteredResults
{
get
@@ -167,7 +197,14 @@
else _cartItems.Add(new CartItem(product.Name, product.Sku, product.Price));
}
private record Product(string Name, string Sku, decimal Price, int Stock, string Category, string Icon);
private static string GetCategoryIcon(string category) => category switch
{
"Thời trang" => "shirt", "Phụ kiện" => "shopping-bag", "Điện tử" => "headphones",
"Gia dụng" => "cooking-pot", "Mỹ phẩm" => "sparkles", _ => "package"
};
// EN: Models / VI: Mô hình dữ liệu
private record Product(string Name, string Sku, decimal Price, string Category, string Icon);
private class CartItem(string name, string sku, decimal price)
{
public string Name { get; set; } = name;

View File

@@ -151,6 +151,8 @@
</div>
@code {
// EN: Static UI configuration — does not require DB data / VI: Cấu hình UI tĩnh — không cần dữ liệu từ DB
private string _receiptInput = "R2024001234";
private bool _receiptFound = true;
private string _returnReason = "";

View File

@@ -134,9 +134,9 @@
</div>
@code {
private string _searchQuery = "Áo thun nam";
// EN: Static UI configuration — does not require DB data (needs inventory API) / VI: Cấu hình UI tĩnh — không cần dữ liệu từ DB (cần inventory API)
// EN: Demo product / VI: Sản phẩm mẫu
private string _searchQuery = "Áo thun nam";
private readonly StockProduct _product = new("Áo thun nam basic", "SKU-TT001", "Thời trang", 199_000, 72);
// EN: Branch stock data / VI: Dữ liệu tồn kho chi nhánh

View File

@@ -5,8 +5,23 @@
@page "/pos/spa/mobile"
@layout PosLayout
@inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div style="display:flex;flex-direction:column;width:100%;height:100%;overflow:hidden;">
@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: Category tabs / VI: Tab danh mục *@
<div class="pos-category-tabs" style="padding:8px 12px;">
@foreach (var cat in _categories)
@@ -33,6 +48,7 @@
</div>
}
</div>
}
@* EN: Floating appointment button / VI: Nút lịch hẹn nổi *@
@if (_appointmentItems.Any())
@@ -97,31 +113,53 @@
</div>
@code {
private readonly string[] _categories = { "Tất cả", "Massage", "Facial", "Body", "Nail", "Hair" };
// EN: Spa shop ID / VI: ID cửa hàng spa
private static readonly Guid SpaShopId = Guid.Parse("b0000004-0000-0000-0000-000000000004");
// EN: Loading state / VI: Trạng thái tải
private bool _isLoading = true;
private bool _loadError;
// EN: Categories / VI: Danh mục
private string[] _categories = { "Tất cả" };
private string _selectedCategory = "Tất cả";
private bool _showSheet;
private readonly List<SpaService> _services = new()
{
new("Massage toàn thân", 500_000, 60, "Massage"),
new("Massage chân", 250_000, 45, "Massage"),
new("Massage đầu vai cổ", 300_000, 30, "Massage"),
new("Facial cơ bản", 350_000, 45, "Facial"),
new("Facial collagen", 600_000, 60, "Facial"),
new("Tắm trắng toàn thân", 800_000, 90, "Body"),
new("Tẩy tế bào chết", 400_000, 45, "Body"),
new("Sơn gel", 150_000, 30, "Nail"),
new("Nail art cao cấp", 300_000, 60, "Nail"),
new("Chăm sóc móng tay", 120_000, 30, "Nail"),
new("Gội đầu dưỡng sinh", 200_000, 40, "Hair"),
new("Ủ tóc phục hồi", 350_000, 45, "Hair"),
};
// EN: Service list from API / VI: Danh sách dịch vụ từ API
private List<SpaService> _services = new();
// EN: Appointment items / VI: Mục lịch hẹn
private readonly List<AppointmentItem> _appointmentItems = new();
private IEnumerable<SpaService> FilteredServices =>
_selectedCategory == "Tất cả" ? _services : _services.Where(s => s.Category == _selectedCategory);
private decimal AppointmentTotal => _appointmentItems.Sum(i => i.Price);
protected override async Task OnInitializedAsync()
{
try
{
var apiProducts = await DataService.GetProductsAsync(SpaShopId);
_services = apiProducts.Select(p => new SpaService(
p.Name,
p.Price,
p.DurationMinutes ?? 60,
p.Category ?? "Khác"
)).ToList();
var cats = _services.Select(s => s.Category).Distinct().ToList();
_categories = new[] { "Tất cả" }.Concat(cats).ToArray();
}
catch
{
_loadError = true;
}
finally
{
_isLoading = false;
}
}
private void AddToAppointment(SpaService svc)
{
_appointmentItems.Add(new AppointmentItem(svc.Name, svc.Price, svc.Duration));
@@ -135,6 +173,7 @@
"Nail" => "paintbrush", "Hair" => "scissors", _ => "heart"
};
// EN: Models / VI: Mô hình dữ liệu
private record SpaService(string Name, decimal Price, int Duration, string Category);
private record AppointmentItem(string Name, decimal Price, int Duration);
}

View File

@@ -5,9 +5,24 @@
@page "/pos/spa/tablet"
@layout PosLayout
@inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
@* ═══ SERVICE PANEL / PANEL DỊCH VỤ ═══ *@
<div class="pos-product-panel">
@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
{
<div class="pos-category-tabs">
@foreach (var cat in _categories)
{
@@ -34,6 +49,7 @@
</div>
}
</div>
}
</div>
@* ═══ APPOINTMENT SIDEBAR / SIDEBAR LỊCH HẸN ═══ *@
@@ -85,30 +101,52 @@
</div>
@code {
private readonly string[] _categories = { "Tất cả", "Massage", "Facial", "Body", "Nail", "Hair" };
// EN: Spa shop ID / VI: ID cửa hàng spa
private static readonly Guid SpaShopId = Guid.Parse("b0000004-0000-0000-0000-000000000004");
// EN: Loading state / VI: Trạng thái tải
private bool _isLoading = true;
private bool _loadError;
// EN: Categories / VI: Danh mục
private string[] _categories = { "Tất cả" };
private string _selectedCategory = "Tất cả";
private readonly List<SpaService> _services = new()
{
new("Massage toàn thân", 500_000, 60, "Massage"),
new("Massage chân", 250_000, 45, "Massage"),
new("Massage đầu vai cổ", 300_000, 30, "Massage"),
new("Facial cơ bản", 350_000, 45, "Facial"),
new("Facial collagen", 600_000, 60, "Facial"),
new("Tắm trắng toàn thân", 800_000, 90, "Body"),
new("Tẩy tế bào chết", 400_000, 45, "Body"),
new("Sơn gel", 150_000, 30, "Nail"),
new("Nail art cao cấp", 300_000, 60, "Nail"),
new("Chăm sóc móng tay", 120_000, 30, "Nail"),
new("Gội đầu dưỡng sinh", 200_000, 40, "Hair"),
new("Ủ tóc phục hồi", 350_000, 45, "Hair"),
};
// EN: Service list from API / VI: Danh sách dịch vụ từ API
private List<SpaService> _services = new();
// EN: Appointment items / VI: Mục lịch hẹn
private readonly List<AppointmentItem> _appointmentItems = new();
private IEnumerable<SpaService> FilteredServices =>
_selectedCategory == "Tất cả" ? _services : _services.Where(s => s.Category == _selectedCategory);
private decimal AppointmentTotal => _appointmentItems.Sum(i => i.Price);
protected override async Task OnInitializedAsync()
{
try
{
var apiProducts = await DataService.GetProductsAsync(SpaShopId);
_services = apiProducts.Select(p => new SpaService(
p.Name,
p.Price,
p.DurationMinutes ?? 60,
p.Category ?? "Khác"
)).ToList();
var cats = _services.Select(s => s.Category).Distinct().ToList();
_categories = new[] { "Tất cả" }.Concat(cats).ToArray();
}
catch
{
_loadError = true;
}
finally
{
_isLoading = false;
}
}
private void AddToAppointment(SpaService svc)
{
_appointmentItems.Add(new AppointmentItem(svc.Name, svc.Price, svc.Duration));
@@ -122,6 +160,7 @@
"Nail" => "paintbrush", "Hair" => "scissors", _ => "heart"
};
// EN: Models / VI: Mô hình dữ liệu
private record SpaService(string Name, decimal Price, int Duration, string Category);
private record AppointmentItem(string Name, decimal Price, int Duration);
}

View File

@@ -5,6 +5,7 @@
@page "/pos/spa/appointment-book"
@layout PosLayout
@inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div style="flex:1;display:flex;overflow:hidden;">
@* ═══ SCHEDULE PANEL (LEFT) / PANEL LỊCH (TRÁI) ═══ *@
@@ -19,6 +20,20 @@
<span style="font-size:16px;font-weight:700;">Đặt lịch hẹn</span>
</div>
@if (_isLoading)
{
<div style="display:flex;align-items:center;justify-content:center;flex:1;color:var(--pos-text-tertiary);">
Đang tải...
</div>
}
else if (_loadError)
{
<div style="display:flex;align-items:center;justify-content:center;flex:1;color:var(--pos-text-tertiary);">
Không thể tải dữ liệu
</div>
}
else
{
@* ═══ DATE PICKER / CHỌN NGÀY ═══ *@
<div style="margin-bottom:16px;">
<div style="font-size:13px;font-weight:600;margin-bottom:8px;">Chọn ngày</div>
@@ -81,6 +96,7 @@
</button>
}
</div>
}
</div>
@* ═══ BOOKING SUMMARY (RIGHT) / TÓM TẮT ĐẶT LỊCH (PHẢI) ═══ *@
@@ -143,6 +159,13 @@
</div>
@code {
// EN: Spa shop ID / VI: ID cửa hàng spa
private static readonly Guid SpaShopId = Guid.Parse("b0000004-0000-0000-0000-000000000004");
// EN: Loading state / VI: Trạng thái tải
private bool _isLoading = true;
private bool _loadError;
private string _selectedDate = "Hôm nay";
private string? _selectedTime = "10:00";
private string _selectedStaff = "Chị Hoa";
@@ -159,25 +182,51 @@
// EN: Staff list / VI: Danh sách nhân viên
private readonly string[] _staffList = { "Chị Hoa", "Anh Minh", "Chị Lan", "Chị Trang", "Bất kỳ" };
// EN: Time slots / VI: Khung giờ
private readonly List<TimeSlot> _timeSlots = new()
{
new("09:00", "available"), new("09:30", "available"), new("10:00", "available"),
new("10:30", "booked"), new("11:00", "booked"), new("11:30", "available"),
new("12:00", "available"), new("12:30", "available"), new("13:00", "available"),
new("13:30", "booked"), new("14:00", "available"), new("14:30", "available"),
new("15:00", "available"), new("15:30", "booked"), new("16:00", "available"),
new("16:30", "available"), new("17:00", "available"), new("17:30", "available"),
new("18:00", "booked"), new("18:30", "available"), new("19:00", "available"),
new("19:30", "available"), new("20:00", "available"),
};
// EN: Time slots from API / VI: Khung giờ từ API
private List<TimeSlot> _timeSlots = new();
// EN: Demo selected services / VI: Dịch vụ đã chọn mẫu
private readonly List<ServiceInfo> _selectedServices = new()
// EN: Selected services from API / VI: Dịch vụ đã chọn từ API
private List<ServiceInfo> _selectedServices = new();
protected override async Task OnInitializedAsync()
{
new("Massage toàn thân", 500_000, 60),
new("Facial collagen", 600_000, 60),
};
try
{
var appointments = await DataService.GetAppointmentsAsync(SpaShopId);
var bookedTimes = appointments
.Select(a => a.StartTime.ToString("HH:mm"))
.ToHashSet();
var slots = new List<TimeSlot>();
for (var hour = 9; hour <= 20; hour++)
{
foreach (var min in new[] { 0, 30 })
{
if (hour == 20 && min == 30) break;
var time = $"{hour:D2}:{min:D2}";
var status = bookedTimes.Contains(time) ? "booked" : "available";
slots.Add(new TimeSlot(time, status));
}
}
_timeSlots = slots;
var products = await DataService.GetProductsAsync(SpaShopId);
_selectedServices = products.Take(2).Select(p => new ServiceInfo(
p.Name,
p.Price,
p.DurationMinutes ?? 60
)).ToList();
}
catch
{
_loadError = true;
}
finally
{
_isLoading = false;
}
}
private record DateOption(string Label, string Day, string Value);
private record TimeSlot(string Time, string Status);

View File

@@ -86,9 +86,9 @@
</div>
@code {
private string _searchTerm = "Nguyễn";
// EN: Static UI configuration — does not require DB data (needs customer API) / VI: Cấu hình UI tĩnh — không cần dữ liệu từ DB (cần customer API)
// EN: Demo customers / VI: Khách hàng mẫu
private string _searchTerm = "Nguyễn";
private readonly List<CustomerInfo> _customers = new()
{
new("Nguyễn Thị Mai", "0901234567", "Gold", "15/02/2025", 28),

View File

@@ -139,9 +139,9 @@
</div>
@code {
private RewardInfo? _selectedReward;
// EN: Static UI configuration — does not require DB data (needs customer API) / VI: Cấu hình UI tĩnh — không cần dữ liệu từ DB (cần customer API)
// EN: Visit history / VI: Lịch sử ghé
private RewardInfo? _selectedReward;
private readonly List<VisitInfo> _visitHistory = new()
{
new("Massage toàn thân + Facial", "15/02/2025", "Chị Hoa", 850_000, 85),

View File

@@ -118,9 +118,9 @@
</div>
@code {
private string? _selectedCombo;
// EN: Static UI configuration — combo definitions are config, does not require DB data / VI: Cấu hình UI tĩnh — định nghĩa combo là cấu hình, không cần dữ liệu từ DB
// EN: Demo combos / VI: Combo mẫu
private string? _selectedCombo;
private readonly List<ComboInfo> _combos = new()
{
new("CB1", "Mua 2 tặng 1", "Mua 2 dịch vụ Massage bất kỳ, tặng 1 Massage chân miễn phí",

View File

@@ -5,6 +5,7 @@
@page "/pos/spa/service-package"
@layout PosLayout
@inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
@* ═══ HEADER / TIÊU ĐỀ ═══ *@
@@ -19,6 +20,20 @@
</div>
@* ═══ PACKAGE LIST / DANH SÁCH GÓI ═══ *@
@if (_isLoading)
{
<div style="display:flex;align-items:center;justify-content:center;flex:1;color:var(--pos-text-tertiary);">
Đang tải...
</div>
}
else if (_loadError)
{
<div style="display:flex;align-items:center;justify-content:center;flex:1;color:var(--pos-text-tertiary);">
Không thể tải dữ liệu
</div>
}
else
{
<div style="flex:1;overflow-y:auto;padding:16px;">
<div style="display:flex;flex-direction:column;gap:12px;">
@foreach (var pkg in _packages)
@@ -100,43 +115,72 @@
}
</div>
</div>
}
</div>
@code {
// EN: Spa shop ID / VI: ID cửa hàng spa
private static readonly Guid SpaShopId = Guid.Parse("b0000004-0000-0000-0000-000000000004");
// EN: Loading state / VI: Trạng thái tải
private bool _isLoading = true;
private bool _loadError;
private string? _expandedId = "PKG1";
// EN: Demo packages / VI: Gói dịch vụ mẫu
private readonly List<PackageInfo> _packages = new()
// EN: Packages built from DB services / VI: Gói dịch vụ xây dựng từ dữ liệu DB
private List<PackageInfo> _packages = new();
// EN: Static package definitions — service groupings are config, prices come from DB
// VI: Định nghĩa gói tĩnh — nhóm dịch vụ là cấu hình, giá từ DB
private static readonly List<PackageConfig> PackageConfigs = new()
{
new("PKG1", "Gói Thư giãn", 900_000, 1_050_000, 3, 135, true, "leaf", "rgba(34,197,94,.15)", "#22C55E", new()
{
new("Massage toàn thân", 500_000, 60),
new("Facial cơ bản", 350_000, 45),
new("Gội đầu dưỡng sinh", 200_000, 30),
}),
new("PKG2", "Gói VIP", 1_800_000, 2_250_000, 5, 225, true, "crown", "rgba(245,158,11,.15)", "#F59E0B", new()
{
new("Massage toàn thân", 500_000, 60),
new("Facial collagen", 600_000, 60),
new("Tắm trắng toàn thân", 800_000, 90),
new("Sơn gel", 150_000, 30),
new("Gội đầu dưỡng sinh", 200_000, 30),
}),
new("PKG3", "Gói Cặp đôi", 1_500_000, 1_800_000, 4, 180, false, "heart", "rgba(239,68,68,.15)", "#EF4444", new()
{
new("Massage toàn thân x2", 1_000_000, 60),
new("Facial cơ bản x2", 700_000, 45),
new("Trà thảo mộc x2", 100_000, 15),
}),
new("PKG4", "Gói Làm đẹp", 1_200_000, 1_450_000, 4, 165, false, "sparkles", "rgba(139,92,246,.15)", "#8B5CF6", new()
{
new("Facial collagen", 600_000, 60),
new("Tẩy tế bào chết", 400_000, 45),
new("Sơn gel", 150_000, 30),
new("Ủ tóc phục hồi", 300_000, 30),
}),
new("PKG1", "Gói Thư giãn", 0.857m, true, "leaf", "rgba(34,197,94,.15)", "#22C55E",
new() { "Massage toàn thân", "Facial cơ bản", "Gội đầu dưỡng sinh" }),
new("PKG2", "Gói VIP", 0.8m, true, "crown", "rgba(245,158,11,.15)", "#F59E0B",
new() { "Massage toàn thân", "Facial collagen", "Tắm trắng toàn thân", "Sơn gel", "Gội đầu dưỡng sinh" }),
new("PKG3", "Gói Cặp đôi", 0.833m, false, "heart", "rgba(239,68,68,.15)", "#EF4444",
new() { "Massage toàn thân", "Facial cơ bản" }),
new("PKG4", "Gói Làm đẹp", 0.828m, false, "sparkles", "rgba(139,92,246,.15)", "#8B5CF6",
new() { "Facial collagen", "Tẩy tế bào chết", "Sơn gel", "Ủ tóc phục hồi" }),
};
protected override async Task OnInitializedAsync()
{
try
{
var apiProducts = await DataService.GetProductsAsync(SpaShopId);
var productLookup = apiProducts.ToDictionary(p => p.Name, p => p);
_packages = PackageConfigs.Select(cfg =>
{
var services = cfg.ServiceNames.Select(name =>
{
if (productLookup.TryGetValue(name, out var p))
return new PackageService(p.Name, p.Price, p.DurationMinutes ?? 60);
return new PackageService(name, 0, 0);
}).ToList();
var originalPrice = services.Sum(s => s.Price);
var packagePrice = Math.Round(originalPrice * cfg.DiscountFactor);
var totalDuration = services.Sum(s => s.Duration);
return new PackageInfo(cfg.Id, cfg.Name, packagePrice, originalPrice,
services.Count, totalDuration, cfg.Popular, cfg.Icon, cfg.BgColor, cfg.FgColor, services);
}).ToList();
}
catch
{
_loadError = true;
}
finally
{
_isLoading = false;
}
}
private record PackageConfig(string Id, string Name, decimal DiscountFactor, bool Popular,
string Icon, string BgColor, string FgColor, List<string> ServiceNames);
private record PackageService(string Name, decimal Price, int Duration);
private record PackageInfo(string Id, string Name, decimal Price, decimal OriginalPrice, int ServiceCount,
int TotalDuration, bool Popular, string Icon, string BgColor, string FgColor, List<PackageService> Services);

View File

@@ -269,9 +269,9 @@
</div>
@code {
private int _currentStep = 2;
// EN: Static UI configuration — does not require DB data / VI: Cấu hình UI tĩnh — không cần dữ liệu từ DB
// EN: Journey steps / VI: Các bước hành trình
private int _currentStep = 2;
private readonly List<StepInfo> _steps = new()
{
new("Check-in", "check-in", "Tiếp tục"),

View File

@@ -112,6 +112,8 @@
</div>
@code {
// EN: Static UI configuration — does not require DB data (needs staff API) / VI: Cấu hình UI tĩnh — không cần dữ liệu từ DB (cần staff API)
private string _activeFilter = "Tất cả";
private string? _selectedStaff = "S01";
private readonly string[] _filters = { "Tất cả", "Rảnh", "Đang bận", "Nghỉ giải lao" };

View File

@@ -117,10 +117,10 @@
</div>
@code {
// EN: Static UI configuration — does not require DB data (needs schedule API) / VI: Cấu hình UI tĩnh — không cần dữ liệu từ DB (cần schedule API)
// EN: Hours range / VI: Phạm vi giờ
private readonly int[] _hours = { 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 };
// EN: Demo schedule / VI: Lịch mẫu
private readonly List<StaffSchedule> _scheduleData = new()
{
new("Trần Thị Hoa", "Massage", new()

View File

@@ -158,6 +158,8 @@
</div>
@code {
// EN: Static UI configuration — does not require DB data / VI: Cấu hình UI tĩnh — không cần dữ liệu từ DB
private string _remainingTime = "45:00";
private int _totalDuration = 60;
private double _progress = 0.25;