feat: add 11 Blazor Razor payment workflow screens

Create shared payment workflow pages in Pages/Pos/Shared/Payment/:
- MethodSelect.razor: payment method selection (Cash, Card, QR, Gift Card)
- CashPayment.razor: cash payment with quick amounts and change calc
- CardPayment.razor: card reader status with tap/swipe/insert
- QrPayment.razor: QR code display with VietQR/MoMo/ZaloPay tabs
- BankTransfer.razor: bank transfer with account info and reference
- GiftCardPayment.razor: gift card code input and balance lookup
- PartialPayment.razor: split payment across multiple methods
- TipEntry.razor: quick tip buttons and custom tip entry
- PaymentPending.razor: payment processing animation
- PaymentSuccess.razor: success confirmation with print/new order
- Receipt.razor: 80mm thermal receipt template

All files follow POS patterns: @layout PosLayout, @inherits PosBase,
bilingual EN/VI comments, CSS variables, demo data with VND currency.

Co-authored-by: Velik <hongochai10@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-02-26 15:45:18 +00:00
parent a2abc08011
commit 088869f256
11 changed files with 1067 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
@*
EN: Bank Transfer — Bank account info, reference code, transfer verification.
VI: Chuyển khoản ngân hàng — Thông tin tài khoản, mã tham chiếu, xác minh chuyển khoản.
*@
@page "/pos/payment/bank-transfer"
@layout PosLayout
@inherits PosBase
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:32px;gap:24px;">
@* ═══ ORDER TOTAL ═══ *@
<div style="text-align:center;">
<div style="font-size:14px;color:var(--pos-text-tertiary);margin-bottom:4px;">Tổng thanh toán</div>
<div style="font-size:32px;font-weight:700;color:var(--pos-orange-primary);">@FormatPrice(_orderTotal)</div>
</div>
@* ═══ BANK ACCOUNT INFO ═══ *@
<div style="width:100%;max-width:420px;background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:24px;display:flex;flex-direction:column;gap:16px;">
<div style="font-size:16px;font-weight:600;text-align:center;margin-bottom:4px;">Thông tin chuyển khoản</div>
<div style="display:flex;justify-content:space-between;align-items:center;padding:12px;background:var(--pos-bg-interactive);border-radius:8px;">
<span style="font-size:13px;color:var(--pos-text-tertiary);">Ngân hàng</span>
<span style="font-size:14px;font-weight:600;">@_bankName</span>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;padding:12px;background:var(--pos-bg-interactive);border-radius:8px;">
<span style="font-size:13px;color:var(--pos-text-tertiary);">Số tài khoản</span>
<span style="font-size:14px;font-weight:600;letter-spacing:1px;">@_accountNumber</span>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;padding:12px;background:var(--pos-bg-interactive);border-radius:8px;">
<span style="font-size:13px;color:var(--pos-text-tertiary);">Chủ tài khoản</span>
<span style="font-size:14px;font-weight:600;">@_accountHolder</span>
</div>
@* EN: Transfer reference / VI: Mã tham chiếu *@
<div style="padding:16px;background:rgba(255,92,0,0.08);border:1px dashed var(--pos-orange-primary);border-radius:8px;text-align:center;">
<div style="font-size:12px;color:var(--pos-text-tertiary);margin-bottom:6px;">Nội dung chuyển khoản</div>
<div style="font-size:18px;font-weight:700;color:var(--pos-orange-primary);letter-spacing:2px;">@_referenceCode</div>
</div>
</div>
@* ═══ STATUS ═══ *@
<div style="display:flex;align-items:center;gap:8px;color:var(--pos-warning);font-size:14px;">
<div style="width:8px;height:8px;border-radius:50%;background:var(--pos-warning);animation:pulse 2s ease-in-out infinite;"></div>
Chờ xác nhận chuyển khoản...
</div>
@* ═══ ACTIONS ═══ *@
<div style="display:flex;gap:12px;">
<button style="display:flex;align-items:center;gap:6px;padding:12px 24px;border-radius:var(--pos-radius);border:none;
background:var(--pos-success);color:#fff;cursor:pointer;font-size:14px;font-weight:600;"
@onclick="Verify">
<i data-lucide="check-circle" style="width:16px;height:16px;"></i> Xác nhận đã nhận
</button>
<button style="padding:12px 24px;border-radius:var(--pos-radius);border:1px solid var(--pos-border-default);
background:transparent;color:var(--pos-text-secondary);cursor:pointer;font-size:14px;"
@onclick="Cancel">
Hủy
</button>
</div>
</div>
<style>
@@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
</style>
@code {
// EN: Demo data / VI: Dữ liệu mẫu
private decimal _orderTotal = 285_000;
private string _bankName = "Vietcombank";
private string _accountNumber = "1017 6688 9900";
private string _accountHolder = "CONG TY TNHH GOODGO";
private string _referenceCode = "GG240215A1";
private void Verify() => NavigateTo("payment/success");
private void Cancel() => NavigateTo("payment/method-select");
}

View File

