From a9aa03d6a8fd149960381554c37231c41610857b Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Fri, 7 Aug 2026 15:52:55 +0330 Subject: [PATCH 1/9] feat(payment): enhance order details with payment status indicators and update proto version - Updated OrderDetailsDialog.razor to display payment status using dynamic color and icon based on the payment status value. - Added helper methods in OrderDetailsDialog.razor.cs to determine payment status color, text, and icon. - Enhanced UserOrderDetailsDialog.razor to conditionally display postal code and user mobile information. - Updated DiscountOrderService to include PaymentStatusValue from the response. - Bumped Foursat.CMSMicroservice.Protobuf version to 0.0.206 for compatibility with new payment features. --- src/BackOffice/BackOffice.csproj | 2 +- .../Components/OrderDetailsDialog.razor | 35 +++++++++---------- .../Components/OrderDetailsDialog.razor.cs | 26 ++++++++++++++ .../Components/UserOrderDetailsDialog.razor | 8 +++++ .../DiscountOrder/DiscountOrderService.cs | 1 + .../DiscountOrder/IDiscountOrderService.cs | 2 ++ 6 files changed, 54 insertions(+), 20 deletions(-) diff --git a/src/BackOffice/BackOffice.csproj b/src/BackOffice/BackOffice.csproj index 9d6ea7f..23b45c5 100644 --- a/src/BackOffice/BackOffice.csproj +++ b/src/BackOffice/BackOffice.csproj @@ -143,7 +143,7 @@ - + diff --git a/src/BackOffice/Pages/DiscountShop/Components/OrderDetailsDialog.razor b/src/BackOffice/Pages/DiscountShop/Components/OrderDetailsDialog.razor index 44a8fd5..24c2c8a 100644 --- a/src/BackOffice/Pages/DiscountShop/Components/OrderDetailsDialog.razor +++ b/src/BackOffice/Pages/DiscountShop/Components/OrderDetailsDialog.razor @@ -64,23 +64,17 @@ وضعیت پرداخت - @if (Order.PaymentCompleted) + + @GetPaymentStatusText(Order.PaymentStatusValue) + + @if (!string.IsNullOrEmpty(Order.TransactionId)) { - - پرداخت شده - - @if (!string.IsNullOrEmpty(Order.TransactionId)) - { - - کد تراکنش: @Order.TransactionId - - } - } - else - { - - در انتظار پرداخت - + + کد تراکنش: @Order.TransactionId + } @@ -96,10 +90,13 @@ @Order.Address.Title @Order.Address.Address - کد پستی: @Order.Address.PostalCode - @if (!string.IsNullOrEmpty(Order.Address.Phone)) + @if (!string.IsNullOrWhiteSpace(Order.Address.PostalCode)) { - تلفن: @Order.Address.Phone + کد پستی: @Order.Address.PostalCode + } + @if (!string.IsNullOrWhiteSpace(Order.Address.Phone)) + { + تلفن سفارش‌دهنده: @Order.Address.Phone } diff --git a/src/BackOffice/Pages/DiscountShop/Components/OrderDetailsDialog.razor.cs b/src/BackOffice/Pages/DiscountShop/Components/OrderDetailsDialog.razor.cs index b1f2a36..25970ad 100644 --- a/src/BackOffice/Pages/DiscountShop/Components/OrderDetailsDialog.razor.cs +++ b/src/BackOffice/Pages/DiscountShop/Components/OrderDetailsDialog.razor.cs @@ -157,6 +157,32 @@ public partial class OrderDetailsDialog }; } + private Color GetPaymentStatusColor(int status) => status switch + { + 0 => Color.Warning, + 1 => Color.Success, + 2 => Color.Error, + 3 => Color.Info, + _ => Color.Default + }; + + private string GetPaymentStatusText(int status) => status switch + { + 0 => "در انتظار پرداخت", + 1 => "پرداخت شده", + 2 => "ناموفق", + 3 => "بازپرداخت شده", + _ => "نامشخص" + }; + + private string GetPaymentStatusIcon(int status) => status switch + { + 1 => Icons.Material.Filled.CheckCircle, + 2 => Icons.Material.Filled.Cancel, + 3 => Icons.Material.Filled.Replay, + _ => Icons.Material.Filled.Schedule + }; + private enum StepState { Inactive = 0, diff --git a/src/BackOffice/Pages/UserOrder/Components/UserOrderDetailsDialog.razor b/src/BackOffice/Pages/UserOrder/Components/UserOrderDetailsDialog.razor index 5464e1e..63a9b77 100644 --- a/src/BackOffice/Pages/UserOrder/Components/UserOrderDetailsDialog.razor +++ b/src/BackOffice/Pages/UserOrder/Components/UserOrderDetailsDialog.razor @@ -123,6 +123,14 @@ { آدرسی ثبت نشده است. } + @if (!string.IsNullOrWhiteSpace(_model.PostalCode)) + { + کد پستی: @_model.PostalCode + } + @if (!string.IsNullOrWhiteSpace(_model.UserMobile)) + { + تلفن سفارش‌دهنده: @_model.UserMobile + } diff --git a/src/BackOffice/Services/DiscountOrder/DiscountOrderService.cs b/src/BackOffice/Services/DiscountOrder/DiscountOrderService.cs index bcd0828..8172bdc 100644 --- a/src/BackOffice/Services/DiscountOrder/DiscountOrderService.cs +++ b/src/BackOffice/Services/DiscountOrder/DiscountOrderService.cs @@ -102,6 +102,7 @@ public class DiscountOrderService : IDiscountOrderService DiscountBalanceUsed = response.DiscountBalanceUsed, GatewayAmount = response.GatewayAmount, PaymentCompleted = response.PaymentCompleted, + PaymentStatusValue = (int)response.PaymentStatus, TransactionId = response.TransactionId, Status = MapFromProtoStatus(response.DeliveryStatus, response.PaymentCompleted), TrackingCode = response.TrackingCode, diff --git a/src/BackOffice/Services/DiscountOrder/IDiscountOrderService.cs b/src/BackOffice/Services/DiscountOrder/IDiscountOrderService.cs index 07147a1..0367b52 100644 --- a/src/BackOffice/Services/DiscountOrder/IDiscountOrderService.cs +++ b/src/BackOffice/Services/DiscountOrder/IDiscountOrderService.cs @@ -67,6 +67,8 @@ public class DiscountOrderDetailsDto public long DiscountBalanceUsed { get; set; } public long GatewayAmount { get; set; } public bool PaymentCompleted { get; set; } + /// مقدار proto PaymentStatus: 0 Pending, 1 Completed, 2 Failed, 3 Refunded + public int PaymentStatusValue { get; set; } public string? TransactionId { get; set; } public OrderStatus Status { get; set; } public string? TrackingCode { get; set; } From 68d0aed4682158f6e5caa6ca596d517fb37f3b64 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Fri, 7 Aug 2026 17:08:23 +0330 Subject: [PATCH 2/9] 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; } From cbc61ad0f56a32504c9161a15fdb8922cd19262a Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Fri, 7 Aug 2026 17:10:30 +0330 Subject: [PATCH 3/9] feat(order-details): add mobile display logic and remove unused status methods - Introduced a new property to display the user's mobile number or fallback to the address phone number in OrderDetailsDialog. - Removed obsolete methods for determining order status color and text, streamlining the component's code. --- .../Components/OrderDetailsDialog.razor.cs | 33 ++----------------- 1 file changed, 3 insertions(+), 30 deletions(-) diff --git a/src/BackOffice/Pages/DiscountShop/Components/OrderDetailsDialog.razor.cs b/src/BackOffice/Pages/DiscountShop/Components/OrderDetailsDialog.razor.cs index 8696d54..007e8f3 100644 --- a/src/BackOffice/Pages/DiscountShop/Components/OrderDetailsDialog.razor.cs +++ b/src/BackOffice/Pages/DiscountShop/Components/OrderDetailsDialog.razor.cs @@ -20,6 +20,9 @@ public partial class OrderDetailsDialog private bool _isSaving; private List _timelineSteps = new(); + private string? DisplayMobile => + !string.IsNullOrWhiteSpace(Order.UserMobile) ? Order.UserMobile : Order.Address?.Phone; + protected override void OnInitialized() { _newStatus = Order.Status; @@ -130,36 +133,6 @@ public partial class OrderDetailsDialog private void Close() => MudDialog.Close(); - private Color GetStatusColor(OrderStatus status) - { - return status switch - { - OrderStatus.Pending => Color.Warning, - OrderStatus.Paid => Color.Info, - OrderStatus.Processing => Color.Primary, - OrderStatus.Shipped => Color.Secondary, - OrderStatus.Delivered => Color.Success, - OrderStatus.Cancelled => Color.Error, - OrderStatus.Returned => Color.Dark, - _ => Color.Default - }; - } - - private string GetStatusText(OrderStatus status) - { - return status switch - { - OrderStatus.Pending => "در انتظار پرداخت", - OrderStatus.Paid => "پرداخت شده", - OrderStatus.Processing => "در حال آماده‌سازی", - OrderStatus.Shipped => "ارسال شده", - OrderStatus.Delivered => "تحویل داده شده", - OrderStatus.Cancelled => "لغو شده", - OrderStatus.Returned => "مرجوع شده", - _ => "نامشخص" - }; - } - private Color GetPaymentStatusColor(int status) => status switch { 0 => Color.Warning, From 1d78633e175254b2097860c0c8c5f19c79c6e9c9 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Fri, 7 Aug 2026 17:35:19 +0330 Subject: [PATCH 4/9] feat(order-status): add "Ready for Office Pickup" status and update related components - Introduced a new order status "Ready for Office Pickup" across various components including DiscountOrdersMainPage, SalesReports, and UserOrder pages. - Updated the color and text representations for the new status in relevant dialogs and services. - Enhanced the DiscountOrderService to handle the new status in delivery processing logic. - Bumped Foursat.CMSMicroservice.Protobuf version to 0.0.207 for compatibility with the new order status features. --- src/BackOffice/BackOffice.csproj | 2 +- .../Components/ChangeOrderStatusDialog.razor | 16 +++++++++++++-- .../Components/OrderDetailsDialog.razor | 7 +++++++ .../DiscountShop/DiscountOrdersMainPage.razor | 2 ++ .../DiscountOrdersMainPage.razor.cs | 6 ++++-- .../Pages/DiscountShop/SalesReports.razor | 5 ++++- .../Components/ChangeOrderStatusDialog.razor | 20 +++++++++++++++++++ .../ChangeOrderStatusDialog.razor.cs | 4 ++++ .../Components/UserOrderDetailsDialog.razor | 2 ++ .../UserOrderDetailsDialog.razor.cs | 4 ++++ .../Pages/UserOrder/OrderSalesReports.razor | 1 + .../UserOrder/OrderSalesReports.razor.cs | 4 +++- .../Pages/UserOrder/UserOrderMainPage.razor | 1 + .../UserOrder/UserOrderMainPage.razor.cs | 4 ++++ .../DiscountOrder/DiscountOrderService.cs | 9 +++++++-- .../DiscountOrder/IDiscountOrderService.cs | 15 +++++++------- 16 files changed, 86 insertions(+), 16 deletions(-) diff --git a/src/BackOffice/BackOffice.csproj b/src/BackOffice/BackOffice.csproj index 23b45c5..cc3aef2 100644 --- a/src/BackOffice/BackOffice.csproj +++ b/src/BackOffice/BackOffice.csproj @@ -143,7 +143,7 @@ - + diff --git a/src/BackOffice/Pages/DiscountShop/Components/ChangeOrderStatusDialog.razor b/src/BackOffice/Pages/DiscountShop/Components/ChangeOrderStatusDialog.razor index b3d2e8b..51bffb5 100644 --- a/src/BackOffice/Pages/DiscountShop/Components/ChangeOrderStatusDialog.razor +++ b/src/BackOffice/Pages/DiscountShop/Components/ChangeOrderStatusDialog.razor @@ -26,6 +26,7 @@ در حال آماده‌سازی ارسال شده تحویل داده شده + آماده تحویل در دفتر لغو شده مرجوع شده @@ -55,6 +56,15 @@ } + @if (Model.Status == OrderStatus.ReadyForOfficePickup) + { + + + سفارش برای تحویل حضوری در دفتر آماده است. + + + } + @if (Model.Status == OrderStatus.Shipped) { @@ -126,8 +136,9 @@ OrderStatus.Processing => Color.Primary, OrderStatus.Shipped => Color.Secondary, OrderStatus.Delivered => Color.Success, - OrderStatus.Cancelled => Color.Error, - OrderStatus.Returned => Color.Dark, + OrderStatus.ReadyForOfficePickup => Color.Primary, + OrderStatus.Cancelled => Color.Dark, + OrderStatus.Returned => Color.Error, _ => Color.Default }; } @@ -141,6 +152,7 @@ OrderStatus.Processing => "در حال آماده‌سازی", OrderStatus.Shipped => "ارسال شده", OrderStatus.Delivered => "تحویل داده شده", + OrderStatus.ReadyForOfficePickup => "آماده تحویل در دفتر", OrderStatus.Cancelled => "لغو شده", OrderStatus.Returned => "مرجوع شده", _ => "نامشخص" diff --git a/src/BackOffice/Pages/DiscountShop/Components/OrderDetailsDialog.razor b/src/BackOffice/Pages/DiscountShop/Components/OrderDetailsDialog.razor index 81c3ec1..3b22e32 100644 --- a/src/BackOffice/Pages/DiscountShop/Components/OrderDetailsDialog.razor +++ b/src/BackOffice/Pages/DiscountShop/Components/OrderDetailsDialog.razor @@ -91,6 +91,7 @@ در حال آماده‌سازی ارسال شده تحویل داده شده + آماده تحویل در دفتر لغو شده مرجوع شده @@ -114,6 +115,12 @@ توجه: در صورت لغو یا مرجوعی سفارش، موجودی محصولات به انبار بازگردانده می‌شود. } + @if (_newStatus == OrderStatus.ReadyForOfficePickup) + { + + سفارش برای تحویل حضوری در دفتر آماده است. + + } @if (_newStatus == OrderStatus.Shipped) { diff --git a/src/BackOffice/Pages/DiscountShop/DiscountOrdersMainPage.razor b/src/BackOffice/Pages/DiscountShop/DiscountOrdersMainPage.razor index dafd690..802ed20 100644 --- a/src/BackOffice/Pages/DiscountShop/DiscountOrdersMainPage.razor +++ b/src/BackOffice/Pages/DiscountShop/DiscountOrdersMainPage.razor @@ -49,6 +49,8 @@ ارسال شده تحویل داده شده لغو شده + آماده تحویل در دفتر + مرجوع شده diff --git a/src/BackOffice/Pages/DiscountShop/DiscountOrdersMainPage.razor.cs b/src/BackOffice/Pages/DiscountShop/DiscountOrdersMainPage.razor.cs index e93e6fa..0dd0373 100644 --- a/src/BackOffice/Pages/DiscountShop/DiscountOrdersMainPage.razor.cs +++ b/src/BackOffice/Pages/DiscountShop/DiscountOrdersMainPage.razor.cs @@ -187,8 +187,9 @@ public partial class DiscountOrdersMainPage OrderStatus.Processing => Color.Primary, OrderStatus.Shipped => Color.Secondary, OrderStatus.Delivered => Color.Success, - OrderStatus.Cancelled => Color.Error, - OrderStatus.Returned => Color.Dark, + OrderStatus.Cancelled => Color.Dark, + OrderStatus.ReadyForOfficePickup => Color.Primary, + OrderStatus.Returned => Color.Error, _ => Color.Default }; } @@ -203,6 +204,7 @@ public partial class DiscountOrdersMainPage OrderStatus.Shipped => "ارسال شده", OrderStatus.Delivered => "تحویل داده شده", OrderStatus.Cancelled => "لغو شده", + OrderStatus.ReadyForOfficePickup => "آماده تحویل در دفتر", OrderStatus.Returned => "مرجوع شده", _ => "نامشخص" }; diff --git a/src/BackOffice/Pages/DiscountShop/SalesReports.razor b/src/BackOffice/Pages/DiscountShop/SalesReports.razor index aefabdd..a58b7af 100644 --- a/src/BackOffice/Pages/DiscountShop/SalesReports.razor +++ b/src/BackOffice/Pages/DiscountShop/SalesReports.razor @@ -48,6 +48,7 @@ در حال پردازش ارسال شده تحویل شده + آماده تحویل در دفتر لغو شده مرجوع شده @@ -498,7 +499,8 @@ OrderStatus.Processing => Color.Info, OrderStatus.Shipped => Color.Info, OrderStatus.Delivered => Color.Success, - OrderStatus.Cancelled => Color.Error, + OrderStatus.ReadyForOfficePickup => Color.Primary, + OrderStatus.Cancelled => Color.Dark, OrderStatus.Returned => Color.Error, _ => Color.Default }; @@ -513,6 +515,7 @@ OrderStatus.Processing => "در حال پردازش", OrderStatus.Shipped => "ارسال شده", OrderStatus.Delivered => "تحویل شده", + OrderStatus.ReadyForOfficePickup => "آماده تحویل در دفتر", OrderStatus.Cancelled => "لغو شده", OrderStatus.Returned => "مرجوع شده", _ => "نامشخص" diff --git a/src/BackOffice/Pages/UserOrder/Components/ChangeOrderStatusDialog.razor b/src/BackOffice/Pages/UserOrder/Components/ChangeOrderStatusDialog.razor index b62ec9c..f1d7a22 100644 --- a/src/BackOffice/Pages/UserOrder/Components/ChangeOrderStatusDialog.razor +++ b/src/BackOffice/Pages/UserOrder/Components/ChangeOrderStatusDialog.razor @@ -27,6 +27,8 @@ تحویل پست تحویل به مشتری مرجوع شده + لغو شده + آماده تحویل در دفتر @@ -56,6 +58,24 @@ } + @if (_newStatus == 5) + { + + + توجه: وضعیت «لغو شده» یعنی ارسال این سفارش متوقف شده است. + + + } + + @if (_newStatus == 6) + { + + + سفارش برای تحویل حضوری در دفتر آماده است. + + + } + @if (_newStatus == 2) { diff --git a/src/BackOffice/Pages/UserOrder/Components/ChangeOrderStatusDialog.razor.cs b/src/BackOffice/Pages/UserOrder/Components/ChangeOrderStatusDialog.razor.cs index cd1dea1..c22a23e 100644 --- a/src/BackOffice/Pages/UserOrder/Components/ChangeOrderStatusDialog.razor.cs +++ b/src/BackOffice/Pages/UserOrder/Components/ChangeOrderStatusDialog.razor.cs @@ -37,6 +37,8 @@ public partial class ChangeOrderStatusDialog 2 => "تحویل پست", 3 => "تحویل به مشتری", 4 => "مرجوع شده", + 5 => "لغو شده", + 6 => "آماده تحویل در دفتر", _ => "بدون ارسال / نامشخص" }; } @@ -49,6 +51,8 @@ public partial class ChangeOrderStatusDialog 2 => Color.Info, 3 => Color.Success, 4 => Color.Error, + 5 => Color.Dark, + 6 => Color.Primary, _ => Color.Default }; } diff --git a/src/BackOffice/Pages/UserOrder/Components/UserOrderDetailsDialog.razor b/src/BackOffice/Pages/UserOrder/Components/UserOrderDetailsDialog.razor index 99434a8..8e0eba0 100644 --- a/src/BackOffice/Pages/UserOrder/Components/UserOrderDetailsDialog.razor +++ b/src/BackOffice/Pages/UserOrder/Components/UserOrderDetailsDialog.razor @@ -95,6 +95,8 @@ تحویل پست تحویل به مشتری مرجوع شده + لغو شده + آماده تحویل در دفتر Color.Info, // InTransit 3 => Color.Success, // Delivered 4 => Color.Error, // Returned + 5 => Color.Dark, // Cancelled + 6 => Color.Primary, // ReadyForOfficePickup _ => Color.Default // None / Unknown }; } @@ -71,6 +73,8 @@ public partial class UserOrderDetailsDialog 2 => "تحویل پست", 3 => "تحویل به مشتری", 4 => "مرجوع شده", + 5 => "لغو شده", + 6 => "آماده تحویل در دفتر", _ => "بدون ارسال / نامشخص" }; } diff --git a/src/BackOffice/Pages/UserOrder/OrderSalesReports.razor b/src/BackOffice/Pages/UserOrder/OrderSalesReports.razor index 3180253..917f8b6 100644 --- a/src/BackOffice/Pages/UserOrder/OrderSalesReports.razor +++ b/src/BackOffice/Pages/UserOrder/OrderSalesReports.razor @@ -46,6 +46,7 @@ تحویل به مشتری مرجوع شده لغو شده + آماده تحویل در دفتر diff --git a/src/BackOffice/Pages/UserOrder/OrderSalesReports.razor.cs b/src/BackOffice/Pages/UserOrder/OrderSalesReports.razor.cs index 8bc8042..a49698c 100644 --- a/src/BackOffice/Pages/UserOrder/OrderSalesReports.razor.cs +++ b/src/BackOffice/Pages/UserOrder/OrderSalesReports.razor.cs @@ -215,7 +215,8 @@ public partial class OrderSalesReports 2 => Color.Info, 3 => Color.Success, 4 => Color.Error, - 5 => Color.Error, + 5 => Color.Dark, + 6 => Color.Primary, _ => Color.Default }; } @@ -229,6 +230,7 @@ public partial class OrderSalesReports 3 => "تحویل به مشتری", 4 => "مرجوع شده", 5 => "لغو شده", + 6 => "آماده تحویل در دفتر", _ => "بدون ارسال / نامشخص" }; } diff --git a/src/BackOffice/Pages/UserOrder/UserOrderMainPage.razor b/src/BackOffice/Pages/UserOrder/UserOrderMainPage.razor index 306569c..64c4a5d 100644 --- a/src/BackOffice/Pages/UserOrder/UserOrderMainPage.razor +++ b/src/BackOffice/Pages/UserOrder/UserOrderMainPage.razor @@ -56,6 +56,7 @@ تحویل به مشتری مرجوع شده لغو شده + آماده تحویل در دفتر diff --git a/src/BackOffice/Pages/UserOrder/UserOrderMainPage.razor.cs b/src/BackOffice/Pages/UserOrder/UserOrderMainPage.razor.cs index ddc90d0..2681678 100644 --- a/src/BackOffice/Pages/UserOrder/UserOrderMainPage.razor.cs +++ b/src/BackOffice/Pages/UserOrder/UserOrderMainPage.razor.cs @@ -213,6 +213,8 @@ public partial class UserOrderMainPage 2 => Color.Info, // InTransit 3 => Color.Success, // Delivered 4 => Color.Error, // Returned + 5 => Color.Dark, // Cancelled + 6 => Color.Primary, // ReadyForOfficePickup _ => Color.Default // None / Unknown }; } @@ -225,6 +227,8 @@ public partial class UserOrderMainPage 2 => "تحویل پست", 3 => "تحویل به مشتری", 4 => "مرجوع شده", + 5 => "لغو شده", + 6 => "آماده تحویل در دفتر", _ => "بدون ارسال / نامشخص" }; } diff --git a/src/BackOffice/Services/DiscountOrder/DiscountOrderService.cs b/src/BackOffice/Services/DiscountOrder/DiscountOrderService.cs index 8172bdc..4ce9555 100644 --- a/src/BackOffice/Services/DiscountOrder/DiscountOrderService.cs +++ b/src/BackOffice/Services/DiscountOrder/DiscountOrderService.cs @@ -201,12 +201,13 @@ public class DiscountOrderService : IDiscountOrderService return status switch { OrderStatus.Pending => ProtoDeliveryStatus.DeliveryPending, - OrderStatus.Paid => ProtoDeliveryStatus.DeliveryPending, // Paid but not processed yet + OrderStatus.Paid => ProtoDeliveryStatus.DeliveryPending, OrderStatus.Processing => ProtoDeliveryStatus.DeliveryProcessing, OrderStatus.Shipped => ProtoDeliveryStatus.DeliveryShipped, OrderStatus.Delivered => ProtoDeliveryStatus.DeliveryDelivered, OrderStatus.Cancelled => ProtoDeliveryStatus.DeliveryCancelled, - OrderStatus.Returned => ProtoDeliveryStatus.DeliveryCancelled, // Treat as cancelled + OrderStatus.ReadyForOfficePickup => ProtoDeliveryStatus.DeliveryReadyForOffice, + OrderStatus.Returned => ProtoDeliveryStatus.DeliveryReturned, _ => ProtoDeliveryStatus.DeliveryPending }; } @@ -222,6 +223,8 @@ public class DiscountOrderService : IDiscountOrderService ProtoDeliveryStatus.DeliveryShipped => OrderStatus.Shipped, ProtoDeliveryStatus.DeliveryDelivered => OrderStatus.Delivered, ProtoDeliveryStatus.DeliveryCancelled => OrderStatus.Cancelled, + ProtoDeliveryStatus.DeliveryReadyForOffice => OrderStatus.ReadyForOfficePickup, + ProtoDeliveryStatus.DeliveryReturned => OrderStatus.Returned, _ => OrderStatus.Pending }; } @@ -236,6 +239,8 @@ public class DiscountOrderService : IDiscountOrderService ProtoDeliveryStatus.DeliveryShipped => OrderStatus.Shipped, ProtoDeliveryStatus.DeliveryDelivered => OrderStatus.Delivered, ProtoDeliveryStatus.DeliveryCancelled => OrderStatus.Cancelled, + ProtoDeliveryStatus.DeliveryReadyForOffice => OrderStatus.ReadyForOfficePickup, + ProtoDeliveryStatus.DeliveryReturned => OrderStatus.Returned, _ => OrderStatus.Pending }; } diff --git a/src/BackOffice/Services/DiscountOrder/IDiscountOrderService.cs b/src/BackOffice/Services/DiscountOrder/IDiscountOrderService.cs index 826e479..2328c18 100644 --- a/src/BackOffice/Services/DiscountOrder/IDiscountOrderService.cs +++ b/src/BackOffice/Services/DiscountOrder/IDiscountOrderService.cs @@ -27,13 +27,14 @@ public class OrderFilterDto // For backward compatibility with UI - maps to proto DeliveryStatus public enum OrderStatus { - Pending = 0, // DELIVERY_PENDING - Paid = 1, // Not in proto - handle in service - Processing = 2, // DELIVERY_PROCESSING - Shipped = 3, // DELIVERY_SHIPPED - Delivered = 4, // DELIVERY_DELIVERED - Cancelled = 5, // DELIVERY_CANCELLED - Returned = 6 // Not in proto - handle in service + Pending = 0, // DELIVERY_PENDING + Paid = 1, // Not in proto - handle in service + Processing = 2, // DELIVERY_PROCESSING + Shipped = 3, // DELIVERY_SHIPPED + Delivered = 4, // DELIVERY_DELIVERED + Cancelled = 5, // DELIVERY_CANCELLED + Returned = 6, // DELIVERY_RETURNED + ReadyForOfficePickup = 7 // DELIVERY_READY_FOR_OFFICE } public class DiscountOrderDto From 6a9d142dc63a51b9f311e7ddc912b18527136860 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Fri, 7 Aug 2026 18:40:00 +0330 Subject: [PATCH 5/9] refactor(csv-export): streamline CSV export logic and remove redundant methods - Replaced manual CSV string construction with CsvExportHelper for better readability and maintainability across DiscountOrdersMainPage, SalesReports, OrderSalesReports, and UserOrderMainPage. - Removed obsolete EscapeCsv methods to simplify the codebase. - Enhanced CSV export functionality to utilize a unified base64 conversion method for file saving. --- .../Common/Utilities/CsvExportHelper.cs | 57 +++++++++++++++++++ .../DiscountOrdersMainPage.razor.cs | 36 +++++------- .../Pages/DiscountShop/SalesReports.razor | 28 +++------ .../UserOrder/OrderSalesReports.razor.cs | 27 +++------ .../UserOrder/UserOrderMainPage.razor.cs | 42 +++++++------- 5 files changed, 109 insertions(+), 81 deletions(-) create mode 100644 src/BackOffice/Common/Utilities/CsvExportHelper.cs diff --git a/src/BackOffice/Common/Utilities/CsvExportHelper.cs b/src/BackOffice/Common/Utilities/CsvExportHelper.cs new file mode 100644 index 0000000..6e6b4cf --- /dev/null +++ b/src/BackOffice/Common/Utilities/CsvExportHelper.cs @@ -0,0 +1,57 @@ +using System.Globalization; +using System.Text; + +namespace BackOffice.Common.Utilities; + +/// +/// خروجی CSV سازگار با Excel (UTF-8 BOM + sep=, + اعداد بدون جداکننده هزارگان). +/// +public static class CsvExportHelper +{ + public const char Separator = ','; + + public static string Escape(string? value) + { + if (string.IsNullOrEmpty(value)) + return string.Empty; + + if (value.Contains(Separator) || value.Contains('"') || value.Contains('\n') || value.Contains('\r')) + return $"\"{value.Replace("\"", "\"\"")}\""; + + return value; + } + + public static string Cell(object? value) => value switch + { + null => string.Empty, + string s => Escape(s), + bool b => b ? "1" : "0", + IFormattable f => Escape(f.ToString(null, CultureInfo.InvariantCulture) ?? string.Empty), + _ => Escape(Convert.ToString(value, CultureInfo.InvariantCulture)) + }; + + public static string Row(params object?[] cells) => + string.Join(Separator, cells.Select(Cell)); + + /// + /// نام نمایشی مشتری؛ اگر خالی باشد موبایل یا شناسه کاربر. + /// + public static string CustomerName(string? fullName, string? mobile = null, long? userId = null) + { + if (!string.IsNullOrWhiteSpace(fullName)) + return fullName.Trim(); + if (!string.IsNullOrWhiteSpace(mobile)) + return mobile.Trim(); + if (userId is > 0) + return $"کاربر {userId}"; + return "—"; + } + + public static string ToBase64(StringBuilder bodyWithoutSepHint) + { + // Excel در لوکال فارسی با این خط، جداکننده کاما را درست تشخیص می‌دهد + var content = "sep=," + Environment.NewLine + bodyWithoutSepHint; + var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(content)).ToArray(); + return Convert.ToBase64String(bytes); + } +} diff --git a/src/BackOffice/Pages/DiscountShop/DiscountOrdersMainPage.razor.cs b/src/BackOffice/Pages/DiscountShop/DiscountOrdersMainPage.razor.cs index 0dd0373..50db2e1 100644 --- a/src/BackOffice/Pages/DiscountShop/DiscountOrdersMainPage.razor.cs +++ b/src/BackOffice/Pages/DiscountShop/DiscountOrdersMainPage.razor.cs @@ -1,5 +1,6 @@ using System.Text; using BackOffice.Common.BaseComponents; +using BackOffice.Common.Utilities; using BackOffice.Services.DiscountOrder; using BackOffice.Pages.DiscountShop.Components; using Microsoft.AspNetCore.Components; @@ -245,30 +246,29 @@ public partial class DiscountOrdersMainPage try { var sb = new StringBuilder(); - sb.AppendLine("شناسه,نام کاربر,موبایل,مبلغ کل,کیف پول اعتباری,پرداخت درگاه,تعداد آیتم,وضعیت پرداخت,وضعیت ارسال,تاریخ پرداخت,تاریخ ثبت,آدرس"); + sb.AppendLine(CsvExportHelper.Row( + "شناسه", "نام مشتری", "موبایل", "مبلغ کل", "کیف پول اعتباری", "پرداخت درگاه", + "تعداد آیتم", "وضعیت پرداخت", "وضعیت ارسال", "تاریخ پرداخت", "تاریخ ثبت", "آدرس")); foreach (var o in _orders) { - sb.AppendLine(string.Join(",", + sb.AppendLine(CsvExportHelper.Row( o.Id, - EscapeCsv(o.UserFullName), - EscapeCsv(o.UserMobile), - o.TotalPrice.ToString("N0"), - o.DiscountBalanceUsed.ToString("N0"), - o.GatewayAmount.ToString("N0"), + CsvExportHelper.CustomerName(o.UserFullName, o.UserMobile, o.UserId), + o.UserMobile, + o.TotalPrice, + o.DiscountBalanceUsed, + o.GatewayAmount, o.ItemsCount, - EscapeCsv(GetPaymentStatusText(o.PaymentStatusValue)), - EscapeCsv(GetStatusText(o.Status)), + GetPaymentStatusText(o.PaymentStatusValue), + GetStatusText(o.Status), o.PaymentDate?.MiladiToJalaliWithTime() ?? "-", o.Created?.MiladiToJalaliWithTime() ?? "-", - EscapeCsv(o.ShippingAddress ?? "-"))); + o.ShippingAddress ?? "-")); } - var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray(); - var base64 = Convert.ToBase64String(bytes); var filename = $"discount-orders-{DateTime.Now:yyyyMMddHHmmss}.csv"; - - await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64); + await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, CsvExportHelper.ToBase64(sb)); Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success); } catch (Exception ex) @@ -276,12 +276,4 @@ public partial class DiscountOrdersMainPage Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error); } } - - private static string EscapeCsv(string value) - { - if (string.IsNullOrEmpty(value)) return ""; - if (value.Contains(',') || value.Contains('"') || value.Contains('\n')) - return $"\"{value.Replace("\"", "\"\"")}\""; - return value; - } } diff --git a/src/BackOffice/Pages/DiscountShop/SalesReports.razor b/src/BackOffice/Pages/DiscountShop/SalesReports.razor index a58b7af..fc65969 100644 --- a/src/BackOffice/Pages/DiscountShop/SalesReports.razor +++ b/src/BackOffice/Pages/DiscountShop/SalesReports.razor @@ -531,14 +531,15 @@ } var sb = new StringBuilder(); - sb.AppendLine("شناسه,نام کاربر,موبایل,تاریخ ثبت,تعداد آیتم,مبلغ نهایی,تخفیف,وضعیت"); + sb.AppendLine(CsvExportHelper.Row( + "شناسه", "نام مشتری", "موبایل", "تاریخ ثبت", "تعداد آیتم", "مبلغ نهایی", "تخفیف", "وضعیت")); foreach (var o in _orders.Where(o => o.Created.HasValue).OrderBy(o => o.Created)) { - sb.AppendLine(string.Join(",", + sb.AppendLine(CsvExportHelper.Row( o.Id, - EscapeCsv(o.UserFullName), - EscapeCsv(o.UserMobile), + CsvExportHelper.CustomerName(o.UserFullName, o.UserMobile, o.UserId), + o.UserMobile, o.Created?.MiladiToJalaliWithTime() ?? "-", o.ItemsCount, o.GatewayAmount, @@ -546,11 +547,8 @@ GetStatusText(o.Status))); } - var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray(); - var base64 = Convert.ToBase64String(bytes); var filename = $"discount-sales-{DateTime.Now:yyyyMMddHHmmss}.csv"; - - await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64); + await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, CsvExportHelper.ToBase64(sb)); Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success); } @@ -575,12 +573,12 @@ sb.AppendLine(); sb.AppendLine("جزئیات سفارش‌ها:"); - sb.AppendLine("شناسه | نام کاربر | تاریخ | تعداد آیتم | مبلغ نهایی | تخفیف | وضعیت"); + sb.AppendLine("شناسه | نام مشتری | تاریخ | تعداد آیتم | مبلغ نهایی | تخفیف | وضعیت"); foreach (var o in _orders.Where(o => o.Created.HasValue).OrderBy(o => o.Created)) { sb.AppendLine( - $"{o.Id} | {o.UserFullName} | {o.Created?.MiladiToJalaliWithTime() ?? "-"} | {o.ItemsCount} | {o.GatewayAmount:N0} | {o.DiscountBalanceUsed:N0} | {GetStatusText(o.Status)}"); + $"{o.Id} | {CsvExportHelper.CustomerName(o.UserFullName, o.UserMobile, o.UserId)} | {o.Created?.MiladiToJalaliWithTime() ?? "-"} | {o.ItemsCount} | {o.GatewayAmount:N0} | {o.DiscountBalanceUsed:N0} | {GetStatusText(o.Status)}"); } var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray(); @@ -590,14 +588,4 @@ await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64); Snackbar.Add("خروجی PDF (نسخه متنی) آماده شد.", Severity.Success); } - - private static string EscapeCsv(string value) - { - if (string.IsNullOrEmpty(value)) - return ""; - - var needsQuotes = value.Contains(',') || value.Contains('"') || value.Contains('\n'); - var escaped = value.Replace("\"", "\"\""); - return needsQuotes ? $"\"{escaped}\"" : escaped; - } } diff --git a/src/BackOffice/Pages/UserOrder/OrderSalesReports.razor.cs b/src/BackOffice/Pages/UserOrder/OrderSalesReports.razor.cs index a49698c..4fe7a70 100644 --- a/src/BackOffice/Pages/UserOrder/OrderSalesReports.razor.cs +++ b/src/BackOffice/Pages/UserOrder/OrderSalesReports.razor.cs @@ -1,4 +1,5 @@ using System.Text; +using BackOffice.Common.Utilities; using CMSMicroservice.Protobuf.Protos; using CMSMicroservice.Protobuf.Protos.UserOrder; using Microsoft.AspNetCore.Components; @@ -244,25 +245,23 @@ public partial class OrderSalesReports } var sb = new StringBuilder(); - sb.AppendLine("شناسه,کاربر,نام کاربر,مبلغ,وضعیت پرداخت,وضعیت ارسال,تاریخ پرداخت"); + sb.AppendLine(CsvExportHelper.Row( + "شناسه", "کاربر", "نام مشتری", "مبلغ", "وضعیت پرداخت", "وضعیت ارسال", "تاریخ پرداخت")); foreach (var o in _orders) { - sb.AppendLine(string.Join(",", + sb.AppendLine(CsvExportHelper.Row( o.Id, o.UserId, - EscapeCsv(o.UserFullName), + CsvExportHelper.CustomerName(o.UserFullName, userId: o.UserId), o.Amount, - EscapeCsv(o.PaymentStatus == PaymentStatus.Success ? "پرداخت شده" : "پرداخت نشده"), - EscapeCsv(GetDeliveryStatusText(o.DeliveryStatus.GetHashCode())), + o.PaymentStatus == PaymentStatus.Success ? "پرداخت شده" : "پرداخت نشده", + GetDeliveryStatusText(o.DeliveryStatus.GetHashCode()), o.PaymentDate != null ? o.PaymentDate.ToDateTime().MiladiToJalaliWithTime() : "-")); } - var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray(); - var base64 = Convert.ToBase64String(bytes); var filename = $"store-sales-{DateTime.Now:yyyyMMddHHmmss}.csv"; - - await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64); + await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, CsvExportHelper.ToBase64(sb)); Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success); } @@ -292,7 +291,7 @@ public partial class OrderSalesReports foreach (var o in _orders) { sb.AppendLine( - $"{o.Id} | {o.UserFullName} | {o.Amount:N0} | {(o.PaymentStatus == PaymentStatus.Success ? "پرداخت شده" : "پرداخت نشده")} | {GetDeliveryStatusText(o.DeliveryStatus.GetHashCode())} | {(o.PaymentDate != null ? o.PaymentDate.ToDateTime().MiladiToJalaliWithTime() : "-")}"); + $"{o.Id} | {CsvExportHelper.CustomerName(o.UserFullName, userId: o.UserId)} | {o.Amount:N0} | {(o.PaymentStatus == PaymentStatus.Success ? "پرداخت شده" : "پرداخت نشده")} | {GetDeliveryStatusText(o.DeliveryStatus.GetHashCode())} | {(o.PaymentDate != null ? o.PaymentDate.ToDateTime().MiladiToJalaliWithTime() : "-")}"); } var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray(); @@ -351,12 +350,4 @@ public partial class OrderSalesReports } }; } - - private static string EscapeCsv(string value) - { - if (string.IsNullOrEmpty(value)) return ""; - if (value.Contains(',') || value.Contains('"') || value.Contains('\n')) - return $"\"{value.Replace("\"", "\"\"")}\""; - return value; - } } diff --git a/src/BackOffice/Pages/UserOrder/UserOrderMainPage.razor.cs b/src/BackOffice/Pages/UserOrder/UserOrderMainPage.razor.cs index 2681678..04aaeb4 100644 --- a/src/BackOffice/Pages/UserOrder/UserOrderMainPage.razor.cs +++ b/src/BackOffice/Pages/UserOrder/UserOrderMainPage.razor.cs @@ -1,8 +1,9 @@ -using System.Text.Json; -using System.Text; +using System.Text; +using System.Text.Json; +using BackOffice.Common.BaseComponents; +using BackOffice.Common.Utilities; using CMSMicroservice.Protobuf.Protos; using CMSMicroservice.Protobuf.Protos.UserOrder; -using BackOffice.Common.BaseComponents; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; using MudBlazor; @@ -242,6 +243,15 @@ public partial class UserOrderMainPage }; } + // Domain PaymentStatus: Success=0, Reject=1, Pending=2 + private static string GetPaymentStatusText(int status) => status switch + { + 0 => "پرداخت شده", + 1 => "پرداخت ناموفق", + 2 => "در انتظار پرداخت", + _ => "نامشخص" + }; + private async Task OpenChangeStatus(DataModel model) { var parameters = new DialogParameters @@ -320,26 +330,24 @@ public partial class UserOrderMainPage } var sb = new StringBuilder(); - sb.AppendLine("شناسه,کاربر,نام کاربر,مبلغ,وضعیت پرداخت,وضعیت ارسال,روش پرداخت,تاریخ پرداخت"); + sb.AppendLine(CsvExportHelper.Row( + "شناسه", "کاربر", "نام مشتری", "مبلغ", "وضعیت پرداخت", "وضعیت ارسال", "روش پرداخت", "تاریخ پرداخت")); foreach (var o in result.Models) { - sb.AppendLine(string.Join(",", + sb.AppendLine(CsvExportHelper.Row( o.Id, o.UserId, - EscapeCsv(o.UserFullName), + CsvExportHelper.CustomerName(o.UserFullName, userId: o.UserId), o.Amount, - EscapeCsv(o.PaymentStatus.ToString()), - EscapeCsv(GetDeliveryStatusText(o.DeliveryStatus.GetHashCode())), - EscapeCsv(GetPaymentMethodText(o.PaymentMethod.GetHashCode())), + GetPaymentStatusText(o.PaymentStatus.GetHashCode()), + GetDeliveryStatusText(o.DeliveryStatus.GetHashCode()), + GetPaymentMethodText(o.PaymentMethod.GetHashCode()), o.PaymentDate?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-")); } - var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray(); - var base64 = Convert.ToBase64String(bytes); var filename = $"user-orders-{DateTime.Now:yyyyMMddHHmmss}.csv"; - - await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64); + await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, CsvExportHelper.ToBase64(sb)); Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success); } catch (Exception ex) @@ -347,12 +355,4 @@ public partial class UserOrderMainPage Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error); } } - - private static string EscapeCsv(string value) - { - if (string.IsNullOrEmpty(value)) return ""; - if (value.Contains(',') || value.Contains('"') || value.Contains('\n')) - return $"\"{value.Replace("\"", "\"\"")}\""; - return value; - } } From e9f83284046ae8784a5daeb07d3ed2839c6bb1d0 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Sat, 8 Aug 2026 21:43:54 +0330 Subject: [PATCH 6/9] feat(export): enhance CSV and Excel export functionality across multiple pages - Updated CsvExportHelper to support Excel XML format for exporting data, improving compatibility with Excel. - Refactored export logic in DiscountOrdersMainPage, SalesReports, OrderSalesReports, and UserOrderMainPage to utilize the new Excel export method. - Changed file extensions from .csv to .xls for better clarity in exported files. - Improved header and row data handling for consistency in exported reports. --- docs/BACKOFFICE-AUDIT.md | 1 + .../Common/Utilities/CsvExportHelper.cs | 110 ++++++++++++++++-- .../DiscountOrdersMainPage.razor.cs | 47 ++++---- .../Pages/DiscountShop/SalesReports.razor | 31 ++--- .../UserOrder/OrderSalesReports.razor.cs | 35 +++--- .../UserOrder/UserOrderMainPage.razor.cs | 37 +++--- 6 files changed, 179 insertions(+), 82 deletions(-) diff --git a/docs/BACKOFFICE-AUDIT.md b/docs/BACKOFFICE-AUDIT.md index 18c6348..88d2fa8 100644 --- a/docs/BACKOFFICE-AUDIT.md +++ b/docs/BACKOFFICE-AUDIT.md @@ -1,3 +1,4 @@ # Docs moved to totalDoc See [totalDoc/INDEX.md](../../totalDoc/INDEX.md) for all documentation. + \ No newline at end of file diff --git a/src/BackOffice/Common/Utilities/CsvExportHelper.cs b/src/BackOffice/Common/Utilities/CsvExportHelper.cs index 6e6b4cf..f0943f5 100644 --- a/src/BackOffice/Common/Utilities/CsvExportHelper.cs +++ b/src/BackOffice/Common/Utilities/CsvExportHelper.cs @@ -1,31 +1,35 @@ using System.Globalization; +using System.Net; using System.Text; namespace BackOffice.Common.Utilities; /// -/// خروجی CSV سازگار با Excel (UTF-8 BOM + sep=, + اعداد بدون جداکننده هزارگان). +/// خروجی سازگار با Excel فارسی: +/// - XML Spreadsheet (.xls) برای ستون‌بندی مطمئن + متن فارسی +/// - CSV با جداکننده ; به‌عنوان جایگزین سبک /// public static class CsvExportHelper { - public const char Separator = ','; + /// جداکننده پیش‌فرض ویندوز فارسی برای CSV. + public const char Separator = ';'; public static string Escape(string? value) { if (string.IsNullOrEmpty(value)) return string.Empty; - if (value.Contains(Separator) || value.Contains('"') || value.Contains('\n') || value.Contains('\r')) - return $"\"{value.Replace("\"", "\"\"")}\""; - - return value; + // همیشه quote تا Excel فارسی ستون را نشکند + return $"\"{value.Replace("\"", "\"\"")}\""; } public static string Cell(object? value) => value switch { - null => string.Empty, + null => Escape(string.Empty), string s => Escape(s), bool b => b ? "1" : "0", + byte or sbyte or short or ushort or int or uint or long or ulong or float or double or decimal + => Convert.ToString(value, CultureInfo.InvariantCulture) ?? "0", IFormattable f => Escape(f.ToString(null, CultureInfo.InvariantCulture) ?? string.Empty), _ => Escape(Convert.ToString(value, CultureInfo.InvariantCulture)) }; @@ -47,11 +51,95 @@ public static class CsvExportHelper return "—"; } - public static string ToBase64(StringBuilder bodyWithoutSepHint) + public static string ToBase64(StringBuilder body) { - // Excel در لوکال فارسی با این خط، جداکننده کاما را درست تشخیص می‌دهد - var content = "sep=," + Environment.NewLine + bodyWithoutSepHint; - var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(content)).ToArray(); + // CRLF صریح — Excel روی ویندوز با \n لینوکس گاهی به‌هم می‌ریزد + var text = body.ToString().Replace("\r\n", "\n").Replace("\r", "\n").Replace("\n", "\r\n"); + var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(text)).ToArray(); return Convert.ToBase64String(bytes); } + + /// + /// خروجی SpreadsheetML که Excel به‌صورت ستون‌ستون و با UTF-8 درست باز می‌کند. + /// فایل را با پسوند .xls ذخیره کنید. + /// + public static string ToExcelXmlBase64(string sheetName, IReadOnlyList headers, IEnumerable rows) + { + var sb = new StringBuilder(); + sb.AppendLine(@""); + sb.AppendLine(@""); + sb.AppendLine(@""); + sb.AppendLine(@""); + sb.AppendLine(@""); + sb.AppendLine(@""); + sb.AppendLine(@""); + sb.Append(""); + sb.AppendLine(""); + + sb.AppendLine(""); + foreach (var header in headers) + { + sb.Append("") + .Append(XmlEscape(header)) + .AppendLine(""); + } + sb.AppendLine(""); + + foreach (var row in rows) + { + sb.AppendLine(""); + foreach (var cell in row) + AppendExcelCell(sb, cell); + sb.AppendLine(""); + } + + sb.AppendLine("
"); + sb.AppendLine("
"); + sb.AppendLine("
"); + + var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray(); + return Convert.ToBase64String(bytes); + } + + private static void AppendExcelCell(StringBuilder sb, object? value) + { + switch (value) + { + case null: + sb.AppendLine(@""); + break; + case string s: + sb.Append(@"") + .Append(XmlEscape(s)) + .AppendLine(""); + break; + case bool b: + sb.Append(@"") + .Append(b ? "1" : "0") + .AppendLine(""); + break; + case byte or sbyte or short or ushort or int or uint or long or ulong: + sb.Append(@"") + .Append(Convert.ToString(value, CultureInfo.InvariantCulture)) + .AppendLine(""); + break; + case float or double or decimal: + sb.Append(@"") + .Append(Convert.ToString(value, CultureInfo.InvariantCulture)) + .AppendLine(""); + break; + default: + sb.Append(@"") + .Append(XmlEscape(Convert.ToString(value, CultureInfo.InvariantCulture))) + .AppendLine(""); + break; + } + } + + private static string XmlEscape(string? value) => + WebUtility.HtmlEncode(value ?? string.Empty); } diff --git a/src/BackOffice/Pages/DiscountShop/DiscountOrdersMainPage.razor.cs b/src/BackOffice/Pages/DiscountShop/DiscountOrdersMainPage.razor.cs index 50db2e1..abe5e7b 100644 --- a/src/BackOffice/Pages/DiscountShop/DiscountOrdersMainPage.razor.cs +++ b/src/BackOffice/Pages/DiscountShop/DiscountOrdersMainPage.razor.cs @@ -245,31 +245,32 @@ public partial class DiscountOrdersMainPage try { - var sb = new StringBuilder(); - sb.AppendLine(CsvExportHelper.Row( - "شناسه", "نام مشتری", "موبایل", "مبلغ کل", "کیف پول اعتباری", "پرداخت درگاه", - "تعداد آیتم", "وضعیت پرداخت", "وضعیت ارسال", "تاریخ پرداخت", "تاریخ ثبت", "آدرس")); - - foreach (var o in _orders) + var headers = new[] { - sb.AppendLine(CsvExportHelper.Row( - o.Id, - CsvExportHelper.CustomerName(o.UserFullName, o.UserMobile, o.UserId), - o.UserMobile, - o.TotalPrice, - o.DiscountBalanceUsed, - o.GatewayAmount, - o.ItemsCount, - GetPaymentStatusText(o.PaymentStatusValue), - GetStatusText(o.Status), - o.PaymentDate?.MiladiToJalaliWithTime() ?? "-", - o.Created?.MiladiToJalaliWithTime() ?? "-", - o.ShippingAddress ?? "-")); - } + "شناسه", "نام مشتری", "موبایل", "مبلغ کل", "کیف پول اعتباری", "پرداخت درگاه", + "تعداد آیتم", "وضعیت پرداخت", "وضعیت ارسال", "تاریخ پرداخت", "تاریخ ثبت", "آدرس" + }; - var filename = $"discount-orders-{DateTime.Now:yyyyMMddHHmmss}.csv"; - await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, CsvExportHelper.ToBase64(sb)); - Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success); + var rows = _orders.Select(o => new object?[] + { + o.Id, + CsvExportHelper.CustomerName(o.UserFullName, o.UserMobile, o.UserId), + o.UserMobile ?? "", + o.TotalPrice, + o.DiscountBalanceUsed, + o.GatewayAmount, + o.ItemsCount, + GetPaymentStatusText(o.PaymentStatusValue), + GetStatusText(o.Status), + o.PaymentDate?.MiladiToJalaliWithTime() ?? "-", + o.Created?.MiladiToJalaliWithTime() ?? "-", + o.ShippingAddress ?? "-" + }); + + var filename = $"discount-orders-{DateTime.Now:yyyyMMddHHmmss}.xls"; + await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, + CsvExportHelper.ToExcelXmlBase64("سفارش‌های اعتباری", headers, rows)); + Snackbar.Add("خروجی Excel آماده شد.", Severity.Success); } catch (Exception ex) { diff --git a/src/BackOffice/Pages/DiscountShop/SalesReports.razor b/src/BackOffice/Pages/DiscountShop/SalesReports.razor index fc65969..5e05c9c 100644 --- a/src/BackOffice/Pages/DiscountShop/SalesReports.razor +++ b/src/BackOffice/Pages/DiscountShop/SalesReports.razor @@ -530,26 +530,31 @@ return; } - var sb = new StringBuilder(); - sb.AppendLine(CsvExportHelper.Row( - "شناسه", "نام مشتری", "موبایل", "تاریخ ثبت", "تعداد آیتم", "مبلغ نهایی", "تخفیف", "وضعیت")); - - foreach (var o in _orders.Where(o => o.Created.HasValue).OrderBy(o => o.Created)) + var headers = new[] { - sb.AppendLine(CsvExportHelper.Row( + "شناسه", "نام مشتری", "موبایل", "تاریخ ثبت", "تعداد آیتم", "مبلغ نهایی", "تخفیف", "وضعیت" + }; + + var rows = _orders + .OrderBy(o => o.Created ?? o.PaymentDate ?? DateTime.MaxValue) + .Select(o => new object?[] + { o.Id, CsvExportHelper.CustomerName(o.UserFullName, o.UserMobile, o.UserId), - o.UserMobile, - o.Created?.MiladiToJalaliWithTime() ?? "-", + o.UserMobile ?? "", + o.Created?.MiladiToJalaliWithTime() + ?? o.PaymentDate?.MiladiToJalaliWithTime() + ?? "-", o.ItemsCount, o.GatewayAmount, o.DiscountBalanceUsed, - GetStatusText(o.Status))); - } + GetStatusText(o.Status) + }); - var filename = $"discount-sales-{DateTime.Now:yyyyMMddHHmmss}.csv"; - await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, CsvExportHelper.ToBase64(sb)); - Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success); + var filename = $"discount-sales-{DateTime.Now:yyyyMMddHHmmss}.xls"; + await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, + CsvExportHelper.ToExcelXmlBase64("گزارش فروش اعتباری", headers, rows)); + Snackbar.Add("خروجی Excel آماده شد.", Severity.Success); } private async Task ExportToPdf() diff --git a/src/BackOffice/Pages/UserOrder/OrderSalesReports.razor.cs b/src/BackOffice/Pages/UserOrder/OrderSalesReports.razor.cs index 4fe7a70..504cfed 100644 --- a/src/BackOffice/Pages/UserOrder/OrderSalesReports.razor.cs +++ b/src/BackOffice/Pages/UserOrder/OrderSalesReports.razor.cs @@ -244,25 +244,26 @@ public partial class OrderSalesReports return; } - var sb = new StringBuilder(); - sb.AppendLine(CsvExportHelper.Row( - "شناسه", "کاربر", "نام مشتری", "مبلغ", "وضعیت پرداخت", "وضعیت ارسال", "تاریخ پرداخت")); - - foreach (var o in _orders) + var headers = new[] { - sb.AppendLine(CsvExportHelper.Row( - o.Id, - o.UserId, - CsvExportHelper.CustomerName(o.UserFullName, userId: o.UserId), - o.Amount, - o.PaymentStatus == PaymentStatus.Success ? "پرداخت شده" : "پرداخت نشده", - GetDeliveryStatusText(o.DeliveryStatus.GetHashCode()), - o.PaymentDate != null ? o.PaymentDate.ToDateTime().MiladiToJalaliWithTime() : "-")); - } + "شناسه", "کاربر", "نام مشتری", "مبلغ", "وضعیت پرداخت", "وضعیت ارسال", "تاریخ پرداخت" + }; - var filename = $"store-sales-{DateTime.Now:yyyyMMddHHmmss}.csv"; - await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, CsvExportHelper.ToBase64(sb)); - Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success); + var rows = _orders.Select(o => new object?[] + { + o.Id, + o.UserId, + CsvExportHelper.CustomerName(o.UserFullName, userId: o.UserId), + o.Amount, + o.PaymentStatus == PaymentStatus.Success ? "پرداخت شده" : "پرداخت نشده", + GetDeliveryStatusText(o.DeliveryStatus.GetHashCode()), + o.PaymentDate != null ? o.PaymentDate.ToDateTime().MiladiToJalaliWithTime() : "-" + }); + + var filename = $"store-sales-{DateTime.Now:yyyyMMddHHmmss}.xls"; + await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, + CsvExportHelper.ToExcelXmlBase64("گزارش فروش", headers, rows)); + Snackbar.Add("خروجی Excel آماده شد.", Severity.Success); } protected async Task ExportToPdf() diff --git a/src/BackOffice/Pages/UserOrder/UserOrderMainPage.razor.cs b/src/BackOffice/Pages/UserOrder/UserOrderMainPage.razor.cs index 04aaeb4..55b161c 100644 --- a/src/BackOffice/Pages/UserOrder/UserOrderMainPage.razor.cs +++ b/src/BackOffice/Pages/UserOrder/UserOrderMainPage.razor.cs @@ -329,26 +329,27 @@ public partial class UserOrderMainPage return; } - var sb = new StringBuilder(); - sb.AppendLine(CsvExportHelper.Row( - "شناسه", "کاربر", "نام مشتری", "مبلغ", "وضعیت پرداخت", "وضعیت ارسال", "روش پرداخت", "تاریخ پرداخت")); - - foreach (var o in result.Models) + var headers = new[] { - sb.AppendLine(CsvExportHelper.Row( - o.Id, - o.UserId, - CsvExportHelper.CustomerName(o.UserFullName, userId: o.UserId), - o.Amount, - GetPaymentStatusText(o.PaymentStatus.GetHashCode()), - GetDeliveryStatusText(o.DeliveryStatus.GetHashCode()), - GetPaymentMethodText(o.PaymentMethod.GetHashCode()), - o.PaymentDate?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-")); - } + "شناسه", "کاربر", "نام مشتری", "مبلغ", "وضعیت پرداخت", "وضعیت ارسال", "روش پرداخت", "تاریخ پرداخت" + }; - var filename = $"user-orders-{DateTime.Now:yyyyMMddHHmmss}.csv"; - await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, CsvExportHelper.ToBase64(sb)); - Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success); + var rows = result.Models.Select(o => new object?[] + { + o.Id, + o.UserId, + CsvExportHelper.CustomerName(o.UserFullName, userId: o.UserId), + o.Amount, + GetPaymentStatusText(o.PaymentStatus.GetHashCode()), + GetDeliveryStatusText(o.DeliveryStatus.GetHashCode()), + GetPaymentMethodText(o.PaymentMethod.GetHashCode()), + o.PaymentDate?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-" + }); + + var filename = $"user-orders-{DateTime.Now:yyyyMMddHHmmss}.xls"; + await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, + CsvExportHelper.ToExcelXmlBase64("سفارش‌ها", headers, rows)); + Snackbar.Add("خروجی Excel آماده شد.", Severity.Success); } catch (Exception ex) { From c3ee46ff6297f7743e62d19f5163fe744552ff36 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Sat, 8 Aug 2026 21:59:30 +0330 Subject: [PATCH 7/9] feat(club): update club reports and navigation with new customer package insights - Updated ReportLabels.cs to refine purchase method labels for clarity. - Enhanced ClubHub.razor by adding a new tab for customer package reports and adjusting tab indices. - Modified ClubPackagePurchasesReport.razor to reflect updated purchase method options. - Updated NavMenu.razor to include a link to the new customer packages report. - Bumped Foursat.CMSMicroservice.Protobuf version to 0.0.208 for compatibility with recent changes. --- src/BackOffice/BackOffice.csproj | 2 +- .../Common/Utilities/ReportLabels.cs | 5 +- src/BackOffice/Pages/Club/ClubHub.razor | 11 +- .../Reports/ClubPackagePurchasesReport.razor | 5 +- ...CustomerPackagePurchaseDetailsDialog.razor | 105 +++++++ .../CustomerPackagePurchasesReport.razor | 294 ++++++++++++++++++ src/BackOffice/Shared/NavMenu.razor | 5 + 7 files changed, 419 insertions(+), 8 deletions(-) create mode 100644 src/BackOffice/Pages/Club/Reports/Components/CustomerPackagePurchaseDetailsDialog.razor create mode 100644 src/BackOffice/Pages/Club/Reports/CustomerPackagePurchasesReport.razor diff --git a/src/BackOffice/BackOffice.csproj b/src/BackOffice/BackOffice.csproj index cc3aef2..605345f 100644 --- a/src/BackOffice/BackOffice.csproj +++ b/src/BackOffice/BackOffice.csproj @@ -143,7 +143,7 @@ - + diff --git a/src/BackOffice/Common/Utilities/ReportLabels.cs b/src/BackOffice/Common/Utilities/ReportLabels.cs index 912915d..cc967fa 100644 --- a/src/BackOffice/Common/Utilities/ReportLabels.cs +++ b/src/BackOffice/Common/Utilities/ReportLabels.cs @@ -42,8 +42,9 @@ public static class ReportLabels public static string GetPurchaseMethodLabel(int method) => method switch { - 1 => "وام دایا", - 2 => "خرید مستقیم", + 1 => "دایا", + 2 => "درگاه", + 3 => "دستی", _ => "نامشخص" }; diff --git a/src/BackOffice/Pages/Club/ClubHub.razor b/src/BackOffice/Pages/Club/ClubHub.razor index ef21fcd..6d0a626 100644 --- a/src/BackOffice/Pages/Club/ClubHub.razor +++ b/src/BackOffice/Pages/Club/ClubHub.razor @@ -1,6 +1,7 @@ @page "/club" @page "/club/members" @page "/club/purchases" +@page "/club/customer-packages" @page "/club/cycles" @page "/club/history" @page "/club/statistics" @@ -18,6 +19,9 @@ + + + @@ -39,9 +43,10 @@ _activeTab = uri switch { "club/purchases" => 1, - "club/cycles" => 2, - "club/history" => 3, - "club/statistics" => 4, + "club/customer-packages" => 2, + "club/cycles" => 3, + "club/history" => 4, + "club/statistics" => 5, _ => 0 }; } diff --git a/src/BackOffice/Pages/Club/Reports/ClubPackagePurchasesReport.razor b/src/BackOffice/Pages/Club/Reports/ClubPackagePurchasesReport.razor index a4e7a7e..3c49538 100644 --- a/src/BackOffice/Pages/Club/Reports/ClubPackagePurchasesReport.razor +++ b/src/BackOffice/Pages/Club/Reports/ClubPackagePurchasesReport.razor @@ -17,8 +17,9 @@ - وام دایا - خرید مستقیم + دایا + دستی + درگاه diff --git a/src/BackOffice/Pages/Club/Reports/Components/CustomerPackagePurchaseDetailsDialog.razor b/src/BackOffice/Pages/Club/Reports/Components/CustomerPackagePurchaseDetailsDialog.razor new file mode 100644 index 0000000..5da3165 --- /dev/null +++ b/src/BackOffice/Pages/Club/Reports/Components/CustomerPackagePurchaseDetailsDialog.razor @@ -0,0 +1,105 @@ +@using CMSMicroservice.Protobuf.Protos.UserPackagePurchase +@using Google.Protobuf.WellKnownTypes +@using DateTimeConverterCL + +@inject UserPackagePurchaseContract.UserPackagePurchaseContractClient PurchaseClient + + + + + جزئیات خرید پکیج — @UserName + + + + @if (_loading) + { + + } + else if (_items.Count == 0) + { + خریدی برای این کاربر یافت نشد. + } + else + { + + + شناسه + پکیج + روش + مبلغ (ریال) + تاریخ خرید + + + @context.Id + @context.PackageName + + + @ReportLabels.GetPurchaseMethodLabel(context.PurchaseMethod) + + + @context.Amount.ToString("N0") + + @(context.PurchasedAt != null + ? context.PurchasedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() + : "-") + + + + } + + + بستن + + + +@code { + [CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!; + [Parameter] public long UserId { get; set; } + [Parameter] public string UserName { get; set; } = string.Empty; + + private bool _loading = true; + private List _items = new(); + + protected override async Task OnInitializedAsync() + { + try + { + var result = await PurchaseClient.GetAllUserPackagePurchaseByFilterAsync( + new GetAllUserPackagePurchaseByFilterRequest + { + PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState + { + PageNumber = 1, + PageSize = 500 + }, + Filter = new GetAllUserPackagePurchaseByFilterFilter + { + UserId = UserId + } + }); + + _items = result?.Models? + .OrderBy(x => x.PurchasedAt?.ToDateTime() ?? DateTime.MinValue) + .ToList() ?? new(); + } + catch (Exception ex) + { + Snackbar.Add($"خطا در بارگذاری جزئیات: {ex.Message}", Severity.Error); + } + finally + { + _loading = false; + } + } + + private static Color GetMethodColor(int method) => method switch + { + 1 => Color.Info, // دایا + 2 => Color.Primary, // درگاه + 3 => Color.Secondary, // دستی + _ => Color.Default + }; + + private void Close() => MudDialog.Close(); +} diff --git a/src/BackOffice/Pages/Club/Reports/CustomerPackagePurchasesReport.razor b/src/BackOffice/Pages/Club/Reports/CustomerPackagePurchasesReport.razor new file mode 100644 index 0000000..66ed35e --- /dev/null +++ b/src/BackOffice/Pages/Club/Reports/CustomerPackagePurchasesReport.razor @@ -0,0 +1,294 @@ +@using System.Text +@using BackOffice.Common.BaseComponents +@using BackOffice.Common.Utilities +@using BackOffice.Pages.AutoComplete +@using BackOffice.Pages.Club.Reports.Components +@using CMSMicroservice.Protobuf.Protos.UserPackagePurchase +@using Google.Protobuf.WellKnownTypes +@using DateTimeConverterCL +@using Microsoft.JSInterop + +@inject UserPackagePurchaseContract.UserPackagePurchaseContractClient PurchaseClient +@inject IDialogService DialogService +@inject IJSRuntime JsRuntime + + + گزارش تجمیعی مشتریان — اولین/آخرین پکیج، تفکیک دایا / دستی / درگاه. خروجی Excel مطابق فیلتر فعال است. + + + + + + + + + + دایا + دستی + درگاه + + + + + + + + + + خروجی Excel + + + + + + + + + @context.Item.UserName + @context.Item.UserMobile + + + + + + + @context.Item.FirstPackageName + @context.Item.FirstAmount.ToString("N0") ریال + + @(context.Item.FirstPurchasedAt?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-") + + + + + + + + @context.Item.LastPackageName + @context.Item.LastAmount.ToString("N0") ریال + + @(context.Item.LastPurchasedAt?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-") + + + @ReportLabels.GetPurchaseMethodLabel(context.Item.LastPurchaseMethod) + + + + + + + + @context.Item.PurchaseCount خرید + @context.Item.TotalAmount.ToString("N0") ریال + + + + + + @context.Item.DayaCount عدد + @context.Item.DayaAmount.ToString("N0") + + + + + @context.Item.ManualCount عدد + @context.Item.ManualAmount.ToString("N0") + + + + + @context.Item.GatewayCount عدد + @context.Item.GatewayAmount.ToString("N0") + + + + + + جزئیات + + + + + + موردی یافت نشد. + + + + + + + + +@code { + private BasePageComponent? _basePage; + private MudDataGrid? _grid; + private long? _userId; + private int? _purchaseMethod; + private DateTime? _fromDate, _toDate; + private bool _exporting; + + private GetCustomerPackagePurchaseRollupFilter BuildFilter() + { + var filter = new GetCustomerPackagePurchaseRollupFilter(); + if (_userId is > 0) + filter.UserId = _userId; + if (_purchaseMethod.HasValue) + filter.PurchaseMethod = _purchaseMethod.Value; + if (_fromDate.HasValue) + filter.PurchasedFrom = Timestamp.FromDateTime(DateTime.SpecifyKind(_fromDate.Value.Date, DateTimeKind.Utc)); + if (_toDate.HasValue) + filter.PurchasedTo = Timestamp.FromDateTime(DateTime.SpecifyKind(_toDate.Value.Date.AddDays(1).AddTicks(-1), DateTimeKind.Utc)); + return filter; + } + + private async Task> LoadData(GridState state) + { + try + { + var result = await PurchaseClient.GetCustomerPackagePurchaseRollupAsync( + new GetCustomerPackagePurchaseRollupRequest + { + PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState + { + PageNumber = state.Page + 1, + PageSize = state.PageSize + }, + Filter = BuildFilter() + }); + + return new GridData + { + Items = result?.Models?.ToList() ?? new(), + TotalItems = (int)(result?.MetaData?.TotalCount ?? 0) + }; + } + catch (Exception ex) + { + Snackbar.Add($"خطا: {ex.Message}", Severity.Error); + return new GridData(); + } + } + + private async Task OnFilterSubmit() + { + if (_grid != null) await _grid.ReloadServerData(); + } + + private async Task OnFilterCleared() + { + _userId = null; + _purchaseMethod = null; + _fromDate = _toDate = null; + if (_grid != null) await _grid.ReloadServerData(); + } + + private async Task OpenDetails(CustomerPackagePurchaseRollupModel row) + { + var parameters = new DialogParameters + { + { nameof(CustomerPackagePurchaseDetailsDialog.UserId), row.UserId }, + { nameof(CustomerPackagePurchaseDetailsDialog.UserName), row.UserName } + }; + var options = new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true, CloseButton = true }; + await DialogService.ShowAsync("جزئیات خرید پکیج", parameters, options); + } + + private async Task ExportToExcel() + { + _exporting = true; + try + { + var filter = BuildFilter(); + var all = new List(); + const int pageSize = 500; + var page = 1; + long totalCount; + + do + { + var result = await PurchaseClient.GetCustomerPackagePurchaseRollupAsync( + new GetCustomerPackagePurchaseRollupRequest + { + PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState + { + PageNumber = page, + PageSize = pageSize + }, + Filter = filter + }); + + var batch = result?.Models?.ToList() ?? new(); + all.AddRange(batch); + totalCount = result?.MetaData?.TotalCount ?? 0; + page++; + } while (all.Count < totalCount && page <= 50); + + if (all.Count == 0) + { + Snackbar.Add("داده‌ای برای خروجی با فیلتر فعلی وجود ندارد.", Severity.Info); + return; + } + + var headers = new[] + { + "شناسه کاربر", "نام مشتری", "موبایل", + "اولین پکیج", "مبلغ اولین", "تاریخ اولین", + "آخرین پکیج", "مبلغ آخرین", "تاریخ آخرین", "روش آخرین", + "تعداد خرید", "جمع مبلغ", + "تعداد دایا", "مبلغ دایا", + "تعداد دستی", "مبلغ دستی", + "تعداد درگاه", "مبلغ درگاه" + }; + + var rows = all.Select(o => new object?[] + { + o.UserId, + o.UserName, + o.UserMobile, + o.FirstPackageName, + o.FirstAmount, + o.FirstPurchasedAt?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-", + o.LastPackageName, + o.LastAmount, + o.LastPurchasedAt?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-", + ReportLabels.GetPurchaseMethodLabel(o.LastPurchaseMethod), + o.PurchaseCount, + o.TotalAmount, + o.DayaCount, + o.DayaAmount, + o.ManualCount, + o.ManualAmount, + o.GatewayCount, + o.GatewayAmount + }); + + var filename = $"customer-packages-{DateTime.Now:yyyyMMddHHmmss}.xls"; + await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, + CsvExportHelper.ToExcelXmlBase64("مشتریان پکیج", headers, rows)); + Snackbar.Add($"خروجی Excel آماده شد ({all.Count} مشتری).", Severity.Success); + } + catch (Exception ex) + { + Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error); + } + finally + { + _exporting = false; + } + } + + private static Color GetMethodColor(int method) => method switch + { + 1 => Color.Info, + 2 => Color.Primary, + 3 => Color.Secondary, + _ => Color.Default + }; +} diff --git a/src/BackOffice/Shared/NavMenu.razor b/src/BackOffice/Shared/NavMenu.razor index e228e37..c2c9776 100644 --- a/src/BackOffice/Shared/NavMenu.razor +++ b/src/BackOffice/Shared/NavMenu.razor @@ -77,6 +77,11 @@ Icon="@Icons.Material.Filled.Groups"> اعضا و گزارش‌ها + + گزارش مشتریان پکیج + From 24ae4ec15c10b7c6b3925ac38ea443403a6cd1d4 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Sun, 9 Aug 2026 00:02:54 +0330 Subject: [PATCH 8/9] feat(date-range-picker): enhance date range picker functionality with external parameters and notifications - Added support for external From and To parameters to synchronize date range selections. - Implemented OnParametersSet to handle updates from parent components without triggering unnecessary updates. - Introduced NotifyRangeChangedAsync to streamline notifications for date range changes. - Refactored date range application logic to improve clarity and maintainability. --- .../BaseComponents/DateRangePicker.razor.cs | 65 +++++++++++++++---- 1 file changed, 53 insertions(+), 12 deletions(-) diff --git a/src/BackOffice/Common/BaseComponents/DateRangePicker.razor.cs b/src/BackOffice/Common/BaseComponents/DateRangePicker.razor.cs index 39cd928..d7d31dc 100644 --- a/src/BackOffice/Common/BaseComponents/DateRangePicker.razor.cs +++ b/src/BackOffice/Common/BaseComponents/DateRangePicker.razor.cs @@ -8,21 +8,46 @@ namespace BackOffice.Common.BaseComponents; public partial class DateRangePicker { private DateRange _dateRange = new(); + private MudDateRangePicker _picker = default!; + private DateTime? _lastSyncedFrom; + private DateTime? _lastSyncedTo; - private MudDateRangePicker _picker; [Parameter] public string Label { get; set; } = "انتخاب بازه زمانی"; [Parameter] public DateTime? DefaultStart { get; set; } [Parameter] public DateTime? DefaultEnd { get; set; } [Parameter] public EventCallback OnChanged { get; set; } + + [Parameter] public DateTime? From { get; set; } + [Parameter] public EventCallback FromChanged { get; set; } + [Parameter] public DateTime? To { get; set; } + [Parameter] public EventCallback ToChanged { get; set; } + protected override void OnInitialized() { base.OnInitialized(); - if (DefaultStart.HasValue) - _dateRange.Start = DefaultStart.Value.Date; - - if (DefaultEnd.HasValue) - _dateRange.End = DefaultEnd.Value.Date; + var start = From ?? DefaultStart; + var end = To ?? DefaultEnd; + ApplyExternalRange(start, end); + _lastSyncedFrom = From; + _lastSyncedTo = To; } + + protected override void OnParametersSet() + { + // فقط وقتی والد From/To را عوض کرده (مثلاً پاک کردن فیلتر)، نه هنگام انتخاب داخل پیکر + if (From == _lastSyncedFrom && To == _lastSyncedTo) + return; + + _lastSyncedFrom = From; + _lastSyncedTo = To; + ApplyExternalRange(From, To); + } + + private void ApplyExternalRange(DateTime? start, DateTime? end) + { + _dateRange = new DateRange(start?.Date, end?.Date); + } + public CultureInfo GetPersianCulture() { var culture = new CultureInfo("fa-IR"); @@ -53,19 +78,35 @@ public partial class DateRangePicker culture.NumberFormat.NumberNegativePattern = 0; return culture; } + private async Task OnClickOK() { - if (_picker.DateRange is not null) - { - await OnChanged.InvokeAsync(_picker.DateRange); - } - + var range = _picker.DateRange ?? _dateRange; + _dateRange = range ?? new DateRange(); + await NotifyRangeChangedAsync(_dateRange); await _picker.CloseAsync(); } + private async Task OnClickClear() { _dateRange = new DateRange(); - await OnChanged.InvokeAsync(_dateRange); + await NotifyRangeChangedAsync(_dateRange); await _picker.CloseAsync(); } + + private async Task NotifyRangeChangedAsync(DateRange range) + { + var start = range.Start?.Date; + var end = range.End?.Date; + + _lastSyncedFrom = start; + _lastSyncedTo = end; + + if (From != start) + await FromChanged.InvokeAsync(start); + if (To != end) + await ToChanged.InvokeAsync(end); + + await OnChanged.InvokeAsync(range); + } } From aa97d25302ee1d516c2c70f4c53bffee31975d79 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Sun, 9 Aug 2026 00:40:36 +0330 Subject: [PATCH 9/9] feat(report): optimize customer package purchases report by removing pagination and enhancing filter application - Simplified the data retrieval process by eliminating pagination, allowing for all matching records to be fetched in a single request. - Improved filter application logic to streamline the report generation process. - Updated the handling of the result set to ensure compatibility with the new data retrieval method. --- .../CustomerPackagePurchasesReport.razor | 34 ++++++------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/src/BackOffice/Pages/Club/Reports/CustomerPackagePurchasesReport.razor b/src/BackOffice/Pages/Club/Reports/CustomerPackagePurchasesReport.razor index 66ed35e..f77910b 100644 --- a/src/BackOffice/Pages/Club/Reports/CustomerPackagePurchasesReport.razor +++ b/src/BackOffice/Pages/Club/Reports/CustomerPackagePurchasesReport.razor @@ -205,31 +205,19 @@ _exporting = true; try { - var filter = BuildFilter(); - var all = new List(); - const int pageSize = 500; - var page = 1; - long totalCount; - - do - { - var result = await PurchaseClient.GetCustomerPackagePurchaseRollupAsync( - new GetCustomerPackagePurchaseRollupRequest + // PageSize=0 → همهٔ ردیف‌های مطابق فیلتر، مستقل از صفحه‌بندی گرید + var result = await PurchaseClient.GetCustomerPackagePurchaseRollupAsync( + new GetCustomerPackagePurchaseRollupRequest + { + PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { - PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState - { - PageNumber = page, - PageSize = pageSize - }, - Filter = filter - }); - - var batch = result?.Models?.ToList() ?? new(); - all.AddRange(batch); - totalCount = result?.MetaData?.TotalCount ?? 0; - page++; - } while (all.Count < totalCount && page <= 50); + PageNumber = 1, + PageSize = 0 + }, + Filter = BuildFilter() + }); + var all = result?.Models?.ToList() ?? new(); if (all.Count == 0) { Snackbar.Add("داده‌ای برای خروجی با فیلتر فعلی وجود ندارد.", Severity.Info);