From 68d0aed4682158f6e5caa6ca596d517fb37f3b64 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Fri, 7 Aug 2026 17:08:23 +0330 Subject: [PATCH] feat(invoice): add print functionality for order invoices and enhance order details - Implemented a print button in OrderDetailsDialog and UserOrderDetailsDialog for generating invoices. - Updated DiscountOrdersMainPage to populate order details from the list. - Enhanced DiscountOrderDetailsDto to include UserFullName, UserMobile, and VatAmount for invoice generation. - Integrated JavaScript functionality to convert HTML invoices to PDF format. --- .../Common/Utilities/OrderInvoicePrint.cs | 161 +++++++ .../Components/OrderDetailsDialog.razor | 411 ++++++++---------- .../Components/OrderDetailsDialog.razor.cs | 42 ++ .../DiscountOrdersMainPage.razor.cs | 9 + .../Components/UserOrderDetailsDialog.razor | 7 + .../UserOrderDetailsDialog.razor.cs | 59 +++ .../DiscountOrder/IDiscountOrderService.cs | 4 + 7 files changed, 472 insertions(+), 221 deletions(-) create mode 100644 src/BackOffice/Common/Utilities/OrderInvoicePrint.cs diff --git a/src/BackOffice/Common/Utilities/OrderInvoicePrint.cs b/src/BackOffice/Common/Utilities/OrderInvoicePrint.cs new file mode 100644 index 0000000..1d45b98 --- /dev/null +++ b/src/BackOffice/Common/Utilities/OrderInvoicePrint.cs @@ -0,0 +1,161 @@ +using System.Globalization; +using System.Net; +using System.Text; +using DateTimeConverterCL; + +namespace BackOffice.Common.Utilities; + +/// ساخت HTML فاکتور برای چاپ مرورگر (jsHtmlToPdf). +public static class OrderInvoicePrint +{ + public sealed class InvoiceModel + { + public string ShopTitle { get; set; } = "فروشگاه"; + public string OrderId { get; set; } = ""; + public string? CustomerName { get; set; } + public string? CustomerMobile { get; set; } + public string? CustomerNationalCode { get; set; } + public string? Address { get; set; } + public string? PostalCode { get; set; } + public string? PaymentStatus { get; set; } + public string? PaymentMethod { get; set; } + public string? TransactionId { get; set; } + public string? TrackingCode { get; set; } + public DateTime? Created { get; set; } + public DateTime? PaymentDate { get; set; } + public long Subtotal { get; set; } + public long DiscountTotal { get; set; } + public long CreditUsed { get; set; } + public long GatewayAmount { get; set; } + public long VatAmount { get; set; } + public long TotalAmount { get; set; } + public List Lines { get; set; } = new(); + } + + public sealed class InvoiceLine + { + public string Title { get; set; } = ""; + public int Quantity { get; set; } + public long UnitPrice { get; set; } + public long Discount { get; set; } + public long LineTotal { get; set; } + } + + public static string BuildHtml(InvoiceModel model) + { + var sb = new StringBuilder(); + sb.Append(""); + sb.Append("").Append(Esc($"فاکتور {model.OrderId}")).Append(""); + sb.Append(""" + +"""); + sb.Append(""); + sb.Append("
"); + sb.Append("

").Append(Esc(model.ShopTitle)).Append("