@@ -0,0 +1,76 @@
@*
EN: Card Payment — Card reader status, tap/swipe/insert instructions.
VI: Thanh toán thẻ — Trạng thái đầu đọc thẻ, hướng dẫn chạm/quẹt/cắm.
*@
@page "/pos/payment/card"
@layout PosLayout
@inherits PosBase
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:32px;gap:32px;">
@* ═══ ORDER TOTAL ═══ *@
<div style="text-align:center;">
<div style="font-size:14px;color:var(--pos-text-tertiary);margin-bottom:4px;">Tổng thanh toán</div>
<div style="font-size:32px;font-weight:700;color:var(--pos-orange-primary);">@FormatPrice(_orderTotal)</div>
</div>
@* ═══ CARD READER STATUS ═══ *@
<div style="display:flex;flex-direction:column;align-items:center;gap:20px;padding:40px;background:var(--pos-bg-elevated);border-radius:16px;width:100%;max-width:400px;">
@if (_isProcessing)
{
@* EN: Processing animation / VI: Hiệu ứng đang xử lý *@
<div style="width:80px;height:80px;border-radius:50%;border:4px solid var(--pos-border-subtle);border-top-color:var(--pos-orange-primary);
animation:spin 1s linear infinite;"></div>
<div style="font-size:18px;font-weight:600;color:var(--pos-text-primary);">Đang xử lý...</div>
<div style="font-size:13px;color:var(--pos-text-tertiary);">Vui lòng không rút thẻ</div>
}
else
{
@* EN: Waiting for card / VI: Chờ thẻ *@
<div style="width:80px;height:80px;border-radius:16px;background:var(--pos-bg-interactive);display:flex;align-items:center;justify-content:center;
animation:pulse 2s ease-in-out infinite;">
<i data-lucide="credit-card" style="width:40px;height:40px;color:var(--pos-orange-primary);"></i>
</div>
<div style="font-size:18px;font-weight:600;color:var(--pos-text-primary);">Chạm, quẹt hoặc cắm thẻ</div>
<div style="font-size:13px;color:var(--pos-text-tertiary);">Tap, swipe or insert card</div>
}
</div>
@* ═══ ACTIONS ═══ *@
<div style="display:flex;gap:12px;">
@if (!_isProcessing)
{
<button style="padding:12px 32px;border-radius:var(--pos-radius);border:none;background:var(--pos-orange-primary);color:#fff;
cursor:pointer;font-size:14px;font-weight:600;"
@onclick="SimulateProcess">
Mô phỏng thanh toán
</button>
}
<button style="padding:12px 24px;border-radius:var(--pos-radius);border:1px solid var(--pos-border-default);
background:transparent;color:var(--pos-text-secondary);cursor:pointer;font-size:14px;"
@onclick="Cancel">
Hủy
</button>
</div>
</div>
@* EN: CSS animations / VI: Hiệu ứng CSS *@
<style>
@@keyframes spin { to { transform: rotate(360deg); } }
@@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
</style>
@code {
// EN: Demo order total / VI: Tổng đơn hàng mẫu
private decimal _orderTotal = 285_000;
private bool _isProcessing = false;
private async Task SimulateProcess()
{
_isProcessing = true;
StateHasChanged();
await Task.Delay(3000);
NavigateTo("payment/success");
}
private void Cancel() => NavigateTo("payment/method-select");
}

View File

@@ -0,0 +1,109 @@
@*
EN: Cash Payment — Cash payment with quick amount buttons and change calculation.
VI: Thanh toán tiền mặt — Nút số tiền nhanh và tính tiền thối.
*@
@page "/pos/payment/cash"
@layout PosLayout
@inherits PosBase
<div style="display:flex;height:100%;">
@* ═══ MAIN PANEL ═══ *@
<div style="flex:1;padding:32px;display:flex;flex-direction:column;align-items:center;gap:24px;overflow-y:auto;">
@* ═══ ORDER TOTAL ═══ *@
<div style="text-align:center;">
<div style="font-size:14px;color:var(--pos-text-tertiary);margin-bottom:4px;">Tổng thanh toán</div>
<div style="font-size:32px;font-weight:700;color:var(--pos-orange-primary);">@FormatPrice(_orderTotal)</div>
</div>
@* ═══ QUICK AMOUNT BUTTONS ═══ *@
<div style="width:100%;max-width:480px;">
<div style="font-size:14px;font-weight:600;color:var(--pos-text-secondary);margin-bottom:12px;">Số tiền nhanh</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;">
@foreach (var amount in _quickAmounts)
{
<button style="padding:16px;border-radius:var(--pos-radius);border:2px solid @(_receivedAmount == amount.Value ? "var(--pos-orange-primary)" : "var(--pos-border-default)");
background:@(_receivedAmount == amount.Value ? "rgba(255,92,0,0.1)" : "var(--pos-bg-elevated)");
color:var(--pos-text-primary);cursor:pointer;font-size:15px;font-weight:600;text-align:center;"
@onclick="() => SetAmount(amount.Value)">
@amount.Label
</button>
}
</div>
</div>
@* ═══ CUSTOM AMOUNT INPUT ═══ *@
<div style="width:100%;max-width:480px;">
<div style="font-size:14px;font-weight:600;color:var(--pos-text-secondary);margin-bottom:8px;">Nhập số tiền khác</div>
<div style="display:flex;align-items:center;gap:8px;">
<input type="number" @bind="_customInput" @bind:event="oninput"
placeholder="Nhập số tiền..."
style="flex:1;padding:14px 16px;border-radius:var(--pos-radius);border:1px solid var(--pos-border-default);
background:var(--pos-bg-elevated);color:var(--pos-text-primary);font-size:16px;outline:none;" />
<button style="padding:14px 20px;border-radius:var(--pos-radius);border:none;background:var(--pos-bg-interactive);
color:var(--pos-text-primary);cursor:pointer;font-size:14px;font-weight:600;"
@onclick="ApplyCustom">
Áp dụng
</button>
</div>
</div>
@* ═══ CHANGE DISPLAY ═══ *@
<div style="width:100%;max-width:480px;background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:20px;">
<div style="display:flex;justify-content:space-between;margin-bottom:12px;">
<span style="color:var(--pos-text-secondary);font-size:14px;">Khách đưa</span>
<span style="font-size:16px;font-weight:600;">@FormatPrice(_receivedAmount)</span>
</div>
<div style="display:flex;justify-content:space-between;padding-top:12px;border-top:1px dashed var(--pos-border-subtle);">
<span style="color:var(--pos-text-secondary);font-size:14px;">Tiền thối</span>
<span style="font-size:20px;font-weight:700;color:@(_changeAmount >= 0 ? "var(--pos-success)" : "var(--pos-danger)");">
@FormatPrice(_changeAmount)
</span>
</div>
</div>
</div>
@* ═══ CONFIRM PANEL ═══ *@
<div style="width:280px;background:var(--pos-bg-elevated);border-left:1px solid var(--pos-border-subtle);padding:24px;display:flex;flex-direction:column;justify-content:flex-end;gap:12px;">
<button style="padding:8px 16px;border-radius:var(--pos-radius);border:1px solid var(--pos-border-default);
background:transparent;color:var(--pos-text-secondary);cursor:pointer;font-size:14px;"
@onclick="GoBack">
<i data-lucide="arrow-left" style="width:14px;height:14px;"></i> Quay lại
</button>
<button class="pos-btn-checkout" @onclick="Confirm"
disabled="@(_receivedAmount < _orderTotal)">
Xác nhận thanh toán
</button>
</div>
</div>
@code {
// EN: Demo order total / VI: Tổng đơn hàng mẫu
private decimal _orderTotal = 285_000;
private decimal _receivedAmount = 0;
private decimal _changeAmount => _receivedAmount - _orderTotal;
private string _customInput = "";
// EN: Quick amount options / VI: Tùy chọn số tiền nhanh
private readonly List<QuickAmount> _quickAmounts = new()
{
new("300,000₫", 300_000),
new("350,000₫", 350_000),
new("400,000₫", 400_000),
new("500,000₫", 500_000),
new("1,000,000₫", 1_000_000),
new("Đúng tiền", 285_000),
};
private void SetAmount(decimal amount) => _receivedAmount = amount;
private void ApplyCustom()
{
if (decimal.TryParse(_customInput, out var val))
_receivedAmount = val;
}
private void Confirm() => NavigateTo("payment/success");
private void GoBack() => NavigateTo("payment/method-select");
private record QuickAmount(string Label, decimal Value);
}

