feat(web-client-tpos): unify POS with inline payment and tabs (path fix)
This commit is contained in:
@@ -1,105 +1,391 @@
|
||||
@*
|
||||
EN: Café POS Desktop — 3-column layout: categories, product grid, cart.
|
||||
VI: POS Café Desktop — Bố cục 3 cột: danh mục, lưới sản phẩm, giỏ hàng.
|
||||
EN: Café POS Desktop — Unified single-page with tabs: Sale, History, Dashboard.
|
||||
Payment flow is inline (cart panel transforms to payment panel).
|
||||
VI: POS Café Desktop — Trang đơn với tabs: Bán hàng, Lịch sử, Dashboard.
|
||||
Thanh toán inline (panel giỏ hàng chuyển thành panel thanh toán).
|
||||
*@
|
||||
@page "/pos/{ShopId:guid}/cafe"
|
||||
@layout PosLayout
|
||||
@inherits PosBase
|
||||
@inject WebClientTpos.Client.Services.PosDataService DataService
|
||||
|
||||
@* ═══ PRODUCT PANEL ═══ *@
|
||||
<div class="pos-product-panel">
|
||||
@if (_isLoading)
|
||||
@* ═══════════════ MAIN CONTENT AREA ═══════════════ *@
|
||||
<div class="pos-content-area">
|
||||
@switch (_activeTab)
|
||||
{
|
||||
<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>
|
||||
case PosTab.Sale:
|
||||
@* ═══ PRODUCT PANEL (LEFT) ═══ *@
|
||||
<div class="pos-product-panel @(_paymentStep != PayStep.None ? "pos-product-panel--dimmed" : "")">
|
||||
@if (_isLoading)
|
||||
{
|
||||
<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--pos-text-tertiary);">
|
||||
Đang tải...
|
||||
</div>
|
||||
<span class="pos-product-card__name">@product.Name</span>
|
||||
<span class="pos-product-card__price">@FormatPrice(product.Price)</span>
|
||||
}
|
||||
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)
|
||||
{
|
||||
<button class="pos-category-tab @(cat == _selectedCategory ? "pos-category-tab--active" : "")"
|
||||
@onclick="() => _selectedCategory = cat">
|
||||
@cat
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@* ═══ CART / PAYMENT PANEL (RIGHT) ═══ *@
|
||||
<div class="pos-cart-panel">
|
||||
@if (_paymentStep == PayStep.None)
|
||||
{
|
||||
@* ─── NORMAL CART MODE ─── *@
|
||||
<div class="pos-cart-header">
|
||||
<span class="pos-cart-header__title">Đơn hàng</span>
|
||||
<span style="font-size:12px;color:var(--pos-text-tertiary);">@_cartItems.Count món</span>
|
||||
</div>
|
||||
<div class="pos-cart-items">
|
||||
@foreach (var item in _cartItems)
|
||||
{
|
||||
<div class="pos-cart-item">
|
||||
<div class="pos-cart-item__info">
|
||||
<span class="pos-cart-item__name">@item.Name</span>
|
||||
<span class="pos-cart-item__price">@FormatPrice(item.Price)</span>
|
||||
</div>
|
||||
<div class="pos-cart-item__qty">
|
||||
<button @onclick="() => ChangeQty(item, -1)">−</button>
|
||||
<span style="font-size:14px;font-weight:600;min-width:20px;text-align:center;">@item.Qty</span>
|
||||
<button @onclick="() => ChangeQty(item, 1)">+</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="pos-cart-footer">
|
||||
<div class="pos-cart-total">
|
||||
<span class="pos-cart-total__label">Tổng cộng</span>
|
||||
<span class="pos-cart-total__value">@FormatPrice(CartTotal)</span>
|
||||
</div>
|
||||
<button class="pos-btn-checkout" @onclick="StartPayment" disabled="@(!_cartItems.Any())">
|
||||
<i data-lucide="credit-card" style="width:18px;height:18px;"></i>
|
||||
Thanh toán
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
else if (_paymentStep == PayStep.MethodSelect)
|
||||
{
|
||||
@* ─── PAYMENT: METHOD SELECT ─── *@
|
||||
<div class="pos-payment-panel">
|
||||
<div class="pos-payment-header">
|
||||
<button class="pos-payment-header__back" @onclick="CancelPayment">
|
||||
<i data-lucide="arrow-left" style="width:16px;height:16px;"></i>
|
||||
</button>
|
||||
<span style="font-weight:600;">Thanh toán</span>
|
||||
<span style="margin-left:auto;font-size:18px;font-weight:700;color:var(--pos-orange-primary);">@FormatPrice(CartTotal)</span>
|
||||
</div>
|
||||
<div class="pos-payment-methods">
|
||||
<button class="pos-payment-method-btn" @onclick='() => SelectPaymentMethod("cash")'>
|
||||
<span class="pos-payment-method-btn__icon">💵</span>
|
||||
<span class="pos-payment-method-btn__label">Tiền mặt</span>
|
||||
</button>
|
||||
<button class="pos-payment-method-btn" @onclick='() => SelectPaymentMethod("card")'>
|
||||
<span class="pos-payment-method-btn__icon">💳</span>
|
||||
<span class="pos-payment-method-btn__label">Thẻ</span>
|
||||
</button>
|
||||
<button class="pos-payment-method-btn" @onclick='() => SelectPaymentMethod("qr")'>
|
||||
<span class="pos-payment-method-btn__icon">📱</span>
|
||||
<span class="pos-payment-method-btn__label">Mã QR</span>
|
||||
</button>
|
||||
<button class="pos-payment-method-btn" @onclick='() => SelectPaymentMethod("transfer")'>
|
||||
<span class="pos-payment-method-btn__icon">🏦</span>
|
||||
<span class="pos-payment-method-btn__label">Chuyển khoản</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (_paymentStep == PayStep.AmountInput)
|
||||
{
|
||||
@* ─── PAYMENT: CASH AMOUNT INPUT ─── *@
|
||||
<div class="pos-payment-panel">
|
||||
<div class="pos-payment-header">
|
||||
<button class="pos-payment-header__back" @onclick="() => _paymentStep = PayStep.MethodSelect">
|
||||
<i data-lucide="arrow-left" style="width:16px;height:16px;"></i>
|
||||
</button>
|
||||
<span style="font-weight:600;">💵 Tiền mặt</span>
|
||||
<span style="margin-left:auto;font-size:16px;font-weight:700;color:var(--pos-orange-primary);">@FormatPrice(CartTotal)</span>
|
||||
</div>
|
||||
<div class="pos-payment-amount-section">
|
||||
<div style="font-size:13px;font-weight:600;color:var(--pos-text-secondary);margin-bottom:10px;">Số tiền nhanh</div>
|
||||
<div class="pos-payment-quick-amounts">
|
||||
@foreach (var qa in GetQuickAmounts())
|
||||
{
|
||||
<button class="pos-payment-quick-btn @(_receivedAmount == qa.Value ? "pos-payment-quick-btn--selected" : "")"
|
||||
@onclick="() => _receivedAmount = qa.Value">
|
||||
@qa.Label
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div style="font-size:13px;font-weight:600;color:var(--pos-text-secondary);margin-bottom:8px;">Nhập số tiền</div>
|
||||
<input type="number" class="pos-payment-input" placeholder="Nhập số tiền..."
|
||||
@bind="_customAmountInput" @bind:event="oninput" />
|
||||
@if (!string.IsNullOrEmpty(_customAmountInput))
|
||||
{
|
||||
<button style="width:100%;padding:10px;border-radius:var(--pos-radius);border:none;background:var(--pos-bg-interactive);color:var(--pos-text-primary);cursor:pointer;font-size:13px;font-weight:600;margin-bottom:16px;"
|
||||
@onclick="ApplyCustomAmount">Áp dụng</button>
|
||||
}
|
||||
<div class="pos-payment-change">
|
||||
<div class="pos-payment-change__row">
|
||||
<span style="color:var(--pos-text-tertiary);">Khách đưa</span>
|
||||
<span style="font-weight:600;">@FormatPrice(_receivedAmount)</span>
|
||||
</div>
|
||||
<div class="pos-payment-change__row" style="padding-top:8px;border-top:1px dashed var(--pos-border-subtle);">
|
||||
<span style="color:var(--pos-text-tertiary);">Tiền thối</span>
|
||||
<span style="font-size:18px;font-weight:700;color:@(ChangeAmount >= 0 ? "var(--pos-success)" : "var(--pos-danger)");">
|
||||
@FormatPrice(ChangeAmount)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding:16px;border-top:1px solid var(--pos-border-subtle);">
|
||||
<button class="pos-btn-checkout" @onclick="ConfirmPayment" disabled="@(_receivedAmount < CartTotal)">
|
||||
Xác nhận thanh toán
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (_paymentStep == PayStep.Processing)
|
||||
{
|
||||
@* ─── PAYMENT: QR/CARD/TRANSFER — Confirm ─── *@
|
||||
<div class="pos-payment-panel">
|
||||
<div class="pos-payment-header">
|
||||
<button class="pos-payment-header__back" @onclick="() => _paymentStep = PayStep.MethodSelect">
|
||||
<i data-lucide="arrow-left" style="width:16px;height:16px;"></i>
|
||||
</button>
|
||||
<span style="font-weight:600;">@GetMethodLabel()</span>
|
||||
</div>
|
||||
<div style="flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:20px;padding:24px;">
|
||||
<div style="font-size:32px;font-weight:700;color:var(--pos-orange-primary);">@FormatPrice(CartTotal)</div>
|
||||
@if (_selectedMethod == "qr")
|
||||
{
|
||||
<div style="width:180px;height:180px;background:white;border-radius:12px;display:flex;align-items:center;justify-content:center;">
|
||||
<div style="color:#333;font-size:14px;font-weight:600;text-align:center;">QR Code<br/>VietQR</div>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--pos-text-tertiary);">Quét mã bằng app Ngân hàng / MoMo / ZaloPay</div>
|
||||
}
|
||||
else if (_selectedMethod == "card")
|
||||
{
|
||||
<div style="font-size:48px;">💳</div>
|
||||
<div style="font-size:14px;color:var(--pos-text-secondary);">Chạm, quẹt hoặc cắm thẻ</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div style="font-size:48px;">🏦</div>
|
||||
<div style="font-size:14px;color:var(--pos-text-secondary);">Xác nhận đã nhận chuyển khoản</div>
|
||||
}
|
||||
</div>
|
||||
<div style="padding:16px;border-top:1px solid var(--pos-border-subtle);">
|
||||
<button class="pos-btn-checkout" @onclick="ConfirmPayment">
|
||||
Xác nhận đã thanh toán
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (_paymentStep == PayStep.Success)
|
||||
{
|
||||
@* ─── PAYMENT: SUCCESS ─── *@
|
||||
<div class="pos-payment-success">
|
||||
<div class="pos-payment-success__circle">
|
||||
<div class="pos-payment-success__inner">
|
||||
<i data-lucide="check" style="width:28px;height:28px;color:#fff;stroke-width:3;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size:18px;font-weight:700;color:var(--pos-success);">Thanh toán thành công!</div>
|
||||
<div style="font-size:14px;color:var(--pos-text-tertiary);">@FormatPrice(_lastOrderTotal)</div>
|
||||
<div style="font-size:12px;color:var(--pos-text-tertiary);">Mã: @_lastTransactionId</div>
|
||||
<div style="display:flex;gap:10px;margin-top:8px;">
|
||||
<button style="padding:10px 20px;border-radius:var(--pos-radius);border:1px solid var(--pos-border-default);background:transparent;color:var(--pos-text-primary);cursor:pointer;font-size:13px;font-weight:600;">
|
||||
<i data-lucide="printer" style="width:14px;height:14px;vertical-align:middle;margin-right:4px;"></i>In hóa đơn
|
||||
</button>
|
||||
<button style="padding:10px 20px;border-radius:var(--pos-radius);border:none;background:var(--pos-orange-primary);color:#fff;cursor:pointer;font-size:13px;font-weight:600;"
|
||||
@onclick="ResetAfterPayment">
|
||||
<i data-lucide="plus" style="width:14px;height:14px;vertical-align:middle;margin-right:4px;"></i>Đơn mới
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
break;
|
||||
|
||||
case PosTab.History:
|
||||
@* ═══ ORDER HISTORY TAB ═══ *@
|
||||
<div class="pos-history" style="width:100%;">
|
||||
<div class="pos-history__toolbar">
|
||||
<input class="pos-history__search" placeholder="Tìm mã đơn, tên khách..."
|
||||
@bind="_historySearch" @bind:event="oninput" />
|
||||
@foreach (var f in _historyFilters)
|
||||
{
|
||||
<button class="pos-history__filter-btn @(_historyFilter == f.Key ? "pos-history__filter-btn--active" : "")"
|
||||
@onclick="() => _historyFilter = f.Key">
|
||||
@f.Label
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="pos-history__list">
|
||||
@foreach (var order in FilteredOrders)
|
||||
{
|
||||
<div class="pos-history__card" @onclick="() => _selectedOrder = order">
|
||||
<div class="pos-history__card-header">
|
||||
<span class="pos-history__order-id">@order.Id</span>
|
||||
<span class="pos-history__status @(order.Status == "Hoàn thành" ? "pos-history__status--completed" : "pos-history__status--refunded")">
|
||||
@order.Status
|
||||
</span>
|
||||
</div>
|
||||
<div class="pos-history__card-body">
|
||||
<span class="pos-history__items-preview">@order.Items</span>
|
||||
<div class="pos-history__card-meta">
|
||||
<div class="pos-history__total">@FormatPrice(order.Total)</div>
|
||||
<div class="pos-history__time">@order.Time</div>
|
||||
<div class="pos-history__method">@order.Method</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (!FilteredOrders.Any())
|
||||
{
|
||||
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;padding:60px 0;color:var(--pos-text-tertiary);">
|
||||
<i data-lucide="search" style="width:40px;height:40px;margin-bottom:12px;opacity:0.4;"></i>
|
||||
<div style="font-size:14px;">Không tìm thấy đơn hàng</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
|
||||
case PosTab.Dashboard:
|
||||
@* ═══ DASHBOARD TAB ═══ *@
|
||||
<div class="pos-dashboard" style="width:100%;">
|
||||
<div class="pos-dashboard__header">
|
||||
<div>
|
||||
<div class="pos-dashboard__title">Dashboard bán hàng</div>
|
||||
<div class="pos-dashboard__subtitle">@DateTime.Now.ToString("dd/MM/yyyy") — Hôm nay</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pos-dashboard__stats">
|
||||
@foreach (var stat in _dashStats)
|
||||
{
|
||||
<div class="pos-dashboard__stat-card">
|
||||
<div class="pos-dashboard__stat-label">@stat.Label</div>
|
||||
<div class="pos-dashboard__stat-value" style="color:@stat.Color;">@stat.Value</div>
|
||||
<div class="pos-dashboard__stat-sub">@stat.Sub</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="pos-dashboard__grid">
|
||||
@* Popular items *@
|
||||
<div class="pos-dashboard__section">
|
||||
<div class="pos-dashboard__section-title">Món bán chạy</div>
|
||||
@foreach (var item in _dashPopular)
|
||||
{
|
||||
<div class="pos-dashboard__popular-item">
|
||||
<div>
|
||||
<div style="font-size:13px;font-weight:500;">@item.Name</div>
|
||||
<div style="font-size:11px;color:var(--pos-text-tertiary);">@item.Qty đã bán</div>
|
||||
</div>
|
||||
<span style="font-size:13px;font-weight:600;color:var(--pos-orange-primary);">@FormatPrice(item.Revenue)</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@* Payment breakdown + Hourly chart *@
|
||||
<div class="pos-dashboard__section">
|
||||
<div class="pos-dashboard__section-title">Hình thức thanh toán</div>
|
||||
@foreach (var p in _dashPayments)
|
||||
{
|
||||
<div class="pos-dashboard__payment-row">
|
||||
<div class="pos-dashboard__payment-header">
|
||||
<span>@p.Method</span>
|
||||
<span style="font-weight:600;">@FormatPrice(p.Amount)</span>
|
||||
</div>
|
||||
<div class="pos-dashboard__payment-bar">
|
||||
<div class="pos-dashboard__payment-fill" style="width:@(p.Pct)%;background:@p.Color;"></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="pos-dashboard__section-title" style="margin-top:20px;">Doanh thu theo giờ</div>
|
||||
<div class="pos-dashboard__hourly-chart">
|
||||
@foreach (var h in _dashHourly)
|
||||
{
|
||||
<div class="pos-dashboard__hourly-bar">
|
||||
<div class="pos-dashboard__hourly-fill" style="height:@(h.Pct)%;opacity:@(h.Pct > 0 ? 1 : 0.3);"></div>
|
||||
<span class="pos-dashboard__hourly-label">@h.Hour</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
}
|
||||
</div>
|
||||
|
||||
@* ═══ CART PANEL ═══ *@
|
||||
<div class="pos-cart-panel">
|
||||
<div class="pos-cart-header">
|
||||
<span class="pos-cart-header__title">Đơn hàng</span>
|
||||
<span style="font-size:12px;color:var(--pos-text-tertiary);">@_cartItems.Count món</span>
|
||||
</div>
|
||||
|
||||
<div class="pos-cart-items">
|
||||
@foreach (var item in _cartItems)
|
||||
{
|
||||
<div class="pos-cart-item">
|
||||
<div class="pos-cart-item__info">
|
||||
<span class="pos-cart-item__name">@item.Name</span>
|
||||
<span class="pos-cart-item__price">@FormatPrice(item.Price)</span>
|
||||
</div>
|
||||
<div class="pos-cart-item__qty">
|
||||
<button @onclick="() => ChangeQty(item, -1)">−</button>
|
||||
<span style="font-size:14px;font-weight:600;min-width:20px;text-align:center;">@item.Qty</span>
|
||||
<button @onclick="() => ChangeQty(item, 1)">+</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="pos-cart-footer">
|
||||
<div class="pos-cart-total">
|
||||
<span class="pos-cart-total__label">Tổng cộng</span>
|
||||
<span class="pos-cart-total__value">@FormatPrice(CartTotal)</span>
|
||||
</div>
|
||||
<button class="pos-btn-checkout" @onclick="Checkout">
|
||||
<i data-lucide="credit-card" style="width:18px;height:18px;"></i>
|
||||
Thanh toán
|
||||
</button>
|
||||
</div>
|
||||
@* ═══════════════ BOTTOM NAVIGATION ═══════════════ *@
|
||||
<div class="pos-bottom-nav">
|
||||
<button class="pos-bottom-nav__tab @(_activeTab == PosTab.Sale ? "pos-bottom-nav__tab--active" : "")"
|
||||
@onclick="() => SwitchTab(PosTab.Sale)">
|
||||
<i data-lucide="coffee" class="pos-bottom-nav__icon"></i>
|
||||
<span>Bán hàng</span>
|
||||
</button>
|
||||
<button class="pos-bottom-nav__tab @(_activeTab == PosTab.History ? "pos-bottom-nav__tab--active" : "")"
|
||||
@onclick="() => SwitchTab(PosTab.History)">
|
||||
<i data-lucide="clock" class="pos-bottom-nav__icon"></i>
|
||||
<span>Lịch sử</span>
|
||||
</button>
|
||||
<button class="pos-bottom-nav__tab @(_activeTab == PosTab.Dashboard ? "pos-bottom-nav__tab--active" : "")"
|
||||
@onclick="() => SwitchTab(PosTab.Dashboard)">
|
||||
<i data-lucide="bar-chart-3" class="pos-bottom-nav__icon"></i>
|
||||
<span>Dashboard</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
// ═══════════════ TAB SYSTEM ═══════════════
|
||||
private enum PosTab { Sale, History, Dashboard }
|
||||
private PosTab _activeTab = PosTab.Sale;
|
||||
|
||||
// EN: Loading state / VI: Trạng thái tải
|
||||
private void SwitchTab(PosTab tab)
|
||||
{
|
||||
if (_paymentStep != PayStep.None && _paymentStep != PayStep.Success) return; // Block tab switch during payment
|
||||
_activeTab = tab;
|
||||
}
|
||||
|
||||
// ═══════════════ SALE TAB — Product & Cart ═══════════════
|
||||
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: Product list / VI: Danh sách sản phẩm
|
||||
private List<Product> _products = new();
|
||||
|
||||
// EN: Cart items / VI: Mục giỏ hàng
|
||||
private readonly List<CartItem> _cartItems = new();
|
||||
private IEnumerable<Product> FilteredProducts =>
|
||||
_selectedCategory == "Tất cả" ? _products : _products.Where(p => p.Category == _selectedCategory);
|
||||
@@ -116,11 +402,7 @@
|
||||
var apiProducts = await productsTask;
|
||||
var apiCategories = await categoriesTask;
|
||||
|
||||
_products = apiProducts.Select(p => new Product(
|
||||
p.Name,
|
||||
p.Price,
|
||||
p.Category ?? "Khác"
|
||||
)).ToList();
|
||||
_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)
|
||||
@@ -131,18 +413,13 @@
|
||||
_categories = new[] { "Tất cả" }.Concat(productCats).ToArray();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
_loadError = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
}
|
||||
catch { _loadError = true; }
|
||||
finally { _isLoading = false; }
|
||||
}
|
||||
|
||||
private void AddToCart(Product product)
|
||||
{
|
||||
if (_paymentStep != PayStep.None) return;
|
||||
var existing = _cartItems.FirstOrDefault(i => i.Name == product.Name);
|
||||
if (existing != null) existing.Qty++;
|
||||
else _cartItems.Add(new CartItem(product.Name, product.Price));
|
||||
@@ -154,9 +431,166 @@
|
||||
if (item.Qty <= 0) _cartItems.Remove(item);
|
||||
}
|
||||
|
||||
private void Checkout() => NavigateTo("cafe/order-customize");
|
||||
// ═══════════════ INLINE PAYMENT ═══════════════
|
||||
private enum PayStep { None, MethodSelect, AmountInput, Processing, Success }
|
||||
private PayStep _paymentStep = PayStep.None;
|
||||
private string _selectedMethod = "";
|
||||
private decimal _receivedAmount;
|
||||
private string _customAmountInput = "";
|
||||
private decimal _lastOrderTotal;
|
||||
private string _lastTransactionId = "";
|
||||
private decimal ChangeAmount => _receivedAmount - CartTotal;
|
||||
|
||||
// EN: Models / VI: Mô hình dữ liệu
|
||||
private void StartPayment()
|
||||
{
|
||||
if (!_cartItems.Any()) return;
|
||||
_paymentStep = PayStep.MethodSelect;
|
||||
}
|
||||
|
||||
private void CancelPayment()
|
||||
{
|
||||
_paymentStep = PayStep.None;
|
||||
_selectedMethod = "";
|
||||
_receivedAmount = 0;
|
||||
_customAmountInput = "";
|
||||
}
|
||||
|
||||
private void SelectPaymentMethod(string method)
|
||||
{
|
||||
_selectedMethod = method;
|
||||
_receivedAmount = 0;
|
||||
_customAmountInput = "";
|
||||
|
||||
if (method == "cash")
|
||||
_paymentStep = PayStep.AmountInput;
|
||||
else
|
||||
_paymentStep = PayStep.Processing;
|
||||
}
|
||||
|
||||
private string GetMethodLabel() => _selectedMethod switch
|
||||
{
|
||||
"cash" => "💵 Tiền mặt",
|
||||
"card" => "💳 Thẻ",
|
||||
"qr" => "📱 Mã QR",
|
||||
"transfer" => "🏦 Chuyển khoản",
|
||||
_ => "Thanh toán"
|
||||
};
|
||||
|
||||
private List<(string Label, decimal Value)> GetQuickAmounts()
|
||||
{
|
||||
var total = CartTotal;
|
||||
var amounts = new List<(string, decimal)>();
|
||||
var roundUp = Math.Ceiling(total / 50_000) * 50_000;
|
||||
if (roundUp == total) roundUp += 50_000;
|
||||
amounts.Add(("Đúng tiền", total));
|
||||
amounts.Add((FormatPrice(roundUp), roundUp));
|
||||
amounts.Add((FormatPrice(roundUp + 50_000), roundUp + 50_000));
|
||||
amounts.Add((FormatPrice(500_000), 500_000));
|
||||
amounts.Add((FormatPrice(200_000), 200_000));
|
||||
amounts.Add((FormatPrice(1_000_000), 1_000_000));
|
||||
return amounts;
|
||||
}
|
||||
|
||||
private void ApplyCustomAmount()
|
||||
{
|
||||
if (decimal.TryParse(_customAmountInput, out var val))
|
||||
_receivedAmount = val;
|
||||
}
|
||||
|
||||
private void ConfirmPayment()
|
||||
{
|
||||
_lastOrderTotal = CartTotal;
|
||||
_lastTransactionId = $"TXN-{DateTime.Now:yyyyMMdd}-{Random.Shared.Next(100, 999)}";
|
||||
|
||||
// EN: Save to history / VI: Lưu vào lịch sử
|
||||
var methodLabel = _selectedMethod switch { "cash" => "Tiền mặt", "card" => "Thẻ", "qr" => "QR Code", _ => "Chuyển khoản" };
|
||||
_orderHistory.Insert(0, new OrderRecord(
|
||||
_lastTransactionId,
|
||||
string.Join(", ", _cartItems.Select(i => $"{i.Name} x{i.Qty}")),
|
||||
_lastOrderTotal,
|
||||
DateTime.Now.ToString("HH:mm"),
|
||||
methodLabel,
|
||||
"Hoàn thành"
|
||||
));
|
||||
|
||||
_paymentStep = PayStep.Success;
|
||||
}
|
||||
|
||||
private void ResetAfterPayment()
|
||||
{
|
||||
_cartItems.Clear();
|
||||
_paymentStep = PayStep.None;
|
||||
_selectedMethod = "";
|
||||
_receivedAmount = 0;
|
||||
_customAmountInput = "";
|
||||
}
|
||||
|
||||
// ═══════════════ HISTORY TAB ═══════════════
|
||||
private string _historySearch = "";
|
||||
private string _historyFilter = "today";
|
||||
private OrderRecord? _selectedOrder;
|
||||
|
||||
private readonly List<HistoryFilter> _historyFilters = new()
|
||||
{
|
||||
new("today", "Hôm nay"),
|
||||
new("week", "7 ngày"),
|
||||
new("month", "30 ngày"),
|
||||
};
|
||||
|
||||
// EN: Demo history data + real-time additions / VI: Dữ liệu lịch sử mẫu + thêm real-time
|
||||
private readonly List<OrderRecord> _orderHistory = new()
|
||||
{
|
||||
new("TXN-20260303-001", "Cà phê sữa đá x2, Bạc xỉu x1", 103_000, "10:15", "Tiền mặt", "Hoàn thành"),
|
||||
new("TXN-20260303-002", "Cappuccino x1, Croissant bơ x2", 125_000, "10:02", "QR Code", "Hoàn thành"),
|
||||
new("TXN-20260303-003", "Trà đào cam sả x3, Sinh tố bơ x1", 190_000, "09:48", "Thẻ", "Hoàn thành"),
|
||||
new("TXN-20260303-004", "Matcha Latte x1", 59_000, "09:30", "Tiền mặt", "Hoàn thành"),
|
||||
new("TXN-20260303-005", "Espresso x2, Latte x1", 145_000, "09:15", "Chuyển khoản", "Hoàn thành"),
|
||||
new("TXN-20260303-006", "Cà phê đen đá x4", 116_000, "08:55", "Tiền mặt", "Hoàn thành"),
|
||||
new("TXN-20260303-007", "Trà sen vàng x2, Bánh mì bơ x1", 133_000, "08:40", "QR Code", "Hoàn thành"),
|
||||
new("TXN-20260303-008", "Sinh tố bơ x1", 55_000, "08:25", "Tiền mặt", "Hoàn trả"),
|
||||
};
|
||||
|
||||
private IEnumerable<OrderRecord> FilteredOrders =>
|
||||
string.IsNullOrWhiteSpace(_historySearch)
|
||||
? _orderHistory
|
||||
: _orderHistory.Where(o =>
|
||||
o.Id.Contains(_historySearch, StringComparison.OrdinalIgnoreCase) ||
|
||||
o.Items.Contains(_historySearch, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// ═══════════════ DASHBOARD TAB ═══════════════
|
||||
private readonly List<DashStat> _dashStats = new()
|
||||
{
|
||||
new("Doanh thu", "8,450,000₫", "var(--pos-orange-primary)", "+12% so với hôm qua"),
|
||||
new("Đơn hàng", "156", "var(--pos-success)", "TB 54,167₫/đơn"),
|
||||
new("Khách hàng", "132", "var(--pos-info)", "18% khách quay lại"),
|
||||
new("Món bán ra", "347", "var(--pos-warning)", "2.2 món/đơn"),
|
||||
};
|
||||
|
||||
private readonly List<DashPopular> _dashPopular = new()
|
||||
{
|
||||
new("Cà phê sữa đá", 52, 1_820_000),
|
||||
new("Bạc xỉu", 38, 1_482_000),
|
||||
new("Trà đào cam sả", 29, 1_305_000),
|
||||
new("Cappuccino", 24, 1_320_000),
|
||||
new("Sinh tố bơ", 18, 990_000),
|
||||
};
|
||||
|
||||
private readonly List<DashPayment> _dashPayments = new()
|
||||
{
|
||||
new("Tiền mặt", 4_650_000, 55, "var(--pos-success)"),
|
||||
new("Chuyển khoản", 2_535_000, 30, "var(--pos-info)"),
|
||||
new("Thẻ", 845_000, 10, "var(--pos-warning)"),
|
||||
new("Ví điện tử", 420_000, 5, "var(--pos-orange-primary)"),
|
||||
};
|
||||
|
||||
private readonly List<DashHour> _dashHourly = new()
|
||||
{
|
||||
new("7h", 30), new("8h", 65), new("9h", 85), new("10h", 55),
|
||||
new("11h", 90), new("12h", 100), new("13h", 70), new("14h", 45),
|
||||
new("15h", 60), new("16h", 75), new("17h", 50), new("18h", 20),
|
||||
};
|
||||
|
||||
// ═══════════════ RECORDS ═══════════════
|
||||
private record Product(string Name, decimal Price, string Category);
|
||||
private class CartItem(string name, decimal price)
|
||||
{
|
||||
@@ -164,4 +598,10 @@
|
||||
public decimal Price { get; set; } = price;
|
||||
public int Qty { get; set; } = 1;
|
||||
}
|
||||
private record OrderRecord(string Id, string Items, decimal Total, string Time, string Method, string Status);
|
||||
private record HistoryFilter(string Key, string Label);
|
||||
private record DashStat(string Label, string Value, string Color, string Sub);
|
||||
private record DashPopular(string Name, int Qty, decimal Revenue);
|
||||
private record DashPayment(string Method, decimal Amount, int Pct, string Color);
|
||||
private record DashHour(string Hour, int Pct);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user