diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/CafeDesktop.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/CafeDesktop.razor
index 2fb4f001..892ad327 100644
--- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/CafeDesktop.razor
+++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/CafeDesktop.razor
@@ -10,6 +10,7 @@
@using WebClientTpos.Client.Services
@inject PosDataService DataService
@inject IJSRuntime JS
+@inject ReceiptPrintService ReceiptPrint
@* ═══════════════ MAIN CONTENT AREA + VERTICAL NAV ═══════════════ *@
@@ -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($"
| {System.Net.WebUtility.HtmlEncode(item.Name)} | ");
- sb.AppendLine($"{item.Qty} | ");
- sb.AppendLine($"{item.Price:N0} | ");
- sb.AppendLine($"{item.Qty * item.Price:N0} |
");
- }
-
- var receiptHtml = "
" +
- $"
Hóa đơn - {_lastTransactionId}" +
- "" +
- "
aPOS POS
" +
- "
Hệ thống quản lý bán hàng thông minh
" +
- "
" +
- $"
Mã đơn: {_lastTransactionId}
" +
- $"
Ngày: {now:dd/MM/yyyy} — {now:HH:mm:ss}
" +
- $"
Thanh toán: {payLabel}
" +
- "
" +
- "
| Sản phẩm | SL | Đ.Giá | T.Tiền |
" +
- sb.ToString() +
- "
" +
- $"
TỔNG CỘNG{_lastOrderTotal:N0}₫
" +
- "
" +
- "
Cảm ơn quý khách! Hẹn gặp lại
" +
- "
Powered by aPOS Platform
" +
- "" +
- "";
-
- // 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($"
| {System.Net.WebUtility.HtmlEncode(item.ProductName ?? "—")} | ");
- sb.AppendLine($"{item.Quantity} | ");
- sb.AppendLine($"{item.UnitPrice:N0} | ");
- sb.AppendLine($"{item.Subtotal:N0} |
");
- }
-
- var receiptHtml = "
" +
- $"
Hóa đơn - {od.Id.ToString()[..8].ToUpper()}" +
- "" +
- "
aPOS POS
" +
- "
Hệ thống quản lý bán hàng thông minh
" +
- "
" +
- $"
Mã đơn: {od.Id.ToString()[..8].ToUpper()}
" +
- $"
Ngày: {od.CreatedAt:dd/MM/yyyy} — {od.CreatedAt:HH:mm:ss}
" +
- $"
Thanh toán: {payLabel}
" +
- "
" +
- "
| Sản phẩm | SL | Đ.Giá | T.Tiền |
" +
- sb.ToString() +
- "
" +
- $"
TỔNG CỘNG{od.TotalAmount:N0}₫
" +
- "
" +
- "
Cảm ơn quý khách! Hẹn gặp lại
" +
- "
Powered by aPOS Platform
" +
- "" +
- "";
-
- 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 = "
" +
- $"
Hóa đơn - {System.Net.WebUtility.HtmlEncode(so.Id)}" +
- "" +
- "
aPOS POS
" +
- "
Hệ thống quản lý bán hàng thông minh
" +
- "
" +
- $"
Mã đơn: {System.Net.WebUtility.HtmlEncode(so.Id)}
" +
- $"
Thời gian: {System.Net.WebUtility.HtmlEncode(so.Time)}
" +
- $"
Thanh toán: {System.Net.WebUtility.HtmlEncode(so.Method)}
" +
- "
" +
- $"
{System.Net.WebUtility.HtmlEncode(so.Items)}
" +
- "
" +
- $"
TỔNG CỘNG{so.Total:N0}₫
" +
- "
" +
- "
Cảm ơn quý khách! Hẹn gặp lại
" +
- "
Powered by aPOS Platform
" +
- "" +
- "";
-
- 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 ═══════════════
diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Program.cs b/apps/web-client-tpos-net/src/WebClientTpos.Client/Program.cs
index c47ac999..8e125bf4 100644
--- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Program.cs
+++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Program.cs
@@ -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
();
+builder.Services.AddScoped();
// EN: Add auth state service for role-based redirects
// VI: Thêm auth state service cho điều hướng theo vai trò
diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/ReceiptPrintService.cs b/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/ReceiptPrintService.cs
new file mode 100644
index 00000000..79bba90d
--- /dev/null
+++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/ReceiptPrintService.cs
@@ -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;
+
+///
+/// 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.
+///
+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 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);
+
+///
+/// EN: Generates receipt HTML using saved templates from localStorage.
+/// VI: Tạo HTML hoá đơn sử dụng mẫu đã lưu từ localStorage.
+///
+public class ReceiptPrintService
+{
+ private readonly IJSRuntime _js;
+ private static readonly JsonSerializerOptions _jsonOptions = new()
+ {
+ PropertyNameCaseInsensitive = true,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+
+ public ReceiptPrintService(IJSRuntime js) => _js = js;
+
+ ///
+ /// 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.
+ ///
+ public async Task PrintAsync(Guid shopId, ReceiptData data)
+ {
+ var template = await LoadDefaultTemplateAsync(shopId);
+ var html = BuildReceiptHtml(template, data);
+ await _js.InvokeVoidAsync("printPosReceipt", html);
+ }
+
+ ///
+ /// 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.
+ ///
+ private async Task LoadDefaultTemplateAsync(Guid shopId)
+ {
+ try
+ {
+ var key = $"aPOS_receipt_templates_{shopId}";
+ var json = await _js.InvokeAsync("localStorage.getItem", key);
+ if (!string.IsNullOrWhiteSpace(json))
+ {
+ var templates = JsonSerializer.Deserialize>(json, _jsonOptions);
+ var def = templates?.FirstOrDefault(t => t.IsDefault) ?? templates?.FirstOrDefault();
+ if (def != null) return def;
+ }
+ }
+ catch { /* fallback to default */ }
+
+ return new ReceiptTemplateSettings();
+ }
+
+ ///
+ /// 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.
+ ///
+ 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("");
+ sb.Append($"Hóa đơn - {Enc(d.OrderNumber)}");
+ sb.Append("");
+
+ // ── HEADER ──
+ if (t.ShowLogo)
+ {
+ sb.Append($"");
+ sb.Append(Enc(string.IsNullOrWhiteSpace(t.HeaderText) ? d.ShopName : t.HeaderText));
+ sb.Append("
");
+ }
+ if (t.ShowAddress && !string.IsNullOrEmpty(d.ShopAddress))
+ sb.Append($"{Enc(d.ShopAddress)}
");
+ if (t.ShowPhone && !string.IsNullOrEmpty(d.ShopPhone))
+ sb.Append($"ĐT: {Enc(d.ShopPhone)}
");
+ if (t.ShowTaxId && !string.IsNullOrEmpty(d.TaxId))
+ sb.Append($"MST: {Enc(d.TaxId)}
");
+
+ sb.Append("");
+
+ // ── ORDER INFO ──
+ if (t.ShowOrderNumber)
+ sb.Append($"Đơn #:{Enc(d.OrderNumber)}
");
+ if (t.ShowDateTime)
+ sb.Append($"Ngày:{d.OrderDate:dd/MM/yyyy HH:mm}
");
+ if (t.ShowStaffName && !string.IsNullOrEmpty(d.StaffName))
+ sb.Append($"NV:{Enc(d.StaffName)}
");
+
+ sb.Append("");
+
+ // ── ITEMS ──
+ if (t.ShowItemList)
+ {
+ sb.Append("| Sản phẩm | SL | Tiền |
");
+ foreach (var item in d.Items)
+ {
+ sb.Append($"| {Enc(item.Name)} | ");
+ sb.Append($"x{item.Qty} | ");
+ sb.Append($"{item.Qty * item.Price:N0} |
");
+ }
+ sb.Append("
");
+ sb.Append("");
+ }
+
+ // ── TOTALS ──
+ if (t.ShowSubtotal)
+ sb.Append($"Tạm tính:{d.Subtotal:N0}
");
+ if (t.ShowDiscount && d.DiscountAmount > 0)
+ sb.Append($"Giảm giá:-{d.DiscountAmount:N0}
");
+ if (t.ShowServiceCharge && d.ServiceCharge > 0)
+ sb.Append($"Phí dịch vụ:{d.ServiceCharge:N0}
");
+ if (t.ShowVat && d.VatAmount > 0)
+ sb.Append($"VAT:{d.VatAmount:N0}
");
+ if (t.ShowTotal)
+ sb.Append($"TỔNG CỘNG:{d.Total:N0}₫
");
+
+ sb.Append("");
+
+ // ── PAYMENT ──
+ if (t.ShowPaymentMethod)
+ sb.Append($"Thanh toán:{Enc(d.PaymentMethod)}
");
+ if (t.ShowChangeAmount && d.AmountTendered.HasValue)
+ {
+ sb.Append($"Tiền khách đưa:{d.AmountTendered.Value:N0}
");
+ if (d.ChangeAmount.HasValue)
+ sb.Append($"Tiền thừa:{d.ChangeAmount.Value:N0}
");
+ }
+ if (t.ShowTransactionId && !string.IsNullOrEmpty(d.TransactionId))
+ sb.Append($"Mã GD:{Enc(d.TransactionId)}
");
+
+ // ── FOOTER ──
+ if (t.ShowBarcode && !string.IsNullOrEmpty(d.OrderNumber))
+ sb.Append($"|||{Enc(d.OrderNumber)}|||
");
+ if (t.ShowWifiPassword && !string.IsNullOrEmpty(t.WifiPassword))
+ sb.Append($"WiFi: {Enc(t.WifiPassword)}
");
+ if (!string.IsNullOrEmpty(t.FooterText))
+ sb.Append($"{Enc(t.FooterText)}
");
+ sb.Append("Powered by aPOS
");
+
+ sb.Append("");
+ sb.Append("");
+
+ return sb.ToString();
+ }
+
+ private static string Enc(string? s) => System.Net.WebUtility.HtmlEncode(s ?? "");
+
+ ///
+ /// 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.
+ ///
+ 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; }
+ }
+}