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:
@@ -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
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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í",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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" };
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user