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:
@@ -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");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user