From e9f83284046ae8784a5daeb07d3ed2839c6bb1d0 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Sat, 8 Aug 2026 21:43:54 +0330 Subject: [PATCH] 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) {