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; - } }