refactor(tpos): replace hardcoded POS data with BFF API calls
- CafeDesktop: load products & categories from DataService.GetProductsAsync and GetCategoriesAsync with cafe shop ID - RestaurantDesktop: load tables from DataService.GetTablesAsync with restaurant shop ID, map zones from API data - KaraokeDesktop: keep mock data, add TODO comment for future FnB engine rooms endpoint integration - SpaDesktop: load spa services from DataService.GetProductsAsync with spa shop ID, map duration from product attributes - All refactored pages show loading state and error handling Co-authored-by: Velik <hongochai10@users.noreply.github.com>
This commit is contained in:
@@ -5,33 +5,49 @@
|
||||
@page "/pos/cafe"
|
||||
@layout PosLayout
|
||||
@inherits PosBase
|
||||
@inject WebClientTpos.Client.Services.PosDataService DataService
|
||||
|
||||
@* ═══ PRODUCT PANEL ═══ *@
|
||||
<div class="pos-product-panel">
|
||||
@* 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;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">
|
||||
@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="coffee" 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="coffee" style="width:32px;height:32px;color:var(--pos-text-tertiary);"></i>
|
||||
</div>
|
||||
<span class="pos-product-card__name">@product.Name</span>
|
||||
<span class="pos-product-card__price">@FormatPrice(product.Price)</span>
|
||||
</div>
|
||||
<span class="pos-product-card__name">@product.Name</span>
|
||||
<span class="pos-product-card__price">@FormatPrice(product.Price)</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@* ═══ CART PANEL ═══ *@
|
||||
@@ -71,29 +87,19 @@
|
||||
</div>
|
||||
|
||||
@code {
|
||||
// EN: Cafe shop ID / VI: ID cửa hàng cafe
|
||||
private static readonly Guid CafeShopId = 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ả", "Cà phê", "Trà", "Sinh tố", "Đồ ăn" };
|
||||
private string[] _categories = { "Tất cả" };
|
||||
private string _selectedCategory = "Tất cả";
|
||||
|
||||
// EN: Product list / VI: Danh sách sản phẩm
|
||||
private readonly List<Product> _products = new()
|
||||
{
|
||||
new("Cà phê sữa đá", 35_000, "Cà phê"),
|
||||
new("Cà phê đen", 29_000, "Cà phê"),
|
||||
new("Bạc xỉu", 39_000, "Cà phê"),
|
||||
new("Espresso", 45_000, "Cà phê"),
|
||||
new("Cappuccino", 55_000, "Cà phê"),
|
||||
new("Latte", 55_000, "Cà phê"),
|
||||
new("Trà đào", 45_000, "Trà"),
|
||||
new("Trà vải", 45_000, "Trà"),
|
||||
new("Trà sen vàng", 49_000, "Trà"),
|
||||
new("Sinh tố bơ", 55_000, "Sinh tố"),
|
||||
new("Sinh tố xoài", 49_000, "Sinh tố"),
|
||||
new("Sinh tố dâu", 49_000, "Sinh tố"),
|
||||
new("Bánh mì", 25_000, "Đồ ăn"),
|
||||
new("Croissant", 35_000, "Đồ ăn"),
|
||||
new("Cookie", 20_000, "Đồ ăn"),
|
||||
};
|
||||
private List<Product> _products = new();
|
||||
|
||||
// EN: Cart items / VI: Mục giỏ hàng
|
||||
private readonly List<CartItem> _cartItems = new();
|
||||
@@ -101,6 +107,42 @@
|
||||
_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 productsTask = DataService.GetProductsAsync(CafeShopId);
|
||||
var categoriesTask = DataService.GetCategoriesAsync(CafeShopId);
|
||||
await Task.WhenAll(productsTask, categoriesTask);
|
||||
|
||||
var apiProducts = await productsTask;
|
||||
var apiCategories = await categoriesTask;
|
||||
|
||||
_products = apiProducts.Select(p => new Product(
|
||||
p.Name,
|
||||
p.Price,
|
||||
p.Category ?? "Khác"
|
||||
)).ToList();
|
||||
|
||||
var catNames = apiCategories.Select(c => c.Name).ToList();
|
||||
if (catNames.Count > 0)
|
||||
_categories = new[] { "Tất cả" }.Concat(catNames).ToArray();
|
||||
else
|
||||
{
|
||||
var productCats = _products.Select(p => p.Category).Distinct().ToList();
|
||||
_categories = new[] { "Tất cả" }.Concat(productCats).ToArray();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
_loadError = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddToCart(Product product)
|
||||
{
|
||||
var existing = _cartItems.FirstOrDefault(i => i.Name == product.Name);
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
@*
|
||||
EN: Karaoke POS Desktop — Room map grid + session panel for karaoke room management.
|
||||
TODO: Replace mock room data with API call to FnB engine rooms endpoint
|
||||
(e.g. DataService.GetTablesAsync with room-type filter) when the karaoke-specific
|
||||
table/room schema is implemented in the FnB engine database.
|
||||
VI: POS Karaoke Desktop — Lưới sơ đồ phòng + panel phiên hát cho quản lý phòng karaoke.
|
||||
TODO: Thay dữ liệu phòng giả bằng API call đến FnB engine rooms endpoint
|
||||
(ví dụ DataService.GetTablesAsync với bộ lọc loại phòng) khi schema bàn/phòng
|
||||
karaoke được triển khai trong cơ sở dữ liệu FnB engine.
|
||||
*@
|
||||
@page "/pos/karaoke"
|
||||
@layout PosLayout
|
||||
|
||||
@@ -5,35 +5,51 @@
|
||||
@page "/pos/restaurant"
|
||||
@layout PosLayout
|
||||
@inherits PosBase
|
||||
@inject WebClientTpos.Client.Services.PosDataService DataService
|
||||
|
||||
<div class="pos-product-panel" style="padding:16px;overflow-y:auto;">
|
||||
@* ═══ SECTION TABS / TAB KHU VỰC ═══ *@
|
||||
<div class="pos-category-tabs">
|
||||
@foreach (var section in _sections)
|
||||
{
|
||||
<button class="pos-category-tab @(section == _activeSection ? "pos-category-tab--active" : "")"
|
||||
@onclick="() => _activeSection = section">
|
||||
@(section)
|
||||
</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 TABS / TAB KHU VỰC ═══ *@
|
||||
<div class="pos-category-tabs">
|
||||
@foreach (var section in _sections)
|
||||
{
|
||||
<button class="pos-category-tab @(section == _activeSection ? "pos-category-tab--active" : "")"
|
||||
@onclick="() => _activeSection = section">
|
||||
@(section)
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@* ═══ TABLE MAP GRID / LƯỚI SƠ ĐỒ BÀN ═══ *@
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:12px;padding:8px 0;">
|
||||
@foreach (var table in FilteredTables)
|
||||
{
|
||||
<div @onclick="() => SelectTable(table)"
|
||||
style="background:@GetStatusColor(table.Status);border-radius:var(--pos-radius);
|
||||
padding:16px;text-align:center;cursor:pointer;border:2px solid @(SelectedTable?.Id == table.Id ? "var(--pos-orange-primary)" : "transparent");
|
||||
transition:all .2s ease;">
|
||||
<div style="font-size:20px;font-weight:700;">@table.Name</div>
|
||||
<div style="font-size:12px;color:rgba(255,255,255,.7);margin-top:4px;">@table.Seats chỗ</div>
|
||||
<div style="font-size:11px;margin-top:6px;font-weight:600;text-transform:uppercase;">
|
||||
@GetStatusLabel(table.Status)
|
||||
@* ═══ TABLE MAP GRID / LƯỚI SƠ ĐỒ BÀN ═══ *@
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:12px;padding:8px 0;">
|
||||
@foreach (var table in FilteredTables)
|
||||
{
|
||||
<div @onclick="() => SelectTable(table)"
|
||||
style="background:@GetStatusColor(table.Status);border-radius:var(--pos-radius);
|
||||
padding:16px;text-align:center;cursor:pointer;border:2px solid @(SelectedTable?.Id == table.Id ? "var(--pos-orange-primary)" : "transparent");
|
||||
transition:all .2s ease;">
|
||||
<div style="font-size:20px;font-weight:700;">@table.Name</div>
|
||||
<div style="font-size:12px;color:rgba(255,255,255,.7);margin-top:4px;">@table.Seats chỗ</div>
|
||||
<div style="font-size:11px;margin-top:6px;font-weight:600;text-transform:uppercase;">
|
||||
@GetStatusLabel(table.Status)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@* ═══ ORDER PANEL (RIGHT) / PANEL ĐẶT MÓN (PHẢI) ═══ *@
|
||||
@@ -86,23 +102,22 @@
|
||||
</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? SelectedTable { get; set; }
|
||||
|
||||
// EN: Demo table data / VI: Dữ liệu bàn mẫu
|
||||
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", "Trong nhà"),
|
||||
new("T05","Bàn 5", 2, "available", "Trong nhà"), new("T06","Bàn 6", 8, "available", "VIP"),
|
||||
new("T07","Bàn 7", 4, "occupied", "Ngoài trời"), new("T08","Bàn 8", 4, "available", "Ngoài trời"),
|
||||
new("T09","Bàn 9", 10, "reserved", "VIP"), new("T10","Bàn 10", 2, "available", "Ngoài trời"),
|
||||
new("T11","Bàn 11", 6, "occupied", "VIP"), new("T12","Bàn 12", 4, "available", "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);
|
||||
@@ -114,6 +129,33 @@
|
||||
new("Cơm tấm sườn", 65_000, 1), new("Trà đá", 10_000, 3),
|
||||
};
|
||||
|
||||
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 void SelectTable(TableInfo table) => SelectedTable = table;
|
||||
|
||||
private static string GetStatusColor(string status) => status switch
|
||||
|
||||
@@ -5,36 +5,52 @@
|
||||
@page "/pos/spa"
|
||||
@layout PosLayout
|
||||
@inherits PosBase
|
||||
@inject WebClientTpos.Client.Services.PosDataService DataService
|
||||
|
||||
@* ═══ SERVICE PANEL (LEFT) / PANEL DỊCH VỤ (TRÁI) ═══ *@
|
||||
<div class="pos-product-panel" style="padding:16px;overflow-y:auto;">
|
||||
@* 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;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">
|
||||
@foreach (var cat in _categories)
|
||||
{
|
||||
<button class="pos-category-tab @(cat == _selectedCategory ? "pos-category-tab--active" : "")"
|
||||
@onclick="() => _selectedCategory = cat">
|
||||
@cat
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@* ═══ SERVICE GRID / LƯỚI DỊCH VỤ ═══ *@
|
||||
<div class="pos-product-grid">
|
||||
@foreach (var svc in FilteredServices)
|
||||
{
|
||||
<div class="pos-product-card" @onclick="() => AddToAppointment(svc)">
|
||||
<div class="pos-product-card__image" style="display:flex;align-items:center;justify-content:center;">
|
||||
<i data-lucide="@GetCategoryIcon(svc.Category)" style="width:32px;height:32px;color:var(--pos-text-tertiary);"></i>
|
||||
@* ═══ SERVICE GRID / LƯỚI DỊCH VỤ ═══ *@
|
||||
<div class="pos-product-grid">
|
||||
@foreach (var svc in FilteredServices)
|
||||
{
|
||||
<div class="pos-product-card" @onclick="() => AddToAppointment(svc)">
|
||||
<div class="pos-product-card__image" style="display:flex;align-items:center;justify-content:center;">
|
||||
<i data-lucide="@GetCategoryIcon(svc.Category)" style="width:32px;height:32px;color:var(--pos-text-tertiary);"></i>
|
||||
</div>
|
||||
<span class="pos-product-card__name">@svc.Name</span>
|
||||
<span class="pos-product-card__price">@FormatPrice(svc.Price)</span>
|
||||
<span style="font-size:11px;color:var(--pos-text-tertiary);">
|
||||
<i data-lucide="clock" style="width:10px;height:10px;display:inline;"></i> @svc.Duration phút
|
||||
</span>
|
||||
</div>
|
||||
<span class="pos-product-card__name">@svc.Name</span>
|
||||
<span class="pos-product-card__price">@FormatPrice(svc.Price)</span>
|
||||
<span style="font-size:11px;color:var(--pos-text-tertiary);">
|
||||
<i data-lucide="clock" style="width:10px;height:10px;display:inline;"></i> @svc.Duration phút
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@* ═══ APPOINTMENT PANEL (RIGHT) / PANEL LỊCH HẸN (PHẢI) ═══ *@
|
||||
@@ -110,8 +126,15 @@
|
||||
</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;
|
||||
|
||||
// EN: Categories / VI: Danh mục
|
||||
private readonly string[] _categories = { "Tất cả", "Massage", "Facial", "Body", "Nail", "Hair" };
|
||||
private string[] _categories = { "Tất cả" };
|
||||
private string _selectedCategory = "Tất cả";
|
||||
|
||||
// EN: Demo customer / VI: Khách hàng mẫu
|
||||
@@ -119,22 +142,8 @@
|
||||
private string _customerPhone = "0901234567";
|
||||
private string _customerTier = "Gold";
|
||||
|
||||
// EN: Service list / VI: Danh sách dịch vụ
|
||||
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();
|
||||
@@ -142,6 +151,32 @@
|
||||
_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));
|
||||
|
||||
Reference in New Issue
Block a user