197 lines
8.4 KiB
Plaintext
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);
|
|
}
|