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:
@@ -1,31 +1,35 @@
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
|
||||
namespace BackOffice.Common.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// خروجی CSV سازگار با Excel (UTF-8 BOM + sep=, + اعداد بدون جداکننده هزارگان).
|
||||
/// خروجی سازگار با Excel فارسی:
|
||||
/// - XML Spreadsheet (.xls) برای ستونبندی مطمئن + متن فارسی
|
||||
/// - CSV با جداکننده ; بهعنوان جایگزین سبک
|
||||
/// </summary>
|
||||
public static class CsvExportHelper
|
||||
{
|
||||
public const char Separator = ',';
|
||||
/// <summary>جداکننده پیشفرض ویندوز فارسی برای CSV.</summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <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,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)
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user