Compare commits

..

10 Commits

Author SHA1 Message Date
masoodafar-web 3eff5b6dfa Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 10m5s
2026-08-09 02:33:49 +03:30
masoodafar-web aa97d25302 feat(report): optimize customer package purchases report by removing pagination and enhancing filter application
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m55s
- Simplified the data retrieval process by eliminating pagination, allowing for all matching records to be fetched in a single request.
- Improved filter application logic to streamline the report generation process.
- Updated the handling of the result set to ensure compatibility with the new data retrieval method.
2026-08-09 00:40:36 +03:30
masoodafar-web 24ae4ec15c feat(date-range-picker): enhance date range picker functionality with external parameters and notifications
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 7m20s
- Added support for external From and To parameters to synchronize date range selections.
- Implemented OnParametersSet to handle updates from parent components without triggering unnecessary updates.
- Introduced NotifyRangeChangedAsync to streamline notifications for date range changes.
- Refactored date range application logic to improve clarity and maintainability.
2026-08-09 00:02:54 +03:30
masoodafar-web c3ee46ff62 feat(club): update club reports and navigation with new customer package insights
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m14s
- Updated ReportLabels.cs to refine purchase method labels for clarity.
- Enhanced ClubHub.razor by adding a new tab for customer package reports and adjusting tab indices.
- Modified ClubPackagePurchasesReport.razor to reflect updated purchase method options.
- Updated NavMenu.razor to include a link to the new customer packages report.
- Bumped Foursat.CMSMicroservice.Protobuf version to 0.0.208 for compatibility with recent changes.
2026-08-08 21:59:30 +03:30
masoodafar-web e9f8328404 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.
2026-08-08 21:43:54 +03:30
masoodafar-web 6a9d142dc6 refactor(csv-export): streamline CSV export logic and remove redundant methods
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m36s
- Replaced manual CSV string construction with CsvExportHelper for better readability and maintainability across DiscountOrdersMainPage, SalesReports, OrderSalesReports, and UserOrderMainPage.
- Removed obsolete EscapeCsv methods to simplify the codebase.
- Enhanced CSV export functionality to utilize a unified base64 conversion method for file saving.
2026-08-07 18:40:00 +03:30
masoodafar-web 1d78633e17 feat(order-status): add "Ready for Office Pickup" status and update related components
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m50s
- Introduced a new order status "Ready for Office Pickup" across various components including DiscountOrdersMainPage, SalesReports, and UserOrder pages.
- Updated the color and text representations for the new status in relevant dialogs and services.
- Enhanced the DiscountOrderService to handle the new status in delivery processing logic.
- Bumped Foursat.CMSMicroservice.Protobuf version to 0.0.207 for compatibility with the new order status features.
2026-08-07 17:35:19 +03:30
masoodafar-web cbc61ad0f5 feat(order-details): add mobile display logic and remove unused status methods
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m36s
- Introduced a new property to display the user's mobile number or fallback to the address phone number in OrderDetailsDialog.
- Removed obsolete methods for determining order status color and text, streamlining the component's code.
2026-08-07 17:10:30 +03:30
masoodafar-web 68d0aed468 feat(invoice): add print functionality for order invoices and enhance order details
- Implemented a print button in OrderDetailsDialog and UserOrderDetailsDialog for generating invoices.
- Updated DiscountOrdersMainPage to populate order details from the list.
- Enhanced DiscountOrderDetailsDto to include UserFullName, UserMobile, and VatAmount for invoice generation.
- Integrated JavaScript functionality to convert HTML invoices to PDF format.
2026-08-07 17:08:23 +03:30
masoodafar-web a9aa03d6a8 feat(payment): enhance order details with payment status indicators and update proto version
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 9m37s
- Updated OrderDetailsDialog.razor to display payment status using dynamic color and icon based on the payment status value.
- Added helper methods in OrderDetailsDialog.razor.cs to determine payment status color, text, and icon.
- Enhanced UserOrderDetailsDialog.razor to conditionally display postal code and user mobile information.
- Updated DiscountOrderService to include PaymentStatusValue from the response.
- Bumped Foursat.CMSMicroservice.Protobuf version to 0.0.206 for compatibility with new payment features.
2026-08-07 15:52:55 +03:30
27 changed files with 1292 additions and 399 deletions
+1
View File
@@ -1,3 +1,4 @@
# Docs moved to totalDoc
See [totalDoc/INDEX.md](../../totalDoc/INDEX.md) for all documentation.
+1 -1
View File
@@ -143,7 +143,7 @@
<ProjectReference Include="../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj" />
</ItemGroup>
<ItemGroup Condition="!Exists('../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj')">
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.205" />
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.208" />
</ItemGroup>
<!-- ============================================================ -->
@@ -8,21 +8,46 @@ namespace BackOffice.Common.BaseComponents;
public partial class DateRangePicker
{
private DateRange _dateRange = new();
private MudDateRangePicker _picker = default!;
private DateTime? _lastSyncedFrom;
private DateTime? _lastSyncedTo;
private MudDateRangePicker _picker;
[Parameter] public string Label { get; set; } = "انتخاب بازه زمانی";
[Parameter] public DateTime? DefaultStart { get; set; }
[Parameter] public DateTime? DefaultEnd { get; set; }
[Parameter] public EventCallback<DateRange> OnChanged { get; set; }
[Parameter] public DateTime? From { get; set; }
[Parameter] public EventCallback<DateTime?> FromChanged { get; set; }
[Parameter] public DateTime? To { get; set; }
[Parameter] public EventCallback<DateTime?> ToChanged { get; set; }
protected override void OnInitialized()
{
base.OnInitialized();
if (DefaultStart.HasValue)
_dateRange.Start = DefaultStart.Value.Date;
if (DefaultEnd.HasValue)
_dateRange.End = DefaultEnd.Value.Date;
var start = From ?? DefaultStart;
var end = To ?? DefaultEnd;
ApplyExternalRange(start, end);
_lastSyncedFrom = From;
_lastSyncedTo = To;
}
protected override void OnParametersSet()
{
// فقط وقتی والد From/To را عوض کرده (مثلاً پاک کردن فیلتر)، نه هنگام انتخاب داخل پیکر
if (From == _lastSyncedFrom && To == _lastSyncedTo)
return;
_lastSyncedFrom = From;
_lastSyncedTo = To;
ApplyExternalRange(From, To);
}
private void ApplyExternalRange(DateTime? start, DateTime? end)
{
_dateRange = new DateRange(start?.Date, end?.Date);
}
public CultureInfo GetPersianCulture()
{
var culture = new CultureInfo("fa-IR");
@@ -53,19 +78,35 @@ public partial class DateRangePicker
culture.NumberFormat.NumberNegativePattern = 0;
return culture;
}
private async Task OnClickOK()
{
if (_picker.DateRange is not null)
{
await OnChanged.InvokeAsync(_picker.DateRange);
}
var range = _picker.DateRange ?? _dateRange;
_dateRange = range ?? new DateRange();
await NotifyRangeChangedAsync(_dateRange);
await _picker.CloseAsync();
}
private async Task OnClickClear()
{
_dateRange = new DateRange();
await OnChanged.InvokeAsync(_dateRange);
await NotifyRangeChangedAsync(_dateRange);
await _picker.CloseAsync();
}
private async Task NotifyRangeChangedAsync(DateRange range)
{
var start = range.Start?.Date;
var end = range.End?.Date;
_lastSyncedFrom = start;
_lastSyncedTo = end;
if (From != start)
await FromChanged.InvokeAsync(start);
if (To != end)
await ToChanged.InvokeAsync(end);
await OnChanged.InvokeAsync(range);
}
}
@@ -0,0 +1,145 @@
using System.Globalization;
using System.Net;
using System.Text;
namespace BackOffice.Common.Utilities;
/// <summary>
/// خروجی سازگار با Excel فارسی:
/// - XML Spreadsheet (.xls) برای ستون‌بندی مطمئن + متن فارسی
/// - CSV با جداکننده ; به‌عنوان جایگزین سبک
/// </summary>
public static class CsvExportHelper
{
/// <summary>جداکننده پیش‌فرض ویندوز فارسی برای CSV.</summary>
public const char Separator = ';';
public static string Escape(string? value)
{
if (string.IsNullOrEmpty(value))
return string.Empty;
// همیشه quote تا Excel فارسی ستون را نشکند
return $"\"{value.Replace("\"", "\"\"")}\"";
}
public static string Cell(object? value) => value switch
{
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))
};
public static string Row(params object?[] cells) =>
string.Join(Separator, cells.Select(Cell));
/// <summary>
/// نام نمایشی مشتری؛ اگر خالی باشد موبایل یا شناسه کاربر.
/// </summary>
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 body)
{
// 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);
}
@@ -0,0 +1,161 @@
using System.Globalization;
using System.Net;
using System.Text;
using DateTimeConverterCL;
namespace BackOffice.Common.Utilities;
/// <summary>ساخت HTML فاکتور برای چاپ مرورگر (jsHtmlToPdf).</summary>
public static class OrderInvoicePrint
{
public sealed class InvoiceModel
{
public string ShopTitle { get; set; } = "فروشگاه";
public string OrderId { get; set; } = "";
public string? CustomerName { get; set; }
public string? CustomerMobile { get; set; }
public string? CustomerNationalCode { get; set; }
public string? Address { get; set; }
public string? PostalCode { get; set; }
public string? PaymentStatus { get; set; }
public string? PaymentMethod { get; set; }
public string? TransactionId { get; set; }
public string? TrackingCode { get; set; }
public DateTime? Created { get; set; }
public DateTime? PaymentDate { get; set; }
public long Subtotal { get; set; }
public long DiscountTotal { get; set; }
public long CreditUsed { get; set; }
public long GatewayAmount { get; set; }
public long VatAmount { get; set; }
public long TotalAmount { get; set; }
public List<InvoiceLine> Lines { get; set; } = new();
}
public sealed class InvoiceLine
{
public string Title { get; set; } = "";
public int Quantity { get; set; }
public long UnitPrice { get; set; }
public long Discount { get; set; }
public long LineTotal { get; set; }
}
public static string BuildHtml(InvoiceModel model)
{
var sb = new StringBuilder();
sb.Append("<!DOCTYPE html><html lang=\"fa\" dir=\"rtl\"><head><meta charset=\"utf-8\"/>");
sb.Append("<title>").Append(Esc($"فاکتور {model.OrderId}")).Append("</title>");
sb.Append("""
<style>
* { box-sizing: border-box; }
body { font-family: Tahoma, "Segoe UI", sans-serif; color: #111; margin: 24px; font-size: 13px; }
h1 { font-size: 20px; margin: 0 0 4px; }
.muted { color: #555; }
.header { display: flex; justify-content: space-between; align-items: flex-start; border-bottom: 2px solid #222; padding-bottom: 12px; margin-bottom: 16px; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 24px; margin-bottom: 16px; }
.label { color: #666; font-size: 11px; }
.value { font-weight: 600; }
table { width: 100%; border-collapse: collapse; margin: 12px 0 16px; }
th, td { border: 1px solid #ccc; padding: 8px; text-align: right; }
th { background: #f3f3f3; }
.num { text-align: left; font-variant-numeric: tabular-nums; white-space: nowrap; }
.totals { width: 320px; margin-right: auto; }
.totals td { border: none; padding: 4px 0; }
.totals .grand { font-size: 15px; font-weight: 700; border-top: 2px solid #222; padding-top: 8px; }
.footer { margin-top: 28px; font-size: 11px; color: #666; border-top: 1px dashed #bbb; padding-top: 8px; }
@media print {
body { margin: 0; }
.no-print { display: none !important; }
}
</style>
""");
sb.Append("</head><body>");
sb.Append("<div class=\"header\"><div>");
sb.Append("<h1>").Append(Esc(model.ShopTitle)).Append("</h1>");
sb.Append("<div class=\"muted\">فاکتور فروش</div></div><div>");
sb.Append("<div><span class=\"label\">شماره فاکتور: </span><span class=\"value\">").Append(Esc(model.OrderId)).Append("</span></div>");
if (model.Created.HasValue)
sb.Append("<div><span class=\"label\">تاریخ ثبت: </span>").Append(Esc(FormatDate(model.Created.Value))).Append("</div>");
if (model.PaymentDate.HasValue)
sb.Append("<div><span class=\"label\">تاریخ پرداخت: </span>").Append(Esc(FormatDate(model.PaymentDate.Value))).Append("</div>");
sb.Append("</div></div>");
sb.Append("<div class=\"grid\">");
AppendField(sb, "نام مشتری", model.CustomerName);
AppendField(sb, "موبایل", model.CustomerMobile);
AppendField(sb, "کد ملی", model.CustomerNationalCode);
AppendField(sb, "کد پستی", model.PostalCode);
AppendField(sb, "وضعیت پرداخت", model.PaymentStatus);
AppendField(sb, "روش پرداخت", model.PaymentMethod);
AppendField(sb, "کد تراکنش", model.TransactionId);
AppendField(sb, "کد رهگیری", model.TrackingCode);
if (!string.IsNullOrWhiteSpace(model.Address))
{
sb.Append("<div style=\"grid-column:1/-1\"><div class=\"label\">آدرس</div><div class=\"value\">")
.Append(Esc(model.Address)).Append("</div></div>");
}
sb.Append("</div>");
sb.Append("<table><thead><tr>");
sb.Append("<th>#</th><th>کالا</th><th>تعداد</th><th>قیمت واحد</th><th>تخفیف</th><th>مبلغ</th>");
sb.Append("</tr></thead><tbody>");
var i = 1;
foreach (var line in model.Lines)
{
sb.Append("<tr>");
sb.Append("<td class=\"num\">").Append(i++).Append("</td>");
sb.Append("<td>").Append(Esc(line.Title)).Append("</td>");
sb.Append("<td class=\"num\">").Append(line.Quantity).Append("</td>");
sb.Append("<td class=\"num\">").Append(Money(line.UnitPrice)).Append("</td>");
sb.Append("<td class=\"num\">").Append(Money(line.Discount)).Append("</td>");
sb.Append("<td class=\"num\">").Append(Money(line.LineTotal)).Append("</td>");
sb.Append("</tr>");
}
if (model.Lines.Count == 0)
sb.Append("<tr><td colspan=\"6\" class=\"muted\">آیتمی ثبت نشده است</td></tr>");
sb.Append("</tbody></table>");
sb.Append("<table class=\"totals\"><tbody>");
if (model.Subtotal > 0)
AppendTotalRow(sb, "جمع اقلام", model.Subtotal);
if (model.DiscountTotal > 0)
AppendTotalRow(sb, "تخفیف", model.DiscountTotal);
if (model.VatAmount > 0)
AppendTotalRow(sb, "مالیات بر ارزش افزوده", model.VatAmount);
if (model.CreditUsed > 0)
AppendTotalRow(sb, "پرداخت از کیف اعتباری", model.CreditUsed);
if (model.GatewayAmount > 0)
AppendTotalRow(sb, "پرداخت درگاه / نقدی", model.GatewayAmount);
sb.Append("<tr class=\"grand\"><td>مبلغ قابل پرداخت</td><td class=\"num\">")
.Append(Money(model.TotalAmount)).Append(" تومان</td></tr>");
sb.Append("</tbody></table>");
sb.Append("<div class=\"footer\">چاپ‌شده از پنل ادمین — ").Append(Esc(FormatDate(DateTime.Now))).Append("</div>");
sb.Append("</body></html>");
return sb.ToString();
}
private static void AppendField(StringBuilder sb, string label, string? value)
{
if (string.IsNullOrWhiteSpace(value)) return;
sb.Append("<div><div class=\"label\">").Append(Esc(label)).Append("</div><div class=\"value\">")
.Append(Esc(value)).Append("</div></div>");
}
private static void AppendTotalRow(StringBuilder sb, string label, long amount)
{
sb.Append("<tr><td>").Append(Esc(label)).Append("</td><td class=\"num\">")
.Append(Money(amount)).Append(" تومان</td></tr>");
}
private static string Money(long amount) => amount.ToString("N0", CultureInfo.InvariantCulture);
private static string FormatDate(DateTime dt)
{
try { return dt.ToLocalTime().MiladiToJalaliWithTime(); }
catch { return dt.ToLocalTime().ToString("yyyy/MM/dd HH:mm"); }
}
private static string Esc(string? s) => WebUtility.HtmlEncode(s ?? "");
}
@@ -42,8 +42,9 @@ public static class ReportLabels
public static string GetPurchaseMethodLabel(int method) => method switch
{
1 => "وام دایا",
2 => "خرید مستقیم",
1 => "دایا",
2 => "درگاه",
3 => "دستی",
_ => "نامشخص"
};
+8 -3
View File
@@ -1,6 +1,7 @@
@page "/club"
@page "/club/members"
@page "/club/purchases"
@page "/club/customer-packages"
@page "/club/cycles"
@page "/club/history"
@page "/club/statistics"
@@ -18,6 +19,9 @@
<MudTabPanel Text="گزارش خرید پکیج" Icon="@Icons.Material.Filled.ShoppingBag">
<ClubPackagePurchasesReport />
</MudTabPanel>
<MudTabPanel Text="گزارش مشتریان پکیج" Icon="@Icons.Material.Filled.PeopleAlt">
<CustomerPackagePurchasesReport />
</MudTabPanel>
<MudTabPanel Text="گزارش دوره‌ها" Icon="@Icons.Material.Filled.Loop">
<ClubCyclesReport />
</MudTabPanel>
@@ -39,9 +43,10 @@
_activeTab = uri switch
{
"club/purchases" => 1,
"club/cycles" => 2,
"club/history" => 3,
"club/statistics" => 4,
"club/customer-packages" => 2,
"club/cycles" => 3,
"club/history" => 4,
"club/statistics" => 5,
_ => 0
};
}
@@ -17,8 +17,9 @@
<MudItem xs="12" sm="6" md="3">
<MudSelect T="int?" Clearable="true" Label="روش خرید" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_purchaseMethod">
<MudSelectItem T="int?" Value="1">وام دایا</MudSelectItem>
<MudSelectItem T="int?" Value="2">خرید مستقیم</MudSelectItem>
<MudSelectItem T="int?" Value="1">دایا</MudSelectItem>
<MudSelectItem T="int?" Value="3">دستی</MudSelectItem>
<MudSelectItem T="int?" Value="2">درگاه</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="4">
@@ -0,0 +1,105 @@
@using CMSMicroservice.Protobuf.Protos.UserPackagePurchase
@using Google.Protobuf.WellKnownTypes
@using DateTimeConverterCL
@inject UserPackagePurchaseContract.UserPackagePurchaseContractClient PurchaseClient
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">
جزئیات خرید پکیج — @UserName
</MudText>
</TitleContent>
<DialogContent>
@if (_loading)
{
<MudProgressLinear Indeterminate="true" Color="Color.Primary" Class="my-4" />
}
else if (_items.Count == 0)
{
<MudAlert Severity="Severity.Info" Dense="true">خریدی برای این کاربر یافت نشد.</MudAlert>
}
else
{
<MudTable Items="_items" Dense="true" Hover="true" Breakpoint="Breakpoint.Sm">
<HeaderContent>
<MudTh>شناسه</MudTh>
<MudTh>پکیج</MudTh>
<MudTh>روش</MudTh>
<MudTh>مبلغ (ریال)</MudTh>
<MudTh>تاریخ خرید</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="شناسه">@context.Id</MudTd>
<MudTd DataLabel="پکیج">@context.PackageName</MudTd>
<MudTd DataLabel="روش">
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined"
Color="@GetMethodColor(context.PurchaseMethod)">
@ReportLabels.GetPurchaseMethodLabel(context.PurchaseMethod)
</MudChip>
</MudTd>
<MudTd DataLabel="مبلغ">@context.Amount.ToString("N0")</MudTd>
<MudTd DataLabel="تاریخ">
@(context.PurchasedAt != null
? context.PurchasedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()
: "-")
</MudTd>
</RowTemplate>
</MudTable>
}
</DialogContent>
<DialogActions>
<MudButton OnClick="Close" Variant="Variant.Text">بستن</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!;
[Parameter] public long UserId { get; set; }
[Parameter] public string UserName { get; set; } = string.Empty;
private bool _loading = true;
private List<UserPackagePurchaseModel> _items = new();
protected override async Task OnInitializedAsync()
{
try
{
var result = await PurchaseClient.GetAllUserPackagePurchaseByFilterAsync(
new GetAllUserPackagePurchaseByFilterRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
{
PageNumber = 1,
PageSize = 500
},
Filter = new GetAllUserPackagePurchaseByFilterFilter
{
UserId = UserId
}
});
_items = result?.Models?
.OrderBy(x => x.PurchasedAt?.ToDateTime() ?? DateTime.MinValue)
.ToList() ?? new();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری جزئیات: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
}
}
private static Color GetMethodColor(int method) => method switch
{
1 => Color.Info, // دایا
2 => Color.Primary, // درگاه
3 => Color.Secondary, // دستی
_ => Color.Default
};
private void Close() => MudDialog.Close();
}
@@ -0,0 +1,282 @@
@using System.Text
@using BackOffice.Common.BaseComponents
@using BackOffice.Common.Utilities
@using BackOffice.Pages.AutoComplete
@using BackOffice.Pages.Club.Reports.Components
@using CMSMicroservice.Protobuf.Protos.UserPackagePurchase
@using Google.Protobuf.WellKnownTypes
@using DateTimeConverterCL
@using Microsoft.JSInterop
@inject UserPackagePurchaseContract.UserPackagePurchaseContractClient PurchaseClient
@inject IDialogService DialogService
@inject IJSRuntime JsRuntime
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3">
گزارش تجمیعی مشتریان — اولین/آخرین پکیج، تفکیک دایا / دستی / درگاه. خروجی Excel مطابق فیلتر فعال است.
</MudAlert>
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
<Filters>
<MudItem xs="12" sm="6" md="3">
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_userId" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudSelect T="int?" Clearable="true" Label="روش خرید" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_purchaseMethod">
<MudSelectItem T="int?" Value="1">دایا</MudSelectItem>
<MudSelectItem T="int?" Value="3">دستی</MudSelectItem>
<MudSelectItem T="int?" Value="2">درگاه</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<DateRangePicker @bind-From="_fromDate" @bind-To="_toDate" Label="بازه خرید" />
</MudItem>
</Filters>
<Content>
<MudStack Row="true" Justify="Justify.FlexEnd" Class="mb-3">
<MudButton Variant="Variant.Outlined" Color="Color.Success"
StartIcon="@Icons.Material.Filled.FileDownload"
OnClick="ExportToExcel" Disabled="_exporting">
خروجی Excel
</MudButton>
</MudStack>
<MudDataGrid @ref="_grid" T="CustomerPackagePurchaseRollupModel" ServerData="LoadData" ReadOnly="true"
Hover="true" Dense="true" Height="calc(100vh - 420px)">
<Columns>
<TemplateColumn Title="مشتری">
<CellTemplate>
<MudStack Spacing="0">
<MudText Typo="Typo.body2">@context.Item.UserName</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">@context.Item.UserMobile</MudText>
</MudStack>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="اولین پکیج">
<CellTemplate>
<MudStack Spacing="0">
<MudText Typo="Typo.body2">@context.Item.FirstPackageName</MudText>
<MudText Typo="Typo.caption">@context.Item.FirstAmount.ToString("N0") ریال</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">
@(context.Item.FirstPurchasedAt?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-")
</MudText>
</MudStack>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="آخرین پکیج">
<CellTemplate>
<MudStack Spacing="0">
<MudText Typo="Typo.body2">@context.Item.LastPackageName</MudText>
<MudText Typo="Typo.caption">@context.Item.LastAmount.ToString("N0") ریال</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">
@(context.Item.LastPurchasedAt?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-")
</MudText>
<MudChip T="string" Size="Size.Small" Class="mt-1" Variant="Variant.Filled"
Color="@GetMethodColor(context.Item.LastPurchaseMethod)">
@ReportLabels.GetPurchaseMethodLabel(context.Item.LastPurchaseMethod)
</MudChip>
</MudStack>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="تعداد / جمع">
<CellTemplate>
<MudStack Spacing="0">
<MudText>@context.Item.PurchaseCount خرید</MudText>
<MudText Typo="Typo.caption">@context.Item.TotalAmount.ToString("N0") ریال</MudText>
</MudStack>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="دایا">
<CellTemplate>
<MudText Typo="Typo.caption">@context.Item.DayaCount عدد</MudText>
<MudText Typo="Typo.caption">@context.Item.DayaAmount.ToString("N0")</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="دستی">
<CellTemplate>
<MudText Typo="Typo.caption">@context.Item.ManualCount عدد</MudText>
<MudText Typo="Typo.caption">@context.Item.ManualAmount.ToString("N0")</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="درگاه">
<CellTemplate>
<MudText Typo="Typo.caption">@context.Item.GatewayCount عدد</MudText>
<MudText Typo="Typo.caption">@context.Item.GatewayAmount.ToString("N0")</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="عملیات">
<CellTemplate>
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary"
OnClick="() => OpenDetails(context.Item)">
جزئیات
</MudButton>
</CellTemplate>
</TemplateColumn>
</Columns>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
</NoRecordsContent>
<PagerContent>
<MudDataGridPager T="CustomerPackagePurchaseRollupModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد در صفحه" />
</PagerContent>
</MudDataGrid>
</Content>
</BasePageComponent>
@code {
private BasePageComponent? _basePage;
private MudDataGrid<CustomerPackagePurchaseRollupModel>? _grid;
private long? _userId;
private int? _purchaseMethod;
private DateTime? _fromDate, _toDate;
private bool _exporting;
private GetCustomerPackagePurchaseRollupFilter BuildFilter()
{
var filter = new GetCustomerPackagePurchaseRollupFilter();
if (_userId is > 0)
filter.UserId = _userId;
if (_purchaseMethod.HasValue)
filter.PurchaseMethod = _purchaseMethod.Value;
if (_fromDate.HasValue)
filter.PurchasedFrom = Timestamp.FromDateTime(DateTime.SpecifyKind(_fromDate.Value.Date, DateTimeKind.Utc));
if (_toDate.HasValue)
filter.PurchasedTo = Timestamp.FromDateTime(DateTime.SpecifyKind(_toDate.Value.Date.AddDays(1).AddTicks(-1), DateTimeKind.Utc));
return filter;
}
private async Task<GridData<CustomerPackagePurchaseRollupModel>> LoadData(GridState<CustomerPackagePurchaseRollupModel> state)
{
try
{
var result = await PurchaseClient.GetCustomerPackagePurchaseRollupAsync(
new GetCustomerPackagePurchaseRollupRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
{
PageNumber = state.Page + 1,
PageSize = state.PageSize
},
Filter = BuildFilter()
});
return new GridData<CustomerPackagePurchaseRollupModel>
{
Items = result?.Models?.ToList() ?? new(),
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
};
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
return new GridData<CustomerPackagePurchaseRollupModel>();
}
}
private async Task OnFilterSubmit()
{
if (_grid != null) await _grid.ReloadServerData();
}
private async Task OnFilterCleared()
{
_userId = null;
_purchaseMethod = null;
_fromDate = _toDate = null;
if (_grid != null) await _grid.ReloadServerData();
}
private async Task OpenDetails(CustomerPackagePurchaseRollupModel row)
{
var parameters = new DialogParameters
{
{ nameof(CustomerPackagePurchaseDetailsDialog.UserId), row.UserId },
{ nameof(CustomerPackagePurchaseDetailsDialog.UserName), row.UserName }
};
var options = new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true, CloseButton = true };
await DialogService.ShowAsync<CustomerPackagePurchaseDetailsDialog>("جزئیات خرید پکیج", parameters, options);
}
private async Task ExportToExcel()
{
_exporting = true;
try
{
// PageSize=0 → همهٔ ردیف‌های مطابق فیلتر، مستقل از صفحه‌بندی گرید
var result = await PurchaseClient.GetCustomerPackagePurchaseRollupAsync(
new GetCustomerPackagePurchaseRollupRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
{
PageNumber = 1,
PageSize = 0
},
Filter = BuildFilter()
});
var all = result?.Models?.ToList() ?? new();
if (all.Count == 0)
{
Snackbar.Add("داده‌ای برای خروجی با فیلتر فعلی وجود ندارد.", Severity.Info);
return;
}
var headers = new[]
{
"شناسه کاربر", "نام مشتری", "موبایل",
"اولین پکیج", "مبلغ اولین", "تاریخ اولین",
"آخرین پکیج", "مبلغ آخرین", "تاریخ آخرین", "روش آخرین",
"تعداد خرید", "جمع مبلغ",
"تعداد دایا", "مبلغ دایا",
"تعداد دستی", "مبلغ دستی",
"تعداد درگاه", "مبلغ درگاه"
};
var rows = all.Select(o => new object?[]
{
o.UserId,
o.UserName,
o.UserMobile,
o.FirstPackageName,
o.FirstAmount,
o.FirstPurchasedAt?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-",
o.LastPackageName,
o.LastAmount,
o.LastPurchasedAt?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-",
ReportLabels.GetPurchaseMethodLabel(o.LastPurchaseMethod),
o.PurchaseCount,
o.TotalAmount,
o.DayaCount,
o.DayaAmount,
o.ManualCount,
o.ManualAmount,
o.GatewayCount,
o.GatewayAmount
});
var filename = $"customer-packages-{DateTime.Now:yyyyMMddHHmmss}.xls";
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename,
CsvExportHelper.ToExcelXmlBase64("مشتریان پکیج", headers, rows));
Snackbar.Add($"خروجی Excel آماده شد ({all.Count} مشتری).", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error);
}
finally
{
_exporting = false;
}
}
private static Color GetMethodColor(int method) => method switch
{
1 => Color.Info,
2 => Color.Primary,
3 => Color.Secondary,
_ => Color.Default
};
}
@@ -26,6 +26,7 @@
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Processing">در حال آماده‌سازی</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.ReadyForOfficePickup">آماده تحویل در دفتر</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
</MudSelect>
@@ -55,6 +56,15 @@
</MudItem>
}
@if (Model.Status == OrderStatus.ReadyForOfficePickup)
{
<MudItem xs="12">
<MudAlert Severity="Severity.Info">
<MudText>سفارش برای تحویل حضوری در دفتر آماده است.</MudText>
</MudAlert>
</MudItem>
}
@if (Model.Status == OrderStatus.Shipped)
{
<MudItem xs="12">
@@ -126,8 +136,9 @@
OrderStatus.Processing => Color.Primary,
OrderStatus.Shipped => Color.Secondary,
OrderStatus.Delivered => Color.Success,
OrderStatus.Cancelled => Color.Error,
OrderStatus.Returned => Color.Dark,
OrderStatus.ReadyForOfficePickup => Color.Primary,
OrderStatus.Cancelled => Color.Dark,
OrderStatus.Returned => Color.Error,
_ => Color.Default
};
}
@@ -141,6 +152,7 @@
OrderStatus.Processing => "در حال آماده‌سازی",
OrderStatus.Shipped => "ارسال شده",
OrderStatus.Delivered => "تحویل داده شده",
OrderStatus.ReadyForOfficePickup => "آماده تحویل در دفتر",
OrderStatus.Cancelled => "لغو شده",
OrderStatus.Returned => "مرجوع شده",
_ => "نامشخص"
@@ -1,36 +1,72 @@
@using BackOffice.Services.DiscountOrder
@using BackOffice.Common.BaseComponents
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.ShoppingCart" Class="ml-2" />
جزئیات سفارش #@Order.OrderNumber
</MudText>
</TitleContent>
<DialogContent>
<MudGrid>
<!-- اطلاعات کاربر -->
<MudItem xs="12">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" GutterBottom="true">اطلاعات سفارش</MudText>
<MudGrid>
<MudItem xs="6">
<MudText Typo="Typo.body2" Color="Color.Secondary">شماره سفارش:</MudText>
<MudText Typo="Typo.body1"><strong>@Order.OrderNumber</strong></MudText>
</MudItem>
<MudItem xs="6">
<MudText Typo="Typo.body2" Color="Color.Secondary">شناسه کاربر:</MudText>
<MudText Typo="Typo.body1">@Order.UserId</MudText>
</MudItem>
</MudGrid>
</MudPaper>
</MudItem>
<MudStack Spacing="2">
<MudText Typo="Typo.h6">جزئیات سفارش شماره @(string.IsNullOrWhiteSpace(Order.OrderNumber) ? Order.Id.ToString() : Order.OrderNumber)</MudText>
<MudText Typo="Typo.body2">
کاربر:
@if (!string.IsNullOrWhiteSpace(Order.UserFullName))
{
@Order.UserFullName
}
else
{
<text>#@Order.UserId</text>
}
@if (!string.IsNullOrWhiteSpace(DisplayMobile))
{
<text> (@DisplayMobile)</text>
}
</MudText>
<!-- Timeline وضعیت -->
<MudItem xs="12">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.subtitle2" GutterBottom="true">Timeline وضعیت</MudText>
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center" Justify="Justify.Center">
<MudPaper Class="pa-3">
<MudStack Spacing="2">
<MudText Typo="Typo.subtitle2">خلاصه سفارش</MudText>
<MudDivider />
<MudText Typo="Typo.body2">مبلغ کل: @Order.TotalPrice.ToString("N0") تومان</MudText>
<MudText Typo="Typo.body2" Color="Color.Success">
از کیف پول اعتباری: @Order.DiscountBalanceUsed.ToString("N0") تومان
</MudText>
<MudText Typo="Typo.body2">
مبلغ پرداختی از درگاه: @Order.GatewayAmount.ToString("N0") تومان
</MudText>
@if (!string.IsNullOrEmpty(Order.TransactionId))
{
<MudText Typo="Typo.body2">شناسه پرداخت: @Order.TransactionId</MudText>
}
@if (Order.Created.HasValue)
{
<MudText Typo="Typo.body2">
تاریخ ثبت: @Order.Created.Value.MiladiToJalaliWithTime()
</MudText>
}
<MudText Typo="Typo.body2">
وضعیت پرداخت:
<MudChip T="string"
Color="@GetPaymentStatusColor(Order.PaymentStatusValue)"
Size="Size.Small"
Icon="@GetPaymentStatusIcon(Order.PaymentStatusValue)">
@GetPaymentStatusText(Order.PaymentStatusValue)
</MudChip>
</MudText>
@if (Order.VatAmount > 0)
{
<MudDivider Class="my-2" />
<MudText Typo="Typo.subtitle2">جزئیات مالیات بر ارزش افزوده</MudText>
<MudText Typo="Typo.body2">
مالیات: @Order.VatAmount.ToString("N0") تومان
</MudText>
<MudText Typo="Typo.body2">
مبلغ نهایی (شامل مالیات): @Order.TotalPrice.ToString("N0") تومان
</MudText>
}
<MudDivider Class="my-2" />
<MudText Typo="Typo.subtitle2">Timeline وضعیت</MudText>
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center">
@foreach (var step in _timelineSteps)
{
<MudStack Spacing="0" AlignItems="AlignItems.Center">
@@ -41,218 +77,155 @@
</MudStack>
}
</MudStack>
</MudPaper>
</MudItem>
<!-- وضعیت سفارش -->
<MudItem xs="12" sm="6">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" GutterBottom="true">وضعیت سفارش</MudText>
<MudChip T="string" Color="@GetStatusColor(Order.Status)" Size="Size.Large">
@GetStatusText(Order.Status)
</MudChip>
@if (Order.Created.HasValue)
{
<MudText Typo="Typo.body2" Class="mt-2">
تاریخ ثبت: @Order.Created.Value.MiladiToJalaliWithTime()
</MudText>
}
</MudPaper>
</MudItem>
<!-- وضعیت پرداخت -->
<MudItem xs="12" sm="6">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" GutterBottom="true">وضعیت پرداخت</MudText>
@if (Order.PaymentCompleted)
{
<MudChip T="string" Color="Color.Success" Size="Size.Large" Icon="@Icons.Material.Filled.CheckCircle">
پرداخت شده
</MudChip>
@if (!string.IsNullOrEmpty(Order.TransactionId))
{
<MudText Typo="Typo.body2" Class="mt-2">
کد تراکنش: @Order.TransactionId
</MudText>
}
}
else
{
<MudChip T="string" Color="Color.Warning" Size="Size.Large" Icon="@Icons.Material.Filled.Schedule">
در انتظار پرداخت
</MudChip>
}
</MudPaper>
</MudItem>
<!-- آدرس ارسال -->
@if (Order.Address != null)
{
<MudItem xs="12">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" GutterBottom="true">
<MudIcon Icon="@Icons.Material.Filled.LocationOn" Size="Size.Small" />
آدرس ارسال
</MudText>
<MudText Typo="Typo.body1"><strong>@Order.Address.Title</strong></MudText>
<MudText Typo="Typo.body2">@Order.Address.Address</MudText>
<MudText Typo="Typo.body2">کد پستی: @Order.Address.PostalCode</MudText>
@if (!string.IsNullOrEmpty(Order.Address.Phone))
{
<MudText Typo="Typo.body2">تلفن: @Order.Address.Phone</MudText>
}
</MudPaper>
</MudItem>
}
<!-- آیتم‌های سفارش -->
<MudItem xs="12">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" GutterBottom="true">آیتم‌های سفارش</MudText>
<MudTable Items="@Order.Items" Hover="true" Dense="true" Elevation="0">
<HeaderContent>
<MudTh>محصول</MudTh>
<MudTh>تعداد</MudTh>
<MudTh>قیمت واحد</MudTh>
<MudTh>تخفیف</MudTh>
<MudTh>قیمت نهایی</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
@if (!string.IsNullOrWhiteSpace(context.ThumbnailPath))
{
<MudImage Src="@context.ThumbnailPath" Width="40" Height="40"
ObjectFit="ObjectFit.Cover" Class="rounded" />
}
<MudText>@context.ProductTitle</MudText>
</MudStack>
</MudTd>
<MudTd>@context.Count</MudTd>
<MudTd>@context.UnitPrice.ToString("N0") تومان</MudTd>
<MudTd>
<MudChip T="string" Size="Size.Small" Color="Color.Success">@context.MaxDiscountPercent%</MudChip>
</MudTd>
<MudTd><strong>@context.FinalPrice.ToString("N0") تومان</strong></MudTd>
</RowTemplate>
</MudTable>
</MudPaper>
</MudItem>
<!-- خلاصه مالی -->
<MudItem xs="12">
<MudPaper Class="pa-4" Elevation="2">
<MudText Typo="Typo.h6" GutterBottom="true">خلاصه مالی</MudText>
<MudGrid>
<MudItem xs="6">
<MudText Color="Color.Secondary">مبلغ کل:</MudText>
</MudItem>
<MudItem xs="6" Class="text-left">
<MudText>@Order.TotalPrice.ToString("N0") تومان</MudText>
</MudItem>
<MudItem xs="6">
<MudText Color="Color.Success">از کیف پول اعتباری:</MudText>
</MudItem>
<MudItem xs="6" Class="text-left">
<MudText Color="Color.Success">@Order.DiscountBalanceUsed.ToString("N0") تومان</MudText>
</MudItem>
<MudItem xs="12"><MudDivider /></MudItem>
<MudItem xs="6">
<MudText Typo="Typo.h6">مبلغ پرداختی از درگاه:</MudText>
</MudItem>
<MudItem xs="6" Class="text-left">
<MudText Typo="Typo.h6" Color="Color.Primary">
<strong>@Order.GatewayAmount.ToString("N0") تومان</strong>
</MudText>
</MudItem>
</MudGrid>
</MudPaper>
</MudItem>
<!-- ویرایش وضعیت ارسال -->
<MudItem xs="12">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.subtitle2" GutterBottom="true">مدیریت وضعیت ارسال</MudText>
<MudDivider Class="mb-3" />
<MudGrid>
<MudItem xs="12" sm="6">
<MudSelect T="OrderStatus"
Label="وضعیت ارسال"
Dense="true"
Variant="Variant.Outlined"
@bind-Value="_newStatus">
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Pending">در انتظار پرداخت</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Paid">پرداخت شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Processing">در حال آماده‌سازی</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6">
<MudTextField T="string"
Label="کد رهگیری"
Variant="Variant.Outlined"
Margin="Margin.Dense"
@bind-Value="_trackingCode" />
</MudItem>
<MudItem xs="12">
<MudTextField T="string"
Label="یادداشت ادمین"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Lines="3"
HelperText="دلیل تغییر وضعیت یا توضیحات اضافی"
@bind-Value="_adminNotes" />
</MudItem>
</MudGrid>
<MudDivider Class="my-2" />
<MudText Typo="Typo.body2">
وضعیت ارسال:
<MudSelect T="OrderStatus"
Label="وضعیت ارسال"
Dense="true"
Variant="Variant.Outlined"
@bind-Value="_newStatus">
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Pending">در انتظار پرداخت</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Paid">پرداخت شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Processing">در حال آماده‌سازی</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.ReadyForOfficePickup">آماده تحویل در دفتر</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
</MudSelect>
</MudText>
<MudTextField T="string"
Label="کد رهگیری"
Variant="Variant.Outlined"
Dense="true"
@bind-Value="_trackingCode" />
<MudTextField T="string"
Label="یادداشت ادمین"
Variant="Variant.Outlined"
Dense="true"
Lines="3"
HelperText="دلیل تغییر وضعیت یا توضیحات اضافی"
@bind-Value="_adminNotes" />
@if (_newStatus == OrderStatus.Cancelled || _newStatus == OrderStatus.Returned)
{
<MudAlert Severity="Severity.Warning" Class="mt-2">
<MudAlert Severity="Severity.Warning" Dense="true">
توجه: در صورت لغو یا مرجوعی سفارش، موجودی محصولات به انبار بازگردانده می‌شود.
</MudAlert>
}
@if (_newStatus == OrderStatus.ReadyForOfficePickup)
{
<MudAlert Severity="Severity.Info" Dense="true">
سفارش برای تحویل حضوری در دفتر آماده است.
</MudAlert>
}
@if (_newStatus == OrderStatus.Shipped)
{
<MudAlert Severity="Severity.Info" Class="mt-2">
پس از تغییر وضعیت به "ارسال شده"، اطلاع‌رسانی به کاربر ارسال خواهد شد.
<MudAlert Severity="Severity.Info" Dense="true">
پس از تغییر وضعیت به «ارسال شده»، اطلاع‌رسانی به کاربر ارسال خواهد شد.
</MudAlert>
}
</MudPaper>
</MudItem>
</MudStack>
</MudPaper>
<!-- یادداشت‌ها -->
@if (!string.IsNullOrEmpty(Order.Notes) || !string.IsNullOrEmpty(Order.AdminNotes))
<MudPaper Class="pa-3">
<MudStack Spacing="1">
<MudText Typo="Typo.subtitle2">آدرس تحویل</MudText>
<MudDivider />
@if (Order.Address != null && !string.IsNullOrWhiteSpace(Order.Address.Address))
{
@if (!string.IsNullOrWhiteSpace(Order.Address.Title))
{
<MudText Typo="Typo.body2"><strong>@Order.Address.Title</strong></MudText>
}
<MudText Typo="Typo.body2">@Order.Address.Address</MudText>
@if (!string.IsNullOrWhiteSpace(Order.Address.PostalCode))
{
<MudText Typo="Typo.body2">کد پستی: @Order.Address.PostalCode</MudText>
}
@if (!string.IsNullOrWhiteSpace(DisplayMobile))
{
<MudText Typo="Typo.body2">تلفن سفارش‌دهنده: @DisplayMobile</MudText>
}
}
else
{
<MudText Typo="Typo.caption">آدرسی ثبت نشده است.</MudText>
}
</MudStack>
</MudPaper>
<MudPaper Class="pa-3">
<MudStack Spacing="1">
<MudText Typo="Typo.subtitle2">اقلام فاکتور</MudText>
<MudDivider />
@if (Order.Items.Count > 0)
{
<MudTable Items="Order.Items" Dense="true">
<HeaderContent>
<MudTh>تصویر</MudTh>
<MudTh>محصول</MudTh>
<MudTh>قیمت واحد</MudTh>
<MudTh>تعداد</MudTh>
<MudTh>تخفیف</MudTh>
<MudTh>قیمت نهایی</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
@if (!string.IsNullOrWhiteSpace(context.ThumbnailPath))
{
<Image Src="@context.ThumbnailPath" Height="50" Width="50" />
}
</MudTd>
<MudTd>@context.ProductTitle</MudTd>
<MudTd>@context.UnitPrice.ToString("N0")</MudTd>
<MudTd>@context.Count</MudTd>
<MudTd>
@if (context.MaxDiscountPercent > 0)
{
<MudChip T="string" Size="Size.Small" Color="Color.Success">@context.MaxDiscountPercent%</MudChip>
}
else if (context.DiscountAmount > 0)
{
@context.DiscountAmount.ToString("N0")
}
else
{
<text>-</text>
}
</MudTd>
<MudTd>@context.FinalPrice.ToString("N0")</MudTd>
</RowTemplate>
</MudTable>
}
else
{
<MudText Typo="Typo.caption">آیتمی برای این سفارش ثبت نشده است.</MudText>
}
</MudStack>
</MudPaper>
@if (!string.IsNullOrEmpty(Order.Notes))
{
<MudItem xs="12">
<MudPaper Class="pa-4" Elevation="1">
@if (!string.IsNullOrEmpty(Order.Notes))
{
<MudText Typo="Typo.h6" GutterBottom="true">
<MudIcon Icon="@Icons.Material.Filled.Comment" Size="Size.Small" />
یادداشت کاربر
</MudText>
<MudText Typo="Typo.body1" Class="mb-4">@Order.Notes</MudText>
}
@if (!string.IsNullOrEmpty(Order.AdminNotes))
{
<MudText Typo="Typo.h6" GutterBottom="true">
<MudIcon Icon="@Icons.Material.Filled.Note" Size="Size.Small" />
یادداشت ادمین
</MudText>
<MudText Typo="Typo.body1">@Order.AdminNotes</MudText>
}
</MudPaper>
</MudItem>
<MudPaper Class="pa-3">
<MudStack Spacing="1">
<MudText Typo="Typo.subtitle2">یادداشت کاربر</MudText>
<MudDivider />
<MudText Typo="Typo.body2">@Order.Notes</MudText>
</MudStack>
</MudPaper>
}
</MudGrid>
</MudStack>
</DialogContent>
<DialogActions>
<MudButton Variant="Variant.Outlined" Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.Print"
OnClick="PrintInvoiceAsync">
چاپ فاکتور
</MudButton>
<MudSpacer />
<MudButton Variant="Variant.Outlined" Color="Color.Default" OnClick="Close">
بستن
</MudButton>
@@ -1,5 +1,7 @@
using BackOffice.Common.Utilities;
using BackOffice.Services.DiscountOrder;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using MudBlazor;
namespace BackOffice.Pages.DiscountShop.Components;
@@ -8,6 +10,7 @@ public partial class OrderDetailsDialog
{
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!;
[Inject] public IDiscountOrderService OrderService { get; set; } = default!;
[Inject] private IJSRuntime JsRuntime { get; set; } = default!;
[Parameter] public DiscountOrderDetailsDto Order { get; set; } = null!;
@@ -17,6 +20,9 @@ public partial class OrderDetailsDialog
private bool _isSaving;
private List<TimelineStep> _timelineSteps = new();
private string? DisplayMobile =>
!string.IsNullOrWhiteSpace(Order.UserMobile) ? Order.UserMobile : Order.Address?.Phone;
protected override void OnInitialized()
{
_newStatus = Order.Status;
@@ -127,34 +133,69 @@ public partial class OrderDetailsDialog
private void Close() => MudDialog.Close();
private Color GetStatusColor(OrderStatus status)
private Color GetPaymentStatusColor(int status) => status switch
{
return status switch
{
OrderStatus.Pending => Color.Warning,
OrderStatus.Paid => Color.Info,
OrderStatus.Processing => Color.Primary,
OrderStatus.Shipped => Color.Secondary,
OrderStatus.Delivered => Color.Success,
OrderStatus.Cancelled => Color.Error,
OrderStatus.Returned => Color.Dark,
_ => Color.Default
};
}
0 => Color.Warning,
1 => Color.Success,
2 => Color.Error,
3 => Color.Info,
_ => Color.Default
};
private string GetStatusText(OrderStatus status)
private string GetPaymentStatusText(int status) => status switch
{
return status switch
0 => "در انتظار پرداخت",
1 => "پرداخت شده",
2 => "ناموفق",
3 => "بازپرداخت شده",
_ => "نامشخص"
};
private string GetPaymentStatusIcon(int status) => status switch
{
1 => Icons.Material.Filled.CheckCircle,
2 => Icons.Material.Filled.Cancel,
3 => Icons.Material.Filled.Replay,
_ => Icons.Material.Filled.Schedule
};
private async Task PrintInvoiceAsync()
{
var lines = Order.Items.Select(i => new OrderInvoicePrint.InvoiceLine
{
OrderStatus.Pending => "در انتظار پرداخت",
OrderStatus.Paid => "پرداخت شده",
OrderStatus.Processing => "در حال آماده‌سازی",
OrderStatus.Shipped => "ارسال شده",
OrderStatus.Delivered => "تحویل داده شده",
OrderStatus.Cancelled => "لغو شده",
OrderStatus.Returned => "مرجوع شده",
_ => "نامشخص"
};
Title = i.ProductTitle,
Quantity = i.Count,
UnitPrice = i.UnitPrice,
Discount = i.DiscountAmount,
LineTotal = i.FinalPrice > 0 ? i.FinalPrice : i.UnitPrice * i.Count
}).ToList();
var mobile = !string.IsNullOrWhiteSpace(Order.UserMobile)
? Order.UserMobile
: Order.Address?.Phone;
var html = OrderInvoicePrint.BuildHtml(new OrderInvoicePrint.InvoiceModel
{
ShopTitle = "فروشگاه اعتباری — کارا بازار سلامت",
OrderId = string.IsNullOrWhiteSpace(Order.OrderNumber) ? Order.Id.ToString() : Order.OrderNumber,
CustomerName = Order.UserFullName,
CustomerMobile = mobile,
Address = Order.Address?.Address,
PostalCode = Order.Address?.PostalCode,
PaymentStatus = GetPaymentStatusText(Order.PaymentStatusValue),
TransactionId = Order.TransactionId,
TrackingCode = Order.TrackingCode,
Created = Order.Created,
Subtotal = lines.Sum(l => l.UnitPrice * l.Quantity),
DiscountTotal = lines.Sum(l => l.Discount),
CreditUsed = Order.DiscountBalanceUsed,
GatewayAmount = Order.GatewayAmount,
VatAmount = Order.VatAmount,
TotalAmount = Order.TotalPrice,
Lines = lines
});
await JsRuntime.InvokeVoidAsync("jsHtmlToPdf", html, $"فاکتور سفارش {Order.Id}");
}
private enum StepState
@@ -49,6 +49,8 @@
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.ReadyForOfficePickup">آماده تحویل در دفتر</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
</MudSelect>
</MudItem>
</Filters>
@@ -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;
@@ -74,6 +75,15 @@ public partial class DiscountOrdersMainPage
return;
}
// تکمیل فیلدهای فاکتور از ردیف لیست (جزئیات API نام/موبایل/VAT ندارد)
var listRow = _orders?.FirstOrDefault(o => o.Id == orderId);
if (listRow != null)
{
order.UserFullName = listRow.UserFullName;
order.UserMobile = listRow.UserMobile;
order.VatAmount = listRow.VatAmount;
}
var parameters = new DialogParameters<OrderDetailsDialog> { { x => x.Order, order } };
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Large, FullWidth = true };
var dialog = await DialogService.ShowAsync<OrderDetailsDialog>("جزئیات سفارش", parameters, options);
@@ -178,8 +188,9 @@ public partial class DiscountOrdersMainPage
OrderStatus.Processing => Color.Primary,
OrderStatus.Shipped => Color.Secondary,
OrderStatus.Delivered => Color.Success,
OrderStatus.Cancelled => Color.Error,
OrderStatus.Returned => Color.Dark,
OrderStatus.Cancelled => Color.Dark,
OrderStatus.ReadyForOfficePickup => Color.Primary,
OrderStatus.Returned => Color.Error,
_ => Color.Default
};
}
@@ -194,6 +205,7 @@ public partial class DiscountOrdersMainPage
OrderStatus.Shipped => "ارسال شده",
OrderStatus.Delivered => "تحویل داده شده",
OrderStatus.Cancelled => "لغو شده",
OrderStatus.ReadyForOfficePickup => "آماده تحویل در دفتر",
OrderStatus.Returned => "مرجوع شده",
_ => "نامشخص"
};
@@ -233,44 +245,36 @@ public partial class DiscountOrdersMainPage
try
{
var sb = new StringBuilder();
sb.AppendLine("شناسه,نام کاربر,موبایل,مبلغ کل,کیف پول اعتباری,پرداخت درگاه,تعداد آیتم,وضعیت پرداخت,وضعیت ارسال,تاریخ پرداخت,تاریخ ثبت,آدرس");
foreach (var o in _orders)
var headers = new[]
{
sb.AppendLine(string.Join(",",
o.Id,
EscapeCsv(o.UserFullName),
EscapeCsv(o.UserMobile),
o.TotalPrice.ToString("N0"),
o.DiscountBalanceUsed.ToString("N0"),
o.GatewayAmount.ToString("N0"),
o.ItemsCount,
EscapeCsv(GetPaymentStatusText(o.PaymentStatusValue)),
EscapeCsv(GetStatusText(o.Status)),
o.PaymentDate?.MiladiToJalaliWithTime() ?? "-",
o.Created?.MiladiToJalaliWithTime() ?? "-",
EscapeCsv(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";
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 ?? "-"
});
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
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)
{
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;
}
}
@@ -48,6 +48,7 @@
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Processing">در حال پردازش</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Shipped">ارسال شده</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Delivered">تحویل شده</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="OrderStatus.ReadyForOfficePickup">آماده تحویل در دفتر</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Cancelled">لغو شده</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Returned">مرجوع شده</MudSelectItem>
</MudSelect>
@@ -498,7 +499,8 @@
OrderStatus.Processing => Color.Info,
OrderStatus.Shipped => Color.Info,
OrderStatus.Delivered => Color.Success,
OrderStatus.Cancelled => Color.Error,
OrderStatus.ReadyForOfficePickup => Color.Primary,
OrderStatus.Cancelled => Color.Dark,
OrderStatus.Returned => Color.Error,
_ => Color.Default
};
@@ -513,6 +515,7 @@
OrderStatus.Processing => "در حال پردازش",
OrderStatus.Shipped => "ارسال شده",
OrderStatus.Delivered => "تحویل شده",
OrderStatus.ReadyForOfficePickup => "آماده تحویل در دفتر",
OrderStatus.Cancelled => "لغو شده",
OrderStatus.Returned => "مرجوع شده",
_ => "نامشخص"
@@ -527,28 +530,31 @@
return;
}
var sb = new StringBuilder();
sb.AppendLine("شناسه,نام کاربر,موبایل,تاریخ ثبت,تعداد آیتم,مبلغ نهایی,تخفیف,وضعیت");
foreach (var o in _orders.Where(o => o.Created.HasValue).OrderBy(o => o.Created))
var headers = new[]
{
sb.AppendLine(string.Join(",",
"شناسه", "نام مشتری", "موبایل", "تاریخ ثبت", "تعداد آیتم", "مبلغ نهایی", "تخفیف", "وضعیت"
};
var rows = _orders
.OrderBy(o => o.Created ?? o.PaymentDate ?? DateTime.MaxValue)
.Select(o => new object?[]
{
o.Id,
EscapeCsv(o.UserFullName),
EscapeCsv(o.UserMobile),
o.Created?.MiladiToJalaliWithTime() ?? "-",
CsvExportHelper.CustomerName(o.UserFullName, o.UserMobile, o.UserId),
o.UserMobile ?? "",
o.Created?.MiladiToJalaliWithTime()
?? o.PaymentDate?.MiladiToJalaliWithTime()
?? "-",
o.ItemsCount,
o.GatewayAmount,
o.DiscountBalanceUsed,
GetStatusText(o.Status)));
}
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);
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()
@@ -572,12 +578,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();
@@ -587,14 +593,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;
}
}
@@ -27,6 +27,8 @@
<MudSelectItem T="int" Value="2">تحویل پست</MudSelectItem>
<MudSelectItem T="int" Value="3">تحویل به مشتری</MudSelectItem>
<MudSelectItem T="int" Value="4">مرجوع شده</MudSelectItem>
<MudSelectItem T="int" Value="5">لغو شده</MudSelectItem>
<MudSelectItem T="int" Value="6">آماده تحویل در دفتر</MudSelectItem>
</MudSelect>
</MudItem>
@@ -56,6 +58,24 @@
</MudItem>
}
@if (_newStatus == 5)
{
<MudItem xs="12">
<MudAlert Severity="Severity.Warning">
توجه: وضعیت «لغو شده» یعنی ارسال این سفارش متوقف شده است.
</MudAlert>
</MudItem>
}
@if (_newStatus == 6)
{
<MudItem xs="12">
<MudAlert Severity="Severity.Info">
سفارش برای تحویل حضوری در دفتر آماده است.
</MudAlert>
</MudItem>
}
@if (_newStatus == 2)
{
<MudItem xs="12">
@@ -37,6 +37,8 @@ public partial class ChangeOrderStatusDialog
2 => "تحویل پست",
3 => "تحویل به مشتری",
4 => "مرجوع شده",
5 => "لغو شده",
6 => "آماده تحویل در دفتر",
_ => "بدون ارسال / نامشخص"
};
}
@@ -49,6 +51,8 @@ public partial class ChangeOrderStatusDialog
2 => Color.Info,
3 => Color.Success,
4 => Color.Error,
5 => Color.Dark,
6 => Color.Primary,
_ => Color.Default
};
}
@@ -95,6 +95,8 @@
<MudSelectItem T="int" Value="2">تحویل پست</MudSelectItem>
<MudSelectItem T="int" Value="3">تحویل به مشتری</MudSelectItem>
<MudSelectItem T="int" Value="4">مرجوع شده</MudSelectItem>
<MudSelectItem T="int" Value="5">لغو شده</MudSelectItem>
<MudSelectItem T="int" Value="6">آماده تحویل در دفتر</MudSelectItem>
</MudSelect>
</MudText>
<MudTextField T="string"
@@ -123,6 +125,14 @@
{
<MudText Typo="Typo.caption">آدرسی ثبت نشده است.</MudText>
}
@if (!string.IsNullOrWhiteSpace(_model.PostalCode))
{
<MudText Typo="Typo.body2">کد پستی: @_model.PostalCode</MudText>
}
@if (!string.IsNullOrWhiteSpace(_model.UserMobile))
{
<MudText Typo="Typo.body2">تلفن سفارش‌دهنده: @_model.UserMobile</MudText>
}
</MudStack>
</MudPaper>
@@ -162,6 +172,13 @@
</MudStack>
</DialogContent>
<DialogActions>
<MudButton Variant="Variant.Outlined" Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.Print"
OnClick="PrintInvoiceAsync"
Disabled="_model is null || _isLoading">
چاپ فاکتور
</MudButton>
<MudSpacer />
<MudButton Variant="Variant.Outlined" Color="Color.Default" OnClick="Close">
بستن
</MudButton>
@@ -1,6 +1,8 @@
using BackOffice.Common.Utilities;
using CMSMicroservice.Protobuf.Protos;
using CMSMicroservice.Protobuf.Protos.UserOrder;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using MudBlazor;
namespace BackOffice.Pages.UserOrder.Components;
@@ -9,6 +11,7 @@ public partial class UserOrderDetailsDialog
{
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = default!;
[Inject] public UserOrderContract.UserOrderContractClient UserOrderContract { get; set; } = default!;
[Inject] private IJSRuntime JsRuntime { get; set; } = default!;
[Parameter] public long OrderId { get; set; }
@@ -56,6 +59,8 @@ public partial class UserOrderDetailsDialog
2 => Color.Info, // InTransit
3 => Color.Success, // Delivered
4 => Color.Error, // Returned
5 => Color.Dark, // Cancelled
6 => Color.Primary, // ReadyForOfficePickup
_ => Color.Default // None / Unknown
};
}
@@ -68,6 +73,8 @@ public partial class UserOrderDetailsDialog
2 => "تحویل پست",
3 => "تحویل به مشتری",
4 => "مرجوع شده",
5 => "لغو شده",
6 => "آماده تحویل در دفتر",
_ => "بدون ارسال / نامشخص"
};
}
@@ -178,5 +185,61 @@ public partial class UserOrderDetailsDialog
MudDialog.Close(DialogResult.Ok(true));
}
private async Task PrintInvoiceAsync()
{
if (_model is null) return;
var vat = _model.VatInfo;
var lines = _model.FactorDetails.Select(f =>
{
var unit = f.UnitPrice ?? 0;
var count = f.Count ?? 0;
var discountUnit = f.UnitDiscountPrice ?? 0;
var lineDiscount = Math.Max(0, (unit - discountUnit) * count);
var lineTotal = discountUnit > 0 ? discountUnit * count : unit * count;
return new OrderInvoicePrint.InvoiceLine
{
Title = f.ProductTitle ?? "",
Quantity = count,
UnitPrice = unit,
Discount = lineDiscount,
LineTotal = lineTotal
};
}).ToList();
var subtotal = lines.Sum(l => l.UnitPrice * l.Quantity);
var discountTotal = lines.Sum(l => l.Discount);
var total = vat is { TotalAmount: > 0 } ? vat.TotalAmount : _model.Amount;
var html = OrderInvoicePrint.BuildHtml(new OrderInvoicePrint.InvoiceModel
{
ShopTitle = "فروشگاه عادی — کارا بازار سلامت",
OrderId = _model.Id.ToString(),
CustomerName = _model.UserFullName,
CustomerMobile = _model.UserMobile,
CustomerNationalCode = _model.UserNationalCode,
Address = _model.UserAddressText,
PostalCode = _model.PostalCode,
PaymentStatus = _model.PaymentStatus switch
{
PaymentStatus.Success => "پرداخت شده",
PaymentStatus.Pending => "در انتظار پرداخت",
_ => "پرداخت نشده"
},
PaymentMethod = GetPaymentMethodText(_model.PaymentMethod.GetHashCode()),
TransactionId = _model.TransactionId?.ToString(),
TrackingCode = _trackingCode ?? _model.TrackingCode,
Created = null,
PaymentDate = _model.PaymentDate?.ToDateTime(),
Subtotal = subtotal,
DiscountTotal = discountTotal,
VatAmount = vat?.VatAmount ?? 0,
TotalAmount = total,
Lines = lines
});
await JsRuntime.InvokeVoidAsync("jsHtmlToPdf", html, $"فاکتور سفارش {_model.Id}");
}
private void Close() => MudDialog.Close();
}
@@ -46,6 +46,7 @@
<MudSelectItem T="int?" Value="@(3)">تحویل به مشتری</MudSelectItem>
<MudSelectItem T="int?" Value="@(4)">مرجوع شده</MudSelectItem>
<MudSelectItem T="int?" Value="@(5)">لغو شده</MudSelectItem>
<MudSelectItem T="int?" Value="@(6)">آماده تحویل در دفتر</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="3">
@@ -1,4 +1,5 @@
using System.Text;
using BackOffice.Common.Utilities;
using CMSMicroservice.Protobuf.Protos;
using CMSMicroservice.Protobuf.Protos.UserOrder;
using Microsoft.AspNetCore.Components;
@@ -215,7 +216,8 @@ public partial class OrderSalesReports
2 => Color.Info,
3 => Color.Success,
4 => Color.Error,
5 => Color.Error,
5 => Color.Dark,
6 => Color.Primary,
_ => Color.Default
};
}
@@ -229,6 +231,7 @@ public partial class OrderSalesReports
3 => "تحویل به مشتری",
4 => "مرجوع شده",
5 => "لغو شده",
6 => "آماده تحویل در دفتر",
_ => "بدون ارسال / نامشخص"
};
}
@@ -241,27 +244,26 @@ public partial class OrderSalesReports
return;
}
var sb = new StringBuilder();
sb.AppendLine("شناسه,کاربر,نام کاربر,مبلغ,وضعیت پرداخت,وضعیت ارسال,تاریخ پرداخت");
foreach (var o in _orders)
var headers = new[]
{
sb.AppendLine(string.Join(",",
o.Id,
o.UserId,
EscapeCsv(o.UserFullName),
o.Amount,
EscapeCsv(o.PaymentStatus == PaymentStatus.Success ? "پرداخت شده" : "پرداخت نشده"),
EscapeCsv(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";
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() : "-"
});
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
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()
@@ -290,7 +292,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();
@@ -349,12 +351,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;
}
}
@@ -56,6 +56,7 @@
<MudSelectItem T="int?" Value="@(3)">تحویل به مشتری</MudSelectItem>
<MudSelectItem T="int?" Value="@(4)">مرجوع شده</MudSelectItem>
<MudSelectItem T="int?" Value="@(5)">لغو شده</MudSelectItem>
<MudSelectItem T="int?" Value="@(6)">آماده تحویل در دفتر</MudSelectItem>
</MudSelect>
</MudItem>
</Filters>
@@ -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;
@@ -213,6 +214,8 @@ public partial class UserOrderMainPage
2 => Color.Info, // InTransit
3 => Color.Success, // Delivered
4 => Color.Error, // Returned
5 => Color.Dark, // Cancelled
6 => Color.Primary, // ReadyForOfficePickup
_ => Color.Default // None / Unknown
};
}
@@ -225,6 +228,8 @@ public partial class UserOrderMainPage
2 => "تحویل پست",
3 => "تحویل به مشتری",
4 => "مرجوع شده",
5 => "لغو شده",
6 => "آماده تحویل در دفتر",
_ => "بدون ارسال / نامشخص"
};
}
@@ -238,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
@@ -315,40 +329,31 @@ public partial class UserOrderMainPage
return;
}
var sb = new StringBuilder();
sb.AppendLine("شناسه,کاربر,نام کاربر,مبلغ,وضعیت پرداخت,وضعیت ارسال,روش پرداخت,تاریخ پرداخت");
foreach (var o in result.Models)
var headers = new[]
{
sb.AppendLine(string.Join(",",
o.Id,
o.UserId,
EscapeCsv(o.UserFullName),
o.Amount,
EscapeCsv(o.PaymentStatus.ToString()),
EscapeCsv(GetDeliveryStatusText(o.DeliveryStatus.GetHashCode())),
EscapeCsv(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";
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() ?? "-"
});
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
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)
{
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;
}
}
@@ -102,6 +102,7 @@ public class DiscountOrderService : IDiscountOrderService
DiscountBalanceUsed = response.DiscountBalanceUsed,
GatewayAmount = response.GatewayAmount,
PaymentCompleted = response.PaymentCompleted,
PaymentStatusValue = (int)response.PaymentStatus,
TransactionId = response.TransactionId,
Status = MapFromProtoStatus(response.DeliveryStatus, response.PaymentCompleted),
TrackingCode = response.TrackingCode,
@@ -200,12 +201,13 @@ public class DiscountOrderService : IDiscountOrderService
return status switch
{
OrderStatus.Pending => ProtoDeliveryStatus.DeliveryPending,
OrderStatus.Paid => ProtoDeliveryStatus.DeliveryPending, // Paid but not processed yet
OrderStatus.Paid => ProtoDeliveryStatus.DeliveryPending,
OrderStatus.Processing => ProtoDeliveryStatus.DeliveryProcessing,
OrderStatus.Shipped => ProtoDeliveryStatus.DeliveryShipped,
OrderStatus.Delivered => ProtoDeliveryStatus.DeliveryDelivered,
OrderStatus.Cancelled => ProtoDeliveryStatus.DeliveryCancelled,
OrderStatus.Returned => ProtoDeliveryStatus.DeliveryCancelled, // Treat as cancelled
OrderStatus.ReadyForOfficePickup => ProtoDeliveryStatus.DeliveryReadyForOffice,
OrderStatus.Returned => ProtoDeliveryStatus.DeliveryReturned,
_ => ProtoDeliveryStatus.DeliveryPending
};
}
@@ -221,6 +223,8 @@ public class DiscountOrderService : IDiscountOrderService
ProtoDeliveryStatus.DeliveryShipped => OrderStatus.Shipped,
ProtoDeliveryStatus.DeliveryDelivered => OrderStatus.Delivered,
ProtoDeliveryStatus.DeliveryCancelled => OrderStatus.Cancelled,
ProtoDeliveryStatus.DeliveryReadyForOffice => OrderStatus.ReadyForOfficePickup,
ProtoDeliveryStatus.DeliveryReturned => OrderStatus.Returned,
_ => OrderStatus.Pending
};
}
@@ -235,6 +239,8 @@ public class DiscountOrderService : IDiscountOrderService
ProtoDeliveryStatus.DeliveryShipped => OrderStatus.Shipped,
ProtoDeliveryStatus.DeliveryDelivered => OrderStatus.Delivered,
ProtoDeliveryStatus.DeliveryCancelled => OrderStatus.Cancelled,
ProtoDeliveryStatus.DeliveryReadyForOffice => OrderStatus.ReadyForOfficePickup,
ProtoDeliveryStatus.DeliveryReturned => OrderStatus.Returned,
_ => OrderStatus.Pending
};
}
@@ -27,13 +27,14 @@ public class OrderFilterDto
// For backward compatibility with UI - maps to proto DeliveryStatus
public enum OrderStatus
{
Pending = 0, // DELIVERY_PENDING
Paid = 1, // Not in proto - handle in service
Processing = 2, // DELIVERY_PROCESSING
Shipped = 3, // DELIVERY_SHIPPED
Delivered = 4, // DELIVERY_DELIVERED
Cancelled = 5, // DELIVERY_CANCELLED
Returned = 6 // Not in proto - handle in service
Pending = 0, // DELIVERY_PENDING
Paid = 1, // Not in proto - handle in service
Processing = 2, // DELIVERY_PROCESSING
Shipped = 3, // DELIVERY_SHIPPED
Delivered = 4, // DELIVERY_DELIVERED
Cancelled = 5, // DELIVERY_CANCELLED
Returned = 6, // DELIVERY_RETURNED
ReadyForOfficePickup = 7 // DELIVERY_READY_FOR_OFFICE
}
public class DiscountOrderDto
@@ -62,11 +63,17 @@ public class DiscountOrderDetailsDto
public long Id { get; set; }
public long UserId { get; set; }
public string OrderNumber { get; set; } = string.Empty;
/// <summary>از ردیف لیست پر می‌شود (GetOrderById این فیلدها را ندارد)</summary>
public string UserFullName { get; set; } = string.Empty;
public string UserMobile { get; set; } = string.Empty;
public AddressInfoDto? Address { get; set; }
public long TotalPrice { get; set; }
public long DiscountBalanceUsed { get; set; }
public long GatewayAmount { get; set; }
public long VatAmount { get; set; }
public bool PaymentCompleted { get; set; }
/// <summary>مقدار proto PaymentStatus: 0 Pending, 1 Completed, 2 Failed, 3 Refunded</summary>
public int PaymentStatusValue { get; set; }
public string? TransactionId { get; set; }
public OrderStatus Status { get; set; }
public string? TrackingCode { get; set; }
+5
View File
@@ -77,6 +77,11 @@
Icon="@Icons.Material.Filled.Groups">
اعضا و گزارش‌ها
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/club/customer-packages"
Icon="@Icons.Material.Filled.PeopleAlt">
گزارش مشتریان پکیج
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/club/features"
Icon="@Icons.Material.Filled.Star">