View File

@@ -0,0 +1,109 @@
@*
EN: Gift Card Payment — Gift card code input, balance lookup, apply payment.
VI: Thanh toán thẻ quà tặng — Nhập mã thẻ, tra cứu số dư, áp dụng thanh toán.
*@
@page "/pos/payment/gift-card"
@layout PosLayout
@inherits PosBase
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:32px;gap:24px;">
@* ═══ ORDER TOTAL ═══ *@
<div style="text-align:center;">
<div style="font-size:14px;color:var(--pos-text-tertiary);margin-bottom:4px;">Tổng thanh toán</div>
<div style="font-size:32px;font-weight:700;color:var(--pos-orange-primary);">@FormatPrice(_orderTotal)</div>
</div>
@* ═══ GIFT CARD INPUT ═══ *@
<div style="width:100%;max-width:440px;background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:24px;display:flex;flex-direction:column;gap:16px;">
<div style="font-size:16px;font-weight:600;text-align:center;">
<i data-lucide="gift" style="width:20px;height:20px;color:var(--pos-orange-primary);vertical-align:middle;"></i>
Thẻ quà tặng
</div>
<div style="display:flex;gap:8px;">
<input type="text" @bind="_cardCode" placeholder="Nhập mã thẻ quà tặng..."
style="flex:1;padding:14px 16px;border-radius:var(--pos-radius);border:1px solid var(--pos-border-default);
background:var(--pos-bg-interactive);color:var(--pos-text-primary);font-size:16px;letter-spacing:2px;outline:none;" />
<button style="padding:14px 20px;border-radius:var(--pos-radius);border:none;background:var(--pos-orange-primary);
color:#fff;cursor:pointer;font-size:14px;font-weight:600;"
@onclick="LookupCard">
Tra cứu
</button>
</div>
@if (_cardLookedUp)
{
@* ═══ CARD BALANCE ═══ *@
<div style="padding:16px;background:var(--pos-bg-interactive);border-radius:8px;display:flex;flex-direction:column;gap:12px;">
<div style="display:flex;justify-content:space-between;">
<span style="font-size:13px;color:var(--pos-text-tertiary);">Số dư thẻ</span>
<span style="font-size:16px;font-weight:600;color:var(--pos-success);">@FormatPrice(_cardBalance)</span>
</div>
<div style="display:flex;justify-content:space-between;">
<span style="font-size:13px;color:var(--pos-text-tertiary);">Số tiền áp dụng</span>
<span style="font-size:16px;font-weight:600;color:var(--pos-orange-primary);">@FormatPrice(_appliedAmount)</span>
</div>
@if (_remainingAmount > 0)
{
@* EN: Remaining balance warning / VI: Cảnh báo số dư còn thiếu *@
<div style="padding:12px;background:rgba(239,68,68,0.1);border-radius:8px;border:1px solid var(--pos-danger);">
<div style="font-size:13px;color:var(--pos-danger);font-weight:600;">
<i data-lucide="alert-triangle" style="width:14px;height:14px;vertical-align:middle;"></i>
Còn thiếu: @FormatPrice(_remainingAmount)
</div>
<div style="font-size:12px;color:var(--pos-text-tertiary);margin-top:4px;">
Vui lòng thanh toán phần còn lại bằng phương thức khác
</div>
</div>
}
</div>
}
</div>
@* ═══ ACTIONS ═══ *@
<div style="display:flex;gap:12px;">
@if (_cardLookedUp)
{
@if (_remainingAmount > 0)
{
<button style="padding:12px 24px;border-radius:var(--pos-radius);border:none;background:var(--pos-orange-primary);
color:#fff;cursor:pointer;font-size:14px;font-weight:600;"
@onclick="GoToPartial">
Thanh toán kết hợp
</button>
}
else
{
<button class="pos-btn-checkout" @onclick="Confirm">
Xác nhận thanh toán
</button>
}
}
<button style="padding:12px 24px;border-radius:var(--pos-radius);border:1px solid var(--pos-border-default);
background:transparent;color:var(--pos-text-secondary);cursor:pointer;font-size:14px;"
@onclick="Cancel">
Quay lại
</button>
</div>
</div>
@code {
// EN: Demo data / VI: Dữ liệu mẫu
private decimal _orderTotal = 285_000;
private string _cardCode = "";
private bool _cardLookedUp = false;
private decimal _cardBalance = 200_000;
private decimal _appliedAmount => Math.Min(_cardBalance, _orderTotal);
private decimal _remainingAmount => Math.Max(0, _orderTotal - _cardBalance);
private void LookupCard()
{
// EN: Simulate card lookup / VI: Mô phỏng tra cứu thẻ
_cardLookedUp = true;
}
private void Confirm() => NavigateTo("payment/success");
private void GoToPartial() => NavigateTo("payment/partial");
private void Cancel() => NavigateTo("payment/method-select");
}

View File

