feat(pos): integrate receipt templates into POS printing flow
- Create ReceiptPrintService that reads default template from
localStorage and generates receipt HTML with template settings
(paper width, font size, field visibility, header/footer)
- Replace 3 hardcoded PrintReceipt methods in CafeDesktop.razor
with ReceiptPrintService.PrintAsync() calls
- Load shop info (name, phone) for receipt header
- Register ReceiptPrintService in DI container
Receipt templates configured in /admin/shop/{id}/receipt-templates
are now applied when printing from POS.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
@using WebClientTpos.Client.Services
|
||||
@inject PosDataService DataService
|
||||
@inject IJSRuntime JS
|
||||
@inject ReceiptPrintService ReceiptPrint
|
||||
|
||||
@* ═══════════════ MAIN CONTENT AREA + VERTICAL NAV ═══════════════ *@
|
||||
<div style="display:flex;flex:1;overflow:hidden;">
|
||||
@@ -756,6 +757,11 @@
|
||||
private decimal CartTotal => _cartItems.Sum(i => i.Price * i.Qty);
|
||||
private decimal FinalTotal => Math.Max(0, CartTotal - _discountAmount);
|
||||
|
||||
// EN: Shop info for receipt printing / VI: Thông tin shop cho in hoá đơn
|
||||
private string? _shopName;
|
||||
private string? _shopAddress;
|
||||
private string? _shopPhone;
|
||||
|
||||
// Voucher state
|
||||
private string _voucherCode = "";
|
||||
private string? _voucherMessage;
|
||||
@@ -770,7 +776,12 @@
|
||||
{
|
||||
var productsTask = DataService.GetProductsAsync(ShopId);
|
||||
var categoriesTask = DataService.GetCategoriesAsync(ShopId);
|
||||
await Task.WhenAll(productsTask, categoriesTask);
|
||||
var shopTask = DataService.GetShopByIdAsync(ShopId);
|
||||
await Task.WhenAll(productsTask, categoriesTask, shopTask);
|
||||
|
||||
// EN: Load shop info for receipt / VI: Tải thông tin shop cho hoá đơn
|
||||
var shop = await shopTask;
|
||||
if (shop != null) { _shopName = shop.Name; _shopPhone = shop.Phone; }
|
||||
|
||||
var apiProducts = await productsTask;
|
||||
var apiCategories = await categoriesTask;
|
||||
@@ -1047,49 +1058,25 @@
|
||||
private async Task PrintReceipt()
|
||||
{
|
||||
var payLabel = _lastPaymentMethod switch { "cash" => "Tiền mặt", "card" => "Thẻ", "qr" => "QR Code", _ => "Chuyển khoản" };
|
||||
var now = DateTime.Now;
|
||||
|
||||
// EN: Build item rows HTML / VI: Tạo HTML hàng sản phẩm
|
||||
var sb = new System.Text.StringBuilder();
|
||||
foreach (var item in _lastReceiptItems)
|
||||
// EN: Use ReceiptPrintService to generate HTML from saved template
|
||||
// VI: Dùng ReceiptPrintService để tạo HTML từ mẫu đã lưu
|
||||
await ReceiptPrint.PrintAsync(ShopId, new ReceiptData
|
||||
{
|
||||
sb.AppendLine($"<tr><td style='text-align:left;padding:3px 0;'>{System.Net.WebUtility.HtmlEncode(item.Name)}</td>");
|
||||
sb.AppendLine($"<td style='text-align:center;padding:3px 4px;'>{item.Qty}</td>");
|
||||
sb.AppendLine($"<td style='text-align:right;padding:3px 0;'>{item.Price:N0}</td>");
|
||||
sb.AppendLine($"<td style='text-align:right;padding:3px 0;font-weight:600;'>{item.Qty * item.Price:N0}</td></tr>");
|
||||
}
|
||||
|
||||
var receiptHtml = "<!DOCTYPE html><html><head><meta charset='utf-8'>" +
|
||||
$"<title>Hóa đơn - {_lastTransactionId}</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;'>aPOS 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> {_lastTransactionId}</div>" +
|
||||
$"<div><b>Ngày:</b> {now:dd/MM/yyyy} — {now: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>{_lastOrderTotal: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 aPOS Platform</div>" +
|
||||
"<script>window.onload=function(){window.print();window.onafterprint=function(){window.close();}}</script>" +
|
||||
"</body></html>";
|
||||
|
||||
// EN: Open popup window and write receipt HTML / VI: Mở popup và ghi HTML hóa đơn
|
||||
// EN: Call JS helper to open print window / VI: Gọi JS helper mở cửa sổ in
|
||||
await JS.InvokeVoidAsync("printPosReceipt", receiptHtml);
|
||||
ShopName = _shopName ?? "Cửa hàng",
|
||||
ShopAddress = _shopAddress,
|
||||
ShopPhone = _shopPhone,
|
||||
OrderNumber = _lastTransactionId,
|
||||
OrderDate = DateTime.Now,
|
||||
Items = _lastReceiptItems.Select(i => new ReceiptItem(i.Name, i.Qty, i.Price)).ToList(),
|
||||
Subtotal = _lastReceiptItems.Sum(i => i.Qty * i.Price),
|
||||
DiscountAmount = _discountAmount,
|
||||
Total = _lastOrderTotal,
|
||||
PaymentMethod = payLabel,
|
||||
AmountTendered = _lastPaymentMethod == "cash" ? _receivedAmount : null,
|
||||
ChangeAmount = _lastPaymentMethod == "cash" && _receivedAmount > _lastOrderTotal ? _receivedAmount - _lastOrderTotal : null,
|
||||
TransactionId = _lastTransactionId
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════ HISTORY TAB — API-driven ═══════════════
|
||||
@@ -1200,47 +1187,23 @@
|
||||
{
|
||||
if (_selectedOrderDetail?.Order == null) return;
|
||||
var od = _selectedOrderDetail.Order;
|
||||
var items = _selectedOrderDetail.Items ?? new();
|
||||
var payLabel = MapPaymentMethodLabel(od.PaymentMethod);
|
||||
var items = (_selectedOrderDetail.Items ?? new())
|
||||
.Select(i => new ReceiptItem(i.ProductName ?? "—", i.Quantity, i.UnitPrice)).ToList();
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
foreach (var item in items)
|
||||
await ReceiptPrint.PrintAsync(ShopId, new ReceiptData
|
||||
{
|
||||
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;'>aPOS 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 aPOS Platform</div>" +
|
||||
"<script>window.onload=function(){window.print();window.onafterprint=function(){window.close();}}</script>" +
|
||||
"</body></html>";
|
||||
|
||||
await JS.InvokeVoidAsync("printPosReceipt", receiptHtml);
|
||||
ShopName = _shopName ?? "Cửa hàng",
|
||||
ShopAddress = _shopAddress,
|
||||
ShopPhone = _shopPhone,
|
||||
OrderNumber = od.Id.ToString()[..8].ToUpper(),
|
||||
OrderDate = od.CreatedAt,
|
||||
Items = items,
|
||||
Subtotal = items.Sum(i => i.Qty * i.Price),
|
||||
Total = od.TotalAmount,
|
||||
PaymentMethod = payLabel,
|
||||
TransactionId = od.Id.ToString()[..8].ToUpper()
|
||||
});
|
||||
}
|
||||
|
||||
private async Task PrintSessionOrderDetail()
|
||||
@@ -1248,32 +1211,19 @@
|
||||
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;'>aPOS 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 aPOS Platform</div>" +
|
||||
"<script>window.onload=function(){window.print();window.onafterprint=function(){window.close();}}</script>" +
|
||||
"</body></html>";
|
||||
|
||||
await JS.InvokeVoidAsync("printPosReceipt", receiptHtml);
|
||||
await ReceiptPrint.PrintAsync(ShopId, new ReceiptData
|
||||
{
|
||||
ShopName = _shopName ?? "Cửa hàng",
|
||||
ShopAddress = _shopAddress,
|
||||
ShopPhone = _shopPhone,
|
||||
OrderNumber = so.Id,
|
||||
OrderDate = DateTime.Now,
|
||||
Items = new(), // session order doesn't have item detail
|
||||
Subtotal = so.Total,
|
||||
Total = so.Total,
|
||||
PaymentMethod = so.Method,
|
||||
TransactionId = so.Id
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════ DASHBOARD TAB — API-driven ═══════════════
|
||||
|
||||
@@ -19,6 +19,7 @@ builder.Services.AddSingleton(sp => new HttpClient { BaseAddress = new Uri(new U
|
||||
// EN: Add POS data service for BFF API calls
|
||||
// VI: Thêm POS data service cho BFF API calls
|
||||
builder.Services.AddScoped<WebClientTpos.Client.Services.PosDataService>();
|
||||
builder.Services.AddScoped<WebClientTpos.Client.Services.ReceiptPrintService>();
|
||||
|
||||
// EN: Add auth state service for role-based redirects
|
||||
// VI: Thêm auth state service cho điều hướng theo vai trò
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
// EN: Service to generate receipt HTML from saved templates and print via browser popup.
|
||||
// VI: Service tạo HTML hoá đơn từ mẫu đã lưu và in qua popup trình duyệt.
|
||||
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace WebClientTpos.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Receipt data passed from POS page to the print service.
|
||||
/// VI: Dữ liệu hoá đơn truyền từ POS page đến service in.
|
||||
/// </summary>
|
||||
public record ReceiptData
|
||||
{
|
||||
public string ShopName { get; init; } = "Cửa hàng";
|
||||
public string? ShopAddress { get; init; }
|
||||
public string? ShopPhone { get; init; }
|
||||
public string? TaxId { get; init; }
|
||||
public string OrderNumber { get; init; } = "";
|
||||
public DateTime OrderDate { get; init; } = DateTime.Now;
|
||||
public string? StaffName { get; init; }
|
||||
public List<ReceiptItem> Items { get; init; } = new();
|
||||
public decimal Subtotal { get; init; }
|
||||
public decimal DiscountAmount { get; init; }
|
||||
public decimal ServiceCharge { get; init; }
|
||||
public decimal VatAmount { get; init; }
|
||||
public decimal Total { get; init; }
|
||||
public string PaymentMethod { get; init; } = "Tiền mặt";
|
||||
public decimal? AmountTendered { get; init; }
|
||||
public decimal? ChangeAmount { get; init; }
|
||||
public string? TransactionId { get; init; }
|
||||
}
|
||||
|
||||
public record ReceiptItem(string Name, int Qty, decimal Price);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Generates receipt HTML using saved templates from localStorage.
|
||||
/// VI: Tạo HTML hoá đơn sử dụng mẫu đã lưu từ localStorage.
|
||||
/// </summary>
|
||||
public class ReceiptPrintService
|
||||
{
|
||||
private readonly IJSRuntime _js;
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
public ReceiptPrintService(IJSRuntime js) => _js = js;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Print receipt using default template for the given shop.
|
||||
/// VI: In hoá đơn sử dụng mẫu mặc định của cửa hàng.
|
||||
/// </summary>
|
||||
public async Task PrintAsync(Guid shopId, ReceiptData data)
|
||||
{
|
||||
var template = await LoadDefaultTemplateAsync(shopId);
|
||||
var html = BuildReceiptHtml(template, data);
|
||||
await _js.InvokeVoidAsync("printPosReceipt", html);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Load default receipt template from localStorage for a shop.
|
||||
/// VI: Tải mẫu hoá đơn mặc định từ localStorage cho cửa hàng.
|
||||
/// </summary>
|
||||
private async Task<ReceiptTemplateSettings> LoadDefaultTemplateAsync(Guid shopId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var key = $"aPOS_receipt_templates_{shopId}";
|
||||
var json = await _js.InvokeAsync<string?>("localStorage.getItem", key);
|
||||
if (!string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
var templates = JsonSerializer.Deserialize<List<ReceiptTemplateSettings>>(json, _jsonOptions);
|
||||
var def = templates?.FirstOrDefault(t => t.IsDefault) ?? templates?.FirstOrDefault();
|
||||
if (def != null) return def;
|
||||
}
|
||||
}
|
||||
catch { /* fallback to default */ }
|
||||
|
||||
return new ReceiptTemplateSettings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Build complete receipt HTML from template settings and order data.
|
||||
/// VI: Tạo HTML hoá đơn hoàn chỉnh từ cài đặt mẫu và dữ liệu đơn hàng.
|
||||
/// </summary>
|
||||
private static string BuildReceiptHtml(ReceiptTemplateSettings t, ReceiptData d)
|
||||
{
|
||||
var paperMm = t.PaperWidth == "58mm" ? 58 : 80;
|
||||
var contentW = paperMm - 8; // 4mm margin each side
|
||||
var fontSize = t.FontSize switch { "small" => "11px", "large" => "15px", _ => "13px" };
|
||||
var titleSize = t.FontSize switch { "small" => "14px", "large" => "18px", _ => "16px" };
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("<!DOCTYPE html><html><head><meta charset='utf-8'>");
|
||||
sb.Append($"<title>Hóa đơn - {Enc(d.OrderNumber)}</title>");
|
||||
sb.Append("<style>");
|
||||
sb.Append($"@page {{ margin: 4mm; size: {paperMm}mm auto; }}");
|
||||
sb.Append($"body {{ font-family: 'Courier New', monospace; font-size: {fontSize}; width: {contentW}mm; margin: 0 auto; color: #000; }}");
|
||||
sb.Append(".c { text-align: center; } .b { font-weight: bold; }");
|
||||
sb.Append(".d { border-top: 1px dashed #000; margin: 6px 0; }");
|
||||
sb.Append("table { width: 100%; border-collapse: collapse; }");
|
||||
sb.Append("th { text-align: left; border-bottom: 1px solid #000; padding: 2px 0; }");
|
||||
sb.Append(".r { display: flex; justify-content: space-between; padding: 2px 0; }");
|
||||
sb.Append(".f { text-align: center; margin-top: 8px; color: #555; }");
|
||||
sb.Append("</style></head><body>");
|
||||
|
||||
// ── HEADER ──
|
||||
if (t.ShowLogo)
|
||||
{
|
||||
sb.Append($"<div class='c b' style='font-size:{titleSize};'>");
|
||||
sb.Append(Enc(string.IsNullOrWhiteSpace(t.HeaderText) ? d.ShopName : t.HeaderText));
|
||||
sb.Append("</div>");
|
||||
}
|
||||
if (t.ShowAddress && !string.IsNullOrEmpty(d.ShopAddress))
|
||||
sb.Append($"<div class='c' style='font-size:10px;'>{Enc(d.ShopAddress)}</div>");
|
||||
if (t.ShowPhone && !string.IsNullOrEmpty(d.ShopPhone))
|
||||
sb.Append($"<div class='c' style='font-size:10px;'>ĐT: {Enc(d.ShopPhone)}</div>");
|
||||
if (t.ShowTaxId && !string.IsNullOrEmpty(d.TaxId))
|
||||
sb.Append($"<div class='c' style='font-size:10px;'>MST: {Enc(d.TaxId)}</div>");
|
||||
|
||||
sb.Append("<div class='d'></div>");
|
||||
|
||||
// ── ORDER INFO ──
|
||||
if (t.ShowOrderNumber)
|
||||
sb.Append($"<div class='r'><span>Đơn #:</span><b>{Enc(d.OrderNumber)}</b></div>");
|
||||
if (t.ShowDateTime)
|
||||
sb.Append($"<div class='r'><span>Ngày:</span><span>{d.OrderDate:dd/MM/yyyy HH:mm}</span></div>");
|
||||
if (t.ShowStaffName && !string.IsNullOrEmpty(d.StaffName))
|
||||
sb.Append($"<div class='r'><span>NV:</span><span>{Enc(d.StaffName)}</span></div>");
|
||||
|
||||
sb.Append("<div class='d'></div>");
|
||||
|
||||
// ── ITEMS ──
|
||||
if (t.ShowItemList)
|
||||
{
|
||||
sb.Append("<table><tr><th>Sản phẩm</th><th style='text-align:center;'>SL</th><th style='text-align:right;'>Tiền</th></tr>");
|
||||
foreach (var item in d.Items)
|
||||
{
|
||||
sb.Append($"<tr><td style='padding:2px 0;'>{Enc(item.Name)}</td>");
|
||||
sb.Append($"<td style='text-align:center;'>x{item.Qty}</td>");
|
||||
sb.Append($"<td style='text-align:right;'>{item.Qty * item.Price:N0}</td></tr>");
|
||||
}
|
||||
sb.Append("</table>");
|
||||
sb.Append("<div class='d'></div>");
|
||||
}
|
||||
|
||||
// ── TOTALS ──
|
||||
if (t.ShowSubtotal)
|
||||
sb.Append($"<div class='r'><span>Tạm tính:</span><span>{d.Subtotal:N0}</span></div>");
|
||||
if (t.ShowDiscount && d.DiscountAmount > 0)
|
||||
sb.Append($"<div class='r' style='color:#c00;'><span>Giảm giá:</span><span>-{d.DiscountAmount:N0}</span></div>");
|
||||
if (t.ShowServiceCharge && d.ServiceCharge > 0)
|
||||
sb.Append($"<div class='r'><span>Phí dịch vụ:</span><span>{d.ServiceCharge:N0}</span></div>");
|
||||
if (t.ShowVat && d.VatAmount > 0)
|
||||
sb.Append($"<div class='r'><span>VAT:</span><span>{d.VatAmount:N0}</span></div>");
|
||||
if (t.ShowTotal)
|
||||
sb.Append($"<div class='r b' style='font-size:{titleSize};padding:4px 0;'><span>TỔNG CỘNG:</span><span>{d.Total:N0}₫</span></div>");
|
||||
|
||||
sb.Append("<div class='d'></div>");
|
||||
|
||||
// ── PAYMENT ──
|
||||
if (t.ShowPaymentMethod)
|
||||
sb.Append($"<div class='r'><span>Thanh toán:</span><span>{Enc(d.PaymentMethod)}</span></div>");
|
||||
if (t.ShowChangeAmount && d.AmountTendered.HasValue)
|
||||
{
|
||||
sb.Append($"<div class='r'><span>Tiền khách đưa:</span><span>{d.AmountTendered.Value:N0}</span></div>");
|
||||
if (d.ChangeAmount.HasValue)
|
||||
sb.Append($"<div class='r'><span>Tiền thừa:</span><span>{d.ChangeAmount.Value:N0}</span></div>");
|
||||
}
|
||||
if (t.ShowTransactionId && !string.IsNullOrEmpty(d.TransactionId))
|
||||
sb.Append($"<div class='r' style='font-size:10px;'><span>Mã GD:</span><span>{Enc(d.TransactionId)}</span></div>");
|
||||
|
||||
// ── FOOTER ──
|
||||
if (t.ShowBarcode && !string.IsNullOrEmpty(d.OrderNumber))
|
||||
sb.Append($"<div class='c' style='margin-top:8px;font-family:monospace;letter-spacing:4px;'>|||{Enc(d.OrderNumber)}|||</div>");
|
||||
if (t.ShowWifiPassword && !string.IsNullOrEmpty(t.WifiPassword))
|
||||
sb.Append($"<div class='c' style='margin-top:6px;font-size:10px;'>WiFi: {Enc(t.WifiPassword)}</div>");
|
||||
if (!string.IsNullOrEmpty(t.FooterText))
|
||||
sb.Append($"<div class='f'>{Enc(t.FooterText)}</div>");
|
||||
sb.Append("<div class='f' style='font-size:9px;'>Powered by aPOS</div>");
|
||||
|
||||
sb.Append("<script>window.onload=function(){window.print();window.onafterprint=function(){window.close();}}</script>");
|
||||
sb.Append("</body></html>");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string Enc(string? s) => System.Net.WebUtility.HtmlEncode(s ?? "");
|
||||
|
||||
/// <summary>
|
||||
/// EN: Template settings model — matches ReceiptTemplates.razor localStorage structure.
|
||||
/// VI: Mô hình cài đặt mẫu — khớp cấu trúc localStorage của ReceiptTemplates.razor.
|
||||
/// </summary>
|
||||
private class ReceiptTemplateSettings
|
||||
{
|
||||
public bool IsDefault { get; set; } = true;
|
||||
public string PaperWidth { get; set; } = "80mm";
|
||||
public string FontSize { get; set; } = "medium";
|
||||
public string HeaderText { get; set; } = "";
|
||||
public string FooterText { get; set; } = "Cảm ơn quý khách!";
|
||||
public bool ShowLogo { get; set; } = true;
|
||||
public bool ShowAddress { get; set; } = true;
|
||||
public bool ShowPhone { get; set; } = true;
|
||||
public bool ShowTaxId { get; set; }
|
||||
public bool ShowOrderNumber { get; set; } = true;
|
||||
public bool ShowDateTime { get; set; } = true;
|
||||
public bool ShowStaffName { get; set; }
|
||||
public bool ShowItemList { get; set; } = true;
|
||||
public bool ShowSubtotal { get; set; } = true;
|
||||
public bool ShowServiceCharge { get; set; }
|
||||
public bool ShowVat { get; set; }
|
||||
public bool ShowDiscount { get; set; } = true;
|
||||
public bool ShowTotal { get; set; } = true;
|
||||
public bool ShowPaymentMethod { get; set; } = true;
|
||||
public bool ShowChangeAmount { get; set; } = true;
|
||||
public bool ShowTransactionId { get; set; } = true;
|
||||
public bool ShowBarcode { get; set; }
|
||||
public bool ShowWifiPassword { get; set; }
|
||||
public string? WifiPassword { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user