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)
{