@@ -0,0 +1,57 @@
@*
EN: Payment Method Select — Choose payment method: Cash, Card, QR Code, Gift Card.
VI: Chọn phương thức thanh toán — Tiền mặt, Thẻ, Mã QR, Thẻ quà tặng.
*@
@page "/pos/payment/method-select"
@layout PosLayout
@inherits PosBase
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:32px;gap:32px;">
@* ═══ ORDER TOTAL ═══ *@
<div style="text-align:center;">
<div style="font-size:14px;color:var(--pos-text-tertiary);margin-bottom:8px;">Tổng đơn hàng / Order Total</div>
<div style="font-size:36px;font-weight:700;color:var(--pos-orange-primary);">@FormatPrice(_orderTotal)</div>
</div>
@* ═══ PAYMENT METHODS ═══ *@
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:16px;width:100%;max-width:520px;">
@foreach (var method in _methods)
{
<button style="display:flex;flex-direction:column;align-items:center;gap:12px;padding:32px 16px;border-radius:var(--pos-radius);
border:2px solid var(--pos-border-default);background:var(--pos-bg-elevated);color:var(--pos-text-primary);cursor:pointer;
transition:border-color 0.2s;"
@onclick="() => SelectMethod(method.Route)">
<span style="font-size:40px;">@method.Icon</span>
<span style="font-size:16px;font-weight:600;">@method.Label</span>
<span style="font-size:12px;color:var(--pos-text-tertiary);text-align:center;">@method.Description</span>
</button>
}
</div>
@* ═══ BACK BUTTON ═══ *@
<button style="display:flex;align-items:center;gap:8px;padding:12px 24px;border-radius:var(--pos-radius);border:1px solid var(--pos-border-default);
background:transparent;color:var(--pos-text-secondary);cursor:pointer;font-size:14px;"
@onclick="GoBack">
<i data-lucide="arrow-left" style="width:16px;height:16px;"></i>
Quay lại
</button>
</div>
@code {
// EN: Demo order total / VI: Tổng đơn hàng mẫu
private decimal _orderTotal = 285_000;
// EN: Payment method definitions / VI: Định nghĩa phương thức thanh toán
private readonly List<PaymentMethod> _methods = new()
{
new("💵", "Tiền mặt", "Thanh toán bằng tiền mặt", "payment/cash"),
new("💳", "Thẻ", "Chạm, quẹt hoặc cắm thẻ", "payment/card"),
new("📱", "Mã QR", "VietQR, MoMo, ZaloPay", "payment/qr"),
new("🎁", "Thẻ quà tặng", "Sử dụng thẻ quà tặng", "payment/gift-card"),
};
private void SelectMethod(string route) => NavigateTo(route);
private void GoBack() => NavigateTo("cafe");
private record PaymentMethod(string Icon, string Label, string Description, string Route);
}

View File

@@ -0,0 +1,139 @@
@*
EN: Partial Payment — Split payment across multiple methods.
VI: Thanh toán kết hợp — Chia thanh toán qua nhiều phương thức.
*@
@page "/pos/payment/partial"
@layout PosLayout
@inherits PosBase
<div style="display:flex;height:100%;">
@* ═══ MAIN PANEL ═══ *@
<div style="flex:1;padding:32px;display:flex;flex-direction:column;align-items:center;gap:24px;overflow-y:auto;">
@* ═══ ORDER TOTAL ═══ *@
<div style="text-align:center;">
<div style="font-size:14px;color:var(--pos-text-tertiary);margin-bottom:4px;">Tổng đơn hàng</div>
<div style="font-size:32px;font-weight:700;color:var(--pos-orange-primary);">@FormatPrice(_orderTotal)</div>
</div>
@* ═══ PAYMENT SPLITS ═══ *@
<div style="width:100%;max-width:500px;display:flex;flex-direction:column;gap:12px;">
<div style="font-size:14px;font-weight:600;color:var(--pos-text-secondary);">Phương thức thanh toán đã thêm</div>
@foreach (var split in _splits)
{
<div style="display:flex;align-items:center;gap:12px;padding:16px;background:var(--pos-bg-elevated);border-radius:var(--pos-radius);">
<span style="font-size:24px;">@split.Icon</span>
<div style="flex:1;">
<div style="font-size:14px;font-weight:600;">@split.Method</div>
<div style="font-size:12px;color:var(--pos-text-tertiary);">@split.Description</div>
</div>
<span style="font-size:16px;font-weight:700;color:var(--pos-orange-primary);">@FormatPrice(split.Amount)</span>
<button style="width:28px;height:28px;border-radius:6px;border:1px solid var(--pos-border-default);background:transparent;
color:var(--pos-danger);cursor:pointer;display:flex;align-items:center;justify-content:center;"
@onclick="() => RemoveSplit(split)">
<i data-lucide="x" style="width:14px;height:14px;"></i>
</button>
</div>
}
</div>
@* ═══ ADD METHOD ═══ *@
@if (_remainingBalance > 0)
{
<div style="width:100%;max-width:500px;">
<div style="font-size:14px;font-weight:600;color:var(--pos-text-secondary);margin-bottom:12px;">Thêm phương thức</div>
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:10px;">
@foreach (var option in _addOptions)
{
<button style="display:flex;align-items:center;gap:10px;padding:14px;border-radius:var(--pos-radius);
border:1px dashed var(--pos-border-default);background:transparent;color:var(--pos-text-primary);
cursor:pointer;font-size:14px;"
@onclick="() => AddSplit(option)">
<span style="font-size:20px;">@option.Icon</span>
@option.Label
</button>
}
</div>
</div>
}
</div>
@* ═══ SUMMARY PANEL ═══ *@
<div style="width:300px;background:var(--pos-bg-elevated);border-left:1px solid var(--pos-border-subtle);padding:24px;display:flex;flex-direction:column;">
<div style="font-size:15px;font-weight:600;margin-bottom:20px;">Tóm tắt thanh toán</div>
<div style="flex:1;display:flex;flex-direction:column;gap:12px;">
<div style="display:flex;justify-content:space-between;font-size:14px;">
<span style="color:var(--pos-text-tertiary);">Tổng đơn hàng</span>
<span style="font-weight:600;">@FormatPrice(_orderTotal)</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:14px;">
<span style="color:var(--pos-text-tertiary);">Đã thanh toán</span>
<span style="font-weight:600;color:var(--pos-success);">@FormatPrice(_paidAmount)</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:14px;padding-top:12px;border-top:1px dashed var(--pos-border-subtle);">
<span style="color:var(--pos-text-tertiary);">Còn lại</span>
<span style="font-weight:700;font-size:18px;color:@(_remainingBalance > 0 ? "var(--pos-danger)" : "var(--pos-success)");">
@FormatPrice(_remainingBalance)
</span>
</div>
@* EN: Progress bar / VI: Thanh tiến trình *@
<div style="width:100%;height:6px;background:var(--pos-bg-interactive);border-radius:3px;overflow:hidden;">
<div style="width:@(_progressPercent)%;height:100%;background:var(--pos-success);border-radius:3px;transition:width 0.3s;"></div>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:10px;margin-top:auto;">
<button style="padding:8px;border-radius:var(--pos-radius);border:1px solid var(--pos-border-default);
background:transparent;color:var(--pos-text-secondary);cursor:pointer;font-size:13px;"
@onclick="GoBack">
Quay lại
</button>
<button class="pos-btn-checkout" @onclick="Complete"
disabled="@(_remainingBalance > 0)">
Hoàn tất thanh toán
</button>
</div>
</div>
</div>
@code {
// EN: Demo data / VI: Dữ liệu mẫu
private decimal _orderTotal = 285_000;
private readonly List<PaymentSplit> _splits = new()
{
new("💵", "Tiền mặt", "Cash", 150_000),
new("💳", "Thẻ", "Card ending 4242", 135_000),
};
private readonly List<AddOption> _addOptions = new()
{
new("💵", "Tiền mặt"),
new("💳", "Thẻ"),
new("📱", "Mã QR"),
new("🎁", "Thẻ quà tặng"),
};
private decimal _paidAmount => _splits.Sum(s => s.Amount);
private decimal _remainingBalance => Math.Max(0, _orderTotal - _paidAmount);
private int _progressPercent => (int)Math.Min(100, _paidAmount / _orderTotal * 100);
private void AddSplit(AddOption option)
{
_splits.Add(new(option.Icon, option.Label, "Mới thêm", _remainingBalance));
}
private void RemoveSplit(PaymentSplit split) => _splits.Remove(split);
private void Complete() => NavigateTo("payment/success");
private void GoBack() => NavigateTo("payment/method-select");
private record AddOption(string Icon, string Label);
private class PaymentSplit(string icon, string method, string description, decimal amount)
{
public string Icon { get; set; } = icon;
public string Method { get; set; } = method;
public string Description { get; set; } = description;
public decimal Amount { get; set; } = amount;
}
}

