feat(pos-cafe): implement payment method settings, add detailed order history view, and update payment icons to Lucide

This commit is contained in:
Ho Ngoc Hai
2026-03-05 04:40:06 +07:00
parent b041ba5449
commit 88cd45c3a8
2 changed files with 447 additions and 15 deletions

View File

@@ -126,21 +126,30 @@
</div>
<div class="pos-payment-methods">
<button class="pos-payment-method-btn" @onclick='() => SelectPaymentMethod("cash")'>
<span class="pos-payment-method-btn__icon">💵</span>
<span class="pos-payment-method-btn__icon"><i data-lucide="wallet" style="width:28px;height:28px;"></i></span>
<span class="pos-payment-method-btn__label">Tiền mặt</span>
</button>
@if (_payCardEnabled)
{
<button class="pos-payment-method-btn" @onclick='() => SelectPaymentMethod("card")'>
<span class="pos-payment-method-btn__icon">💳</span>
<span class="pos-payment-method-btn__icon"><i data-lucide="credit-card" style="width:28px;height:28px;"></i></span>
<span class="pos-payment-method-btn__label">Thẻ</span>
</button>
}
@if (_payQrEnabled)
{
<button class="pos-payment-method-btn" @onclick='() => SelectPaymentMethod("qr")'>
<span class="pos-payment-method-btn__icon">📱</span>
<span class="pos-payment-method-btn__icon"><i data-lucide="smartphone" style="width:28px;height:28px;"></i></span>
<span class="pos-payment-method-btn__label">Mã QR</span>
</button>
}
@if (_payTransferEnabled)
{
<button class="pos-payment-method-btn" @onclick='() => SelectPaymentMethod("transfer")'>
<span class="pos-payment-method-btn__icon">🏦</span>
<span class="pos-payment-method-btn__icon"><i data-lucide="building-2" style="width:28px;height:28px;"></i></span>
<span class="pos-payment-method-btn__label">Chuyển khoản</span>
</button>
}
</div>
</div>
}
@@ -152,7 +161,7 @@
<button class="pos-payment-header__back" @onclick="() => _paymentStep = PayStep.MethodSelect">
<i data-lucide="arrow-left" style="width:16px;height:16px;"></i>
</button>
<span style="font-weight:600;">💵 Tiền mặt</span>
<span style="font-weight:600;display:flex;align-items:center;gap:6px;"><i data-lucide="wallet" style="width:16px;height:16px;"></i> Tiền mặt</span>
<span style="margin-left:auto;font-size:16px;font-weight:700;color:var(--pos-orange-primary);">@FormatPrice(FinalTotal)</span>
</div>
<div class="pos-payment-amount-section">
@@ -215,12 +224,12 @@
}
else if (_selectedMethod == "card")
{
<div style="font-size:48px;">💳</div>
<div><i data-lucide="credit-card" style="width:48px;height:48px;color:var(--pos-text-secondary);"></i></div>
<div style="font-size:14px;color:var(--pos-text-secondary);">Chạm, quẹt hoặc cắm thẻ</div>
}
else
{
<div style="font-size:48px;">🏦</div>
<div><i data-lucide="building-2" style="width:48px;height:48px;color:var(--pos-text-secondary);"></i></div>
<div style="font-size:14px;color:var(--pos-text-secondary);">Xác nhận đã nhận chuyển khoản</div>
}
</div>
@@ -261,6 +270,122 @@
case PosTab.History:
@* ═══ ORDER HISTORY TAB ═══ *@
<div class="pos-history" style="width:100%;">
@if (_selectedOrderDetail != null)
{
@* ─── ORDER DETAIL VIEW (API order) ─── *@
var od = _selectedOrderDetail.Order;
var items = _selectedOrderDetail.Items ?? new();
<div style="display:flex;flex-direction:column;height:100%;">
<div class="pos-payment-header">
<button class="pos-payment-header__back" @onclick="CloseOrderDetail">
<i data-lucide="arrow-left" style="width:16px;height:16px;"></i>
</button>
<span style="font-weight:600;">Chi tiết đơn hàng</span>
</div>
<div style="flex:1;overflow-y:auto;padding:16px;">
<div style="display:flex;flex-direction:column;gap:8px;margin-bottom:16px;">
<div style="display:flex;justify-content:space-between;font-size:13px;">
<span style="color:var(--pos-text-tertiary);">Mã đơn</span>
<span style="font-weight:600;">@(od?.Id.ToString()[..8].ToUpper())</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:13px;">
<span style="color:var(--pos-text-tertiary);">Ngày</span>
<span>@(od?.CreatedAt.ToString("dd/MM/yyyy — HH:mm:ss"))</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:13px;">
<span style="color:var(--pos-text-tertiary);">Trạng thái</span>
<span style="color:var(--pos-success);font-weight:600;">@MapApiStatus(od?.Status)</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:13px;">
<span style="color:var(--pos-text-tertiary);">Thanh toán</span>
<span>@MapPaymentMethodLabel(od?.PaymentMethod)</span>
</div>
</div>
<div style="border-top:1px dashed var(--pos-border-subtle);margin-bottom:12px;"></div>
<table style="width:100%;border-collapse:collapse;font-size:13px;">
<thead>
<tr style="color:var(--pos-text-tertiary);font-size:11px;text-transform:uppercase;">
<th style="text-align:left;padding:6px 0;">Sản phẩm</th>
<th style="text-align:center;padding:6px 4px;width:40px;">SL</th>
<th style="text-align:right;padding:6px 0;width:80px;">Đ.Giá</th>
<th style="text-align:right;padding:6px 0;width:90px;">T.Tiền</th>
</tr>
</thead>
<tbody>
@foreach (var item in items)
{
<tr style="border-top:1px solid var(--pos-border-subtle);">
<td style="padding:8px 0;">@(item.ProductName ?? "—")</td>
<td style="text-align:center;padding:8px 4px;">@item.Quantity</td>
<td style="text-align:right;padding:8px 0;">@FormatPrice(item.UnitPrice)</td>
<td style="text-align:right;padding:8px 0;font-weight:600;">@FormatPrice(item.Subtotal)</td>
</tr>
}
</tbody>
</table>
<div style="border-top:1px dashed var(--pos-border-subtle);margin:12px 0;"></div>
<div style="display:flex;justify-content:space-between;font-size:16px;font-weight:700;">
<span>TỔNG CỘNG</span>
<span style="color:var(--pos-orange-primary);">@FormatPrice(od?.TotalAmount ?? 0)</span>
</div>
</div>
<div style="padding:16px;border-top:1px solid var(--pos-border-subtle);display:flex;gap:10px;">
<button style="flex:1;padding:12px;border-radius:var(--pos-radius);border:1px solid var(--pos-border-default);background:transparent;color:var(--pos-text-primary);cursor:pointer;font-size:13px;font-weight:600;"
@onclick="PrintOrderDetail">
<i data-lucide="printer" style="width:14px;height:14px;vertical-align:middle;margin-right:4px;"></i>In hóa đơn
</button>
</div>
</div>
}
else if (_selectedOrder != null)
{
@* ─── SESSION ORDER DETAIL VIEW ─── *@
var so = _selectedOrder;
<div style="display:flex;flex-direction:column;height:100%;">
<div class="pos-payment-header">
<button class="pos-payment-header__back" @onclick="CloseOrderDetail">
<i data-lucide="arrow-left" style="width:16px;height:16px;"></i>
</button>
<span style="font-weight:600;">Chi tiết đơn hàng</span>
</div>
<div style="flex:1;overflow-y:auto;padding:16px;">
<div style="display:flex;flex-direction:column;gap:8px;margin-bottom:16px;">
<div style="display:flex;justify-content:space-between;font-size:13px;">
<span style="color:var(--pos-text-tertiary);">Mã đơn</span>
<span style="font-weight:600;">@so.Id</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:13px;">
<span style="color:var(--pos-text-tertiary);">Thời gian</span>
<span>@so.Time</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:13px;">
<span style="color:var(--pos-text-tertiary);">Trạng thái</span>
<span style="color:var(--pos-success);font-weight:600;">@so.Status</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:13px;">
<span style="color:var(--pos-text-tertiary);">Thanh toán</span>
<span>@so.Method</span>
</div>
</div>
<div style="border-top:1px dashed var(--pos-border-subtle);margin-bottom:12px;"></div>
<div style="font-size:13px;color:var(--pos-text-secondary);margin-bottom:12px;">@so.Items</div>
<div style="border-top:1px dashed var(--pos-border-subtle);margin:12px 0;"></div>
<div style="display:flex;justify-content:space-between;font-size:16px;font-weight:700;">
<span>TỔNG CỘNG</span>
<span style="color:var(--pos-orange-primary);">@FormatPrice(so.Total)</span>
</div>
</div>
<div style="padding:16px;border-top:1px solid var(--pos-border-subtle);display:flex;gap:10px;">
<button style="flex:1;padding:12px;border-radius:var(--pos-radius);border:1px solid var(--pos-border-default);background:transparent;color:var(--pos-text-primary);cursor:pointer;font-size:13px;font-weight:600;"
@onclick="PrintSessionOrderDetail">
<i data-lucide="printer" style="width:14px;height:14px;vertical-align:middle;margin-right:4px;"></i>In hóa đơn
</button>
</div>
</div>
}
else
{
@* ─── ORDER LIST ─── *@
<div class="pos-history__toolbar">
<input class="pos-history__search" placeholder="Tìm mã đơn, tên khách..."
@bind="_historySearch" @bind:event="oninput" />
@@ -273,10 +398,10 @@
}
</div>
<div class="pos-history__list">
@if (_historyLoading)
@if (_historyLoading || _orderDetailLoading)
{
<div style="display:flex;align-items:center;justify-content:center;padding:60px 0;color:var(--pos-text-tertiary);">
<div style="font-size:14px;">Đang tải đơn hàng...</div>
<div style="font-size:14px;">Đang tải...</div>
</div>
}
else
@@ -302,7 +427,7 @@
@* EN: API orders from DB *@
@foreach (var order in FilteredApiOrders)
{
<div class="pos-history__card">
<div class="pos-history__card" @onclick="() => ViewOrderDetail(order.Id)">
<div class="pos-history__card-header">
<span class="pos-history__order-id">@order.Id.ToString()[..8].ToUpper()</span>
<span class="pos-history__status @(order.Status == "Completed" || order.Status == "Delivered" ? "pos-history__status--completed" : "pos-history__status--refunded")">
@@ -328,6 +453,7 @@
}
}
</div>
}
</div>
break;
@@ -446,6 +572,99 @@
}
</div>
break;
case PosTab.Settings:
@* ═══ PAYMENT SETTINGS TAB ═══ *@
<div style="width:100%;max-width:560px;margin:0 auto;padding:16px;">
<div style="font-size:18px;font-weight:700;color:var(--pos-text-primary);margin-bottom:16px;">Cài đặt thanh toán</div>
@* ─── Card ─── *@
<div style="background:var(--pos-bg-card);border:1px solid var(--pos-border-subtle);border-radius:var(--pos-radius);margin-bottom:12px;overflow:hidden;">
<div style="display:flex;align-items:center;justify-content:space-between;padding:14px 16px;cursor:pointer;" @onclick="() => _settingsCardOpen = !_settingsCardOpen">
<div style="display:flex;align-items:center;gap:10px;">
<i data-lucide="credit-card" style="width:20px;height:20px;color:var(--pos-orange-primary);"></i>
<span style="font-weight:600;font-size:14px;">Thẻ (Card)</span>
</div>
<div style="display:flex;align-items:center;gap:12px;">
<button class="pos-toggle @(_payCardEnabled ? "pos-toggle--on" : "")" @onclick="ToggleCard" @onclick:stopPropagation="true">
<span class="pos-toggle__knob"></span>
</button>
<i data-lucide="@(_settingsCardOpen ? "chevron-up" : "chevron-down")" style="width:16px;height:16px;color:var(--pos-text-tertiary);"></i>
</div>
</div>
@if (_settingsCardOpen && _payCardEnabled)
{
<div style="padding:0 16px 14px;border-top:1px solid var(--pos-border-subtle);">
<label style="font-size:12px;color:var(--pos-text-tertiary);display:block;margin:12px 0 4px;">Terminal ID</label>
<input type="text" class="pos-settings-input" placeholder="Nhập mã terminal..." @bind="_cardTerminalId" />
</div>
}
</div>
@* ─── QR ─── *@
<div style="background:var(--pos-bg-card);border:1px solid var(--pos-border-subtle);border-radius:var(--pos-radius);margin-bottom:12px;overflow:hidden;">
<div style="display:flex;align-items:center;justify-content:space-between;padding:14px 16px;cursor:pointer;" @onclick="() => _settingsQrOpen = !_settingsQrOpen">
<div style="display:flex;align-items:center;gap:10px;">
<i data-lucide="smartphone" style="width:20px;height:20px;color:var(--pos-orange-primary);"></i>
<span style="font-weight:600;font-size:14px;">Mã QR</span>
</div>
<div style="display:flex;align-items:center;gap:12px;">
<button class="pos-toggle @(_payQrEnabled ? "pos-toggle--on" : "")" @onclick="ToggleQr" @onclick:stopPropagation="true">
<span class="pos-toggle__knob"></span>
</button>
<i data-lucide="@(_settingsQrOpen ? "chevron-up" : "chevron-down")" style="width:16px;height:16px;color:var(--pos-text-tertiary);"></i>
</div>
</div>
@if (_settingsQrOpen && _payQrEnabled)
{
<div style="padding:0 16px 14px;border-top:1px solid var(--pos-border-subtle);">
<label style="font-size:12px;color:var(--pos-text-tertiary);display:block;margin:12px 0 4px;">Ngân hàng</label>
<input type="text" class="pos-settings-input" placeholder="VD: Vietcombank, MB Bank..." @bind="_qrBankName" />
<label style="font-size:12px;color:var(--pos-text-tertiary);display:block;margin:12px 0 4px;">Số tài khoản</label>
<input type="text" class="pos-settings-input" placeholder="Nhập số tài khoản..." @bind="_qrAccountNumber" />
<label style="font-size:12px;color:var(--pos-text-tertiary);display:block;margin:12px 0 4px;">Tên chủ tài khoản</label>
<input type="text" class="pos-settings-input" placeholder="Nhập tên chủ TK..." @bind="_qrAccountName" />
</div>
}
</div>
@* ─── Transfer ─── *@
<div style="background:var(--pos-bg-card);border:1px solid var(--pos-border-subtle);border-radius:var(--pos-radius);margin-bottom:12px;overflow:hidden;">
<div style="display:flex;align-items:center;justify-content:space-between;padding:14px 16px;cursor:pointer;" @onclick="() => _settingsTransferOpen = !_settingsTransferOpen">
<div style="display:flex;align-items:center;gap:10px;">
<i data-lucide="building-2" style="width:20px;height:20px;color:var(--pos-orange-primary);"></i>
<span style="font-weight:600;font-size:14px;">Chuyển khoản</span>
</div>
<div style="display:flex;align-items:center;gap:12px;">
<button class="pos-toggle @(_payTransferEnabled ? "pos-toggle--on" : "")" @onclick="ToggleTransfer" @onclick:stopPropagation="true">
<span class="pos-toggle__knob"></span>
</button>
<i data-lucide="@(_settingsTransferOpen ? "chevron-up" : "chevron-down")" style="width:16px;height:16px;color:var(--pos-text-tertiary);"></i>
</div>
</div>
@if (_settingsTransferOpen && _payTransferEnabled)
{
<div style="padding:0 16px 14px;border-top:1px solid var(--pos-border-subtle);">
<label style="font-size:12px;color:var(--pos-text-tertiary);display:block;margin:12px 0 4px;">Ngân hàng</label>
<input type="text" class="pos-settings-input" placeholder="VD: Vietcombank, MB Bank..." @bind="_transferBankName" />
<label style="font-size:12px;color:var(--pos-text-tertiary);display:block;margin:12px 0 4px;">Số tài khoản</label>
<input type="text" class="pos-settings-input" placeholder="Nhập số tài khoản..." @bind="_transferAccountNumber" />
<label style="font-size:12px;color:var(--pos-text-tertiary);display:block;margin:12px 0 4px;">Tên chủ tài khoản</label>
<input type="text" class="pos-settings-input" placeholder="Nhập tên chủ TK..." @bind="_transferAccountName" />
</div>
}
</div>
<button style="width:100%;padding:12px;border-radius:var(--pos-radius);border:none;background:var(--pos-orange-primary);color:#fff;font-size:14px;font-weight:600;cursor:pointer;margin-top:8px;"
@onclick="SavePaymentSettings">
<i data-lucide="save" style="width:16px;height:16px;vertical-align:middle;margin-right:6px;"></i>Lưu cài đặt
</button>
@if (!string.IsNullOrEmpty(_settingsSaveMsg))
{
<div style="text-align:center;font-size:13px;color:var(--pos-success);margin-top:8px;">@_settingsSaveMsg</div>
}
</div>
break;
}
</div>
@@ -466,11 +685,16 @@
<i data-lucide="bar-chart-3" class="pos-bottom-nav__icon"></i>
<span>Dashboard</span>
</button>
<button class="pos-bottom-nav__tab @(_activeTab == PosTab.Settings ? "pos-bottom-nav__tab--active" : "")"
@onclick="() => SwitchTab(PosTab.Settings)">
<i data-lucide="settings" class="pos-bottom-nav__icon"></i>
<span>Cài đặt</span>
</button>
</div>
@code {
// ═══════════════ TAB SYSTEM ═══════════════
private enum PosTab { Sale, History, Dashboard }
private enum PosTab { Sale, History, Dashboard, Settings }
private PosTab _activeTab = PosTab.Sale;
private async Task SwitchTab(PosTab tab)
@@ -529,6 +753,8 @@
}
catch { _loadError = true; }
finally { _isLoading = false; }
await LoadPaymentSettings();
}
private void AddToCart(Product product)
@@ -611,10 +837,10 @@
private string GetMethodLabel() => _selectedMethod switch
{
"cash" => "💵 Tiền mặt",
"card" => "💳 Thẻ",
"qr" => "📱 Mã QR",
"transfer" => "🏦 Chuyển khoản",
"cash" => "Tiền mặt",
"card" => "Thẻ",
"qr" => "Mã QR",
"transfer" => "Chuyển khoản",
_ => "Thanh toán"
};
@@ -764,6 +990,8 @@
private SessionOrder? _selectedOrder;
private bool _historyLoading;
private bool _historyLoaded;
private PosDataService.OrderDetailResponse? _selectedOrderDetail;
private bool _orderDetailLoading;
// EN: Session orders created during this POS session (in-memory, real-time)
// VI: Đơn hàng tạo trong phiên POS này (in-memory, real-time)
@@ -839,6 +1067,102 @@
_ => method ?? "Tiền mặt"
};
private async Task ViewOrderDetail(Guid orderId)
{
_orderDetailLoading = true;
StateHasChanged();
try { _selectedOrderDetail = await DataService.GetOrderDetailAsync(orderId, ShopId); }
catch { _selectedOrderDetail = null; }
_orderDetailLoading = false;
StateHasChanged();
}
private void CloseOrderDetail()
{
_selectedOrderDetail = null;
_selectedOrder = null;
}
private async Task PrintOrderDetail()
{
if (_selectedOrderDetail?.Order == null) return;
var od = _selectedOrderDetail.Order;
var items = _selectedOrderDetail.Items ?? new();
var payLabel = MapPaymentMethodLabel(od.PaymentMethod);
var sb = new System.Text.StringBuilder();
foreach (var item in items)
{
sb.AppendLine($"<tr><td style='text-align:left;padding:3px 0;'>{System.Net.WebUtility.HtmlEncode(item.ProductName ?? "—")}</td>");
sb.AppendLine($"<td style='text-align:center;padding:3px 4px;'>{item.Quantity}</td>");
sb.AppendLine($"<td style='text-align:right;padding:3px 0;'>{item.UnitPrice:N0}</td>");
sb.AppendLine($"<td style='text-align:right;padding:3px 0;font-weight:600;'>{item.Subtotal:N0}</td></tr>");
}
var receiptHtml = "<!DOCTYPE html><html><head><meta charset='utf-8'>" +
$"<title>Hóa đơn - {od.Id.ToString()[..8].ToUpper()}</title>" +
"<style>" +
"@page { margin: 4mm; size: 80mm auto; }" +
"body { font-family: 'Courier New', monospace; font-size: 12px; width: 72mm; margin: 0 auto; color: #000; }" +
".c { text-align: center; } .b { font-weight: bold; }" +
".d { border-top: 1px dashed #000; margin: 6px 0; }" +
"table { width: 100%; border-collapse: collapse; }" +
"th { text-align: left; font-size: 11px; border-bottom: 1px solid #000; padding: 2px 0; }" +
".f { font-size: 10px; text-align: center; margin-top: 8px; color: #555; }" +
"</style></head><body>" +
"<div class='c b' style='font-size:16px;'>GoodGo POS</div>" +
"<div class='c' style='font-size:10px;margin-bottom:4px;'>Hệ thống quản lý bán hàng thông minh</div>" +
"<div class='d'></div>" +
$"<div><b>Mã đơn:</b> {od.Id.ToString()[..8].ToUpper()}</div>" +
$"<div><b>Ngày:</b> {od.CreatedAt:dd/MM/yyyy} — {od.CreatedAt:HH:mm:ss}</div>" +
$"<div><b>Thanh toán:</b> {payLabel}</div>" +
"<div class='d'></div>" +
"<table><tr><th>Sản phẩm</th><th style='text-align:center;'>SL</th><th style='text-align:right;'>Đ.Giá</th><th style='text-align:right;'>T.Tiền</th></tr>" +
sb.ToString() +
"</table><div class='d'></div>" +
$"<div style='display:flex;justify-content:space-between;font-size:14px;font-weight:bold;'><span>TỔNG CỘNG</span><span>{od.TotalAmount:N0}₫</span></div>" +
"<div class='d'></div>" +
"<div class='f'>Cảm ơn quý khách! Hẹn gặp lại</div>" +
"<div class='f'>Powered by GoodGo Platform</div>" +
"<script>window.onload=function(){window.print();window.onafterprint=function(){window.close();}}</script>" +
"</body></html>";
await JS.InvokeVoidAsync("printPosReceipt", receiptHtml);
}
private async Task PrintSessionOrderDetail()
{
if (_selectedOrder == null) return;
var so = _selectedOrder;
var receiptHtml = "<!DOCTYPE html><html><head><meta charset='utf-8'>" +
$"<title>Hóa đơn - {System.Net.WebUtility.HtmlEncode(so.Id)}</title>" +
"<style>" +
"@page { margin: 4mm; size: 80mm auto; }" +
"body { font-family: 'Courier New', monospace; font-size: 12px; width: 72mm; margin: 0 auto; color: #000; }" +
".c { text-align: center; } .b { font-weight: bold; }" +
".d { border-top: 1px dashed #000; margin: 6px 0; }" +
".f { font-size: 10px; text-align: center; margin-top: 8px; color: #555; }" +
"</style></head><body>" +
"<div class='c b' style='font-size:16px;'>GoodGo POS</div>" +
"<div class='c' style='font-size:10px;margin-bottom:4px;'>Hệ thống quản lý bán hàng thông minh</div>" +
"<div class='d'></div>" +
$"<div><b>Mã đơn:</b> {System.Net.WebUtility.HtmlEncode(so.Id)}</div>" +
$"<div><b>Thời gian:</b> {System.Net.WebUtility.HtmlEncode(so.Time)}</div>" +
$"<div><b>Thanh toán:</b> {System.Net.WebUtility.HtmlEncode(so.Method)}</div>" +
"<div class='d'></div>" +
$"<div style='font-size:12px;'>{System.Net.WebUtility.HtmlEncode(so.Items)}</div>" +
"<div class='d'></div>" +
$"<div style='display:flex;justify-content:space-between;font-size:14px;font-weight:bold;'><span>TỔNG CỘNG</span><span>{so.Total:N0}₫</span></div>" +
"<div class='d'></div>" +
"<div class='f'>Cảm ơn quý khách! Hẹn gặp lại</div>" +
"<div class='f'>Powered by GoodGo Platform</div>" +
"<script>window.onload=function(){window.print();window.onafterprint=function(){window.close();}}</script>" +
"</body></html>";
await JS.InvokeVoidAsync("printPosReceipt", receiptHtml);
}
// ═══════════════ DASHBOARD TAB — API-driven ═══════════════
private bool _dashLoading;
private bool _dashLoaded;
@@ -890,6 +1214,74 @@
StateHasChanged();
}
// ═══════════════ PAYMENT SETTINGS ═══════════════
private bool _payCardEnabled = true;
private bool _payQrEnabled = true;
private bool _payTransferEnabled = true;
private string _cardTerminalId = "";
private string _qrBankName = "";
private string _qrAccountNumber = "";
private string _qrAccountName = "";
private string _transferBankName = "";
private string _transferAccountNumber = "";
private string _transferAccountName = "";
private bool _settingsCardOpen;
private bool _settingsQrOpen;
private bool _settingsTransferOpen;
private string? _settingsSaveMsg;
private void ToggleCard() => _payCardEnabled = !_payCardEnabled;
private void ToggleQr() => _payQrEnabled = !_payQrEnabled;
private void ToggleTransfer() => _payTransferEnabled = !_payTransferEnabled;
private async Task LoadPaymentSettings()
{
try
{
var json = await JS.InvokeAsync<string?>("localStorage.getItem", "pos_payment_settings");
if (!string.IsNullOrEmpty(json))
{
var doc = System.Text.Json.JsonDocument.Parse(json);
var r = doc.RootElement;
if (r.TryGetProperty("cardEnabled", out var ce)) _payCardEnabled = ce.GetBoolean();
if (r.TryGetProperty("qrEnabled", out var qe)) _payQrEnabled = qe.GetBoolean();
if (r.TryGetProperty("transferEnabled", out var te)) _payTransferEnabled = te.GetBoolean();
if (r.TryGetProperty("cardTerminalId", out var ct)) _cardTerminalId = ct.GetString() ?? "";
if (r.TryGetProperty("qrBankName", out var qb)) _qrBankName = qb.GetString() ?? "";
if (r.TryGetProperty("qrAccountNumber", out var qa)) _qrAccountNumber = qa.GetString() ?? "";
if (r.TryGetProperty("qrAccountName", out var qn)) _qrAccountName = qn.GetString() ?? "";
if (r.TryGetProperty("transferBankName", out var tb)) _transferBankName = tb.GetString() ?? "";
if (r.TryGetProperty("transferAccountNumber", out var ta)) _transferAccountNumber = ta.GetString() ?? "";
if (r.TryGetProperty("transferAccountName", out var tn)) _transferAccountName = tn.GetString() ?? "";
}
}
catch { /* first load — no settings yet */ }
}
private async Task SavePaymentSettings()
{
var settings = new
{
cardEnabled = _payCardEnabled,
qrEnabled = _payQrEnabled,
transferEnabled = _payTransferEnabled,
cardTerminalId = _cardTerminalId,
qrBankName = _qrBankName,
qrAccountNumber = _qrAccountNumber,
qrAccountName = _qrAccountName,
transferBankName = _transferBankName,
transferAccountNumber = _transferAccountNumber,
transferAccountName = _transferAccountName
};
var json = System.Text.Json.JsonSerializer.Serialize(settings);
await JS.InvokeVoidAsync("localStorage.setItem", "pos_payment_settings", json);
_settingsSaveMsg = "Đã lưu cài đặt!";
StateHasChanged();
await Task.Delay(2000);
_settingsSaveMsg = null;
StateHasChanged();
}
// ═══════════════ RECORDS ═══════════════
private record Product(Guid Id, string Name, decimal Price, string Category);
private class CartItem(Guid productId, string name, decimal price)

View File

@@ -892,4 +892,44 @@
.pos-product-panel--dimmed {
opacity: 0.4;
pointer-events: none;
}
/* Toggle switch */
.pos-toggle {
position: relative;
width: 40px;
height: 22px;
border-radius: 11px;
border: none;
background: var(--pos-border-default, #444);
cursor: pointer;
transition: background 0.2s;
padding: 0;
}
.pos-toggle--on { background: var(--pos-orange-primary, #ff5c00); }
.pos-toggle__knob {
position: absolute;
top: 2px;
left: 2px;
width: 18px;
height: 18px;
border-radius: 50%;
background: #fff;
transition: transform 0.2s;
}
.pos-toggle--on .pos-toggle__knob { transform: translateX(18px); }
/* Settings input */
.pos-settings-input {
width: 100%;
padding: 10px 12px;
border-radius: var(--pos-radius, 8px);
border: 1px solid var(--pos-border-subtle, #333);
background: var(--pos-bg-interactive, #1a1a2e);
color: var(--pos-text-primary, #fff);
font-size: 13px;
outline: none;
}
.pos-settings-input:focus {
border-color: var(--pos-orange-primary, #ff5c00);
}