"); + sb.Append("
فاکتور فروش
"); + sb.Append("
شماره فاکتور: ").Append(Esc(model.OrderId)).Append("
"); + if (model.Created.HasValue) + sb.Append("
تاریخ ثبت: ").Append(Esc(FormatDate(model.Created.Value))).Append("
"); + if (model.PaymentDate.HasValue) + sb.Append("
تاریخ پرداخت: ").Append(Esc(FormatDate(model.PaymentDate.Value))).Append("
"); + sb.Append("
"); + + sb.Append("
"); + AppendField(sb, "نام مشتری", model.CustomerName); + AppendField(sb, "موبایل", model.CustomerMobile); + AppendField(sb, "کد ملی", model.CustomerNationalCode); + AppendField(sb, "کد پستی", model.PostalCode); + AppendField(sb, "وضعیت پرداخت", model.PaymentStatus); + AppendField(sb, "روش پرداخت", model.PaymentMethod); + AppendField(sb, "کد تراکنش", model.TransactionId); + AppendField(sb, "کد رهگیری", model.TrackingCode); + if (!string.IsNullOrWhiteSpace(model.Address)) + { + sb.Append("
آدرس
") + .Append(Esc(model.Address)).Append("
"); + } + sb.Append("
"); + + sb.Append(""); + sb.Append(""); + sb.Append(""); + var i = 1; + foreach (var line in model.Lines) + { + sb.Append(""); + sb.Append(""); + sb.Append(""); + sb.Append(""); + sb.Append(""); + sb.Append(""); + sb.Append(""); + sb.Append(""); + } + if (model.Lines.Count == 0) + sb.Append(""); + sb.Append("
#کالاتعدادقیمت واحدتخفیفمبلغ
").Append(i++).Append("").Append(Esc(line.Title)).Append("").Append(line.Quantity).Append("").Append(Money(line.UnitPrice)).Append("").Append(Money(line.Discount)).Append("").Append(Money(line.LineTotal)).Append("
آیتمی ثبت نشده است
"); + + sb.Append(""); + if (model.Subtotal > 0) + AppendTotalRow(sb, "جمع اقلام", model.Subtotal); + if (model.DiscountTotal > 0) + AppendTotalRow(sb, "تخفیف", model.DiscountTotal); + if (model.VatAmount > 0) + AppendTotalRow(sb, "مالیات بر ارزش افزوده", model.VatAmount); + if (model.CreditUsed > 0) + AppendTotalRow(sb, "پرداخت از کیف اعتباری", model.CreditUsed); + if (model.GatewayAmount > 0) + AppendTotalRow(sb, "پرداخت درگاه / نقدی", model.GatewayAmount); + sb.Append(""); + sb.Append("
مبلغ قابل پرداخت") + .Append(Money(model.TotalAmount)).Append(" تومان
"); + + sb.Append("
چاپ‌شده از پنل ادمین — ").Append(Esc(FormatDate(DateTime.Now))).Append("
"); + sb.Append(""); + return sb.ToString(); + } + + private static void AppendField(StringBuilder sb, string label, string? value) + { + if (string.IsNullOrWhiteSpace(value)) return; + sb.Append("
").Append(Esc(label)).Append("
") + .Append(Esc(value)).Append("
"); + } + + private static void AppendTotalRow(StringBuilder sb, string label, long amount) + { + sb.Append("").Append(Esc(label)).Append("") + .Append(Money(amount)).Append(" تومان"); + } + + private static string Money(long amount) => amount.ToString("N0", CultureInfo.InvariantCulture); + + private static string FormatDate(DateTime dt) + { + try { return dt.ToLocalTime().MiladiToJalaliWithTime(); } + catch { return dt.ToLocalTime().ToString("yyyy/MM/dd HH:mm"); } + } + + private static string Esc(string? s) => WebUtility.HtmlEncode(s ?? ""); +} diff --git a/src/BackOffice/Pages/DiscountShop/Components/OrderDetailsDialog.razor b/src/BackOffice/Pages/DiscountShop/Components/OrderDetailsDialog.razor index 24c2c8a..81c3ec1 100644 --- a/src/BackOffice/Pages/DiscountShop/Components/OrderDetailsDialog.razor +++ b/src/BackOffice/Pages/DiscountShop/Components/OrderDetailsDialog.razor @@ -1,36 +1,72 @@ @using BackOffice.Services.DiscountOrder +@using BackOffice.Common.BaseComponents - - - - جزئیات سفارش #@Order.OrderNumber - - - - - - - اطلاعات سفارش - - - شماره سفارش: - @Order.OrderNumber - - - شناسه کاربر: - @Order.UserId - - - - + + جزئیات سفارش شماره @(string.IsNullOrWhiteSpace(Order.OrderNumber) ? Order.Id.ToString() : Order.OrderNumber) + + کاربر: + @if (!string.IsNullOrWhiteSpace(Order.UserFullName)) + { + @Order.UserFullName + } + else + { + #@Order.UserId + } + @if (!string.IsNullOrWhiteSpace(DisplayMobile)) + { + (@DisplayMobile) + } + - - - - Timeline وضعیت - + + + خلاصه سفارش + + مبلغ کل: @Order.TotalPrice.ToString("N0") تومان + + از کیف پول اعتباری: @Order.DiscountBalanceUsed.ToString("N0") تومان + + + مبلغ پرداختی از درگاه: @Order.GatewayAmount.ToString("N0") تومان + + @if (!string.IsNullOrEmpty(Order.TransactionId)) + { + شناسه پرداخت: @Order.TransactionId + } + @if (Order.Created.HasValue) + { + + تاریخ ثبت: @Order.Created.Value.MiladiToJalaliWithTime() + + } + + وضعیت پرداخت: + + @GetPaymentStatusText(Order.PaymentStatusValue) + + + + @if (Order.VatAmount > 0) + { + + جزئیات مالیات بر ارزش افزوده + + مالیات: @Order.VatAmount.ToString("N0") تومان + + + مبلغ نهایی (شامل مالیات): @Order.TotalPrice.ToString("N0") تومان + + } + + + Timeline وضعیت + @foreach (var step in _timelineSteps) { @@ -41,215 +77,148 @@ } - - - - - - وضعیت سفارش - - @GetStatusText(Order.Status) - - @if (Order.Created.HasValue) - { - - تاریخ ثبت: @Order.Created.Value.MiladiToJalaliWithTime() - - } - - - - - - - وضعیت پرداخت - - @GetPaymentStatusText(Order.PaymentStatusValue) - - @if (!string.IsNullOrEmpty(Order.TransactionId)) - { - - کد تراکنش: @Order.TransactionId - - } - - - - - @if (Order.Address != null) - { - - - - - آدرس ارسال - - @Order.Address.Title - @Order.Address.Address - @if (!string.IsNullOrWhiteSpace(Order.Address.PostalCode)) - { - کد پستی: @Order.Address.PostalCode - } - @if (!string.IsNullOrWhiteSpace(Order.Address.Phone)) - { - تلفن سفارش‌دهنده: @Order.Address.Phone - } - - - } - - - - - آیتم‌های سفارش - - - محصول - تعداد - قیمت واحد - تخفیف - قیمت نهایی - - - - - @if (!string.IsNullOrWhiteSpace(context.ThumbnailPath)) - { - - } - @context.ProductTitle - - - @context.Count - @context.UnitPrice.ToString("N0") تومان - - @context.MaxDiscountPercent% - - @context.FinalPrice.ToString("N0") تومان - - - - - - - - - خلاصه مالی - - - مبلغ کل: - - - @Order.TotalPrice.ToString("N0") تومان - - - - از کیف پول اعتباری: - - - @Order.DiscountBalanceUsed.ToString("N0") تومان - - - - - - مبلغ پرداختی از درگاه: - - - - @Order.GatewayAmount.ToString("N0") تومان - - - - - - - - - - مدیریت وضعیت ارسال - - - - - در انتظار پرداخت - پرداخت شده - در حال آماده‌سازی - ارسال شده - تحویل داده شده - لغو شده - مرجوع شده - - - - - - - - - + + + وضعیت ارسال: + + در انتظار پرداخت + پرداخت شده + در حال آماده‌سازی + ارسال شده + تحویل داده شده + لغو شده + مرجوع شده + + + + @if (_newStatus == OrderStatus.Cancelled || _newStatus == OrderStatus.Returned) { - + توجه: در صورت لغو یا مرجوعی سفارش، موجودی محصولات به انبار بازگردانده می‌شود. } @if (_newStatus == OrderStatus.Shipped) { - - پس از تغییر وضعیت به "ارسال شده"، اطلاع‌رسانی به کاربر ارسال خواهد شد. + + پس از تغییر وضعیت به «ارسال شده»، اطلاع‌رسانی به کاربر ارسال خواهد شد. } - - + + - - @if (!string.IsNullOrEmpty(Order.Notes) || !string.IsNullOrEmpty(Order.AdminNotes)) + + + آدرس تحویل + + @if (Order.Address != null && !string.IsNullOrWhiteSpace(Order.Address.Address)) + { + @if (!string.IsNullOrWhiteSpace(Order.Address.Title)) + { + @Order.Address.Title + } + @Order.Address.Address + @if (!string.IsNullOrWhiteSpace(Order.Address.PostalCode)) + { + کد پستی: @Order.Address.PostalCode + } + @if (!string.IsNullOrWhiteSpace(DisplayMobile)) + { + تلفن سفارش‌دهنده: @DisplayMobile + } + } + else + { + آدرسی ثبت نشده است. + } + + + + + + اقلام فاکتور + + @if (Order.Items.Count > 0) + { + + + تصویر + محصول + قیمت واحد + تعداد + تخفیف + قیمت نهایی + + + + @if (!string.IsNullOrWhiteSpace(context.ThumbnailPath)) + { + + } + + @context.ProductTitle + @context.UnitPrice.ToString("N0") + @context.Count + + @if (context.MaxDiscountPercent > 0) + { + @context.MaxDiscountPercent% + } + else if (context.DiscountAmount > 0) + { + @context.DiscountAmount.ToString("N0") + } + else + { + - + } + + @context.FinalPrice.ToString("N0") + + + } + else + { + آیتمی برای این سفارش ثبت نشده است. + } + + + + @if (!string.IsNullOrEmpty(Order.Notes)) { - - - @if (!string.IsNullOrEmpty(Order.Notes)) - { - - - یادداشت کاربر - - @Order.Notes - } - @if (!string.IsNullOrEmpty(Order.AdminNotes)) - { - - - یادداشت ادمین - - @Order.AdminNotes - } - - + + + یادداشت کاربر + + @Order.Notes + + } - + + + چاپ فاکتور + + بستن diff --git a/src/BackOffice/Pages/DiscountShop/Components/OrderDetailsDialog.razor.cs b/src/BackOffice/Pages/DiscountShop/Components/OrderDetailsDialog.razor.cs index 25970ad..8696d54 100644 --- a/src/BackOffice/Pages/DiscountShop/Components/OrderDetailsDialog.razor.cs +++ b/src/BackOffice/Pages/DiscountShop/Components/OrderDetailsDialog.razor.cs @@ -1,5 +1,7 @@ +using BackOffice.Common.Utilities; using BackOffice.Services.DiscountOrder; using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; using MudBlazor; namespace BackOffice.Pages.DiscountShop.Components; @@ -8,6 +10,7 @@ public partial class OrderDetailsDialog { [CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!; [Inject] public IDiscountOrderService OrderService { get; set; } = default!; + [Inject] private IJSRuntime JsRuntime { get; set; } = default!; [Parameter] public DiscountOrderDetailsDto Order { get; set; } = null!; @@ -183,6 +186,45 @@ public partial class OrderDetailsDialog _ => Icons.Material.Filled.Schedule }; + private async Task PrintInvoiceAsync() + { + var lines = Order.Items.Select(i => new OrderInvoicePrint.InvoiceLine + { + Title = i.ProductTitle, + Quantity = i.Count, + UnitPrice = i.UnitPrice, + Discount = i.DiscountAmount, + LineTotal = i.FinalPrice > 0 ? i.FinalPrice : i.UnitPrice * i.Count + }).ToList(); + + var mobile = !string.IsNullOrWhiteSpace(Order.UserMobile) + ? Order.UserMobile + : Order.Address?.Phone; + + var html = OrderInvoicePrint.BuildHtml(new OrderInvoicePrint.InvoiceModel + { + ShopTitle = "فروشگاه اعتباری — کارا بازار سلامت", + OrderId = string.IsNullOrWhiteSpace(Order.OrderNumber) ? Order.Id.ToString() : Order.OrderNumber, + CustomerName = Order.UserFullName, + CustomerMobile = mobile, + Address = Order.Address?.Address, + PostalCode = Order.Address?.PostalCode, + PaymentStatus = GetPaymentStatusText(Order.PaymentStatusValue), + TransactionId = Order.TransactionId, + TrackingCode = Order.TrackingCode, + Created = Order.Created, + Subtotal = lines.Sum(l => l.UnitPrice * l.Quantity), + DiscountTotal = lines.Sum(l => l.Discount), + CreditUsed = Order.DiscountBalanceUsed, + GatewayAmount = Order.GatewayAmount, + VatAmount = Order.VatAmount, + TotalAmount = Order.TotalPrice, + Lines = lines + }); + + await JsRuntime.InvokeVoidAsync("jsHtmlToPdf", html, $"فاکتور سفارش {Order.Id}"); + } + private enum StepState { Inactive = 0, diff --git a/src/BackOffice/Pages/DiscountShop/DiscountOrdersMainPage.razor.cs b/src/BackOffice/Pages/DiscountShop/DiscountOrdersMainPage.razor.cs index 44dcd74..e93e6fa 100644 --- a/src/BackOffice/Pages/DiscountShop/DiscountOrdersMainPage.razor.cs +++ b/src/BackOffice/Pages/DiscountShop/DiscountOrdersMainPage.razor.cs @@ -74,6 +74,15 @@ public partial class DiscountOrdersMainPage return; } + // تکمیل فیلدهای فاکتور از ردیف لیست (جزئیات API نام/موبایل/VAT ندارد) + var listRow = _orders?.FirstOrDefault(o => o.Id == orderId); + if (listRow != null) + { + order.UserFullName = listRow.UserFullName; + order.UserMobile = listRow.UserMobile; + order.VatAmount = listRow.VatAmount; + } + var parameters = new DialogParameters { { x => x.Order, order } }; var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Large, FullWidth = true }; var dialog = await DialogService.ShowAsync("جزئیات سفارش", parameters, options); diff --git a/src/BackOffice/Pages/UserOrder/Components/UserOrderDetailsDialog.razor b/src/BackOffice/Pages/UserOrder/Components/UserOrderDetailsDialog.razor index 63a9b77..99434a8 100644 --- a/src/BackOffice/Pages/UserOrder/Components/UserOrderDetailsDialog.razor +++ b/src/BackOffice/Pages/UserOrder/Components/UserOrderDetailsDialog.razor @@ -170,6 +170,13 @@ + + چاپ فاکتور + + بستن diff --git a/src/BackOffice/Pages/UserOrder/Components/UserOrderDetailsDialog.razor.cs b/src/BackOffice/Pages/UserOrder/Components/UserOrderDetailsDialog.razor.cs index a2bbf3e..b5ae7db 100644 --- a/src/BackOffice/Pages/UserOrder/Components/UserOrderDetailsDialog.razor.cs +++ b/src/BackOffice/Pages/UserOrder/Components/UserOrderDetailsDialog.razor.cs @@ -1,6 +1,8 @@ +using BackOffice.Common.Utilities; using CMSMicroservice.Protobuf.Protos; using CMSMicroservice.Protobuf.Protos.UserOrder; using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; using MudBlazor; namespace BackOffice.Pages.UserOrder.Components; @@ -9,6 +11,7 @@ public partial class UserOrderDetailsDialog { [CascadingParameter] IMudDialogInstance MudDialog { get; set; } = default!; [Inject] public UserOrderContract.UserOrderContractClient UserOrderContract { get; set; } = default!; + [Inject] private IJSRuntime JsRuntime { get; set; } = default!; [Parameter] public long OrderId { get; set; } @@ -178,5 +181,61 @@ public partial class UserOrderDetailsDialog MudDialog.Close(DialogResult.Ok(true)); } + private async Task PrintInvoiceAsync() + { + if (_model is null) return; + + var vat = _model.VatInfo; + var lines = _model.FactorDetails.Select(f => + { + var unit = f.UnitPrice ?? 0; + var count = f.Count ?? 0; + var discountUnit = f.UnitDiscountPrice ?? 0; + var lineDiscount = Math.Max(0, (unit - discountUnit) * count); + var lineTotal = discountUnit > 0 ? discountUnit * count : unit * count; + return new OrderInvoicePrint.InvoiceLine + { + Title = f.ProductTitle ?? "", + Quantity = count, + UnitPrice = unit, + Discount = lineDiscount, + LineTotal = lineTotal + }; + }).ToList(); + + var subtotal = lines.Sum(l => l.UnitPrice * l.Quantity); + var discountTotal = lines.Sum(l => l.Discount); + var total = vat is { TotalAmount: > 0 } ? vat.TotalAmount : _model.Amount; + + var html = OrderInvoicePrint.BuildHtml(new OrderInvoicePrint.InvoiceModel + { + ShopTitle = "فروشگاه عادی — کارا بازار سلامت", + OrderId = _model.Id.ToString(), + CustomerName = _model.UserFullName, + CustomerMobile = _model.UserMobile, + CustomerNationalCode = _model.UserNationalCode, + Address = _model.UserAddressText, + PostalCode = _model.PostalCode, + PaymentStatus = _model.PaymentStatus switch + { + PaymentStatus.Success => "پرداخت شده", + PaymentStatus.Pending => "در انتظار پرداخت", + _ => "پرداخت نشده" + }, + PaymentMethod = GetPaymentMethodText(_model.PaymentMethod.GetHashCode()), + TransactionId = _model.TransactionId?.ToString(), + TrackingCode = _trackingCode ?? _model.TrackingCode, + Created = null, + PaymentDate = _model.PaymentDate?.ToDateTime(), + Subtotal = subtotal, + DiscountTotal = discountTotal, + VatAmount = vat?.VatAmount ?? 0, + TotalAmount = total, + Lines = lines + }); + + await JsRuntime.InvokeVoidAsync("jsHtmlToPdf", html, $"فاکتور سفارش {_model.Id}"); + } + private void Close() => MudDialog.Close(); } diff --git a/src/BackOffice/Services/DiscountOrder/IDiscountOrderService.cs b/src/BackOffice/Services/DiscountOrder/IDiscountOrderService.cs index 0367b52..826e479 100644 --- a/src/BackOffice/Services/DiscountOrder/IDiscountOrderService.cs +++ b/src/BackOffice/Services/DiscountOrder/IDiscountOrderService.cs @@ -62,10 +62,14 @@ public class DiscountOrderDetailsDto public long Id { get; set; } public long UserId { get; set; } public string OrderNumber { get; set; } = string.Empty; + /// از ردیف لیست پر می‌شود (GetOrderById این فیلدها را ندارد) + public string UserFullName { get; set; } = string.Empty; + public string UserMobile { get; set; } = string.Empty; public AddressInfoDto? Address { get; set; } public long TotalPrice { get; set; } public long DiscountBalanceUsed { get; set; } public long GatewayAmount { get; set; } + public long VatAmount { get; set; } public bool PaymentCompleted { get; set; } /// مقدار proto PaymentStatus: 0 Pending, 1 Completed, 2 Failed, 3 Refunded public int PaymentStatusValue { get; set; }