View File

@@ -0,0 +1,64 @@
@*
EN: Payment Pending — Processing animation, order total, payment method info.
VI: Đang xử lý thanh toán — Hiệu ứng xử lý, tổng đơn hàng, thông tin phương thức.
*@
@page "/pos/payment/pending"
@layout PosLayout
@inherits PosBase
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:32px;gap:28px;">
@* ═══ PROCESSING ANIMATION ═══ *@
<div style="position:relative;width:100px;height:100px;">
<div style="position:absolute;inset:0;border-radius:50%;border:4px solid var(--pos-border-subtle);"></div>
<div style="position:absolute;inset:0;border-radius:50%;border:4px solid transparent;border-top-color:var(--pos-orange-primary);
animation:spin 1s linear infinite;"></div>
<div style="position:absolute;inset:12px;border-radius:50%;border:4px solid transparent;border-top-color:var(--pos-warning);
animation:spin 1.5s linear infinite reverse;"></div>
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;">
<i data-lucide="loader" style="width:24px;height:24px;color:var(--pos-orange-primary);"></i>
</div>
</div>
@* ═══ STATUS MESSAGE ═══ *@
<div style="text-align:center;">
<div style="font-size:20px;font-weight:600;color:var(--pos-text-primary);margin-bottom:8px;">Đang xử lý thanh toán...</div>
<div style="font-size:14px;color:var(--pos-text-tertiary);">Processing payment...</div>
</div>
@* ═══ PAYMENT INFO ═══ *@
<div style="width:100%;max-width:360px;background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:20px;display:flex;flex-direction:column;gap:12px;">
<div style="display:flex;justify-content:space-between;font-size:14px;">
<span style="color:var(--pos-text-tertiary);">Tổng thanh toán</span>
<span style="font-weight:700;color:var(--pos-orange-primary);font-size:18px;">@FormatPrice(_orderTotal)</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:14px;">
<span style="color:var(--pos-text-tertiary);">Phương thức</span>
<span style="font-weight:600;">@_paymentMethod</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:14px;">
<span style="color:var(--pos-text-tertiary);">Mã giao dịch</span>
<span style="font-weight:500;color:var(--pos-text-secondary);">@_transactionId</span>
</div>
</div>
@* ═══ CANCEL BUTTON ═══ *@
<button style="display:flex;align-items:center;gap:8px;padding:12px 24px;border-radius:var(--pos-radius);border:1px solid var(--pos-danger);
background:transparent;color:var(--pos-danger);cursor:pointer;font-size:14px;"
@onclick="Cancel">
<i data-lucide="x-circle" style="width:16px;height:16px;"></i>
Hủy giao dịch
</button>
</div>
<style>
@@keyframes spin { to { transform: rotate(360deg); } }
</style>
@code {
// EN: Demo data / VI: Dữ liệu mẫu
private decimal _orderTotal = 285_000;
private string _paymentMethod = "Thẻ (Visa •••• 4242)";
private string _transactionId = "TXN-20240215-001";
private void Cancel() => NavigateTo("payment/method-select");
}

View File

