Files
pos-system/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Spa/SpaDesktop.razor

197 lines
8.4 KiB
Plaintext

@*
EN: Spa POS Desktop — 2-panel layout: service categories + grid (left), current appointment/bill (right).
VI: POS Spa Desktop — Bố cục 2 panel: danh mục dịch vụ + lưới (trái), lịch hẹn/hóa đơn hiện tại (phải).
*@
@page "/pos/{ShopId:guid}/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;">
@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>
</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>
@* ═══ APPOINTMENT PANEL (RIGHT) / PANEL LỊCH HẸN (PHẢI) ═══ *@
<div class="pos-cart-panel">
<div class="pos-cart-header">
<span class="pos-cart-header__title">Lịch hẹn</span>
<span style="font-size:12px;color:var(--pos-text-tertiary);">@_appointmentItems.Count dịch vụ</span>
</div>
@* EN: Customer info / VI: Thông tin khách *@
@if (_customerName is not null)
{
<div style="padding:8px 12px;display:flex;align-items:center;gap:10px;border-bottom:1px solid var(--pos-border-subtle);">
<div style="width:36px;height:36px;border-radius:50%;background:var(--pos-orange-primary);display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:700;color:#fff;">
@_customerName[..1]
</div>
<div>
<div style="font-size:13px;font-weight:600;">@_customerName</div>
<div style="font-size:11px;color:var(--pos-text-tertiary);">@_customerPhone • @_customerTier</div>
</div>
</div>
}
else
{
<div style="padding:8px 12px;border-bottom:1px solid var(--pos-border-subtle);">
<button style="width:100%;padding:10px;border-radius:var(--pos-radius);background:var(--pos-bg-interactive);border:1px dashed var(--pos-border-default);color:var(--pos-text-tertiary);cursor:pointer;font-size:13px;"
@onclick="@(() => NavigateTo("spa/customer-lookup"))">
<i data-lucide="user-plus" style="width:14px;height:14px;display:inline;"></i> Chọn khách hàng
</button>
</div>
}
<div class="pos-cart-items">
@foreach (var item in _appointmentItems)
{
<div class="pos-cart-item">
<div class="pos-cart-item__info">
<span class="pos-cart-item__name">@item.Name</span>
<div style="display:flex;align-items:center;gap:8px;">
<span class="pos-cart-item__price">@FormatPrice(item.Price)</span>
<span style="font-size:11px;color:var(--pos-text-tertiary);">@item.Duration phút</span>
</div>
</div>
<div class="pos-cart-item__qty">
<button @onclick="() => RemoveItem(item)">
<i data-lucide="x" style="width:14px;height:14px;"></i>
</button>
</div>
</div>
}
</div>
<div class="pos-cart-footer">
@* EN: Duration total / VI: Tổng thời gian *@
<div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px;">
<span style="color:var(--pos-text-secondary);">Tổng thời gian</span>
<span>@_appointmentItems.Sum(i => i.Duration) phút</span>
</div>
<div class="pos-cart-total">
<span class="pos-cart-total__label">Tổng cộng</span>
<span class="pos-cart-total__value">@FormatPrice(AppointmentTotal)</span>
</div>
<div style="display:flex;gap:8px;">
<button style="flex:1;padding:12px;border-radius:var(--pos-radius);background:var(--pos-bg-interactive);border:1px solid var(--pos-border-default);color:var(--pos-text-primary);cursor:pointer;font-size:13px;font-weight:600;"
@onclick="@(() => NavigateTo("spa/appointment-book"))">
<i data-lucide="calendar" style="width:16px;height:16px;display:inline;"></i> Đặt lịch
</button>
<button class="pos-btn-checkout" style="flex:1;" @onclick="Checkout">
<i data-lucide="credit-card" style="width:18px;height:18px;"></i> Thanh toán
</button>
</div>
</div>
</div>
@code {
// 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ả";
// EN: Demo customer / VI: Khách hàng mẫu
private string? _customerName = "Nguyễn Thị Mai";
private string _customerPhone = "0901234567";
private string _customerTier = "Gold";
// 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(ShopId);
_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));
}
private void RemoveItem(AppointmentItem item) => _appointmentItems.Remove(item);
private void Checkout() => NavigateTo("spa/spa-journey");
private static string GetCategoryIcon(string category) => category switch
{
"Massage" => "hand", "Facial" => "sparkles", "Body" => "bath",
"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);
}