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" @page "/pos/restaurant/mobile"
@layout PosLayout @layout PosLayout
@inherits PosBase @inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;"> <div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
@* ═══ HEADER / TIÊU ĐỀ ═══ *@ @if (_isLoading)
<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> <div style="flex:1;display:flex;align-items:center;justify-content:center;color:var(--pos-text-tertiary);">
<span style="font-size:12px;color:var(--pos-text-tertiary);">@_tables.Count bàn</span> Đang tải...
</div> </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 ═══ *@ @* ═══ SECTION FILTER / LỌC KHU VỰC ═══ *@
<div class="pos-category-tabs" style="padding:8px 12px;"> <div class="pos-category-tabs" style="padding:8px 12px;">
@foreach (var s in _sections) @foreach (var s in _sections)
{ {
<button class="pos-category-tab @(s == _activeSection ? "pos-category-tab--active" : "")" <button class="pos-category-tab @(s == _activeSection ? "pos-category-tab--active" : "")"
style="font-size:12px;padding:6px 12px;" @onclick="() => _activeSection = s">@s</button> style="font-size:12px;padding:6px 12px;" @onclick="() => _activeSection = s">@s</button>
} }
</div> </div>
@* ═══ TABLE LIST / DANH SÁCH BÀN ═══ *@ @* ═══ TABLE LIST / DANH SÁCH BÀN ═══ *@
<div style="flex:1;overflow-y:auto;padding:8px 12px;display:flex;flex-direction:column;gap:8px;"> <div style="flex:1;overflow-y:auto;padding:8px 12px;display:flex;flex-direction:column;gap:8px;">
@foreach (var t in FilteredTables) @foreach (var t in FilteredTables)
{ {
<div @onclick="() => OpenTable(t)" <div @onclick="() => OpenTable(t)"
style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:14px 16px; style="background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:14px 16px;
display:flex;align-items:center;gap:12px;cursor:pointer; display:flex;align-items:center;gap:12px;cursor:pointer;
border-left:4px solid @StatusColor(t.Status);"> border-left:4px solid @StatusColor(t.Status);">
@* EN: Table number badge / VI: Badge số bàn *@ @* EN: Table number badge / VI: Badge số bàn *@
<div style="width:44px;height:44px;border-radius:10px;background:@StatusBg(t.Status); <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;"> display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:700;">
@t.Number @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
</div> </div>
</div> <div style="flex:1;">
<div style="text-align:right;"> <div style="font-size:14px;font-weight:600;">@t.Name</div>
<div style="font-size:11px;font-weight:600;color:@StatusColor(t.Status);"> <div style="font-size:12px;color:var(--pos-text-tertiary);margin-top:2px;">
@StatusLabel(t.Status) @t.Seats chỗ · @t.Section
</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>
} </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> </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 ═══ *@ @* ═══ 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; <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;"> background:var(--pos-bg-elevated);font-size:12px;">
<div style="text-align:center;"> <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="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 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>
<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> </div>
@code { @code {
private string _activeSection = "Tất cả"; // EN: Restaurant shop ID / VI: ID cửa hàng nhà hàng
private readonly string[] _sections = { "Tất cả", "Trong nhà", "Ngoài trời", "VIP" }; private static readonly Guid RestaurantShopId = Guid.Parse("b0000002-0000-0000-0000-000000000002");
// EN: Demo tables / VI: Bàn mẫu // EN: Loading state / VI: Trạng thái tải
private readonly List<MobileTable> _tables = new() private bool _isLoading = true;
{ private bool _loadError;
new(1, "Bàn 1", 4, "available", "Trong nhà", 0),
new(2, "Bàn 2", 2, "occupied", "Trong nhà", 195_000), // EN: Active section filter / VI: Bộ lọc khu vực
new(3, "Bàn 3", 6, "occupied", "Trong nhà", 420_000), private string _activeSection = "Tất cả";
new(4, "Bàn 4", 4, "reserved", "VIP", 0), private string[] _sections = { "Tất cả" };
new(5, "Bàn 5", 8, "available", "VIP", 0),
new(6, "Bàn 6", 4, "occupied", "Ngoài trời", 310_000), // EN: Table data from API / VI: Dữ liệu bàn từ API
new(7, "Bàn 7", 4, "available", "Ngoài trời", 0), private List<MobileTable> _tables = new();
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),
};
private IEnumerable<MobileTable> FilteredTables => private IEnumerable<MobileTable> FilteredTables =>
_activeSection == "Tất cả" ? _tables : _tables.Where(t => t.Section == _activeSection); _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 void OpenTable(MobileTable t) => NavigateTo("restaurant/waiter-pad");
private static string StatusColor(string s) => s switch private static string StatusColor(string s) => s switch

View File

@@ -5,34 +5,50 @@
@page "/pos/restaurant/tablet" @page "/pos/restaurant/tablet"
@layout PosLayout @layout PosLayout
@inherits PosBase @inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div class="pos-product-panel" style="padding:16px;overflow-y:auto;"> <div class="pos-product-panel" style="padding:16px;overflow-y:auto;">
@* ═══ SECTION FILTER / LỌC KHU VỰC ═══ *@ @if (_isLoading)
<div class="pos-category-tabs"> {
@foreach (var s in _sections) <div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--pos-text-tertiary);">
{ Đang tải...
<button class="pos-category-tab @(s == _activeSection ? "pos-category-tab--active" : "")" </div>
style="padding:12px 20px;font-size:15px;" }
@onclick="() => _activeSection = s">@s</button> else if (_loadError)
} {
</div> <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 ═══ *@ @* ═══ TABLE MAP / SƠ ĐỒ BÀN ═══ *@
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:16px;padding:12px 0;"> <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:16px;padding:12px 0;">
@foreach (var t in FilteredTables) @foreach (var t in FilteredTables)
{ {
<div @onclick="() => _selected = t" <div @onclick="() => _selected = t"
style="background:@StatusBg(t.Status);border-radius:var(--pos-radius);padding:20px; 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; text-align:center;cursor:pointer;min-height:100px;display:flex;flex-direction:column;
align-items:center;justify-content:center;gap:6px; align-items:center;justify-content:center;gap:6px;
border:3px solid @(_selected?.Id == t.Id ? "var(--pos-orange-primary)" : "transparent"); border:3px solid @(_selected?.Id == t.Id ? "var(--pos-orange-primary)" : "transparent");
transition:all .2s ease;"> transition:all .2s ease;">
<span style="font-size:22px;font-weight:700;">@t.Name</span> <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: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> <span style="font-size:12px;font-weight:600;margin-top:4px;">@StatusLabel(t.Status)</span>
</div> </div>
} }
</div> </div>
}
</div> </div>
@* ═══ ORDER SIDEBAR / SIDEBAR ĐẶT MÓN ═══ *@ @* ═══ ORDER SIDEBAR / SIDEBAR ĐẶT MÓN ═══ *@
@@ -85,28 +101,60 @@
</div> </div>
@code { @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 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 TableInfo? _selected;
private readonly List<TableInfo> _tables = new() // EN: Table data from API / VI: Dữ liệu bàn từ API
{ private 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à"),
};
private IEnumerable<TableInfo> FilteredTables => private IEnumerable<TableInfo> FilteredTables =>
_activeSection == "Tất cả" ? _tables : _tables.Where(t => t.Section == _activeSection); _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() private readonly List<OrderItem> _items = new()
{ {
new("Bún bò Huế", 80_000, 1), new("Nem rán", 50_000, 2), 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), 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 private static string StatusBg(string s) => s switch
{ {
"available" => "rgba(34,197,94,.15)", "occupied" => "rgba(255,92,0,.18)", "available" => "rgba(34,197,94,.15)", "occupied" => "rgba(255,92,0,.18)",

View File

@@ -5,6 +5,7 @@
@page "/pos/restaurant/kitchen-display" @page "/pos/restaurant/kitchen-display"
@layout PosLayout @layout PosLayout
@inherits PosBase @inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;"> <div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
@* ═══ HEADER / TIÊU ĐỀ ═══ *@ @* ═══ HEADER / TIÊU ĐỀ ═══ *@
@@ -23,6 +24,20 @@
</div> </div>
@* ═══ TICKET COLUMNS / CỘT PHIẾU ═══ *@ @* ═══ 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;"> <div style="flex:1;display:flex;gap:16px;padding:16px;overflow-x:auto;">
@foreach (var status in _statuses) @foreach (var status in _statuses)
{ {
@@ -91,15 +106,23 @@
</div> </div>
} }
</div> </div>
}
</div> </div>
@code { @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() private readonly Dictionary<string, string> _statuses = new()
{ {
["new"] = "Mới", ["cooking"] = "Đang nấu", ["ready"] = "Sẵn sàng" ["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() 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") }), 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", "") }), 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 private static string ColumnBg(string s) => s switch
{ {
"new" => "rgba(239,68,68,.15)", "cooking" => "rgba(245,158,11,.15)", "new" => "rgba(239,68,68,.15)", "cooking" => "rgba(245,158,11,.15)",

View File

@@ -5,6 +5,7 @@
@page "/pos/restaurant/order-history" @page "/pos/restaurant/order-history"
@layout PosLayout @layout PosLayout
@inherits PosBase @inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;"> <div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
@* ═══ HEADER / TIÊU ĐỀ ═══ *@ @* ═══ HEADER / TIÊU ĐỀ ═══ *@
@@ -36,6 +37,20 @@
@* ═══ ORDER LIST / DANH SÁCH ĐƠN ═══ *@ @* ═══ ORDER LIST / DANH SÁCH ĐƠN ═══ *@
<div style="flex:1;overflow-y:auto;padding:8px 16px;"> <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) @foreach (var order in FilteredOrders)
{ {
<div @onclick="() => _expandedId = _expandedId == order.Id ? null : order.Id" <div @onclick="() => _expandedId = _expandedId == order.Id ? null : order.Id"
@@ -82,6 +97,7 @@
} }
</div> </div>
} }
}
</div> </div>
@* ═══ FOOTER SUMMARY / TỔNG KẾT ═══ *@ @* ═══ FOOTER SUMMARY / TỔNG KẾT ═══ *@
@@ -93,12 +109,19 @@
</div> </div>
@code { @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 _searchQuery = string.Empty;
private string _activeFilter = "Tất cả"; private string _activeFilter = "Tất cả";
private string? _expandedId; private string? _expandedId;
private readonly string[] _filters = { "Tất cả", "Tiền mặt", "Thẻ", "Chuyển khoản" }; 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() private readonly List<HistoryOrder> _orders = new()
{ {
new("DH001", "B3", "10:15", "Nguyễn Văn A", 3, 285_000, "Tiền mặt", 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) }), 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 private IEnumerable<HistoryOrder> FilteredOrders
{ {
get get

View File

@@ -5,6 +5,7 @@
@page "/pos/restaurant/menu-management" @page "/pos/restaurant/menu-management"
@layout PosLayout @layout PosLayout
@inherits PosBase @inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;"> <div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
@* ═══ HEADER / TIÊU ĐỀ ═══ *@ @* ═══ HEADER / TIÊU ĐỀ ═══ *@
@@ -53,6 +54,20 @@
@* ═══ MENU ITEM GRID / LƯỚI MÓN ═══ *@ @* ═══ MENU ITEM GRID / LƯỚI MÓN ═══ *@
<div style="flex:1;overflow-y:auto;padding:16px;"> <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;"> <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px;">
@foreach (var item in FilteredMenu) @foreach (var item in FilteredMenu)
{ {
@@ -92,6 +107,7 @@
</div> </div>
} }
</div> </div>
}
</div> </div>
@* ═══ STATS BAR / THANH THỐNG KÊ ═══ *@ @* ═══ STATS BAR / THANH THỐNG KÊ ═══ *@
@@ -104,30 +120,52 @@
</div> </div>
@code { @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 bool _editMode = false;
private string? _selectedItem; 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 // EN: Menu items from API / VI: Các món từ API
private readonly List<RestMenuItem> _menuItems = new() private 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, ""),
};
private IEnumerable<RestMenuItem> FilteredMenu => 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() private void MarkSoldOut()
{ {

View File

@@ -5,6 +5,7 @@
@page "/pos/restaurant/table-map" @page "/pos/restaurant/table-map"
@layout PosLayout @layout PosLayout
@inherits PosBase @inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;"> <div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
@* ═══ TOOLBAR / THANH CÔNG CỤ ═══ *@ @* ═══ TOOLBAR / THANH CÔNG CỤ ═══ *@
@@ -21,6 +22,20 @@
</div> </div>
@* ═══ SECTION TABS / TAB KHU VỰC ═══ *@ @* ═══ 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"> <div class="pos-category-tabs">
@foreach (var s in _sections) @foreach (var s in _sections)
{ {
@@ -57,6 +72,8 @@
</div> </div>
</div> </div>
}
@* ═══ FOOTER STATS / THỐNG KÊ ═══ *@ @* ═══ 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);"> <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> <span>Tổng: <b>@_tables.Count</b> bàn</span>
@@ -71,23 +88,51 @@
</div> </div>
@code { @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 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 HashSet<string> _selectedIds = new();
private readonly List<MapTable> _tables = new() // EN: Table data from API / VI: Dữ liệu bàn từ API
{ private 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"),
};
private IEnumerable<MapTable> FilteredTables => private IEnumerable<MapTable> FilteredTables =>
_activeSection == "Tất cả" ? _tables : _tables.Where(t => t.Section == _activeSection); _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) private void ToggleSelect(MapTable t)
{ {
if (!_selectedIds.Add(t.Id)) _selectedIds.Remove(t.Id); if (!_selectedIds.Add(t.Id)) _selectedIds.Remove(t.Id);

View File

@@ -5,6 +5,7 @@
@page "/pos/restaurant/table-merge-split" @page "/pos/restaurant/table-merge-split"
@layout PosLayout @layout PosLayout
@inherits PosBase @inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;"> <div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
@* ═══ HEADER / TIÊU ĐỀ ═══ *@ @* ═══ HEADER / TIÊU ĐỀ ═══ *@
@@ -30,7 +31,19 @@
</div> </div>
<div style="flex:1;overflow-y:auto;padding:16px;"> <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 ═══ *@ @* ═══ MERGE MODE / CHẾ ĐỘ GHÉP ═══ *@
<div style="margin-bottom:20px;"> <div style="margin-bottom:20px;">
@@ -158,28 +171,53 @@
</div> </div>
@code { @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 string _mode = "merge";
private readonly HashSet<string> _mergeSelected = new(); private readonly HashSet<string> _mergeSelected = new();
private string? _splitSelected; private string? _splitSelected;
// EN: Tables for merge / VI: Bàn để ghép // EN: Tables from API / VI: Bàn từ API
private readonly List<MergeTable> _mergeTables = new() private List<MergeTable> _mergeTables = new();
{ private List<SplitTable> _splitTables = 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 for split / VI: Bàn để tách protected override async Task OnInitializedAsync()
private readonly List<SplitTable> _splitTables = new()
{ {
new("T07", "Bàn 7", 6, 5), try
new("T03", "Bàn 3", 4, 3), {
new("T10", "Bàn 10", 8, 7), 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) private void SwitchMode(string mode)
{ {

View File

@@ -5,6 +5,7 @@
@page "/pos/restaurant/table-select" @page "/pos/restaurant/table-select"
@layout PosLayout @layout PosLayout
@inherits PosBase @inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;"> <div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
@* ═══ HEADER / TIÊU ĐỀ ═══ *@ @* ═══ HEADER / TIÊU ĐỀ ═══ *@
@@ -46,6 +47,20 @@
@* ═══ TABLE GRID / LƯỚI BÀN ═══ *@ @* ═══ TABLE GRID / LƯỚI BÀN ═══ *@
<div style="flex:1;overflow-y:auto;padding:16px;"> <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;"> <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px;">
@foreach (var table in FilteredTables) @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 style="font-size:14px;">Không tìm thấy bàn trống phù hợp</div>
</div> </div>
} }
}
</div> </div>
@* ═══ SELECTED TABLE INFO / THÔNG TIN BÀN ĐÃ CHỌN ═══ *@ @* ═══ SELECTED TABLE INFO / THÔNG TIN BÀN ĐÃ CHỌN ═══ *@
@@ -117,28 +133,52 @@
</div> </div>
@code { @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 int _guestCount = 4;
private string _activeSection = "Tất cả"; private string _activeSection = "Tất cả";
private string? _selectedTable; 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 // EN: Available tables from API / VI: Bàn trống từ API
private readonly List<AvailableTable> _availableTables = new() private 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"),
};
private IEnumerable<AvailableTable> FilteredTables => private IEnumerable<AvailableTable> FilteredTables =>
_activeSection == "Tất cả" ? _availableTables : _availableTables.Where(t => t.Section == _activeSection); _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) => private static string CapacityColor(int seats, int guests) =>
seats >= guests + 2 ? "var(--pos-success)" seats >= guests + 2 ? "var(--pos-success)"
: seats >= guests ? "#F59E0B" : seats >= guests ? "#F59E0B"

View File

@@ -5,6 +5,7 @@
@page "/pos/restaurant/waiter-pad" @page "/pos/restaurant/waiter-pad"
@layout PosLayout @layout PosLayout
@inherits PosBase @inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div class="pos-product-panel" style="display:flex;flex-direction:column;overflow:hidden;"> <div class="pos-product-panel" style="display:flex;flex-direction:column;overflow:hidden;">
@* ═══ HEADER / TIÊU ĐỀ ═══ *@ @* ═══ HEADER / TIÊU ĐỀ ═══ *@
@@ -16,28 +17,43 @@
<span style="font-size:12px;color:var(--pos-text-tertiary);">PV: Nguyễn Văn A</span> <span style="font-size:12px;color:var(--pos-text-tertiary);">PV: Nguyễn Văn A</span>
</div> </div>
@* ═══ COURSE TABS / TAB MÓN THEO COURSE ═══ *@ @if (_isLoading)
<div class="pos-category-tabs"> {
@foreach (var c in _courses) <div style="flex:1;display:flex;align-items:center;justify-content:center;color:var(--pos-text-tertiary);">
{ Đang tải...
<button class="pos-category-tab @(c == _activeCourse ? "pos-category-tab--active" : "")" </div>
@onclick="() => _activeCourse = c">@c</button> }
} else if (_loadError)
</div> {
<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 ═══ *@ @* ═══ MENU ITEMS / DANH SÁCH MÓN ═══ *@
<div class="pos-product-grid" style="grid-template-columns:repeat(auto-fill,minmax(150px,1fr));"> <div class="pos-product-grid" style="grid-template-columns:repeat(auto-fill,minmax(150px,1fr));">
@foreach (var item in FilteredMenu) @foreach (var item in FilteredMenu)
{ {
<div class="pos-product-card" @onclick="() => AddToOrder(item)"> <div class="pos-product-card" @onclick="() => AddToOrder(item)">
<div class="pos-product-card__image" style="display:flex;align-items:center;justify-content:center;"> <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> <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> </div>
<span class="pos-product-card__name">@item.Name</span> }
<span class="pos-product-card__price">@FormatPrice(item.Price)</span> </div>
</div> }
}
</div>
</div> </div>
@* ═══ ORDER PANEL / PANEL ĐƠN GỌI ═══ *@ @* ═══ ORDER PANEL / PANEL ĐƠN GỌI ═══ *@
@@ -88,25 +104,52 @@
</div> </div>
@code { @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 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 // EN: Menu items from API / VI: Thực đơn từ API
private readonly List<MenuItem> _menu = new() 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"), try
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"), var products = await DataService.GetProductsAsync(RestaurantShopId);
new("Bún bò Huế", 80_000, "Món chính", "flame"), new("Cá kho tộ", 120_000, "Món chính", "fish"), var categories = await DataService.GetCategoriesAsync(RestaurantShopId);
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"),
};
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 // EN: Current order / VI: Đơn gọi hiện tại
private readonly List<OrderLine> _orderItems = new() private readonly List<OrderLine> _orderItems = new()

View File

@@ -5,6 +5,7 @@
@page "/pos/retail" @page "/pos/retail"
@layout PosLayout @layout PosLayout
@inherits PosBase @inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
@* ═══ PRODUCT PANEL ═══ *@ @* ═══ PRODUCT PANEL ═══ *@
<div class="pos-product-panel"> <div class="pos-product-panel">
@@ -20,34 +21,48 @@
</div> </div>
</div> </div>
@* EN: Category tabs / VI: Tab danh mục *@ @if (_isLoading)
<div class="pos-category-tabs"> {
@foreach (var cat in _categories) <div style="display:flex;align-items:center;justify-content:center;flex:1;color:var(--pos-text-tertiary);">
{ Đang tải...
<button class="pos-category-tab @(cat == _selectedCategory ? "pos-category-tab--active" : "")" </div>
@onclick="() => _selectedCategory = cat"> }
@cat else if (_loadError)
</button> {
} <div style="display:flex;align-items:center;justify-content:center;flex:1;color:var(--pos-text-tertiary);">
</div> 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 *@ @* EN: Product grid / VI: Lưới sản phẩm *@
<div class="pos-product-grid"> <div class="pos-product-grid">
@foreach (var product in FilteredProducts) @foreach (var product in FilteredProducts)
{ {
<div class="pos-product-card" @onclick="() => AddToCart(product)"> <div class="pos-product-card" @onclick="() => AddToCart(product)">
<div class="pos-product-card__image" style="display:flex;align-items:center;justify-content:center;"> <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> <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> </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;"> </div>
<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 ═══ *@ @* ═══ CART PANEL ═══ *@
@@ -99,30 +114,20 @@
</div> </div>
@code { @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 // 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 _selectedCategory = "Tất cả";
private string _barcodeInput = ""; private string _barcodeInput = "";
// EN: Retail product list / VI: Danh sách sản phẩm bán lẻ // EN: Product list from API / VI: Danh sách sản phẩm từ API
private readonly List<Product> _products = new() private 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: Cart items / VI: Mục giỏ hàng // EN: Cart items / VI: Mục giỏ hàng
private readonly List<CartItem> _cartItems = new(); private readonly List<CartItem> _cartItems = new();
@@ -130,6 +135,33 @@
_selectedCategory == "Tất cả" ? _products : _products.Where(p => p.Category == _selectedCategory); _selectedCategory == "Tất cả" ? _products : _products.Where(p => p.Category == _selectedCategory);
private decimal CartTotal => _cartItems.Sum(i => i.Price * i.Qty); 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) private void AddToCart(Product product)
{ {
var existing = _cartItems.FirstOrDefault(i => i.Sku == product.Sku); var existing = _cartItems.FirstOrDefault(i => i.Sku == product.Sku);
@@ -152,8 +184,14 @@
private void Checkout() { } 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 // 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) private class CartItem(string name, string sku, decimal price)
{ {
public string Name { get; set; } = name; public string Name { get; set; } = name;

View File

@@ -5,6 +5,7 @@
@page "/pos/retail/mobile" @page "/pos/retail/mobile"
@layout PosLayout @layout PosLayout
@inherits PosBase @inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div style="display:flex;flex-direction:column;width:100%;height:100%;overflow:hidden;"> <div style="display:flex;flex-direction:column;width:100%;height:100%;overflow:hidden;">
@* EN: Barcode input / VI: Ô nhập mã vạch *@ @* EN: Barcode input / VI: Ô nhập mã vạch *@
@@ -17,6 +18,20 @@
</div> </div>
</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 *@ @* EN: Category tabs / VI: Tab danh mục *@
<div class="pos-category-tabs" style="padding:8px 12px;"> <div class="pos-category-tabs" style="padding:8px 12px;">
@foreach (var cat in _categories) @foreach (var cat in _categories)
@@ -39,10 +54,10 @@
</div> </div>
<span class="pos-product-card__name" style="font-size:12px;">@product.Name</span> <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 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>
} }
</div> </div>
}
@* EN: Floating cart button / VI: Nút giỏ hàng nổi *@ @* EN: Floating cart button / VI: Nút giỏ hàng nổi *@
@if (_cartItems.Any()) @if (_cartItems.Any())
@@ -103,30 +118,54 @@
</div> </div>
@code { @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 _selectedCategory = "Tất cả";
private string _barcodeInput = ""; private string _barcodeInput = "";
private bool _showCart; private bool _showCart;
private readonly List<Product> _products = new() // EN: Product list from API / VI: Danh sách sản phẩm từ API
{ private 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"),
};
private readonly List<CartItem> _cartItems = new(); private readonly List<CartItem> _cartItems = new();
private IEnumerable<Product> FilteredProducts => private IEnumerable<Product> FilteredProducts =>
_selectedCategory == "Tất cả" ? _products : _products.Where(p => p.Category == _selectedCategory); _selectedCategory == "Tất cả" ? _products : _products.Where(p => p.Category == _selectedCategory);
private decimal CartTotal => _cartItems.Sum(i => i.Price * i.Qty); 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) private void AddToCart(Product product)
{ {
var existing = _cartItems.FirstOrDefault(i => i.Sku == product.Sku); var existing = _cartItems.FirstOrDefault(i => i.Sku == product.Sku);
@@ -142,7 +181,14 @@
private void Checkout() { } 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) private class CartItem(string name, string sku, decimal price)
{ {
public string Name { get; set; } = name; public string Name { get; set; } = name;

View File

@@ -5,6 +5,7 @@
@page "/pos/retail/tablet" @page "/pos/retail/tablet"
@layout PosLayout @layout PosLayout
@inherits PosBase @inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
@* ═══ PRODUCT PANEL ═══ *@ @* ═══ PRODUCT PANEL ═══ *@
<div class="pos-product-panel"> <div class="pos-product-panel">
@@ -18,32 +19,47 @@
</div> </div>
</div> </div>
@* EN: Category tabs / VI: Tab danh mục *@ @if (_isLoading)
<div class="pos-category-tabs"> {
@foreach (var cat in _categories) <div style="display:flex;align-items:center;justify-content:center;flex:1;color:var(--pos-text-tertiary);">
{ Đang tải...
<button class="pos-category-tab @(cat == _selectedCategory ? "pos-category-tab--active" : "")" </div>
style="padding:12px 20px;font-size:15px;" }
@onclick="() => _selectedCategory = cat"> else if (_loadError)
@cat {
</button> <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> </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) *@ @* 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;"> <div class="pos-product-grid" style="grid-template-columns:repeat(auto-fill, minmax(160px, 1fr));gap:16px;padding:20px;">
@foreach (var product in FilteredProducts) @foreach (var product in FilteredProducts)
{ {
<div class="pos-product-card" style="padding:16px;" @onclick="() => AddToCart(product)"> <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;"> <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> <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> </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> </div>
<span style="font-size:11px;color:var(--pos-text-tertiary);">Kho: @product.Stock · @product.Sku</span> }
</div>
}
</div>
</div> </div>
@* ═══ CART SIDEBAR ═══ *@ @* ═══ CART SIDEBAR ═══ *@
@@ -96,30 +112,53 @@
</div> </div>
@code { @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 _selectedCategory = "Tất cả";
private string _barcodeInput = ""; private string _barcodeInput = "";
private readonly List<Product> _products = new() // EN: Product list from API / VI: Danh sách sản phẩm từ API
{ private 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"),
};
private readonly List<CartItem> _cartItems = new(); private readonly List<CartItem> _cartItems = new();
private IEnumerable<Product> FilteredProducts => private IEnumerable<Product> FilteredProducts =>
_selectedCategory == "Tất cả" ? _products : _products.Where(p => p.Category == _selectedCategory); _selectedCategory == "Tất cả" ? _products : _products.Where(p => p.Category == _selectedCategory);
private decimal CartTotal => _cartItems.Sum(i => i.Price * i.Qty); 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) private void AddToCart(Product product)
{ {
var existing = _cartItems.FirstOrDefault(i => i.Sku == product.Sku); var existing = _cartItems.FirstOrDefault(i => i.Sku == product.Sku);
@@ -135,7 +174,14 @@
private void Checkout() { } 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) private class CartItem(string name, string sku, decimal price)
{ {
public string Name { get; set; } = name; public string Name { get; set; } = name;

View File

@@ -5,6 +5,7 @@
@page "/pos/retail/product-search" @page "/pos/retail/product-search"
@layout PosLayout @layout PosLayout
@inherits PosBase @inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;"> <div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
@* ═══ HEADER ═══ *@ @* ═══ HEADER ═══ *@
@@ -56,6 +57,20 @@
</div> </div>
@* ═══ SEARCH RESULTS / KẾT QUẢ TÌM KIẾM ═══ *@ @* ═══ 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="flex:1;overflow-y:auto;padding:12px 16px;">
<div style="font-size:12px;color:var(--pos-text-tertiary);margin-bottom:12px;"> <div style="font-size:12px;color:var(--pos-text-tertiary);margin-bottom:12px;">
@FilteredResults.Count() kết quả @(!string.IsNullOrEmpty(_searchQuery) ? $"cho \"{_searchQuery}\"" : "") @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="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;"> <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: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>
</div> </div>
@@ -91,6 +103,7 @@
</div> </div>
} }
</div> </div>
}
@* ═══ CART SUMMARY BAR / THANH TÓM TẮT GIỎ ═══ *@ @* ═══ CART SUMMARY BAR / THANH TÓM TẮT GIỎ ═══ *@
@if (_cartItems.Any()) @if (_cartItems.Any())
@@ -112,33 +125,50 @@
</div> </div>
@code { @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 _filterCategory = "Tất cả";
private string _priceRange = "all"; 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 // EN: Product list from API / VI: Danh sách sản phẩm từ API
private readonly List<Product> _products = new() private 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"),
};
private readonly List<CartItem> _cartItems = 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 private IEnumerable<Product> FilteredResults
{ {
get get
@@ -167,7 +197,14 @@
else _cartItems.Add(new CartItem(product.Name, product.Sku, product.Price)); 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) private class CartItem(string name, string sku, decimal price)
{ {
public string Name { get; set; } = name; public string Name { get; set; } = name;

View File

@@ -151,6 +151,8 @@
</div> </div>
@code { @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 string _receiptInput = "R2024001234";
private bool _receiptFound = true; private bool _receiptFound = true;
private string _returnReason = ""; private string _returnReason = "";

View File

@@ -134,9 +134,9 @@
</div> </div>
@code { @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); 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 // EN: Branch stock data / VI: Dữ liệu tồn kho chi nhánh

View File

@@ -5,8 +5,23 @@
@page "/pos/spa/mobile" @page "/pos/spa/mobile"
@layout PosLayout @layout PosLayout
@inherits PosBase @inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div style="display:flex;flex-direction:column;width:100%;height:100%;overflow:hidden;"> <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 *@ @* EN: Category tabs / VI: Tab danh mục *@
<div class="pos-category-tabs" style="padding:8px 12px;"> <div class="pos-category-tabs" style="padding:8px 12px;">
@foreach (var cat in _categories) @foreach (var cat in _categories)
@@ -33,6 +48,7 @@
</div> </div>
} }
</div> </div>
}
@* EN: Floating appointment button / VI: Nút lịch hẹn nổi *@ @* EN: Floating appointment button / VI: Nút lịch hẹn nổi *@
@if (_appointmentItems.Any()) @if (_appointmentItems.Any())
@@ -97,31 +113,53 @@
</div> </div>
@code { @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 string _selectedCategory = "Tất cả";
private bool _showSheet; private bool _showSheet;
private readonly List<SpaService> _services = new() // EN: Service list from API / VI: Danh sách dịch vụ từ API
{ private 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: Appointment items / VI: Mục lịch hẹn
private readonly List<AppointmentItem> _appointmentItems = new(); private readonly List<AppointmentItem> _appointmentItems = new();
private IEnumerable<SpaService> FilteredServices => private IEnumerable<SpaService> FilteredServices =>
_selectedCategory == "Tất cả" ? _services : _services.Where(s => s.Category == _selectedCategory); _selectedCategory == "Tất cả" ? _services : _services.Where(s => s.Category == _selectedCategory);
private decimal AppointmentTotal => _appointmentItems.Sum(i => i.Price); 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) private void AddToAppointment(SpaService svc)
{ {
_appointmentItems.Add(new AppointmentItem(svc.Name, svc.Price, svc.Duration)); _appointmentItems.Add(new AppointmentItem(svc.Name, svc.Price, svc.Duration));
@@ -135,6 +173,7 @@
"Nail" => "paintbrush", "Hair" => "scissors", _ => "heart" "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 SpaService(string Name, decimal Price, int Duration, string Category);
private record AppointmentItem(string Name, decimal Price, int Duration); private record AppointmentItem(string Name, decimal Price, int Duration);
} }

View File

@@ -5,9 +5,24 @@
@page "/pos/spa/tablet" @page "/pos/spa/tablet"
@layout PosLayout @layout PosLayout
@inherits PosBase @inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
@* ═══ SERVICE PANEL / PANEL DỊCH VỤ ═══ *@ @* ═══ SERVICE PANEL / PANEL DỊCH VỤ ═══ *@
<div class="pos-product-panel"> <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"> <div class="pos-category-tabs">
@foreach (var cat in _categories) @foreach (var cat in _categories)
{ {
@@ -34,6 +49,7 @@
</div> </div>
} }
</div> </div>
}
</div> </div>
@* ═══ APPOINTMENT SIDEBAR / SIDEBAR LỊCH HẸN ═══ *@ @* ═══ APPOINTMENT SIDEBAR / SIDEBAR LỊCH HẸN ═══ *@
@@ -85,30 +101,52 @@
</div> </div>
@code { @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 string _selectedCategory = "Tất cả";
private readonly List<SpaService> _services = new() // EN: Service list from API / VI: Danh sách dịch vụ từ API
{ private 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: Appointment items / VI: Mục lịch hẹn
private readonly List<AppointmentItem> _appointmentItems = new(); private readonly List<AppointmentItem> _appointmentItems = new();
private IEnumerable<SpaService> FilteredServices => private IEnumerable<SpaService> FilteredServices =>
_selectedCategory == "Tất cả" ? _services : _services.Where(s => s.Category == _selectedCategory); _selectedCategory == "Tất cả" ? _services : _services.Where(s => s.Category == _selectedCategory);
private decimal AppointmentTotal => _appointmentItems.Sum(i => i.Price); 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) private void AddToAppointment(SpaService svc)
{ {
_appointmentItems.Add(new AppointmentItem(svc.Name, svc.Price, svc.Duration)); _appointmentItems.Add(new AppointmentItem(svc.Name, svc.Price, svc.Duration));
@@ -122,6 +160,7 @@
"Nail" => "paintbrush", "Hair" => "scissors", _ => "heart" "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 SpaService(string Name, decimal Price, int Duration, string Category);
private record AppointmentItem(string Name, decimal Price, int Duration); private record AppointmentItem(string Name, decimal Price, int Duration);
} }

View File

@@ -5,6 +5,7 @@
@page "/pos/spa/appointment-book" @page "/pos/spa/appointment-book"
@layout PosLayout @layout PosLayout
@inherits PosBase @inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div style="flex:1;display:flex;overflow:hidden;"> <div style="flex:1;display:flex;overflow:hidden;">
@* ═══ SCHEDULE PANEL (LEFT) / PANEL LỊCH (TRÁI) ═══ *@ @* ═══ 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> <span style="font-size:16px;font-weight:700;">Đặt lịch hẹn</span>
</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
{
@* ═══ DATE PICKER / CHỌN NGÀY ═══ *@ @* ═══ DATE PICKER / CHỌN NGÀY ═══ *@
<div style="margin-bottom:16px;"> <div style="margin-bottom:16px;">
<div style="font-size:13px;font-weight:600;margin-bottom:8px;">Chọn ngày</div> <div style="font-size:13px;font-weight:600;margin-bottom:8px;">Chọn ngày</div>
@@ -81,6 +96,7 @@
</button> </button>
} }
</div> </div>
}
</div> </div>
@* ═══ BOOKING SUMMARY (RIGHT) / TÓM TẮT ĐẶT LỊCH (PHẢI) ═══ *@ @* ═══ BOOKING SUMMARY (RIGHT) / TÓM TẮT ĐẶT LỊCH (PHẢI) ═══ *@
@@ -143,6 +159,13 @@
</div> </div>
@code { @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 _selectedDate = "Hôm nay";
private string? _selectedTime = "10:00"; private string? _selectedTime = "10:00";
private string _selectedStaff = "Chị Hoa"; private string _selectedStaff = "Chị Hoa";
@@ -159,25 +182,51 @@
// EN: Staff list / VI: Danh sách nhân viên // 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ỳ" }; private readonly string[] _staffList = { "Chị Hoa", "Anh Minh", "Chị Lan", "Chị Trang", "Bất kỳ" };
// EN: Time slots / VI: Khung giờ // EN: Time slots from API / VI: Khung giờ từ API
private readonly List<TimeSlot> _timeSlots = new() private 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: Demo selected services / VI: Dịch vụ đã chọn mẫu // EN: Selected services from API / VI: Dịch vụ đã chọn từ API
private readonly List<ServiceInfo> _selectedServices = new() private List<ServiceInfo> _selectedServices = new();
protected override async Task OnInitializedAsync()
{ {
new("Massage toàn thân", 500_000, 60), try
new("Facial collagen", 600_000, 60), {
}; 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 DateOption(string Label, string Day, string Value);
private record TimeSlot(string Time, string Status); private record TimeSlot(string Time, string Status);

View File

@@ -86,9 +86,9 @@
</div> </div>
@code { @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() private readonly List<CustomerInfo> _customers = new()
{ {
new("Nguyễn Thị Mai", "0901234567", "Gold", "15/02/2025", 28), new("Nguyễn Thị Mai", "0901234567", "Gold", "15/02/2025", 28),

View File

@@ -139,9 +139,9 @@
</div> </div>
@code { @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() private readonly List<VisitInfo> _visitHistory = new()
{ {
new("Massage toàn thân + Facial", "15/02/2025", "Chị Hoa", 850_000, 85), new("Massage toàn thân + Facial", "15/02/2025", "Chị Hoa", 850_000, 85),

View File

@@ -118,9 +118,9 @@
</div> </div>
@code { @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() 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í", 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" @page "/pos/spa/service-package"
@layout PosLayout @layout PosLayout
@inherits PosBase @inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;"> <div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
@* ═══ HEADER / TIÊU ĐỀ ═══ *@ @* ═══ HEADER / TIÊU ĐỀ ═══ *@
@@ -19,6 +20,20 @@
</div> </div>
@* ═══ PACKAGE LIST / DANH SÁCH GÓI ═══ *@ @* ═══ 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="flex:1;overflow-y:auto;padding:16px;">
<div style="display:flex;flex-direction:column;gap:12px;"> <div style="display:flex;flex-direction:column;gap:12px;">
@foreach (var pkg in _packages) @foreach (var pkg in _packages)
@@ -100,43 +115,72 @@
} }
</div> </div>
</div> </div>
}
</div> </div>
@code { @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"; private string? _expandedId = "PKG1";
// EN: Demo packages / VI: Gói dịch vụ mẫu // EN: Packages built from DB services / VI: Gói dịch vụ xây dựng từ dữ liệu DB
private readonly List<PackageInfo> _packages = new() 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("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("Massage toàn thân", 500_000, 60), new("PKG2", "Gói VIP", 0.8m, true, "crown", "rgba(245,158,11,.15)", "#F59E0B",
new("Facial cơ bản", 350_000, 45), 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("Gội đầu dưỡng sinh", 200_000, 30), 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("PKG2", "Gói VIP", 1_800_000, 2_250_000, 5, 225, true, "crown", "rgba(245,158,11,.15)", "#F59E0B", new() 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" }),
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),
}),
}; };
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 PackageService(string Name, decimal Price, int Duration);
private record PackageInfo(string Id, string Name, decimal Price, decimal OriginalPrice, int ServiceCount, 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); int TotalDuration, bool Popular, string Icon, string BgColor, string FgColor, List<PackageService> Services);

View File

@@ -269,9 +269,9 @@
</div> </div>
@code { @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() private readonly List<StepInfo> _steps = new()
{ {
new("Check-in", "check-in", "Tiếp tục"), new("Check-in", "check-in", "Tiếp tục"),

View File

@@ -112,6 +112,8 @@
</div> </div>
@code { @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 _activeFilter = "Tất cả";
private string? _selectedStaff = "S01"; private string? _selectedStaff = "S01";
private readonly string[] _filters = { "Tất cả", "Rảnh", "Đang bận", "Nghỉ giải lao" }; private readonly string[] _filters = { "Tất cả", "Rảnh", "Đang bận", "Nghỉ giải lao" };

View File

@@ -117,10 +117,10 @@
</div> </div>
@code { @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ờ // EN: Hours range / VI: Phạm vi giờ
private readonly int[] _hours = { 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 }; 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() private readonly List<StaffSchedule> _scheduleData = new()
{ {
new("Trần Thị Hoa", "Massage", new() new("Trần Thị Hoa", "Massage", new()

View File

@@ -158,6 +158,8 @@
</div> </div>
@code { @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 string _remainingTime = "45:00";
private int _totalDuration = 60; private int _totalDuration = 60;
private double _progress = 0.25; private double _progress = 0.25;