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.
This commit is contained in:
masoodafar-web
2026-08-08 21:43:54 +03:30
parent 6a9d142dc6
commit e9f8328404
6 changed files with 179 additions and 82 deletions
+1
View File
@@ -1,3 +1,4 @@
# Docs moved to totalDoc # Docs moved to totalDoc
See [totalDoc/INDEX.md](../../totalDoc/INDEX.md) for all documentation. See [totalDoc/INDEX.md](../../totalDoc/INDEX.md) for all documentation.
@@ -1,31 +1,35 @@
using System.Globalization; using System.Globalization;
using System.Net;
using System.Text; using System.Text;
namespace BackOffice.Common.Utilities; namespace BackOffice.Common.Utilities;
/// <summary> /// <summary>
/// خروجی CSV سازگار با Excel (UTF-8 BOM + sep=, + اعداد بدون جداکننده هزارگان). /// خروجی سازگار با Excel فارسی:
/// - XML Spreadsheet (.xls) برای ستون‌بندی مطمئن + متن فارسی
/// - CSV با جداکننده ; به‌عنوان جایگزین سبک
/// </summary> /// </summary>
public static class CsvExportHelper public static class CsvExportHelper
{ {
public const char Separator = ','; /// <summary>جداکننده پیش‌فرض ویندوز فارسی برای CSV.</summary>
public const char Separator = ';';
public static string Escape(string? value) public static string Escape(string? value)
{ {
if (string.IsNullOrEmpty(value)) if (string.IsNullOrEmpty(value))
return string.Empty; return string.Empty;
if (value.Contains(Separator) || value.Contains('"') || value.Contains('\n') || value.Contains('\r')) // همیشه quote تا Excel فارسی ستون را نشکند
return $"\"{value.Replace("\"", "\"\"")}\""; return $"\"{value.Replace("\"", "\"\"")}\"";
return value;
} }
public static string Cell(object? value) => value switch public static string Cell(object? value) => value switch
{ {
null => string.Empty, null => Escape(string.Empty),
string s => Escape(s), string s => Escape(s),
bool b => b ? "1" : "0", 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), IFormattable f => Escape(f.ToString(null, CultureInfo.InvariantCulture) ?? string.Empty),
_ => Escape(Convert.ToString(value, CultureInfo.InvariantCulture)) _ => Escape(Convert.ToString(value, CultureInfo.InvariantCulture))
}; };
@@ -47,11 +51,95 @@ public static class CsvExportHelper
return "—"; return "—";
} }
public static string ToBase64(StringBuilder bodyWithoutSepHint) public static string ToBase64(StringBuilder body)
{ {
// Excel در لوکال فارسی با این خط، جداکننده کاما را درست تشخیص می‌دهد // CRLF صریح — Excel روی ویندوز با \n لینوکس گاهی به‌هم می‌ریزد
var content = "sep=," + Environment.NewLine + bodyWithoutSepHint; var text = body.ToString().Replace("\r\n", "\n").Replace("\r", "\n").Replace("\n", "\r\n");
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(content)).ToArray(); var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(text)).ToArray();
return Convert.ToBase64String(bytes); return Convert.ToBase64String(bytes);
} }
/// <summary>
/// خروجی SpreadsheetML که Excel به‌صورت ستون‌ستون و با UTF-8 درست باز می‌کند.
/// فایل را با پسوند .xls ذخیره کنید.
/// </summary>
public static string ToExcelXmlBase64(string sheetName, IReadOnlyList<string> headers, IEnumerable<object?[]> rows)
{
var sb = new StringBuilder();
sb.AppendLine(@"<?xml version=""1.0"" encoding=""UTF-8""?>");
sb.AppendLine(@"<?mso-application progid=""Excel.Sheet""?>");
sb.AppendLine(@"<Workbook xmlns=""urn:schemas-microsoft-com:office:spreadsheet""");
sb.AppendLine(@" xmlns:o=""urn:schemas-microsoft-com:office:office""");
sb.AppendLine(@" xmlns:x=""urn:schemas-microsoft-com:office:excel""");
sb.AppendLine(@" xmlns:ss=""urn:schemas-microsoft-com:office:spreadsheet""");
sb.AppendLine(@" xmlns:html=""http://www.w3.org/TR/REC-html40"">");
sb.AppendLine(@"<Styles>");
sb.AppendLine(@"<Style ss:ID=""Header""><Font ss:Bold=""1""/></Style>");
sb.AppendLine(@"<Style ss:ID=""Text""><NumberFormat ss:Format=""@""/></Style>");
sb.AppendLine(@"</Styles>");
sb.Append("<Worksheet ss:Name=\"").Append(XmlEscape(sheetName)).AppendLine("\">");
sb.AppendLine("<Table>");
sb.AppendLine("<Row>");
foreach (var header in headers)
{
sb.Append("<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">")
.Append(XmlEscape(header))
.AppendLine("</Data></Cell>");
}
sb.AppendLine("</Row>");
foreach (var row in rows)
{
sb.AppendLine("<Row>");
foreach (var cell in row)
AppendExcelCell(sb, cell);
sb.AppendLine("</Row>");
}
sb.AppendLine("</Table>");
sb.AppendLine("</Worksheet>");
sb.AppendLine("</Workbook>");
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(@"<Cell ss:StyleID=""Text""><Data ss:Type=""String""></Data></Cell>");
break;
case string s:
sb.Append(@"<Cell ss:StyleID=""Text""><Data ss:Type=""String"">")
.Append(XmlEscape(s))
.AppendLine("</Data></Cell>");
break;
case bool b:
sb.Append(@"<Cell><Data ss:Type=""Boolean"">")
.Append(b ? "1" : "0")
.AppendLine("</Data></Cell>");
break;
case byte or sbyte or short or ushort or int or uint or long or ulong:
sb.Append(@"<Cell><Data ss:Type=""Number"">")
.Append(Convert.ToString(value, CultureInfo.InvariantCulture))
.AppendLine("</Data></Cell>");
break;
case float or double or decimal:
sb.Append(@"<Cell><Data ss:Type=""Number"">")
.Append(Convert.ToString(value, CultureInfo.InvariantCulture))
.AppendLine("</Data></Cell>");
break;
default:
sb.Append(@"<Cell ss:StyleID=""Text""><Data ss:Type=""String"">")
.Append(XmlEscape(Convert.ToString(value, CultureInfo.InvariantCulture)))
.AppendLine("</Data></Cell>");
break;
}
}
private static string XmlEscape(string? value) =>
WebUtility.HtmlEncode(value ?? string.Empty);
} }
@@ -245,17 +245,17 @@ public partial class DiscountOrdersMainPage
try try
{ {
var sb = new StringBuilder(); var headers = new[]
sb.AppendLine(CsvExportHelper.Row( {
"شناسه", "نام مشتری", "موبایل", "مبلغ کل", "کیف پول اعتباری", "پرداخت درگاه", "شناسه", "نام مشتری", "موبایل", "مبلغ کل", "کیف پول اعتباری", "پرداخت درگاه",
"تعداد آیتم", "وضعیت پرداخت", "وضعیت ارسال", "تاریخ پرداخت", "تاریخ ثبت", "آدرس")); "تعداد آیتم", "وضعیت پرداخت", "وضعیت ارسال", "تاریخ پرداخت", "تاریخ ثبت", "آدرس"
};
foreach (var o in _orders)
var rows = _orders.Select(o => new object?[]
{ {
sb.AppendLine(CsvExportHelper.Row(
o.Id, o.Id,
CsvExportHelper.CustomerName(o.UserFullName, o.UserMobile, o.UserId), CsvExportHelper.CustomerName(o.UserFullName, o.UserMobile, o.UserId),
o.UserMobile, o.UserMobile ?? "",
o.TotalPrice, o.TotalPrice,
o.DiscountBalanceUsed, o.DiscountBalanceUsed,
o.GatewayAmount, o.GatewayAmount,
@@ -264,12 +264,13 @@ public partial class DiscountOrdersMainPage
GetStatusText(o.Status), GetStatusText(o.Status),
o.PaymentDate?.MiladiToJalaliWithTime() ?? "-", o.PaymentDate?.MiladiToJalaliWithTime() ?? "-",
o.Created?.MiladiToJalaliWithTime() ?? "-", o.Created?.MiladiToJalaliWithTime() ?? "-",
o.ShippingAddress ?? "-")); o.ShippingAddress ?? "-"
} });
var filename = $"discount-orders-{DateTime.Now:yyyyMMddHHmmss}.csv"; var filename = $"discount-orders-{DateTime.Now:yyyyMMddHHmmss}.xls";
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, CsvExportHelper.ToBase64(sb)); await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename,
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success); CsvExportHelper.ToExcelXmlBase64("سفارش‌های اعتباری", headers, rows));
Snackbar.Add("خروجی Excel آماده شد.", Severity.Success);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -530,26 +530,31 @@
return; return;
} }
var sb = new StringBuilder(); var headers = new[]
sb.AppendLine(CsvExportHelper.Row( {
"شناسه", "نام مشتری", "موبایل", "تاریخ ثبت", "تعداد آیتم", "مبلغ نهایی", "تخفیف", "وضعیت")); "شناسه", "نام مشتری", "موبایل", "تاریخ ثبت", "تعداد آیتم", "مبلغ نهایی", "تخفیف", "وضعیت"
};
foreach (var o in _orders.Where(o => o.Created.HasValue).OrderBy(o => o.Created))
var rows = _orders
.OrderBy(o => o.Created ?? o.PaymentDate ?? DateTime.MaxValue)
.Select(o => new object?[]
{ {
sb.AppendLine(CsvExportHelper.Row(
o.Id, o.Id,
CsvExportHelper.CustomerName(o.UserFullName, o.UserMobile, o.UserId), CsvExportHelper.CustomerName(o.UserFullName, o.UserMobile, o.UserId),
o.UserMobile, o.UserMobile ?? "",
o.Created?.MiladiToJalaliWithTime() ?? "-", o.Created?.MiladiToJalaliWithTime()
?? o.PaymentDate?.MiladiToJalaliWithTime()
?? "-",
o.ItemsCount, o.ItemsCount,
o.GatewayAmount, o.GatewayAmount,
o.DiscountBalanceUsed, o.DiscountBalanceUsed,
GetStatusText(o.Status))); GetStatusText(o.Status)
} });
var filename = $"discount-sales-{DateTime.Now:yyyyMMddHHmmss}.csv"; var filename = $"discount-sales-{DateTime.Now:yyyyMMddHHmmss}.xls";
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, CsvExportHelper.ToBase64(sb)); await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename,
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success); CsvExportHelper.ToExcelXmlBase64("گزارش فروش اعتباری", headers, rows));
Snackbar.Add("خروجی Excel آماده شد.", Severity.Success);
} }
private async Task ExportToPdf() private async Task ExportToPdf()
@@ -244,25 +244,26 @@ public partial class OrderSalesReports
return; return;
} }
var sb = new StringBuilder(); var headers = new[]
sb.AppendLine(CsvExportHelper.Row( {
"شناسه", "کاربر", "نام مشتری", "مبلغ", "وضعیت پرداخت", "وضعیت ارسال", "تاریخ پرداخت")); "شناسه", "کاربر", "نام مشتری", "مبلغ", "وضعیت پرداخت", "وضعیت ارسال", "تاریخ پرداخت"
};
foreach (var o in _orders)
var rows = _orders.Select(o => new object?[]
{ {
sb.AppendLine(CsvExportHelper.Row(
o.Id, o.Id,
o.UserId, o.UserId,
CsvExportHelper.CustomerName(o.UserFullName, userId: o.UserId), CsvExportHelper.CustomerName(o.UserFullName, userId: o.UserId),
o.Amount, o.Amount,
o.PaymentStatus == PaymentStatus.Success ? "پرداخت شده" : "پرداخت نشده", o.PaymentStatus == PaymentStatus.Success ? "پرداخت شده" : "پرداخت نشده",
GetDeliveryStatusText(o.DeliveryStatus.GetHashCode()), GetDeliveryStatusText(o.DeliveryStatus.GetHashCode()),
o.PaymentDate != null ? o.PaymentDate.ToDateTime().MiladiToJalaliWithTime() : "-")); o.PaymentDate != null ? o.PaymentDate.ToDateTime().MiladiToJalaliWithTime() : "-"
} });
var filename = $"store-sales-{DateTime.Now:yyyyMMddHHmmss}.csv"; var filename = $"store-sales-{DateTime.Now:yyyyMMddHHmmss}.xls";
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, CsvExportHelper.ToBase64(sb)); await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename,
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success); CsvExportHelper.ToExcelXmlBase64("گزارش فروش", headers, rows));
Snackbar.Add("خروجی Excel آماده شد.", Severity.Success);
} }
protected async Task ExportToPdf() protected async Task ExportToPdf()
@@ -329,13 +329,13 @@ public partial class UserOrderMainPage
return; return;
} }
var sb = new StringBuilder(); var headers = new[]
sb.AppendLine(CsvExportHelper.Row( {
"شناسه", "کاربر", "نام مشتری", "مبلغ", "وضعیت پرداخت", "وضعیت ارسال", "روش پرداخت", "تاریخ پرداخت")); "شناسه", "کاربر", "نام مشتری", "مبلغ", "وضعیت پرداخت", "وضعیت ارسال", "روش پرداخت", "تاریخ پرداخت"
};
foreach (var o in result.Models)
var rows = result.Models.Select(o => new object?[]
{ {
sb.AppendLine(CsvExportHelper.Row(
o.Id, o.Id,
o.UserId, o.UserId,
CsvExportHelper.CustomerName(o.UserFullName, userId: o.UserId), CsvExportHelper.CustomerName(o.UserFullName, userId: o.UserId),
@@ -343,12 +343,13 @@ public partial class UserOrderMainPage
GetPaymentStatusText(o.PaymentStatus.GetHashCode()), GetPaymentStatusText(o.PaymentStatus.GetHashCode()),
GetDeliveryStatusText(o.DeliveryStatus.GetHashCode()), GetDeliveryStatusText(o.DeliveryStatus.GetHashCode()),
GetPaymentMethodText(o.PaymentMethod.GetHashCode()), GetPaymentMethodText(o.PaymentMethod.GetHashCode()),
o.PaymentDate?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-")); o.PaymentDate?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-"
} });
var filename = $"user-orders-{DateTime.Now:yyyyMMddHHmmss}.csv"; var filename = $"user-orders-{DateTime.Now:yyyyMMddHHmmss}.xls";
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, CsvExportHelper.ToBase64(sb)); await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename,
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success); CsvExportHelper.ToExcelXmlBase64("سفارش‌ها", headers, rows));
Snackbar.Add("خروجی Excel آماده شد.", Severity.Success);
} }
catch (Exception ex) catch (Exception ex)
{ {