feat(web-client-tpos): unify POS with inline payment and tabs (path fix)

This commit is contained in:
Ho Ngoc Hai
2026-03-03 11:13:27 +07:00
parent 15404a859f
commit fe6e14ce85

View File

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