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:
Cursor Agent
2026-02-26 20:15:20 +00:00
parent 5d02accd29
commit f3c1a86da6
4 changed files with 244 additions and 119 deletions

View File

@@ -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);

View File

@@ -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

View File

@@ -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

View File

@@ -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));