@@ -0,0 +1,84 @@
@*
EN: Payment Success — Success animation, transaction details, print/new order buttons.
VI: Thanh toán thành công — Hiệu ứng thành công, chi tiết giao dịch, nút in/đơn mới.
*@
@page "/pos/payment/success"
@layout PosLayout
@inherits PosBase
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:32px;gap:24px;">
@* ═══ SUCCESS ANIMATION ═══ *@
<div style="width:100px;height:100px;border-radius:50%;background:rgba(34,197,94,0.15);display:flex;align-items:center;justify-content:center;
animation:scaleIn 0.5s ease-out;">
<div style="width:72px;height:72px;border-radius:50%;background:var(--pos-success);display:flex;align-items:center;justify-content:center;">
<i data-lucide="check" style="width:36px;height:36px;color:#fff;stroke-width:3;"></i>
</div>
</div>
@* ═══ SUCCESS MESSAGE ═══ *@
<div style="text-align:center;">
<div style="font-size:24px;font-weight:700;color:var(--pos-success);margin-bottom:8px;">Thanh toán thành công!</div>
<div style="font-size:14px;color:var(--pos-text-tertiary);">Payment successful</div>
</div>
@* ═══ TRANSACTION DETAILS ═══ *@
<div style="width:100%;max-width:400px;background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:20px;display:flex;flex-direction:column;gap:12px;">
<div style="display:flex;justify-content:space-between;font-size:14px;">
<span style="color:var(--pos-text-tertiary);">Tổng thanh toán</span>
<span style="font-weight:700;color:var(--pos-orange-primary);font-size:18px;">@FormatPrice(_orderTotal)</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:14px;">
<span style="color:var(--pos-text-tertiary);">Phương thức</span>
<span style="font-weight:600;">@_paymentMethod</span>
</div>
@if (_changeAmount > 0)
{
<div style="display:flex;justify-content:space-between;font-size:14px;">
<span style="color:var(--pos-text-tertiary);">Tiền thối</span>
<span style="font-weight:600;color:var(--pos-success);">@FormatPrice(_changeAmount)</span>
</div>
}
<div style="display:flex;justify-content:space-between;font-size:14px;padding-top:12px;border-top:1px solid var(--pos-border-subtle);">
<span style="color:var(--pos-text-tertiary);">Mã giao dịch</span>
<span style="font-weight:500;color:var(--pos-text-secondary);font-size:12px;">@_transactionId</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:14px;">
<span style="color:var(--pos-text-tertiary);">Thời gian</span>
<span style="font-weight:500;color:var(--pos-text-secondary);font-size:12px;">@DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss")</span>
</div>
</div>
@* ═══ ACTION BUTTONS ═══ *@
<div style="display:flex;gap:12px;">
<button style="display:flex;align-items:center;gap:8px;padding:14px 28px;border-radius:var(--pos-radius);border:1px solid var(--pos-border-default);
background:var(--pos-bg-elevated);color:var(--pos-text-primary);cursor:pointer;font-size:14px;font-weight:600;"
@onclick="PrintReceipt">
<i data-lucide="printer" style="width:18px;height:18px;"></i>
In hóa đơn
</button>
<button style="display:flex;align-items:center;gap:8px;padding:14px 28px;border-radius:var(--pos-radius);border:none;
background:var(--pos-orange-primary);color:#fff;cursor:pointer;font-size:14px;font-weight:600;"
@onclick="NewOrder">
<i data-lucide="plus" style="width:18px;height:18px;"></i>
Đơn mới
</button>
</div>
</div>
<style>
@@keyframes scaleIn {
from { transform: scale(0); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
</style>
@code {
// EN: Demo data / VI: Dữ liệu mẫu
private decimal _orderTotal = 285_000;
private string _paymentMethod = "Tiền mặt";
private decimal _changeAmount = 15_000;
private string _transactionId = "TXN-20240215-001";
private void PrintReceipt() => NavigateTo("payment/receipt");
private void NewOrder() => NavigateTo("cafe");
}

View File

@@ -0,0 +1,79 @@
@*
EN: QR Payment — QR code display with provider tabs, timer countdown.
VI: Thanh toán QR — Hiển thị mã QR với tab nhà cung cấp, đếm ngược.
*@
@page "/pos/payment/qr"
@layout PosLayout
@inherits PosBase
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:32px;gap:24px;">
@* ═══ ORDER TOTAL ═══ *@
<div style="text-align:center;">
<div style="font-size:14px;color:var(--pos-text-tertiary);margin-bottom:4px;">Tổng thanh toán</div>
<div style="font-size:32px;font-weight:700;color:var(--pos-orange-primary);">@FormatPrice(_orderTotal)</div>
</div>
@* ═══ QR PROVIDER TABS ═══ *@
<div style="display:flex;gap:8px;background:var(--pos-bg-elevated);padding:4px;border-radius:var(--pos-radius);">
@foreach (var provider in _providers)
{
<button style="padding:10px 20px;border-radius:8px;border:none;cursor:pointer;font-size:13px;font-weight:600;
background:@(_selectedProvider == provider ? "var(--pos-orange-primary)" : "transparent");
color:@(_selectedProvider == provider ? "#fff" : "var(--pos-text-secondary)");"
@onclick="() => _selectedProvider = provider">
@provider
</button>
}
</div>
@* ═══ QR CODE DISPLAY ═══ *@
<div style="width:240px;height:240px;background:var(--pos-bg-elevated);border-radius:16px;border:2px solid var(--pos-border-default);
display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;">
<i data-lucide="qr-code" style="width:100px;height:100px;color:var(--pos-text-primary);"></i>
<span style="font-size:12px;color:var(--pos-text-tertiary);">@_selectedProvider</span>
</div>
@* ═══ TIMER ═══ *@
<div style="display:flex;align-items:center;gap:8px;color:var(--pos-warning);font-size:15px;">
<i data-lucide="clock" style="width:16px;height:16px;"></i>
<span style="font-weight:600;">@_timerDisplay</span>
</div>
@* ═══ STATUS ═══ *@
<div style="display:flex;align-items:center;gap:8px;color:var(--pos-text-tertiary);font-size:14px;">
<div style="width:8px;height:8px;border-radius:50%;background:var(--pos-warning);animation:pulse 2s ease-in-out infinite;"></div>
Chờ xác nhận thanh toán...
</div>
@* ═══ ACTIONS ═══ *@
<div style="display:flex;gap:12px;">
<button style="display:flex;align-items:center;gap:6px;padding:10px 20px;border-radius:var(--pos-radius);border:1px solid var(--pos-border-default);
background:var(--pos-bg-elevated);color:var(--pos-text-secondary);cursor:pointer;font-size:13px;"
@onclick="Refresh">
<i data-lucide="refresh-cw" style="width:14px;height:14px;"></i> Làm mới
</button>
<button style="padding:10px 20px;border-radius:var(--pos-radius);border:1px solid var(--pos-border-default);
background:transparent;color:var(--pos-text-secondary);cursor:pointer;font-size:13px;"
@onclick="Cancel">
Hủy
</button>
</div>
</div>
<style>
@@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
</style>
@code {
// EN: Demo order total / VI: Tổng đơn hàng mẫu
private decimal _orderTotal = 285_000;
private string _selectedProvider = "VietQR";
private int _timerSeconds = 300;
private string _timerDisplay => $"{_timerSeconds / 60}:{(_timerSeconds % 60):D2}";
// EN: QR providers / VI: Nhà cung cấp QR
private readonly string[] _providers = { "VietQR", "MoMo", "ZaloPay" };
private void Refresh() => _timerSeconds = 300;
private void Cancel() => NavigateTo("payment/method-select");
}

View File

@@ -0,0 +1,153 @@
@*
EN: Receipt — 80mm thermal printer receipt template with store info, items, totals.
VI: Hóa đơn — Mẫu hóa đơn nhiệt 80mm với thông tin cửa hàng, sản phẩm, tổng tiền.
*@
@page "/pos/payment/receipt"
@layout PosLayout
@inherits PosBase
<div style="display:flex;align-items:flex-start;justify-content:center;height:100%;padding:24px;overflow-y:auto;">
@* ═══ RECEIPT PAPER ═══ *@
<div style="width:100%;max-width:300px;background:#fff;color:#000;border-radius:4px;padding:24px 20px;font-family:'Courier New',monospace;">
@* ═══ STORE HEADER ═══ *@
<div style="text-align:center;margin-bottom:16px;">
<div style="font-size:18px;font-weight:700;">GOODGO COFFEE</div>
<div style="font-size:11px;margin-top:4px;">123 Nguyễn Huệ, Q.1, TP.HCM</div>
<div style="font-size:11px;">ĐT: 028-1234-5678</div>
</div>
@* EN: Dashed separator / VI: Đường kẻ đứt *@
<div style="border-top:1px dashed #000;margin:12px 0;"></div>
@* ═══ ORDER INFO ═══ *@
<div style="display:flex;justify-content:space-between;font-size:11px;margin-bottom:8px;">
<span>Đơn #@_orderNumber</span>
<span>@_orderDate</span>
</div>
<div style="font-size:11px;margin-bottom:4px;">NV: @_staffName</div>
<div style="border-top:1px dashed #000;margin:12px 0;"></div>
@* ═══ ITEM LIST ═══ *@
@foreach (var item in _items)
{
<div style="display:flex;justify-content:space-between;font-size:12px;padding:3px 0;">
<span>@item.Qty x @item.Name</span>
<span>@FormatReceiptPrice(item.Price * item.Qty)</span>
</div>
}
<div style="border-top:1px dashed #000;margin:12px 0;"></div>
@* ═══ TOTALS ═══ *@
<div style="display:flex;flex-direction:column;gap:4px;font-size:12px;">
<div style="display:flex;justify-content:space-between;">
<span>Tạm tính</span>
<span>@FormatReceiptPrice(_subtotal)</span>
</div>
<div style="display:flex;justify-content:space-between;">
<span>Phí dịch vụ (5%)</span>
<span>@FormatReceiptPrice(_serviceCharge)</span>
</div>
<div style="display:flex;justify-content:space-between;">
<span>VAT (8%)</span>
<span>@FormatReceiptPrice(_vat)</span>
</div>
</div>
<div style="border-top:2px solid #000;margin:10px 0;"></div>
<div style="display:flex;justify-content:space-between;font-size:16px;font-weight:700;">
<span>TỔNG CỘNG</span>
<span>@FormatReceiptPrice(_total)</span>
</div>
<div style="border-top:1px dashed #000;margin:12px 0;"></div>
@* ═══ PAYMENT INFO ═══ *@
<div style="display:flex;flex-direction:column;gap:4px;font-size:12px;">
<div style="display:flex;justify-content:space-between;">
<span>Thanh toán</span>
<span>@_paymentMethod</span>
</div>
<div style="display:flex;justify-content:space-between;">
<span>Khách đưa</span>
<span>@FormatReceiptPrice(_amountPaid)</span>
</div>
<div style="display:flex;justify-content:space-between;">
<span>Tiền thối</span>
<span>@FormatReceiptPrice(_changeAmount)</span>
</div>
</div>
<div style="border-top:1px dashed #000;margin:12px 0;"></div>
@* ═══ TRANSACTION ID ═══ *@
<div style="text-align:center;font-size:11px;color:#666;">
<div>Mã GD: @_transactionId</div>
<div>@_orderDate @_orderTime</div>
</div>
<div style="border-top:1px dashed #000;margin:12px 0;"></div>
@* ═══ FOOTER ═══ *@
<div style="text-align:center;font-size:13px;font-weight:700;margin-top:8px;">
Cảm ơn quý khách!
</div>
<div style="text-align:center;font-size:10px;color:#666;margin-top:4px;">
Thank you & see you again!
</div>
</div>
</div>
@* ═══ ACTION BUTTONS (fixed bottom) ═══ *@
<div style="position:fixed;bottom:0;left:0;right:0;padding:16px;display:flex;justify-content:center;gap:12px;background:var(--pos-bg-elevated);border-top:1px solid var(--pos-border-subtle);">
<button style="display:flex;align-items:center;gap:8px;padding:12px 28px;border-radius:var(--pos-radius);border:none;
background:var(--pos-orange-primary);color:#fff;cursor:pointer;font-size:14px;font-weight:600;"
@onclick="Print">
<i data-lucide="printer" style="width:16px;height:16px;"></i>
In hóa đơn
</button>
<button style="padding:12px 24px;border-radius:var(--pos-radius);border:1px solid var(--pos-border-default);
background:transparent;color:var(--pos-text-secondary);cursor:pointer;font-size:14px;"
@onclick="Close">
Đóng
</button>
</div>
@code {
// EN: Demo receipt data / VI: Dữ liệu hóa đơn mẫu
private string _orderNumber = "1042";
private string _orderDate = "15/02/2024";
private string _orderTime = "14:35:22";
private string _staffName = "Nguyễn Thị Mai";
private readonly List<ReceiptItem> _items = new()
{
new("Cà phê sữa đá", 35_000, 2),
new("Cappuccino", 55_000, 1),
new("Croissant bơ", 35_000, 1),
new("Trà đào cam sả", 45_000, 1),
new("Bánh mì thịt", 30_000, 1),
};
private decimal _subtotal => _items.Sum(i => i.Price * i.Qty);
private decimal _serviceCharge => Math.Round(_subtotal * 0.05m);
private decimal _vat => Math.Round(_subtotal * 0.08m);
private decimal _total => _subtotal + _serviceCharge + _vat;
private string _paymentMethod = "Tiền mặt";
private decimal _amountPaid = 300_000;
private decimal _changeAmount => _amountPaid - _total;
private string _transactionId = "TXN-20240215-001";
private static string FormatReceiptPrice(decimal price) => price.ToString("N0") + "₫";
private void Print()
{
// EN: Trigger browser print / VI: Kích hoạt in từ trình duyệt
}
private void Close() => NavigateTo("payment/success");
private record ReceiptItem(string Name, decimal Price, int Qty);
}

View File

@@ -0,0 +1,122 @@
@*
EN: Tip Entry — Quick tip buttons, custom tip amount, total with tip display.
VI: Nhập tiền tip — Nút tip nhanh, nhập số tiền tùy chỉnh, hiển thị tổng kèm tip.
*@
@page "/pos/payment/tip"
@layout PosLayout
@inherits PosBase
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:32px;gap:28px;">
@* ═══ SUBTOTAL ═══ *@
<div style="text-align:center;">
<div style="font-size:14px;color:var(--pos-text-tertiary);margin-bottom:4px;">Tạm tính / Subtotal</div>
<div style="font-size:28px;font-weight:700;color:var(--pos-text-primary);">@FormatPrice(_subtotal)</div>
</div>
@* ═══ QUICK TIP BUTTONS ═══ *@
<div style="width:100%;max-width:480px;">
<div style="font-size:14px;font-weight:600;color:var(--pos-text-secondary);margin-bottom:12px;text-align:center;">Chọn mức tip</div>
<div style="display:flex;gap:10px;">
@foreach (var tip in _tipOptions)
{
<button style="flex:1;padding:16px 8px;border-radius:var(--pos-radius);
border:2px solid @(_selectedTip == tip.Label ? "var(--pos-orange-primary)" : "var(--pos-border-default)");
background:@(_selectedTip == tip.Label ? "rgba(255,92,0,0.1)" : "var(--pos-bg-elevated)");
color:var(--pos-text-primary);cursor:pointer;text-align:center;"
@onclick="() => SelectTip(tip)">
<div style="font-size:18px;font-weight:700;">@tip.Label</div>
@if (tip.Percent > 0)
{
<div style="font-size:12px;color:var(--pos-text-tertiary);margin-top:4px;">@FormatPrice(_subtotal * tip.Percent / 100)</div>
}
</button>
}
</div>
</div>
@* ═══ CUSTOM TIP INPUT ═══ *@
@if (_showCustomInput)
{
<div style="width:100%;max-width:480px;display:flex;gap:8px;">
<input type="number" @bind="_customTipAmount" placeholder="Nhập số tiền tip..."
style="flex:1;padding:14px 16px;border-radius:var(--pos-radius);border:1px solid var(--pos-border-default);
background:var(--pos-bg-elevated);color:var(--pos-text-primary);font-size:16px;outline:none;" />
<button style="padding:14px 20px;border-radius:var(--pos-radius);border:none;background:var(--pos-orange-primary);
color:#fff;cursor:pointer;font-size:14px;font-weight:600;"
@onclick="ApplyCustomTip">
Áp dụng
</button>
</div>
}
@* ═══ TOTAL WITH TIP ═══ *@
<div style="width:100%;max-width:480px;background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:20px;display:flex;flex-direction:column;gap:12px;">
<div style="display:flex;justify-content:space-between;font-size:14px;">
<span style="color:var(--pos-text-tertiary);">Tạm tính</span>
<span>@FormatPrice(_subtotal)</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:14px;">
<span style="color:var(--pos-text-tertiary);">Tiền tip</span>
<span style="color:var(--pos-success);font-weight:600;">+@FormatPrice(_tipAmount)</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:20px;font-weight:700;padding-top:12px;border-top:1px dashed var(--pos-border-subtle);">
<span>Tổng cộng</span>
<span style="color:var(--pos-orange-primary);">@FormatPrice(_subtotal + _tipAmount)</span>
</div>
</div>
@* ═══ ACTIONS ═══ *@
<div style="display:flex;gap:12px;">
<button style="padding:12px 24px;border-radius:var(--pos-radius);border:1px solid var(--pos-border-default);
background:transparent;color:var(--pos-text-secondary);cursor:pointer;font-size:14px;"
@onclick="Skip">
Bỏ qua
</button>
<button class="pos-btn-checkout" @onclick="Confirm">
Xác nhận
</button>
</div>
</div>
@code {
// EN: Demo subtotal / VI: Tạm tính mẫu
private decimal _subtotal = 285_000;
private decimal _tipAmount = 0;
private string _selectedTip = "";
private bool _showCustomInput = false;
private decimal _customTipAmount = 0;
// EN: Tip options / VI: Tùy chọn tip
private readonly List<TipOption> _tipOptions = new()
{
new("5%", 5),
new("10%", 10),
new("15%", 15),
new("20%", 20),
new("Khác", 0),
};
private void SelectTip(TipOption tip)
{
_selectedTip = tip.Label;
if (tip.Percent > 0)
{
_tipAmount = Math.Round(_subtotal * tip.Percent / 100);
_showCustomInput = false;
}
else
{
_showCustomInput = true;
}
}
private void ApplyCustomTip()
{
_tipAmount = _customTipAmount;
}
private void Skip() => NavigateTo("payment/method-select");
private void Confirm() => NavigateTo("payment/method-select");
private record TipOption(string Label, decimal Percent);
}