feat: Implement message templates dialog with local storage support
- Added MessageTemplatesDialog component for managing message templates. - Integrated local storage to persist templates in the user's browser. - Implemented functionality to add, delete, and use templates. - Added UI components for template management including MudBlazor components. feat: Create assign tags dialog for product management - Developed AssignTagsDialog component to assign tags to products. - Integrated tag selection and display of current tags. - Implemented functionality to add and remove tags from products. feat: Implement tag management functionality - Created TagEditDialog for creating and editing tags. - Developed TagManagementPage for listing, searching, and managing tags. - Integrated CRUD operations for tags using ITagService. - Added filtering options for active/inactive tags. feat: Enhance user order management with discount and cancellation dialogs - Implemented ApplyDiscountDialog for applying discounts to orders. - Created CancelOrderDialog for canceling orders with optional refund. - Developed ChangeOrderStatusDialog for updating order delivery statuses. - Integrated gRPC services for order management operations.
This commit is contained in:
@@ -15,12 +15,15 @@ using BackOffice.BFF.Health.Protobuf;
|
|||||||
using BackOffice.BFF.DiscountProduct.Protobuf.Protos.DiscountProduct;
|
using BackOffice.BFF.DiscountProduct.Protobuf.Protos.DiscountProduct;
|
||||||
using BackOffice.BFF.DiscountCategory.Protobuf.Protos.DiscountCategory;
|
using BackOffice.BFF.DiscountCategory.Protobuf.Protos.DiscountCategory;
|
||||||
using BackOffice.BFF.DiscountOrder.Protobuf.Protos.DiscountOrder;
|
using BackOffice.BFF.DiscountOrder.Protobuf.Protos.DiscountOrder;
|
||||||
|
using BackOffice.BFF.Tag.Protobuf.Protos.Tag;
|
||||||
|
using BackOffice.BFF.ProductTag.Protobuf.Protos.ProductTag;
|
||||||
using BackOffice.BFF.PublicMessage.Protobuf.Protos.PublicMessage;
|
using BackOffice.BFF.PublicMessage.Protobuf.Protos.PublicMessage;
|
||||||
using BackOffice.Common.Utilities;
|
using BackOffice.Common.Utilities;
|
||||||
using BackOffice.Services.DiscountProduct;
|
using BackOffice.Services.DiscountProduct;
|
||||||
using BackOffice.Services.DiscountCategory;
|
using BackOffice.Services.DiscountCategory;
|
||||||
using BackOffice.Services.DiscountOrder;
|
using BackOffice.Services.DiscountOrder;
|
||||||
using BackOffice.Services.PublicMessage;
|
using BackOffice.Services.PublicMessage;
|
||||||
|
using BackOffice.Services.Tag;
|
||||||
using Blazored.LocalStorage;
|
using Blazored.LocalStorage;
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
using Grpc.Core.Interceptors;
|
using Grpc.Core.Interceptors;
|
||||||
@@ -60,6 +63,7 @@ public static class ConfigureServices
|
|||||||
services.AddScoped<IDiscountCategoryService, DiscountCategoryService>();
|
services.AddScoped<IDiscountCategoryService, DiscountCategoryService>();
|
||||||
services.AddScoped<IDiscountOrderService, DiscountOrderService>();
|
services.AddScoped<IDiscountOrderService, DiscountOrderService>();
|
||||||
services.AddScoped<IPublicMessageService, PublicMessageService>();
|
services.AddScoped<IPublicMessageService, PublicMessageService>();
|
||||||
|
services.AddScoped<ITagService, TagService>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
@@ -111,6 +115,10 @@ public static class ConfigureServices
|
|||||||
// Public Message Service
|
// Public Message Service
|
||||||
services.AddTransient(sp => new PublicMessagesContract.PublicMessagesContractClient(sp.GetRequiredService<CallInvoker>()));
|
services.AddTransient(sp => new PublicMessagesContract.PublicMessagesContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
|
|
||||||
|
// Tag Management Services
|
||||||
|
services.AddTransient(sp => new TagContract.TagContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
|
services.AddTransient(sp => new ProductTagContract.ProductTagContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,5 +15,6 @@ public static class RouteConstance
|
|||||||
public const string Products = "/ProductsPage/";
|
public const string Products = "/ProductsPage/";
|
||||||
public const string Category = "/CategoryPage/";
|
public const string Category = "/CategoryPage/";
|
||||||
public const string ProductCategories = "/ProductCategoriesPage/";
|
public const string ProductCategories = "/ProductCategoriesPage/";
|
||||||
|
public const string ProductsBulkEdit = "/ProductsBulkEditPage/";
|
||||||
public const string CategoryProducts = "/CategoryProductsPage/";
|
public const string CategoryProducts = "/CategoryProductsPage/";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,4 +15,8 @@ public class WithdrawalRequestModel
|
|||||||
public DateTime RequestDate { get; set; }
|
public DateTime RequestDate { get; set; }
|
||||||
public DateTime? ProcessedDate { get; set; }
|
public DateTime? ProcessedDate { get; set; }
|
||||||
public string? AdminNote { get; set; }
|
public string? AdminNote { get; set; }
|
||||||
|
public string? BankReferenceId { get; set; }
|
||||||
|
public string? BankTrackingCode { get; set; }
|
||||||
|
public string? PaymentFailureReason { get; set; }
|
||||||
|
public string? IbanNumber { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
@using MudBlazor
|
@using MudBlazor
|
||||||
@using BackOffice.BFF.Commission.Protobuf
|
@using BackOffice.BFF.Commission.Protobuf
|
||||||
@using Google.Protobuf.WellKnownTypes
|
@using Google.Protobuf.WellKnownTypes
|
||||||
|
@using Microsoft.JSInterop
|
||||||
|
@using System.Text
|
||||||
|
|
||||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||||
<MudText Typo="Typo.h4" Class="mb-1">گزارش برداشتها</MudText>
|
<MudText Typo="Typo.h4" Class="mb-1">گزارش برداشتها</MudText>
|
||||||
@@ -69,6 +71,21 @@
|
|||||||
جستجو
|
جستجو
|
||||||
</MudButton>
|
</MudButton>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
|
<MudItem xs="12" md="4" Class="d-flex align-end justify-end">
|
||||||
|
<MudButton Variant="Variant.Outlined"
|
||||||
|
Color="Color.Success"
|
||||||
|
StartIcon="@Icons.Material.Filled.Download"
|
||||||
|
OnClick="ExportToExcel"
|
||||||
|
Class="ml-2">
|
||||||
|
خروجی Excel
|
||||||
|
</MudButton>
|
||||||
|
<MudButton Variant="Variant.Outlined"
|
||||||
|
Color="Color.Default"
|
||||||
|
StartIcon="@Icons.Material.Filled.PictureAsPdf"
|
||||||
|
OnClick="ExportToPdf">
|
||||||
|
خروجی PDF
|
||||||
|
</MudButton>
|
||||||
|
</MudItem>
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
</MudCardContent>
|
</MudCardContent>
|
||||||
</MudCard>
|
</MudCard>
|
||||||
@@ -117,6 +134,37 @@
|
|||||||
</MudItem>
|
</MudItem>
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
|
|
||||||
|
<MudGrid Class="mb-4">
|
||||||
|
<MudItem xs="12" md="6">
|
||||||
|
<MudCard>
|
||||||
|
<MudCardHeader>
|
||||||
|
<MudText Typo="Typo.h6">نمودار مبلغ برداشتها</MudText>
|
||||||
|
</MudCardHeader>
|
||||||
|
<MudCardContent>
|
||||||
|
<MudChart ChartType="ChartType.Line"
|
||||||
|
ChartSeries="@_amountSeries"
|
||||||
|
XAxisLabels="@_chartLabels"
|
||||||
|
Width="100%"
|
||||||
|
Height="300px" />
|
||||||
|
</MudCardContent>
|
||||||
|
</MudCard>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" md="6">
|
||||||
|
<MudCard>
|
||||||
|
<MudCardHeader>
|
||||||
|
<MudText Typo="Typo.h6">نمودار تعداد درخواستها</MudText>
|
||||||
|
</MudCardHeader>
|
||||||
|
<MudCardContent>
|
||||||
|
<MudChart ChartType="ChartType.Line"
|
||||||
|
ChartSeries="@_countSeries"
|
||||||
|
XAxisLabels="@_chartLabels"
|
||||||
|
Width="100%"
|
||||||
|
Height="300px" />
|
||||||
|
</MudCardContent>
|
||||||
|
</MudCard>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
|
||||||
<MudCard>
|
<MudCard>
|
||||||
<MudCardContent>
|
<MudCardContent>
|
||||||
<MudDataGrid T="PeriodReportViewModel"
|
<MudDataGrid T="PeriodReportViewModel"
|
||||||
@@ -164,6 +212,7 @@
|
|||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Inject] public CommissionContract.CommissionContractClient CommissionClient { get; set; }
|
[Inject] public CommissionContract.CommissionContractClient CommissionClient { get; set; }
|
||||||
|
[Inject] public IJSRuntime JsRuntime { get; set; }
|
||||||
|
|
||||||
private DateTime? _startDate = DateTime.Today.AddDays(-30);
|
private DateTime? _startDate = DateTime.Today.AddDays(-30);
|
||||||
private DateTime? _endDate = DateTime.Today;
|
private DateTime? _endDate = DateTime.Today;
|
||||||
@@ -174,6 +223,9 @@
|
|||||||
private bool _isLoading;
|
private bool _isLoading;
|
||||||
private WithdrawalSummaryViewModel _summary = new();
|
private WithdrawalSummaryViewModel _summary = new();
|
||||||
private List<PeriodReportViewModel> _periodReports = new();
|
private List<PeriodReportViewModel> _periodReports = new();
|
||||||
|
private string[] _chartLabels = Array.Empty<string>();
|
||||||
|
private List<ChartSeries> _amountSeries = new();
|
||||||
|
private List<ChartSeries> _countSeries = new();
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
@@ -258,12 +310,17 @@
|
|||||||
PendingAmount = p.PendingAmount
|
PendingAmount = p.PendingAmount
|
||||||
})
|
})
|
||||||
.ToList() ?? new List<PeriodReportViewModel>();
|
.ToList() ?? new List<PeriodReportViewModel>();
|
||||||
|
|
||||||
|
BuildCharts();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Snackbar.Add($"خطا در بارگذاری گزارشها: {ex.Message}", Severity.Error);
|
Snackbar.Add($"خطا در بارگذاری گزارشها: {ex.Message}", Severity.Error);
|
||||||
_summary = new WithdrawalSummaryViewModel();
|
_summary = new WithdrawalSummaryViewModel();
|
||||||
_periodReports.Clear();
|
_periodReports.Clear();
|
||||||
|
_chartLabels = Array.Empty<string>();
|
||||||
|
_amountSeries = new List<ChartSeries>();
|
||||||
|
_countSeries = new List<ChartSeries>();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -271,6 +328,141 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void BuildCharts()
|
||||||
|
{
|
||||||
|
if (_periodReports.Count == 0)
|
||||||
|
{
|
||||||
|
_chartLabels = Array.Empty<string>();
|
||||||
|
_amountSeries = new List<ChartSeries>();
|
||||||
|
_countSeries = new List<ChartSeries>();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ordered = _periodReports.OrderBy(r => r.StartDate).ToList();
|
||||||
|
_chartLabels = ordered.Select(r => r.PeriodLabel).ToArray();
|
||||||
|
|
||||||
|
_amountSeries = new List<ChartSeries>
|
||||||
|
{
|
||||||
|
new ChartSeries
|
||||||
|
{
|
||||||
|
Name = "مبلغ کل",
|
||||||
|
Data = ordered.Select(r => (double)r.TotalAmount).ToArray()
|
||||||
|
},
|
||||||
|
new ChartSeries
|
||||||
|
{
|
||||||
|
Name = "پرداخت شده",
|
||||||
|
Data = ordered.Select(r => (double)r.PaidAmount).ToArray()
|
||||||
|
},
|
||||||
|
new ChartSeries
|
||||||
|
{
|
||||||
|
Name = "در انتظار پرداخت",
|
||||||
|
Data = ordered.Select(r => (double)r.PendingAmount).ToArray()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_countSeries = new List<ChartSeries>
|
||||||
|
{
|
||||||
|
new ChartSeries
|
||||||
|
{
|
||||||
|
Name = "کل درخواستها",
|
||||||
|
Data = ordered.Select(r => (double)r.TotalRequests).ToArray()
|
||||||
|
},
|
||||||
|
new ChartSeries
|
||||||
|
{
|
||||||
|
Name = "موفق",
|
||||||
|
Data = ordered.Select(r => (double)r.CompletedCount).ToArray()
|
||||||
|
},
|
||||||
|
new ChartSeries
|
||||||
|
{
|
||||||
|
Name = "ناموفق",
|
||||||
|
Data = ordered.Select(r => (double)r.FailedCount).ToArray()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExportToExcel()
|
||||||
|
{
|
||||||
|
if (_periodReports.Count == 0)
|
||||||
|
{
|
||||||
|
Snackbar.Add("دادهای برای خروجی وجود ندارد.", Severity.Info);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("Period,StartDate,EndDate,TotalRequests,Completed,Failed,Pending,TotalAmount,PaidAmount,PendingAmount");
|
||||||
|
|
||||||
|
foreach (var r in _periodReports.OrderBy(r => r.StartDate))
|
||||||
|
{
|
||||||
|
sb.AppendLine(string.Join(",",
|
||||||
|
EscapeCsv(r.PeriodLabel),
|
||||||
|
r.StartDate.ToString("yyyy-MM-dd"),
|
||||||
|
r.EndDate.ToString("yyyy-MM-dd"),
|
||||||
|
r.TotalRequests,
|
||||||
|
r.CompletedCount,
|
||||||
|
r.FailedCount,
|
||||||
|
r.PendingCount,
|
||||||
|
r.TotalAmount,
|
||||||
|
r.PaidAmount,
|
||||||
|
r.PendingAmount));
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(sb.ToString());
|
||||||
|
var base64 = Convert.ToBase64String(bytes);
|
||||||
|
var filename = $"withdrawal-reports-{DateTime.Now:yyyyMMddHHmmss}.csv";
|
||||||
|
|
||||||
|
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
|
||||||
|
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExportToPdf()
|
||||||
|
{
|
||||||
|
if (_periodReports.Count == 0)
|
||||||
|
{
|
||||||
|
Snackbar.Add("دادهای برای خروجی وجود ندارد.", Severity.Info);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("گزارش برداشتها");
|
||||||
|
sb.AppendLine($"دوره: {_periodType} از تاریخ: {_startDate:yyyy/MM/dd} تا تاریخ: {_endDate:yyyy/MM/dd}");
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
sb.AppendLine("خلاصه:");
|
||||||
|
sb.AppendLine($"تعداد کل درخواستها: {_summary.TotalRequests}");
|
||||||
|
sb.AppendLine($"مبلغ کل برداشتها: {_summary.TotalAmount:N0} ریال");
|
||||||
|
sb.AppendLine($"مبلغ پرداخت شده: {_summary.TotalPaid:N0} ریال");
|
||||||
|
sb.AppendLine($"مبلغ در انتظار: {_summary.TotalPending:N0} ریال");
|
||||||
|
sb.AppendLine($"تعداد کاربران یکتا: {_summary.UniqueUsers}");
|
||||||
|
sb.AppendLine($"نرخ موفقیت: {_summary.SuccessRate:N2}%");
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
sb.AppendLine("جزئیات دورهای:");
|
||||||
|
sb.AppendLine("دوره | تاریخ شروع | تاریخ پایان | تعداد کل | موفق | ناموفق | در انتظار | مبلغ کل | پرداخت شده | در انتظار پرداخت");
|
||||||
|
|
||||||
|
foreach (var r in _periodReports.OrderBy(r => r.StartDate))
|
||||||
|
{
|
||||||
|
sb.AppendLine(
|
||||||
|
$"{r.PeriodLabel} | {r.StartDate:yyyy/MM/dd} | {r.EndDate:yyyy/MM/dd} | {r.TotalRequests} | {r.CompletedCount} | {r.FailedCount} | {r.PendingCount} | {r.TotalAmount:N0} | {r.PaidAmount:N0} | {r.PendingAmount:N0}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(sb.ToString());
|
||||||
|
var base64 = Convert.ToBase64String(bytes);
|
||||||
|
var filename = $"withdrawal-reports-{DateTime.Now:yyyyMMddHHmmss}.txt";
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
private enum PeriodType
|
private enum PeriodType
|
||||||
{
|
{
|
||||||
Daily = 1,
|
Daily = 1,
|
||||||
@@ -306,4 +498,3 @@
|
|||||||
public long PendingAmount { get; set; }
|
public long PendingAmount { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,16 +15,33 @@
|
|||||||
<MudText Typo="Typo.h6">درخواستهای برداشت</MudText>
|
<MudText Typo="Typo.h6">درخواستهای برداشت</MudText>
|
||||||
<MudSpacer />
|
<MudSpacer />
|
||||||
<MudStack Row="true" Spacing="2">
|
<MudStack Row="true" Spacing="2">
|
||||||
|
<MudNumericField T="long?"
|
||||||
|
HideSpinButtons="true"
|
||||||
|
Clearable="true"
|
||||||
|
Label="شناسه کاربر"
|
||||||
|
@bind-Value="_filterUserId"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense"
|
||||||
|
Style="max-width: 140px;" />
|
||||||
|
<MudTextField T="string"
|
||||||
|
Clearable="true"
|
||||||
|
Label="شماره شبا (IBAN)"
|
||||||
|
@bind-Value="_filterIban"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense"
|
||||||
|
Style="max-width: 220px;" />
|
||||||
<MudSelect @bind-Value="_filterStatus"
|
<MudSelect @bind-Value="_filterStatus"
|
||||||
Label="وضعیت"
|
Label="وضعیت"
|
||||||
Variant="Variant.Outlined"
|
Variant="Variant.Outlined"
|
||||||
Margin="Margin.Dense"
|
Margin="Margin.Dense"
|
||||||
Style="max-width: 150px;">
|
Style="max-width: 150px;">
|
||||||
<MudSelectItem Value="@((int?)null)">همه</MudSelectItem>
|
<MudSelectItem Value="@((int?)null)">همه</MudSelectItem>
|
||||||
<MudSelectItem Value="@((int?)0)">در انتظار</MudSelectItem>
|
<MudSelectItem Value="@((int?)0)">در انتظار واریز</MudSelectItem>
|
||||||
<MudSelectItem Value="@((int?)1)">تایید شده</MudSelectItem>
|
<MudSelectItem Value="@((int?)1)">واریز شده به کیف پول</MudSelectItem>
|
||||||
<MudSelectItem Value="@((int?)2)">رد شده</MudSelectItem>
|
<MudSelectItem Value="@((int?)2)">درخواست برداشت</MudSelectItem>
|
||||||
<MudSelectItem Value="@((int?)3)">پردازش شده</MudSelectItem>
|
<MudSelectItem Value="@((int?)3)">برداشت شده</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@((int?)4)">خطای پرداخت بانکی</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@((int?)5)">لغو شده</MudSelectItem>
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
<MudButton Variant="Variant.Filled"
|
<MudButton Variant="Variant.Filled"
|
||||||
Color="Color.Primary"
|
Color="Color.Primary"
|
||||||
@@ -72,6 +89,21 @@
|
|||||||
</CellTemplate>
|
</CellTemplate>
|
||||||
</PropertyColumn>
|
</PropertyColumn>
|
||||||
|
|
||||||
|
<PropertyColumn Property="x => x.BankReferenceId" Title="شماره مرجع بانک" />
|
||||||
|
|
||||||
|
<PropertyColumn Property="x => x.BankTrackingCode" Title="کد پیگیری بانکی" />
|
||||||
|
|
||||||
|
<PropertyColumn Property="x => x.PaymentFailureReason" Title="دلیل خطای پرداخت">
|
||||||
|
<CellTemplate>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(context.Item.PaymentFailureReason))
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.caption" Color="Color.Error">@context.Item.PaymentFailureReason</MudText>
|
||||||
|
}
|
||||||
|
</CellTemplate>
|
||||||
|
</PropertyColumn>
|
||||||
|
|
||||||
|
<PropertyColumn Property="x => x.IbanNumber" Title="شماره شبا" />
|
||||||
|
|
||||||
<PropertyColumn Property="x => x.RequestedAt" Title="تاریخ درخواست">
|
<PropertyColumn Property="x => x.RequestedAt" Title="تاریخ درخواست">
|
||||||
<CellTemplate>
|
<CellTemplate>
|
||||||
@context.Item.RequestedAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm")
|
@context.Item.RequestedAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm")
|
||||||
@@ -87,7 +119,7 @@
|
|||||||
Color="Color.Info"
|
Color="Color.Info"
|
||||||
OnClick="@(() => ViewDetails(context.Item))" />
|
OnClick="@(() => ViewDetails(context.Item))" />
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
@if (context.Item.Status == 0)
|
@if (context.Item.Status == 2)
|
||||||
{
|
{
|
||||||
<MudTooltip Text="تایید">
|
<MudTooltip Text="تایید">
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Check"
|
<MudIconButton Icon="@Icons.Material.Filled.Check"
|
||||||
@@ -102,7 +134,7 @@
|
|||||||
OnClick="@(() => RejectRequest(context.Item))" />
|
OnClick="@(() => RejectRequest(context.Item))" />
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
}
|
}
|
||||||
@if (context.Item.Status == 1)
|
@if (context.Item.Status == 2)
|
||||||
{
|
{
|
||||||
<MudTooltip Text="پردازش">
|
<MudTooltip Text="پردازش">
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Payment"
|
<MudIconButton Icon="@Icons.Material.Filled.Payment"
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
using BackOffice.BFF.Commission.Protobuf;
|
using BackOffice.BFF.Commission.Protobuf;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
|
using Google.Protobuf.WellKnownTypes;
|
||||||
|
using GrpcWithdrawalRequestModel = BackOffice.BFF.Commission.Protobuf.WithdrawalRequestModel;
|
||||||
|
using GrpcGetWithdrawalRequestsRequest = BackOffice.BFF.Commission.Protobuf.GetWithdrawalRequestsRequest;
|
||||||
|
|
||||||
namespace BackOffice.Pages.Commission;
|
namespace BackOffice.Pages.Commission;
|
||||||
|
|
||||||
@@ -10,14 +13,14 @@ public partial class WithdrawalRequests
|
|||||||
|
|
||||||
private MudDataGrid<WithdrawalRequestModel> _gridData;
|
private MudDataGrid<WithdrawalRequestModel> _gridData;
|
||||||
private int? _filterStatus;
|
private int? _filterStatus;
|
||||||
|
private long? _filterUserId;
|
||||||
|
private string? _filterIban;
|
||||||
|
|
||||||
private async Task<GridData<WithdrawalRequestModel>> ServerReload(GridState<WithdrawalRequestModel> state)
|
private async Task<GridData<WithdrawalRequestModel>> ServerReload(GridState<WithdrawalRequestModel> state)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// TODO: Implement GetWithdrawalRequestsRequest in CMS Protobuf
|
var request = new GrpcGetWithdrawalRequestsRequest
|
||||||
/*
|
|
||||||
var request = new GetWithdrawalRequestsRequest
|
|
||||||
{
|
{
|
||||||
PageIndex = state.Page + 1,
|
PageIndex = state.Page + 1,
|
||||||
PageSize = state.PageSize
|
PageSize = state.PageSize
|
||||||
@@ -28,19 +31,26 @@ public partial class WithdrawalRequests
|
|||||||
request.Status = _filterStatus.Value;
|
request.Status = _filterStatus.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_filterUserId.HasValue && _filterUserId.Value > 0)
|
||||||
|
{
|
||||||
|
request.UserId = _filterUserId.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(_filterIban))
|
||||||
|
{
|
||||||
|
request.IbanNumber = _filterIban;
|
||||||
|
}
|
||||||
|
|
||||||
var result = await CommissionContract.GetWithdrawalRequestsAsync(request);
|
var result = await CommissionContract.GetWithdrawalRequestsAsync(request);
|
||||||
*/
|
|
||||||
|
|
||||||
// Mock data until API is ready
|
|
||||||
await Task.CompletedTask;
|
|
||||||
var result = new { Models = new List<WithdrawalRequestModel>(), TotalCount = 0 };
|
|
||||||
|
|
||||||
if (result?.Models != null && result.Models.Any())
|
if (result?.Models != null && result.Models.Any())
|
||||||
{
|
{
|
||||||
|
var items = result.Models.Select(MapToViewModel).ToList();
|
||||||
|
|
||||||
return new GridData<WithdrawalRequestModel>
|
return new GridData<WithdrawalRequestModel>
|
||||||
{
|
{
|
||||||
Items = result.Models.ToList(),
|
Items = items,
|
||||||
TotalItems = result.TotalCount
|
TotalItems = result.MetaData != null ? (int)result.MetaData.TotalCount : items.Count
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +63,39 @@ public partial class WithdrawalRequests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static WithdrawalRequestModel MapToViewModel(GrpcWithdrawalRequestModel grpcModel)
|
||||||
|
{
|
||||||
|
var requestedAt = grpcModel.RequestedAt ?? grpcModel.Created ?? new Timestamp();
|
||||||
|
|
||||||
|
return new WithdrawalRequestModel
|
||||||
|
{
|
||||||
|
Id = grpcModel.Id,
|
||||||
|
UserId = grpcModel.UserId,
|
||||||
|
UserName = grpcModel.UserName,
|
||||||
|
Amount = grpcModel.Amount,
|
||||||
|
Status = grpcModel.Status,
|
||||||
|
Method = MapMethod(grpcModel.WithdrawalMethod),
|
||||||
|
RequestedAt = requestedAt,
|
||||||
|
RequestDate = requestedAt.ToDateTime(),
|
||||||
|
ProcessedDate = grpcModel.ProcessedAt?.ToDateTime(),
|
||||||
|
AdminNote = grpcModel.Reason,
|
||||||
|
BankReferenceId = grpcModel.BankReferenceId,
|
||||||
|
BankTrackingCode = grpcModel.BankTrackingCode,
|
||||||
|
PaymentFailureReason = grpcModel.PaymentFailureReason,
|
||||||
|
IbanNumber = grpcModel.IbanNumber
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string MapMethod(int withdrawalMethod)
|
||||||
|
{
|
||||||
|
return withdrawalMethod switch
|
||||||
|
{
|
||||||
|
0 => "Cash",
|
||||||
|
1 => "Diamond",
|
||||||
|
_ => "Unknown"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private async Task ApplyFilter()
|
private async Task ApplyFilter()
|
||||||
{
|
{
|
||||||
if (_gridData != null)
|
if (_gridData != null)
|
||||||
@@ -65,10 +108,12 @@ public partial class WithdrawalRequests
|
|||||||
{
|
{
|
||||||
return status switch
|
return status switch
|
||||||
{
|
{
|
||||||
0 => Color.Warning, // Pending
|
0 => Color.Warning, // Pending
|
||||||
1 => Color.Success, // Approved
|
1 => Color.Info, // Paid to wallet
|
||||||
2 => Color.Error, // Rejected
|
2 => Color.Warning, // WithdrawRequested
|
||||||
3 => Color.Info, // Processed
|
3 => Color.Success, // Withdrawn
|
||||||
|
4 => Color.Error, // PaymentFailed
|
||||||
|
5 => Color.Default, // Cancelled
|
||||||
_ => Color.Default
|
_ => Color.Default
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -77,10 +122,12 @@ public partial class WithdrawalRequests
|
|||||||
{
|
{
|
||||||
return status switch
|
return status switch
|
||||||
{
|
{
|
||||||
0 => "در انتظار",
|
0 => "در انتظار واریز",
|
||||||
1 => "تایید شده",
|
1 => "واریز شده",
|
||||||
2 => "رد شده",
|
2 => "درخواست برداشت",
|
||||||
3 => "پردازش شده",
|
3 => "برداشت شده",
|
||||||
|
4 => "خطای پرداخت بانکی",
|
||||||
|
5 => "لغو شده",
|
||||||
_ => "نامشخص"
|
_ => "نامشخص"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -89,9 +136,8 @@ public partial class WithdrawalRequests
|
|||||||
{
|
{
|
||||||
return method switch
|
return method switch
|
||||||
{
|
{
|
||||||
"Bank" => "انتقال بانکی",
|
"Cash" => "واریز بانکی/نقدی",
|
||||||
"Crypto" => "ارز دیجیتال",
|
"Diamond" => "الماس شبکه",
|
||||||
"Cash" => "نقدی",
|
|
||||||
_ => "نامشخص"
|
_ => "نامشخص"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -113,7 +159,12 @@ public partial class WithdrawalRequests
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// TODO: Call ApproveWithdrawal API
|
var approveRequest = new ApproveWithdrawalRequest
|
||||||
|
{
|
||||||
|
PayoutId = request.Id
|
||||||
|
};
|
||||||
|
|
||||||
|
await CommissionContract.ApproveWithdrawalAsync(approveRequest);
|
||||||
Snackbar.Add("درخواست با موفقیت تایید شد", Severity.Success);
|
Snackbar.Add("درخواست با موفقیت تایید شد", Severity.Success);
|
||||||
await ApplyFilter();
|
await ApplyFilter();
|
||||||
}
|
}
|
||||||
@@ -135,7 +186,13 @@ public partial class WithdrawalRequests
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// TODO: Call RejectWithdrawal API
|
var rejectRequest = new RejectWithdrawalRequest
|
||||||
|
{
|
||||||
|
PayoutId = request.Id,
|
||||||
|
Reason = "رد توسط ادمین پنل مدیریت"
|
||||||
|
};
|
||||||
|
|
||||||
|
await CommissionContract.RejectWithdrawalAsync(rejectRequest);
|
||||||
Snackbar.Add("درخواست رد شد", Severity.Warning);
|
Snackbar.Add("درخواست رد شد", Severity.Warning);
|
||||||
await ApplyFilter();
|
await ApplyFilter();
|
||||||
}
|
}
|
||||||
@@ -157,7 +214,13 @@ public partial class WithdrawalRequests
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// TODO: Call ProcessWithdrawal API
|
var processRequest = new ProcessWithdrawalRequest
|
||||||
|
{
|
||||||
|
PayoutId = request.Id,
|
||||||
|
IsApproved = true
|
||||||
|
};
|
||||||
|
|
||||||
|
await CommissionContract.ProcessWithdrawalAsync(processRequest);
|
||||||
Snackbar.Add("درخواست با موفقیت پردازش شد", Severity.Success);
|
Snackbar.Add("درخواست با موفقیت پردازش شد", Severity.Success);
|
||||||
await ApplyFilter();
|
await ApplyFilter();
|
||||||
}
|
}
|
||||||
|
|||||||
160
src/BackOffice/Pages/Dashboard/DiscountShopWidget.razor
Normal file
160
src/BackOffice/Pages/Dashboard/DiscountShopWidget.razor
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
@using BackOffice.Services.DiscountOrder
|
||||||
|
|
||||||
|
@inject IDiscountOrderService DiscountOrderService
|
||||||
|
|
||||||
|
<MudCard Class="mb-4">
|
||||||
|
<MudCardHeader>
|
||||||
|
<CardHeaderContent>
|
||||||
|
<MudText Typo="Typo.h6">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.Discount" Class="mr-2" />
|
||||||
|
آمار فروشگاه تخفیفی (۷ روز اخیر)
|
||||||
|
</MudText>
|
||||||
|
</CardHeaderContent>
|
||||||
|
</MudCardHeader>
|
||||||
|
<MudCardContent>
|
||||||
|
@if (_loading)
|
||||||
|
{
|
||||||
|
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudGrid>
|
||||||
|
<MudItem xs="12" md="4">
|
||||||
|
<MudStack Spacing="1">
|
||||||
|
<MudText Typo="Typo.body2" Color="Color.Secondary">تعداد سفارشها (۷ روز)</MudText>
|
||||||
|
<MudText Typo="Typo.h5">@_stats.TotalOrdersLast7Days.ToString("N0")</MudText>
|
||||||
|
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||||
|
امروز: @_stats.TodayOrders.ToString("N0")
|
||||||
|
</MudText>
|
||||||
|
</MudStack>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" md="4">
|
||||||
|
<MudStack Spacing="1">
|
||||||
|
<MudText Typo="Typo.body2" Color="Color.Secondary">مجموع فروش (۷ روز)</MudText>
|
||||||
|
<MudText Typo="Typo.h5" Color="Color.Primary">
|
||||||
|
@_stats.TotalSalesLast7Days.ToString("N0") ریال
|
||||||
|
</MudText>
|
||||||
|
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||||
|
امروز: @_stats.TodaySales.ToString("N0") ریال
|
||||||
|
</MudText>
|
||||||
|
</MudStack>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" md="4">
|
||||||
|
<MudStack Spacing="1">
|
||||||
|
<MudText Typo="Typo.body2" Color="Color.Secondary">میانگین مبلغ هر سفارش</MudText>
|
||||||
|
<MudText Typo="Typo.h5">
|
||||||
|
@_stats.AverageOrderAmount.ToString("N0") ریال
|
||||||
|
</MudText>
|
||||||
|
</MudStack>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
|
||||||
|
<MudDivider Class="my-2" />
|
||||||
|
|
||||||
|
<MudText Typo="Typo.subtitle2" Class="mb-2">روند فروش روزانه (۷ روز اخیر)</MudText>
|
||||||
|
@if (_dailyLabels.Length == 0)
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||||
|
برای این بازه زمانی سفارشی ثبت نشده است.
|
||||||
|
</MudText>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudChart ChartType="ChartType.Line"
|
||||||
|
ChartSeries="@_dailySeries"
|
||||||
|
XAxisLabels="@_dailyLabels"
|
||||||
|
Height="200px" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</MudCardContent>
|
||||||
|
</MudCard>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private bool _loading;
|
||||||
|
private DiscountShopStats _stats = new();
|
||||||
|
private string[] _dailyLabels = Array.Empty<string>();
|
||||||
|
private List<ChartSeries> _dailySeries = new();
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await LoadStatsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadStatsAsync()
|
||||||
|
{
|
||||||
|
_loading = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var today = DateTime.Today;
|
||||||
|
var fromDate = today.AddDays(-6);
|
||||||
|
|
||||||
|
var filter = new OrderFilterDto
|
||||||
|
{
|
||||||
|
FromDate = fromDate,
|
||||||
|
ToDate = today,
|
||||||
|
PageNumber = 1,
|
||||||
|
PageSize = 200
|
||||||
|
};
|
||||||
|
|
||||||
|
var orders = await DiscountOrderService.GetOrdersAsync(filter);
|
||||||
|
|
||||||
|
_stats = new DiscountShopStats();
|
||||||
|
|
||||||
|
if (orders.Count > 0)
|
||||||
|
{
|
||||||
|
_stats.TotalOrdersLast7Days = orders.Count;
|
||||||
|
_stats.TotalSalesLast7Days = orders.Sum(o => o.FinalAmount);
|
||||||
|
|
||||||
|
_stats.TodayOrders = orders.Count(o => o.CreatedAt.Date == today);
|
||||||
|
_stats.TodaySales = orders.Where(o => o.CreatedAt.Date == today)
|
||||||
|
.Sum(o => o.FinalAmount);
|
||||||
|
|
||||||
|
_stats.AverageOrderAmount = _stats.TotalOrdersLast7Days > 0
|
||||||
|
? _stats.TotalSalesLast7Days / _stats.TotalOrdersLast7Days
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
var groups = orders
|
||||||
|
.GroupBy(o => o.CreatedAt.Date)
|
||||||
|
.OrderBy(g => g.Key)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
_dailyLabels = groups.Select(g => g.Key.ToString("MM/dd")).ToArray();
|
||||||
|
|
||||||
|
_dailySeries = new List<ChartSeries>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "مبلغ فروش",
|
||||||
|
Data = groups.Select(g => (double)g.Sum(o => o.FinalAmount)).ToArray()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_dailyLabels = Array.Empty<string>();
|
||||||
|
_dailySeries = new List<ChartSeries>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"خطا در بارگذاری آمار فروشگاه تخفیفی: {ex.Message}", Severity.Warning);
|
||||||
|
_stats = new DiscountShopStats();
|
||||||
|
_dailyLabels = Array.Empty<string>();
|
||||||
|
_dailySeries = new List<ChartSeries>();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DiscountShopStats
|
||||||
|
{
|
||||||
|
public int TotalOrdersLast7Days { get; set; }
|
||||||
|
public long TotalSalesLast7Days { get; set; }
|
||||||
|
public int TodayOrders { get; set; }
|
||||||
|
public long TodaySales { get; set; }
|
||||||
|
public long AverageOrderAmount { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -140,6 +140,9 @@
|
|||||||
</MudCardContent>
|
</MudCardContent>
|
||||||
</MudCard>
|
</MudCard>
|
||||||
|
|
||||||
|
<!-- Discount Shop Stats -->
|
||||||
|
<BackOffice.Pages.Dashboard.DiscountShopWidget />
|
||||||
|
|
||||||
<!-- Quick Actions -->
|
<!-- Quick Actions -->
|
||||||
<MudCard>
|
<MudCard>
|
||||||
<MudCardHeader>
|
<MudCardHeader>
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
|
||||||
|
<MudStack Spacing="2">
|
||||||
|
<MudText Typo="Typo.subtitle2">گالری تصاویر محصول</MudText>
|
||||||
|
|
||||||
|
<MudFileUpload T="IBrowserFile"
|
||||||
|
Accept="image/*"
|
||||||
|
MultiSelection="true"
|
||||||
|
FilesChanged="OnFilesSelected">
|
||||||
|
<ActivatorContent>
|
||||||
|
<MudButton HtmlTag="label"
|
||||||
|
Variant="Variant.Filled"
|
||||||
|
Color="Color.Primary"
|
||||||
|
StartIcon="@Icons.Material.Filled.Collections"
|
||||||
|
Style="cursor:pointer;">
|
||||||
|
انتخاب تصاویر (چندتایی)
|
||||||
|
</MudButton>
|
||||||
|
</ActivatorContent>
|
||||||
|
</MudFileUpload>
|
||||||
|
|
||||||
|
@if (_items.Count == 0)
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||||
|
هنوز تصویری اضافه نشده است.
|
||||||
|
</MudText>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudGrid GutterSize="2">
|
||||||
|
@foreach (var item in _items)
|
||||||
|
{
|
||||||
|
<MudItem xs="6" sm="4" md="3">
|
||||||
|
<MudPaper Class="pa-1"
|
||||||
|
Style="cursor:move;"
|
||||||
|
@ondragstart="@((e) => OnDragStart(item))"
|
||||||
|
@ondragover="OnDragOver"
|
||||||
|
@ondrop="@((e) => OnDrop(item))"
|
||||||
|
draggable="true">
|
||||||
|
<div style="position:relative;">
|
||||||
|
<img src="@item.PreviewUrl"
|
||||||
|
alt="@item.Title"
|
||||||
|
style="width:100%; height:140px; object-fit:cover; border-radius:4px;" />
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Close"
|
||||||
|
Color="Color.Error"
|
||||||
|
Size="Size.Small"
|
||||||
|
Style="position:absolute; top:4px; right:4px;"
|
||||||
|
OnClick="@(() => Remove(item))" />
|
||||||
|
</div>
|
||||||
|
<MudTextField @bind-Value="item.Title"
|
||||||
|
Label="عنوان"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense"
|
||||||
|
Immediate="true"
|
||||||
|
OnBlur="OnItemsChanged" />
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
}
|
||||||
|
</MudGrid>
|
||||||
|
}
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public List<ProductImageItem> Images { get; set; } = new();
|
||||||
|
[Parameter] public EventCallback<List<ProductImageItem>> ImagesChanged { get; set; }
|
||||||
|
|
||||||
|
private readonly List<ProductImageItem> _items = new();
|
||||||
|
private ProductImageItem? _dragging;
|
||||||
|
private readonly long _maxAllowedSize = (1024 * 1024) * 5;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
if (Images != null && Images.Count > 0)
|
||||||
|
{
|
||||||
|
_items.AddRange(Images.Select(x => new ProductImageItem
|
||||||
|
{
|
||||||
|
Id = x.Id == Guid.Empty ? Guid.NewGuid() : x.Id,
|
||||||
|
Title = x.Title,
|
||||||
|
PreviewUrl = x.PreviewUrl,
|
||||||
|
SortOrder = x.SortOrder
|
||||||
|
})
|
||||||
|
.OrderBy(x => x.SortOrder));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnFilesSelected(IReadOnlyList<IBrowserFile> files)
|
||||||
|
{
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
if (file == null) continue;
|
||||||
|
|
||||||
|
var buffer = new byte[file.Size];
|
||||||
|
await file.OpenReadStream(_maxAllowedSize).ReadAsync(buffer);
|
||||||
|
var preview = $"data:{file.ContentType};base64," + Convert.ToBase64String(buffer);
|
||||||
|
|
||||||
|
_items.Add(new ProductImageItem
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Title = file.Name,
|
||||||
|
PreviewUrl = preview,
|
||||||
|
SortOrder = _items.Count
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await OnItemsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnItemsChanged()
|
||||||
|
{
|
||||||
|
if (ImagesChanged.HasDelegate)
|
||||||
|
{
|
||||||
|
var ordered = _items
|
||||||
|
.Select((x, index) =>
|
||||||
|
{
|
||||||
|
x.SortOrder = index;
|
||||||
|
return x;
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
await ImagesChanged.InvokeAsync(ordered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDragStart(ProductImageItem item)
|
||||||
|
{
|
||||||
|
_dragging = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDragOver(DragEventArgs args)
|
||||||
|
{
|
||||||
|
args.PreventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnDrop(ProductImageItem target)
|
||||||
|
{
|
||||||
|
if (_dragging == null || ReferenceEquals(_dragging, target))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var fromIndex = _items.IndexOf(_dragging);
|
||||||
|
var toIndex = _items.IndexOf(target);
|
||||||
|
|
||||||
|
if (fromIndex < 0 || toIndex < 0 || fromIndex == toIndex)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_items.RemoveAt(fromIndex);
|
||||||
|
_items.Insert(toIndex, _dragging);
|
||||||
|
_dragging = null;
|
||||||
|
|
||||||
|
await OnItemsChanged();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Remove(ProductImageItem item)
|
||||||
|
{
|
||||||
|
_items.Remove(item);
|
||||||
|
await OnItemsChanged();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ProductImageItem
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string PreviewUrl { get; set; } = string.Empty;
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
505
src/BackOffice/Pages/DiscountShop/SalesReports.razor
Normal file
505
src/BackOffice/Pages/DiscountShop/SalesReports.razor
Normal file
@@ -0,0 +1,505 @@
|
|||||||
|
@page "/discount-sales-reports"
|
||||||
|
@attribute [Authorize(Roles = "Administrator")]
|
||||||
|
|
||||||
|
@using MudBlazor
|
||||||
|
@using BackOffice.Services.DiscountOrder
|
||||||
|
@using Microsoft.JSInterop
|
||||||
|
@using System.Text
|
||||||
|
|
||||||
|
@inject IDiscountOrderService DiscountOrderService
|
||||||
|
@inject ISnackbar Snackbar
|
||||||
|
@inject IJSRuntime JsRuntime
|
||||||
|
|
||||||
|
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-4">
|
||||||
|
<MudText Typo="Typo.h4" GutterBottom="true">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.BarChart" Class="ml-2" />
|
||||||
|
گزارش فروش فروشگاه تخفیفی
|
||||||
|
</MudText>
|
||||||
|
<MudText Typo="Typo.body2" Color="Color.Secondary" Class="mb-4">
|
||||||
|
آمار فروش، تخفیف و وضعیت سفارشهای فروشگاه تخفیفی بر اساس بازه تاریخ و وضعیت سفارش
|
||||||
|
</MudText>
|
||||||
|
|
||||||
|
<MudPaper Class="pa-4 mb-4">
|
||||||
|
<MudGrid>
|
||||||
|
<MudItem xs="12" sm="6" md="3">
|
||||||
|
<MudDatePicker @bind-Date="_fromDate"
|
||||||
|
Label="از تاریخ"
|
||||||
|
DateFormat="yyyy/MM/dd"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6" md="3">
|
||||||
|
<MudDatePicker @bind-Date="_toDate"
|
||||||
|
Label="تا تاریخ"
|
||||||
|
DateFormat="yyyy/MM/dd"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6" md="3">
|
||||||
|
<MudSelect @bind-Value="_statusFilter"
|
||||||
|
T="OrderStatus?"
|
||||||
|
Label="وضعیت سفارش"
|
||||||
|
Clearable="true"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense">
|
||||||
|
<MudSelectItem Value="@(null as OrderStatus?)">همه وضعیتها</MudSelectItem>
|
||||||
|
<MudSelectItem Value="OrderStatus.Pending">در انتظار</MudSelectItem>
|
||||||
|
<MudSelectItem Value="OrderStatus.Paid">پرداخت شده</MudSelectItem>
|
||||||
|
<MudSelectItem Value="OrderStatus.Processing">در حال پردازش</MudSelectItem>
|
||||||
|
<MudSelectItem Value="OrderStatus.Shipped">ارسال شده</MudSelectItem>
|
||||||
|
<MudSelectItem Value="OrderStatus.Delivered">تحویل شده</MudSelectItem>
|
||||||
|
<MudSelectItem Value="OrderStatus.Cancelled">لغو شده</MudSelectItem>
|
||||||
|
<MudSelectItem Value="OrderStatus.Returned">مرجوع شده</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6" md="3">
|
||||||
|
<MudTextField @bind-Value="_searchQuery"
|
||||||
|
Label="جستجو (شناسه/نام کاربر)"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense"
|
||||||
|
Adornment="Adornment.Start"
|
||||||
|
AdornmentIcon="@Icons.Material.Filled.Search" />
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
|
||||||
|
<MudGrid Class="mt-2">
|
||||||
|
<MudItem xs="12" sm="6" md="3" Class="d-flex align-end">
|
||||||
|
<MudButton Variant="Variant.Filled"
|
||||||
|
Color="Color.Primary"
|
||||||
|
OnClick="LoadReports"
|
||||||
|
StartIcon="@Icons.Material.Filled.Search">
|
||||||
|
بارگذاری گزارش
|
||||||
|
</MudButton>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6" md="3" Class="d-flex align-end justify-end">
|
||||||
|
<MudButton Variant="Variant.Outlined"
|
||||||
|
Color="Color.Success"
|
||||||
|
OnClick="ExportToExcel"
|
||||||
|
StartIcon="@Icons.Material.Filled.Download"
|
||||||
|
Class="ml-2">
|
||||||
|
خروجی Excel
|
||||||
|
</MudButton>
|
||||||
|
<MudButton Variant="Variant.Outlined"
|
||||||
|
Color="Color.Default"
|
||||||
|
OnClick="ExportToPdf"
|
||||||
|
StartIcon="@Icons.Material.Filled.PictureAsPdf">
|
||||||
|
خروجی PDF
|
||||||
|
</MudButton>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
|
@if (_loading)
|
||||||
|
{
|
||||||
|
<MudPaper Class="pa-4">
|
||||||
|
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
|
||||||
|
</MudPaper>
|
||||||
|
}
|
||||||
|
else if (_orders.Count == 0)
|
||||||
|
{
|
||||||
|
<MudAlert Severity="Severity.Info">
|
||||||
|
هیچ سفارشی برای بازه و فیلترهای انتخابشده یافت نشد.
|
||||||
|
</MudAlert>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudGrid Class="mb-4">
|
||||||
|
<MudItem xs="12" md="3">
|
||||||
|
<MudPaper Elevation="1" Class="pa-4 text-center">
|
||||||
|
<MudText Typo="Typo.subtitle2" Color="Color.Primary">تعداد سفارشها</MudText>
|
||||||
|
<MudText Typo="Typo.h5">@_totalOrders.ToString("N0")</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" md="3">
|
||||||
|
<MudPaper Elevation="1" Class="pa-4 text-center">
|
||||||
|
<MudText Typo="Typo.subtitle2" Color="Color.Success">مجموع فروش (مبلغ نهایی)</MudText>
|
||||||
|
<MudText Typo="Typo.h5">@_totalSales.ToString("N0") ریال</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" md="3">
|
||||||
|
<MudPaper Elevation="1" Class="pa-4 text-center">
|
||||||
|
<MudText Typo="Typo.subtitle2" Color="Color.Warning">مجموع تخفیف</MudText>
|
||||||
|
<MudText Typo="Typo.h5">@_totalDiscount.ToString("N0") ریال</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" md="3">
|
||||||
|
<MudPaper Elevation="1" Class="pa-4 text-center">
|
||||||
|
<MudText Typo="Typo.subtitle2" Color="Color.Info">میانگین مبلغ سفارش</MudText>
|
||||||
|
<MudText Typo="Typo.h5">@_averageOrder.ToString("N0") ریال</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
|
||||||
|
<MudGrid Class="mb-4">
|
||||||
|
<MudItem xs="12" md="6">
|
||||||
|
<MudCard>
|
||||||
|
<MudCardHeader>
|
||||||
|
<MudText Typo="Typo.h6">روند فروش</MudText>
|
||||||
|
</MudCardHeader>
|
||||||
|
<MudCardContent>
|
||||||
|
<MudChart ChartType="ChartType.Line"
|
||||||
|
ChartSeries="@_salesSeries"
|
||||||
|
XAxisLabels="@_salesLabels"
|
||||||
|
Width="100%"
|
||||||
|
Height="300px" />
|
||||||
|
</MudCardContent>
|
||||||
|
</MudCard>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" md="6">
|
||||||
|
<MudCard>
|
||||||
|
<MudCardHeader>
|
||||||
|
<MudText Typo="Typo.h6">محصولات پرفروش (بر اساس مبلغ)</MudText>
|
||||||
|
</MudCardHeader>
|
||||||
|
<MudCardContent>
|
||||||
|
@if (_topProductLabels.Length == 0)
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.body2" Color="Color.Secondary">
|
||||||
|
برای محاسبه محصولات پرفروش، تعداد سفارشها کافی نیست یا خطایی رخ داده است.
|
||||||
|
</MudText>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudChart ChartType="ChartType.Bar"
|
||||||
|
ChartSeries="@_topProductSeries"
|
||||||
|
XAxisLabels="@_topProductLabels"
|
||||||
|
Width="100%"
|
||||||
|
Height="300px" />
|
||||||
|
}
|
||||||
|
</MudCardContent>
|
||||||
|
</MudCard>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
|
||||||
|
<MudCard>
|
||||||
|
<MudCardHeader>
|
||||||
|
<MudText Typo="Typo.h6">لیست سفارشها</MudText>
|
||||||
|
</MudCardHeader>
|
||||||
|
<MudCardContent>
|
||||||
|
<MudDataGrid T="DiscountOrderDto"
|
||||||
|
Items="@_orders"
|
||||||
|
Hover="true"
|
||||||
|
Dense="true">
|
||||||
|
<Columns>
|
||||||
|
<PropertyColumn Property="x => x.OrderId" Title="شناسه سفارش" />
|
||||||
|
<PropertyColumn Property="x => x.UserId" Title="شناسه کاربر" />
|
||||||
|
<PropertyColumn Property="x => x.UserFullName" Title="نام کاربر" />
|
||||||
|
<TemplateColumn Title="تاریخ ثبت">
|
||||||
|
<CellTemplate>
|
||||||
|
@context.Item.CreatedAt.ToString("yyyy/MM/dd HH:mm")
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<TemplateColumn Title="مبلغ نهایی">
|
||||||
|
<CellTemplate>
|
||||||
|
@context.Item.FinalAmount.ToString("N0") ریال
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<TemplateColumn Title="تخفیف">
|
||||||
|
<CellTemplate>
|
||||||
|
@context.Item.TotalDiscount.ToString("N0") ریال
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<TemplateColumn Title="وضعیت">
|
||||||
|
<CellTemplate>
|
||||||
|
<MudChip T="string"
|
||||||
|
Size="Size.Small"
|
||||||
|
Color="@GetStatusColor(context.Item.Status)">
|
||||||
|
@GetStatusText(context.Item.Status)
|
||||||
|
</MudChip>
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
</Columns>
|
||||||
|
</MudDataGrid>
|
||||||
|
</MudCardContent>
|
||||||
|
</MudCard>
|
||||||
|
}
|
||||||
|
</MudContainer>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private readonly List<DiscountOrderDto> _orders = new();
|
||||||
|
private bool _loading;
|
||||||
|
|
||||||
|
private DateTime? _fromDate = DateTime.Today.AddDays(-30);
|
||||||
|
private DateTime? _toDate = DateTime.Today;
|
||||||
|
private OrderStatus? _statusFilter;
|
||||||
|
private string? _searchQuery;
|
||||||
|
|
||||||
|
private int _totalOrders;
|
||||||
|
private long _totalSales;
|
||||||
|
private long _totalDiscount;
|
||||||
|
private long _averageOrder;
|
||||||
|
|
||||||
|
private string[] _salesLabels = Array.Empty<string>();
|
||||||
|
private List<ChartSeries> _salesSeries = new();
|
||||||
|
|
||||||
|
private string[] _topProductLabels = Array.Empty<string>();
|
||||||
|
private List<ChartSeries> _topProductSeries = new();
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await LoadReports();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadReports()
|
||||||
|
{
|
||||||
|
if (_fromDate.HasValue && _toDate.HasValue && _fromDate.Value.Date > _toDate.Value.Date)
|
||||||
|
{
|
||||||
|
Snackbar.Add("تاریخ شروع نباید بعد از تاریخ پایان باشد.", Severity.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_loading = true;
|
||||||
|
_orders.Clear();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var filter = new OrderFilterDto
|
||||||
|
{
|
||||||
|
SearchQuery = _searchQuery,
|
||||||
|
Status = _statusFilter,
|
||||||
|
FromDate = _fromDate,
|
||||||
|
ToDate = _toDate,
|
||||||
|
PageNumber = 1,
|
||||||
|
PageSize = 200
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await DiscountOrderService.GetOrdersAsync(filter);
|
||||||
|
_orders.AddRange(result);
|
||||||
|
|
||||||
|
BuildSummary();
|
||||||
|
BuildSalesChart();
|
||||||
|
await BuildTopProductsChart();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"خطا در بارگذاری گزارش فروش: {ex.Message}", Severity.Error);
|
||||||
|
_orders.Clear();
|
||||||
|
_totalOrders = 0;
|
||||||
|
_totalSales = 0;
|
||||||
|
_totalDiscount = 0;
|
||||||
|
_averageOrder = 0;
|
||||||
|
_salesLabels = Array.Empty<string>();
|
||||||
|
_salesSeries = new List<ChartSeries>();
|
||||||
|
_topProductLabels = Array.Empty<string>();
|
||||||
|
_topProductSeries = new List<ChartSeries>();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildSummary()
|
||||||
|
{
|
||||||
|
_totalOrders = _orders.Count;
|
||||||
|
_totalSales = _orders.Sum(o => o.FinalAmount);
|
||||||
|
_totalDiscount = _orders.Sum(o => o.TotalDiscount);
|
||||||
|
_averageOrder = _totalOrders > 0 ? _totalSales / _totalOrders : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildSalesChart()
|
||||||
|
{
|
||||||
|
if (_orders.Count == 0)
|
||||||
|
{
|
||||||
|
_salesLabels = Array.Empty<string>();
|
||||||
|
_salesSeries = new List<ChartSeries>();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var grouped = _orders
|
||||||
|
.GroupBy(o => o.CreatedAt.Date)
|
||||||
|
.OrderBy(g => g.Key)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
_salesLabels = grouped
|
||||||
|
.Select(g => g.Key.ToString("yyyy/MM/dd"))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var totalSeries = grouped
|
||||||
|
.Select(g => (double)g.Sum(o => o.FinalAmount))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var discountSeries = grouped
|
||||||
|
.Select(g => (double)g.Sum(o => o.TotalDiscount))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
_salesSeries = new List<ChartSeries>
|
||||||
|
{
|
||||||
|
new ChartSeries { Name = "مبلغ نهایی", Data = totalSeries },
|
||||||
|
new ChartSeries { Name = "تخفیف", Data = discountSeries }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task BuildTopProductsChart()
|
||||||
|
{
|
||||||
|
_topProductLabels = Array.Empty<string>();
|
||||||
|
_topProductSeries = new List<ChartSeries>();
|
||||||
|
|
||||||
|
if (_orders.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// برای جلوگیری از فشار بیشازحد، فقط روی 50 سفارش اول کار میکنیم
|
||||||
|
var sampleOrders = _orders
|
||||||
|
.OrderByDescending(o => o.CreatedAt)
|
||||||
|
.Take(50)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var productTotals = new Dictionary<long, (string Name, long Amount)>();
|
||||||
|
|
||||||
|
foreach (var order in sampleOrders)
|
||||||
|
{
|
||||||
|
DiscountOrderDetailsDto? details;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
details = await DiscountOrderService.GetByIdAsync(order.OrderId);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details?.Items == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (var item in details.Items)
|
||||||
|
{
|
||||||
|
if (!productTotals.TryGetValue(item.ProductId, out var existing))
|
||||||
|
{
|
||||||
|
productTotals[item.ProductId] = (item.ProductTitle, item.TotalPrice);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
productTotals[item.ProductId] = (existing.Name, existing.Amount + item.TotalPrice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (productTotals.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var top = productTotals.Values
|
||||||
|
.OrderByDescending(x => x.Amount)
|
||||||
|
.Take(5)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
_topProductLabels = top
|
||||||
|
.Select(x => x.Name)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
_topProductSeries = new List<ChartSeries>
|
||||||
|
{
|
||||||
|
new ChartSeries
|
||||||
|
{
|
||||||
|
Name = "مبلغ فروش",
|
||||||
|
Data = top.Select(x => (double)x.Amount).ToArray()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Color GetStatusColor(OrderStatus status)
|
||||||
|
{
|
||||||
|
return status switch
|
||||||
|
{
|
||||||
|
OrderStatus.Pending => Color.Warning,
|
||||||
|
OrderStatus.Paid => Color.Success,
|
||||||
|
OrderStatus.Processing => Color.Info,
|
||||||
|
OrderStatus.Shipped => Color.Info,
|
||||||
|
OrderStatus.Delivered => Color.Success,
|
||||||
|
OrderStatus.Cancelled => Color.Error,
|
||||||
|
OrderStatus.Returned => Color.Error,
|
||||||
|
_ => Color.Default
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetStatusText(OrderStatus status)
|
||||||
|
{
|
||||||
|
return status switch
|
||||||
|
{
|
||||||
|
OrderStatus.Pending => "در انتظار",
|
||||||
|
OrderStatus.Paid => "پرداخت شده",
|
||||||
|
OrderStatus.Processing => "در حال پردازش",
|
||||||
|
OrderStatus.Shipped => "ارسال شده",
|
||||||
|
OrderStatus.Delivered => "تحویل شده",
|
||||||
|
OrderStatus.Cancelled => "لغو شده",
|
||||||
|
OrderStatus.Returned => "مرجوع شده",
|
||||||
|
_ => "نامشخص"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExportToExcel()
|
||||||
|
{
|
||||||
|
if (_orders.Count == 0)
|
||||||
|
{
|
||||||
|
Snackbar.Add("دادهای برای خروجی وجود ندارد.", Severity.Info);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("OrderId,UserId,UserFullName,CreatedAt,FinalAmount,TotalDiscount,Status");
|
||||||
|
|
||||||
|
foreach (var o in _orders.OrderBy(o => o.CreatedAt))
|
||||||
|
{
|
||||||
|
sb.AppendLine(string.Join(",",
|
||||||
|
o.OrderId,
|
||||||
|
o.UserId,
|
||||||
|
EscapeCsv(o.UserFullName),
|
||||||
|
o.CreatedAt.ToString("yyyy-MM-dd HH:mm"),
|
||||||
|
o.FinalAmount,
|
||||||
|
o.TotalDiscount,
|
||||||
|
GetStatusText(o.Status)));
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(sb.ToString());
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExportToPdf()
|
||||||
|
{
|
||||||
|
if (_orders.Count == 0)
|
||||||
|
{
|
||||||
|
Snackbar.Add("دادهای برای خروجی وجود ندارد.", Severity.Info);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("گزارش فروش فروشگاه تخفیفی");
|
||||||
|
sb.AppendLine($"بازه: از {_fromDate:yyyy/MM/dd} تا {_toDate:yyyy/MM/dd}");
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
sb.AppendLine("خلاصه:");
|
||||||
|
sb.AppendLine($"تعداد سفارشها: {_totalOrders}");
|
||||||
|
sb.AppendLine($"مجموع فروش (مبلغ نهایی): {_totalSales:N0} ریال");
|
||||||
|
sb.AppendLine($"مجموع تخفیف: {_totalDiscount:N0} ریال");
|
||||||
|
sb.AppendLine($"میانگین مبلغ سفارش: {_averageOrder:N0} ریال");
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
sb.AppendLine("جزئیات سفارشها:");
|
||||||
|
sb.AppendLine("شناسه | کاربر | تاریخ | مبلغ نهایی | تخفیف | وضعیت");
|
||||||
|
|
||||||
|
foreach (var o in _orders.OrderBy(o => o.CreatedAt))
|
||||||
|
{
|
||||||
|
sb.AppendLine(
|
||||||
|
$"{o.OrderId} | {o.UserFullName} | {o.CreatedAt:yyyy/MM/dd HH:mm} | {o.FinalAmount:N0} | {o.TotalDiscount:N0} | {GetStatusText(o.Status)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(sb.ToString());
|
||||||
|
var base64 = Convert.ToBase64String(bytes);
|
||||||
|
var filename = $"discount-sales-{DateTime.Now:yyyyMMddHHmmss}.txt";
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
191
src/BackOffice/Pages/Products/BulkEdit.razor
Normal file
191
src/BackOffice/Pages/Products/BulkEdit.razor
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
@attribute [Route(RouteConstance.ProductsBulkEdit)]
|
||||||
|
|
||||||
|
@using BackOffice.BFF.Products.Protobuf.Protos.Products
|
||||||
|
@using BackOffice.Common.BaseComponents
|
||||||
|
@using DataModel = BackOffice.BFF.Products.Protobuf.Protos.Products.GetAllProductsByFilterResponseModel
|
||||||
|
|
||||||
|
<BasePageComponent @ref="_basePage"
|
||||||
|
OnClearFilterClick="OnFilterCleared"
|
||||||
|
OnSubmitClick="OnFilterSubmit">
|
||||||
|
<Filters>
|
||||||
|
<MudTextField T="string"
|
||||||
|
Clearable="true"
|
||||||
|
Label="عنوان"
|
||||||
|
@bind-Value="@_request.Filter.Title"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense" />
|
||||||
|
|
||||||
|
<MudNumericField T="long?"
|
||||||
|
HideSpinButtons="true"
|
||||||
|
Clearable="true"
|
||||||
|
Label="شناسه محصول"
|
||||||
|
@bind-Value="@_request.Filter.Id"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense" />
|
||||||
|
|
||||||
|
<MudNumericField T="long?"
|
||||||
|
HideSpinButtons="true"
|
||||||
|
Clearable="true"
|
||||||
|
Label="شناسه دستهبندی"
|
||||||
|
@bind-Value="@_request.Filter.CategoryId"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense" />
|
||||||
|
|
||||||
|
<MudNumericField T="long?"
|
||||||
|
HideSpinButtons="true"
|
||||||
|
Clearable="true"
|
||||||
|
Label="قیمت"
|
||||||
|
@bind-Value="@_request.Filter.Price"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense" />
|
||||||
|
</Filters>
|
||||||
|
<Content>
|
||||||
|
<MudGrid Spacing="3">
|
||||||
|
<MudItem xs="12" md="8">
|
||||||
|
<MudPaper Class="pa-3">
|
||||||
|
<MudStack Spacing="2">
|
||||||
|
<MudStack Row="true"
|
||||||
|
AlignItems="AlignItems.Center"
|
||||||
|
Justify="Justify.SpaceBetween">
|
||||||
|
<MudText Typo="Typo.h6">ویرایش گروهی محصولات</MudText>
|
||||||
|
<MudChip T="string"
|
||||||
|
Color="Color.Primary"
|
||||||
|
Variant="Variant.Filled"
|
||||||
|
Size="Size.Small">
|
||||||
|
انتخابشدهها: @_selectedProductIds.Count
|
||||||
|
</MudChip>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
<MudDataGrid T="DataModel"
|
||||||
|
ServerData="@(new Func<GridState<DataModel>, Task<GridData<DataModel>>>(ServerReload))"
|
||||||
|
Hover="true"
|
||||||
|
@ref="_gridData"
|
||||||
|
Height="72vh">
|
||||||
|
<ColGroup>
|
||||||
|
<col />
|
||||||
|
<col style="width: 80px;" />
|
||||||
|
<col />
|
||||||
|
<col style="width: 120px;" />
|
||||||
|
<col style="width: 120px;" />
|
||||||
|
</ColGroup>
|
||||||
|
<ToolBarContent>
|
||||||
|
<MudText Typo="Typo.subtitle2">لیست محصولات برای ویرایش گروهی</MudText>
|
||||||
|
</ToolBarContent>
|
||||||
|
<Columns>
|
||||||
|
<TemplateColumn Title="">
|
||||||
|
<CellTemplate>
|
||||||
|
<MudCheckBox Checked="@_selectedProductIds.Contains(context.Item.Id)"
|
||||||
|
CheckedChanged="@(checkedValue => ToggleSelection(context.Item.Id, checkedValue))" />
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
|
||||||
|
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||||
|
<PropertyColumn Property="x => x.Title" Title="عنوان" />
|
||||||
|
<PropertyColumn Property="x => x.Price" Title="قیمت فعلی" />
|
||||||
|
<PropertyColumn Property="x => x.Discount" Title="تخفیف (%)" />
|
||||||
|
<PropertyColumn Property="x => x.RemainingCount" Title="موجودی" />
|
||||||
|
</Columns>
|
||||||
|
<PagerContent>
|
||||||
|
<MudDataGridPager T="DataModel"
|
||||||
|
PageSizeOptions="@(new int[] { 30, 60, 90 })" />
|
||||||
|
</PagerContent>
|
||||||
|
</MudDataGrid>
|
||||||
|
</MudStack>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
<MudItem xs="12" md="4">
|
||||||
|
<MudPaper Class="pa-3">
|
||||||
|
<MudStack Spacing="3">
|
||||||
|
<MudText Typo="Typo.subtitle1">تنظیمات ویرایش گروهی</MudText>
|
||||||
|
|
||||||
|
<MudDivider />
|
||||||
|
|
||||||
|
<MudText Typo="Typo.subtitle2">قیمت و تخفیف</MudText>
|
||||||
|
|
||||||
|
<MudNumericField T="long?"
|
||||||
|
HideSpinButtons="true"
|
||||||
|
Label="قیمت جدید (ریال)"
|
||||||
|
@bind-Value="_newPrice"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense" />
|
||||||
|
|
||||||
|
<MudNumericField T="int?"
|
||||||
|
HideSpinButtons="true"
|
||||||
|
Label="درصد تخفیف جدید"
|
||||||
|
@bind-Value="_newDiscount"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense" />
|
||||||
|
|
||||||
|
<MudNumericField T="int?"
|
||||||
|
HideSpinButtons="true"
|
||||||
|
Label="درصد تخفیف باشگاه جدید"
|
||||||
|
@bind-Value="_newClubDiscountPercent"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense" />
|
||||||
|
|
||||||
|
<MudDivider />
|
||||||
|
|
||||||
|
<MudText Typo="Typo.subtitle2">موجودی</MudText>
|
||||||
|
|
||||||
|
<MudSelect T="StockUpdateType?"
|
||||||
|
Label="نوع تغییر موجودی"
|
||||||
|
Clearable="true"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense"
|
||||||
|
@bind-Value="_stockUpdateType">
|
||||||
|
<MudSelectItem Value="@(StockUpdateType.Set)">تنظیم روی مقدار ثابت</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@(StockUpdateType.Add)">افزایش به مقدار فعلی</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@(StockUpdateType.Subtract)">کاهش از مقدار فعلی</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
|
||||||
|
<MudNumericField T="int?"
|
||||||
|
HideSpinButtons="true"
|
||||||
|
Label="مقدار موجودی"
|
||||||
|
@bind-Value="_stockQuantity"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense" />
|
||||||
|
|
||||||
|
<MudDivider />
|
||||||
|
|
||||||
|
<MudText Typo="Typo.subtitle2">وضعیت</MudText>
|
||||||
|
|
||||||
|
<MudSelect T="bool?"
|
||||||
|
Label="وضعیت محصول"
|
||||||
|
Clearable="true"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense"
|
||||||
|
@bind-Value="_statusEnable">
|
||||||
|
<MudSelectItem Value="@(true)">فعالسازی</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@(false)">غیرفعالسازی</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
|
||||||
|
<MudStack Row="true"
|
||||||
|
Justify="Justify.FlexEnd"
|
||||||
|
Spacing="2"
|
||||||
|
Class="mt-2">
|
||||||
|
<MudButton Variant="Variant.Outlined"
|
||||||
|
Color="Color.Default"
|
||||||
|
Disabled="_isApplying"
|
||||||
|
OnClick="ClearForm">
|
||||||
|
پاک کردن فرم
|
||||||
|
</MudButton>
|
||||||
|
<MudButton Variant="Variant.Filled"
|
||||||
|
Color="Color.Primary"
|
||||||
|
Disabled="_isApplying"
|
||||||
|
OnClick="ApplyBulkChanges">
|
||||||
|
اعمال تغییرات
|
||||||
|
</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
@if (_isApplying)
|
||||||
|
{
|
||||||
|
<MudProgressLinear Indeterminate="true" Color="Color.Primary" />
|
||||||
|
}
|
||||||
|
</MudStack>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
</Content>
|
||||||
|
</BasePageComponent>
|
||||||
|
|
||||||
289
src/BackOffice/Pages/Products/BulkEdit.razor.cs
Normal file
289
src/BackOffice/Pages/Products/BulkEdit.razor.cs
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
using BackOffice.BFF.Products.Protobuf.Protos.Products;
|
||||||
|
using CMSMicroservice.Protobuf.Protos;
|
||||||
|
using Google.Protobuf.WellKnownTypes;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using MudBlazor;
|
||||||
|
using DataModel = BackOffice.BFF.Products.Protobuf.Protos.Products.GetAllProductsByFilterResponseModel;
|
||||||
|
|
||||||
|
namespace BackOffice.Pages.Products;
|
||||||
|
|
||||||
|
public partial class BulkEdit
|
||||||
|
{
|
||||||
|
[Inject] public ProductsContract.ProductsContractClient ProductsContract { get; set; } = default!;
|
||||||
|
|
||||||
|
private BackOffice.Common.BaseComponents.BasePageComponent _basePage = default!;
|
||||||
|
private MudDataGrid<DataModel> _gridData = default!;
|
||||||
|
|
||||||
|
private GetAllProductsByFilterRequest _request = new() { Filter = new() };
|
||||||
|
private readonly HashSet<long> _selectedProductIds = new();
|
||||||
|
|
||||||
|
private bool _isLoading;
|
||||||
|
private bool _isApplying;
|
||||||
|
|
||||||
|
private long? _newPrice;
|
||||||
|
private int? _newDiscount;
|
||||||
|
private int? _newClubDiscountPercent;
|
||||||
|
|
||||||
|
private StockUpdateType? _stockUpdateType;
|
||||||
|
private int? _stockQuantity;
|
||||||
|
|
||||||
|
private bool? _statusEnable;
|
||||||
|
|
||||||
|
private async Task<GridData<DataModel>> ServerReload(GridState<DataModel> state)
|
||||||
|
{
|
||||||
|
_isLoading = true;
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
|
_selectedProductIds.Clear();
|
||||||
|
|
||||||
|
_request.Filter ??= new();
|
||||||
|
_request.PaginationState ??= new PaginationState();
|
||||||
|
_request.PaginationState.PageNumber = state.Page + 1;
|
||||||
|
_request.PaginationState.PageSize = state.PageSize;
|
||||||
|
|
||||||
|
var result = await ProductsContract.GetAllProductsByFilterAsync(_request);
|
||||||
|
|
||||||
|
_isLoading = false;
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
|
if (result != null && result.Models != null && result.Models.Any())
|
||||||
|
{
|
||||||
|
return new GridData<DataModel>
|
||||||
|
{
|
||||||
|
Items = result.Models.ToList(),
|
||||||
|
TotalItems = (int)result.MetaData.TotalCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GridData<DataModel>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToggleSelection(long id, bool isSelected)
|
||||||
|
{
|
||||||
|
if (isSelected)
|
||||||
|
{
|
||||||
|
_selectedProductIds.Add(id);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_selectedProductIds.Remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnFilterSubmit()
|
||||||
|
{
|
||||||
|
_basePage.IsFiltered = true;
|
||||||
|
StateHasChanged();
|
||||||
|
await ReloadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnFilterCleared()
|
||||||
|
{
|
||||||
|
_basePage.IsFiltered = false;
|
||||||
|
StateHasChanged();
|
||||||
|
_request = new GetAllProductsByFilterRequest { Filter = new() };
|
||||||
|
await ReloadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReloadData()
|
||||||
|
{
|
||||||
|
if (_gridData != null)
|
||||||
|
{
|
||||||
|
await _gridData.ReloadServerData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearForm()
|
||||||
|
{
|
||||||
|
_newPrice = null;
|
||||||
|
_newDiscount = null;
|
||||||
|
_newClubDiscountPercent = null;
|
||||||
|
_stockUpdateType = null;
|
||||||
|
_stockQuantity = null;
|
||||||
|
_statusEnable = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ApplyBulkChanges()
|
||||||
|
{
|
||||||
|
if (_selectedProductIds.Count == 0)
|
||||||
|
{
|
||||||
|
Snackbar.Add("هیچ محصولی برای ویرایش انتخاب نشده است.", Severity.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasPriceChanges = _newPrice.HasValue;
|
||||||
|
var hasStockChanges = _stockUpdateType.HasValue && _stockQuantity.HasValue && _stockQuantity.Value > 0;
|
||||||
|
var hasStatusChange = _statusEnable.HasValue;
|
||||||
|
|
||||||
|
if (!hasPriceChanges && !hasStockChanges && !hasStatusChange)
|
||||||
|
{
|
||||||
|
Snackbar.Add("هیچ تغییری برای اعمال انتخاب نشده است.", Severity.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var confirmationText = BuildConfirmationText(hasPriceChanges, hasStockChanges, hasStatusChange);
|
||||||
|
|
||||||
|
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small };
|
||||||
|
bool? confirmed = await DialogService.ShowMessageBox(
|
||||||
|
"تأیید ویرایش گروهی",
|
||||||
|
confirmationText,
|
||||||
|
yesText: "اعمال تغییرات",
|
||||||
|
cancelText: "لغو",
|
||||||
|
options: options);
|
||||||
|
|
||||||
|
if (confirmed != true)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_isApplying = true;
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int total = 0;
|
||||||
|
int succeeded = 0;
|
||||||
|
int failed = 0;
|
||||||
|
|
||||||
|
if (hasPriceChanges)
|
||||||
|
{
|
||||||
|
var priceRequest = new BulkUpdateProductPricesRequest();
|
||||||
|
|
||||||
|
foreach (var id in _selectedProductIds)
|
||||||
|
{
|
||||||
|
var update = new ProductPriceUpdate
|
||||||
|
{
|
||||||
|
ProductId = id,
|
||||||
|
NewPrice = _newPrice!.Value
|
||||||
|
};
|
||||||
|
|
||||||
|
if (_newDiscount.HasValue)
|
||||||
|
{
|
||||||
|
update.NewDiscount = new Int32Value { Value = _newDiscount.Value };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_newClubDiscountPercent.HasValue)
|
||||||
|
{
|
||||||
|
update.NewClubDiscountPercent = new Int32Value { Value = _newClubDiscountPercent.Value };
|
||||||
|
}
|
||||||
|
|
||||||
|
priceRequest.Products.Add(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
var priceResponse = await ProductsContract.BulkUpdateProductPricesAsync(priceRequest);
|
||||||
|
|
||||||
|
if (priceResponse != null)
|
||||||
|
{
|
||||||
|
total += priceResponse.Total;
|
||||||
|
succeeded += priceResponse.Succeeded;
|
||||||
|
failed += priceResponse.Failed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasStockChanges)
|
||||||
|
{
|
||||||
|
var stockRequest = new BulkUpdateProductStockRequest
|
||||||
|
{
|
||||||
|
UpdateType = _stockUpdateType!.Value
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var id in _selectedProductIds)
|
||||||
|
{
|
||||||
|
stockRequest.Products.Add(new ProductStockUpdate
|
||||||
|
{
|
||||||
|
ProductId = id,
|
||||||
|
Quantity = _stockQuantity!.Value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var stockResponse = await ProductsContract.BulkUpdateProductStockAsync(stockRequest);
|
||||||
|
|
||||||
|
if (stockResponse != null)
|
||||||
|
{
|
||||||
|
total += stockResponse.Total;
|
||||||
|
succeeded += stockResponse.Succeeded;
|
||||||
|
failed += stockResponse.Failed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasStatusChange)
|
||||||
|
{
|
||||||
|
var toggleRequest = new ToggleProductStatusRequest
|
||||||
|
{
|
||||||
|
Enable = _statusEnable!.Value,
|
||||||
|
DefaultStock = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleRequest.ProductIds.AddRange(_selectedProductIds);
|
||||||
|
|
||||||
|
var toggleResponse = await ProductsContract.ToggleProductStatusAsync(toggleRequest);
|
||||||
|
|
||||||
|
if (toggleResponse != null)
|
||||||
|
{
|
||||||
|
total += toggleResponse.Total;
|
||||||
|
succeeded += toggleResponse.Succeeded;
|
||||||
|
failed += toggleResponse.Failed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failed == 0)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"تغییرات گروهی با موفقیت برای {succeeded} مورد اعمال شد.", Severity.Success);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Snackbar.Add($"تغییرات گروهی انجام شد. موفق: {succeeded} / ناموفق: {failed}", Severity.Warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ReloadData();
|
||||||
|
_selectedProductIds.Clear();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"خطا در اعمال تغییرات گروهی: {ex.Message}", Severity.Error);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isApplying = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BuildConfirmationText(bool hasPriceChanges, bool hasStockChanges, bool hasStatusChange)
|
||||||
|
{
|
||||||
|
var parts = new List<string>();
|
||||||
|
|
||||||
|
if (hasPriceChanges)
|
||||||
|
{
|
||||||
|
var pricePart = $"• قیمت جدید: {_newPrice?.ToString("N0") ?? "-"}";
|
||||||
|
|
||||||
|
if (_newDiscount.HasValue)
|
||||||
|
pricePart += $", تخفیف: {_newDiscount.Value}%";
|
||||||
|
|
||||||
|
if (_newClubDiscountPercent.HasValue)
|
||||||
|
pricePart += $", تخفیف باشگاه: {_newClubDiscountPercent.Value}%";
|
||||||
|
|
||||||
|
parts.Add(pricePart);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasStockChanges)
|
||||||
|
{
|
||||||
|
var stockAction = _stockUpdateType switch
|
||||||
|
{
|
||||||
|
StockUpdateType.Set => "تنظیم موجودی روی",
|
||||||
|
StockUpdateType.Add => "افزایش موجودی به میزان",
|
||||||
|
StockUpdateType.Subtract => "کاهش موجودی به میزان",
|
||||||
|
_ => "تغییر موجودی"
|
||||||
|
};
|
||||||
|
|
||||||
|
parts.Add($"• {stockAction} {_stockQuantity}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasStatusChange)
|
||||||
|
{
|
||||||
|
var statusText = _statusEnable == true ? "فعالسازی" : "غیرفعالسازی";
|
||||||
|
parts.Add($"• {statusText} محصولات انتخابشده");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"آیا از اعمال تغییرات زیر برای {_selectedProductIds.Count} محصول انتخابشده مطمئن هستید؟\n\n{string.Join("\n", parts)}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -7,7 +7,36 @@
|
|||||||
|
|
||||||
<BasePageComponent @ref="_basePage" OnClearFilterClick="OnFilterCleared" OnSubmitClick="OnFilterSubmit">
|
<BasePageComponent @ref="_basePage" OnClearFilterClick="OnFilterCleared" OnSubmitClick="OnFilterSubmit">
|
||||||
<Filters>
|
<Filters>
|
||||||
<MudTextField T="string" Clearable="true" Label="عنوان" @bind-Value="@_request.Filter.Title" Variant="Variant.Outlined" Margin="Margin.Dense" />
|
<MudTextField T="string"
|
||||||
|
Clearable="true"
|
||||||
|
Label="عنوان"
|
||||||
|
@bind-Value="@_request.Filter.Title"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense" />
|
||||||
|
|
||||||
|
<MudNumericField T="long?"
|
||||||
|
HideSpinButtons="true"
|
||||||
|
Clearable="true"
|
||||||
|
Label="شناسه محصول"
|
||||||
|
@bind-Value="@_request.Filter.Id"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense" />
|
||||||
|
|
||||||
|
<MudNumericField T="long?"
|
||||||
|
HideSpinButtons="true"
|
||||||
|
Clearable="true"
|
||||||
|
Label="شناسه دستهبندی"
|
||||||
|
@bind-Value="@_request.Filter.CategoryId"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense" />
|
||||||
|
|
||||||
|
<MudNumericField T="long?"
|
||||||
|
HideSpinButtons="true"
|
||||||
|
Clearable="true"
|
||||||
|
Label="قیمت"
|
||||||
|
@bind-Value="@_request.Filter.Price"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense" />
|
||||||
</Filters>
|
</Filters>
|
||||||
<Content>
|
<Content>
|
||||||
<MudDataGrid T="DataModel" ServerData="@(new Func<GridState<DataModel>, Task<GridData<DataModel>>>(ServerReload))"
|
<MudDataGrid T="DataModel" ServerData="@(new Func<GridState<DataModel>, Task<GridData<DataModel>>>(ServerReload))"
|
||||||
@@ -22,10 +51,64 @@
|
|||||||
<MudText>مدیریت محصولات</MudText>
|
<MudText>مدیریت محصولات</MudText>
|
||||||
<MudSpacer />
|
<MudSpacer />
|
||||||
<MudStack Spacing="2" Row="true" Justify="Justify.Center" AlignItems="AlignItems.Center">
|
<MudStack Spacing="2" Row="true" Justify="Justify.Center" AlignItems="AlignItems.Center">
|
||||||
|
@if (_selectedProductIds.Count > 0)
|
||||||
|
{
|
||||||
|
<MudButton Variant="Variant.Outlined"
|
||||||
|
Color="Color.Error"
|
||||||
|
Size="Size.Small"
|
||||||
|
ButtonType="ButtonType.Button"
|
||||||
|
OnClick="BulkDelete"
|
||||||
|
Style="cursor:pointer;">
|
||||||
|
حذف انتخابشدهها (@_selectedProductIds.Count)
|
||||||
|
</MudButton>
|
||||||
|
|
||||||
|
<MudButton Variant="Variant.Outlined"
|
||||||
|
Color="Color.Success"
|
||||||
|
Size="Size.Small"
|
||||||
|
ButtonType="ButtonType.Button"
|
||||||
|
OnClick="@(() => BulkToggleStatus(true))"
|
||||||
|
Style="cursor:pointer;">
|
||||||
|
فعالسازی انتخابشدهها
|
||||||
|
</MudButton>
|
||||||
|
|
||||||
|
<MudButton Variant="Variant.Outlined"
|
||||||
|
Color="Color.Default"
|
||||||
|
Size="Size.Small"
|
||||||
|
ButtonType="ButtonType.Button"
|
||||||
|
OnClick="@(() => BulkToggleStatus(false))"
|
||||||
|
Style="cursor:pointer;">
|
||||||
|
غیرفعالسازی انتخابشدهها
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
|
<MudButton Variant="Variant.Outlined"
|
||||||
|
Color="Color.Primary"
|
||||||
|
Size="Size.Small"
|
||||||
|
ButtonType="ButtonType.Button"
|
||||||
|
StartIcon="@Icons.Material.Filled.Edit"
|
||||||
|
OnClick="OpenBulkEdit"
|
||||||
|
Style="cursor:pointer;">
|
||||||
|
ویرایش گروهی
|
||||||
|
</MudButton>
|
||||||
|
<MudButton Variant="Variant.Outlined"
|
||||||
|
Color="Color.Secondary"
|
||||||
|
Size="Size.Small"
|
||||||
|
ButtonType="ButtonType.Button"
|
||||||
|
StartIcon="@Icons.Material.Filled.Download"
|
||||||
|
OnClick="ExportToExcel"
|
||||||
|
Style="cursor:pointer;">
|
||||||
|
خروجی Excel
|
||||||
|
</MudButton>
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" Size="Size.Large" ButtonType="ButtonType.Button" OnClick="CreateNew" Style="cursor:pointer;">افزودن</MudButton>
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" Size="Size.Large" ButtonType="ButtonType.Button" OnClick="CreateNew" Style="cursor:pointer;">افزودن</MudButton>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</ToolBarContent>
|
</ToolBarContent>
|
||||||
<Columns>
|
<Columns>
|
||||||
|
<TemplateColumn Title="">
|
||||||
|
<CellTemplate>
|
||||||
|
<MudCheckBox Checked="@_selectedProductIds.Contains(context.Item.Id)"
|
||||||
|
CheckedChanged="@(checkedValue => ToggleSelection(context.Item.Id, checkedValue))" />
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
|
||||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||||
|
|
||||||
<PropertyColumn Property="x => x.Title" Title="عنوان" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
|
<PropertyColumn Property="x => x.Title" Title="عنوان" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
|
||||||
@@ -81,6 +164,14 @@
|
|||||||
OnClick="@(() => OpenCategoryMapping(context.Item))"
|
OnClick="@(() => OpenCategoryMapping(context.Item))"
|
||||||
Style="cursor:pointer;" />
|
Style="cursor:pointer;" />
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
|
|
||||||
|
<MudTooltip Text="تگهای محصول">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Label"
|
||||||
|
Size="Size.Small"
|
||||||
|
ButtonType="ButtonType.Button"
|
||||||
|
OnClick="@(() => OpenTagAssignment(context.Item))"
|
||||||
|
Style="cursor:pointer;" />
|
||||||
|
</MudTooltip>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</CellTemplate>
|
</CellTemplate>
|
||||||
</TemplateColumn>
|
</TemplateColumn>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
using BackOffice.BFF.Products.Protobuf.Protos.Products;
|
using BackOffice.BFF.Products.Protobuf.Protos.Products;
|
||||||
using BackOffice.Common.BaseComponents;
|
using BackOffice.Common.BaseComponents;
|
||||||
using BackOffice.Common.Utilities;
|
using BackOffice.Common.Utilities;
|
||||||
|
using BackOffice.Pages.Tag.Components;
|
||||||
using BackOffice.Pages.Products.Components;
|
using BackOffice.Pages.Products.Components;
|
||||||
using Mapster;
|
using Mapster;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
|
using System.Text;
|
||||||
using DataModel = BackOffice.BFF.Products.Protobuf.Protos.Products.GetAllProductsByFilterResponseModel;
|
using DataModel = BackOffice.BFF.Products.Protobuf.Protos.Products.GetAllProductsByFilterResponseModel;
|
||||||
|
|
||||||
namespace BackOffice.Pages.Products;
|
namespace BackOffice.Pages.Products;
|
||||||
@@ -12,14 +15,18 @@ namespace BackOffice.Pages.Products;
|
|||||||
public partial class ProductsMainPage
|
public partial class ProductsMainPage
|
||||||
{
|
{
|
||||||
[Inject] public ProductsContract.ProductsContractClient ProductsContract { get; set; } = default!;
|
[Inject] public ProductsContract.ProductsContractClient ProductsContract { get; set; } = default!;
|
||||||
|
[Inject] public IJSRuntime JsRuntime { get; set; } = default!;
|
||||||
|
|
||||||
private bool _isLoading = true;
|
private bool _isLoading = true;
|
||||||
private MudDataGrid<DataModel> _gridData;
|
private MudDataGrid<DataModel> _gridData;
|
||||||
private BasePageComponent _basePage;
|
private BasePageComponent _basePage;
|
||||||
private GetAllProductsByFilterRequest _request = new() { Filter = new() };
|
private GetAllProductsByFilterRequest _request = new() { Filter = new() };
|
||||||
|
private readonly HashSet<long> _selectedProductIds = new();
|
||||||
|
|
||||||
private async Task<GridData<DataModel>> ServerReload(GridState<DataModel> state)
|
private async Task<GridData<DataModel>> ServerReload(GridState<DataModel> state)
|
||||||
{
|
{
|
||||||
|
_selectedProductIds.Clear();
|
||||||
|
|
||||||
_request.Filter ??= new();
|
_request.Filter ??= new();
|
||||||
_request.PaginationState ??= new();
|
_request.PaginationState ??= new();
|
||||||
_request.PaginationState.PageNumber = state.Page + 1;
|
_request.PaginationState.PageNumber = state.Page + 1;
|
||||||
@@ -34,6 +41,126 @@ public partial class ProductsMainPage
|
|||||||
return new GridData<DataModel>();
|
return new GridData<DataModel>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ToggleSelection(long id, bool isSelected)
|
||||||
|
{
|
||||||
|
if (isSelected)
|
||||||
|
{
|
||||||
|
_selectedProductIds.Add(id);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_selectedProductIds.Remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task BulkDelete()
|
||||||
|
{
|
||||||
|
if (_selectedProductIds.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small };
|
||||||
|
bool? result = await DialogService.ShowMessageBox(
|
||||||
|
"اخطار",
|
||||||
|
$"آیا از حذف { _selectedProductIds.Count } محصول انتخابشده مطمئن هستید؟",
|
||||||
|
yesText: "حذف", cancelText: "لغو",
|
||||||
|
options: options);
|
||||||
|
|
||||||
|
if (result == true)
|
||||||
|
{
|
||||||
|
foreach (var id in _selectedProductIds.ToList())
|
||||||
|
{
|
||||||
|
await ProductsContract.DeleteProductsAsync(new DeleteProductsRequest { Id = id });
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectedProductIds.Clear();
|
||||||
|
ReLoadData();
|
||||||
|
Snackbar.Add("محصولات انتخابشده حذف شدند", Severity.Success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task BulkToggleStatus(bool enable)
|
||||||
|
{
|
||||||
|
if (_selectedProductIds.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var actionTitle = enable ? "فعالسازی" : "غیرفعالسازی";
|
||||||
|
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small };
|
||||||
|
bool? result = await DialogService.ShowMessageBox(
|
||||||
|
actionTitle,
|
||||||
|
$"آیا از {actionTitle} { _selectedProductIds.Count } محصول انتخابشده مطمئن هستید؟",
|
||||||
|
yesText: actionTitle, cancelText: "لغو",
|
||||||
|
options: options);
|
||||||
|
|
||||||
|
if (result == true)
|
||||||
|
{
|
||||||
|
var request = new ToggleProductStatusRequest
|
||||||
|
{
|
||||||
|
Enable = enable,
|
||||||
|
DefaultStock = 0
|
||||||
|
};
|
||||||
|
request.ProductIds.AddRange(_selectedProductIds);
|
||||||
|
|
||||||
|
var response = await ProductsContract.ToggleProductStatusAsync(request);
|
||||||
|
|
||||||
|
Snackbar.Add(
|
||||||
|
$"عملیات {actionTitle} انجام شد. موفق: {response.Succeeded} / ناموفق: {response.Failed}",
|
||||||
|
response.Failed > 0 ? Severity.Warning : Severity.Success);
|
||||||
|
|
||||||
|
_selectedProductIds.Clear();
|
||||||
|
ReLoadData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExportToExcel()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var exportRequest = new GetAllProductsByFilterRequest
|
||||||
|
{
|
||||||
|
Filter = _request.Filter ?? new GetAllProductsByFilterFilter(),
|
||||||
|
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
|
||||||
|
{
|
||||||
|
PageNumber = 1,
|
||||||
|
PageSize = 1000
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await ProductsContract.GetAllProductsByFilterAsync(exportRequest);
|
||||||
|
|
||||||
|
if (result?.Models == null || !result.Models.Any())
|
||||||
|
{
|
||||||
|
Snackbar.Add("دادهای برای خروجی وجود ندارد.", Severity.Info);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("Id,Title,Price,Discount,RemainingCount,SaleCount,ViewCount");
|
||||||
|
|
||||||
|
foreach (var p in result.Models)
|
||||||
|
{
|
||||||
|
sb.AppendLine(string.Join(",",
|
||||||
|
p.Id,
|
||||||
|
EscapeCsv(p.Title),
|
||||||
|
p.Price,
|
||||||
|
p.Discount,
|
||||||
|
p.RemainingCount,
|
||||||
|
p.SaleCount,
|
||||||
|
p.ViewCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(sb.ToString());
|
||||||
|
var base64 = Convert.ToBase64String(bytes);
|
||||||
|
var filename = $"products-{DateTime.Now:yyyyMMddHHmmss}.csv";
|
||||||
|
|
||||||
|
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
|
||||||
|
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task Update(DataModel model)
|
public async Task Update(DataModel model)
|
||||||
{
|
{
|
||||||
var parameters = new DialogParameters<UpdateDialog> { { x => x.Model, model.Adapt<UpdateProductsRequest>() } };
|
var parameters = new DialogParameters<UpdateDialog> { { x => x.Model, model.Adapt<UpdateProductsRequest>() } };
|
||||||
@@ -112,6 +239,18 @@ public partial class ProductsMainPage
|
|||||||
Navigation.NavigateTo($"{RouteConstance.ProductCategories}{model.Id}");
|
Navigation.NavigateTo($"{RouteConstance.ProductCategories}{model.Id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task OpenTagAssignment(DataModel model)
|
||||||
|
{
|
||||||
|
var parameters = new DialogParameters<AssignTagsDialog>
|
||||||
|
{
|
||||||
|
{ x => x.ProductId, model.Id },
|
||||||
|
{ x => x.ProductTitle, model.Title }
|
||||||
|
};
|
||||||
|
|
||||||
|
await DialogService.ShowAsync<AssignTagsDialog>("مدیریت تگهای محصول", parameters,
|
||||||
|
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true });
|
||||||
|
}
|
||||||
|
|
||||||
public async Task OpenImagePreview(string imagePath, string title)
|
public async Task OpenImagePreview(string imagePath, string title)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(imagePath))
|
if (string.IsNullOrWhiteSpace(imagePath))
|
||||||
@@ -126,4 +265,19 @@ public partial class ProductsMainPage
|
|||||||
await DialogService.ShowAsync<ImagePreviewDialog>("پیشنمایش تصویر محصول", parameters,
|
await DialogService.ShowAsync<ImagePreviewDialog>("پیشنمایش تصویر محصول", parameters,
|
||||||
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true });
|
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OpenBulkEdit()
|
||||||
|
{
|
||||||
|
Navigation.NavigateTo(RouteConstance.ProductsBulkEdit);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
@using Blazored.LocalStorage
|
||||||
|
@using BackOffice.Services.PublicMessage
|
||||||
|
@using static BackOffice.Pages.PublicMessages.Components.MessageFormDialog
|
||||||
|
|
||||||
|
@inject ILocalStorageService LocalStorage
|
||||||
|
|
||||||
|
<MudDialog>
|
||||||
|
<DialogContent>
|
||||||
|
<MudStack Spacing="2">
|
||||||
|
<MudText Typo="Typo.h6">قالبهای آماده پیام</MudText>
|
||||||
|
|
||||||
|
<MudAlert Severity="Severity.Info">
|
||||||
|
قالبها فقط در مرورگر فعلی ذخیره میشوند و روی سرور ذخیره نمیشوند.
|
||||||
|
</MudAlert>
|
||||||
|
|
||||||
|
<MudGrid>
|
||||||
|
<MudItem xs="12" md="4">
|
||||||
|
<MudTextField @bind-Value="_newTemplateTitle"
|
||||||
|
Label="عنوان قالب"
|
||||||
|
Variant="Variant.Outlined" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" md="4">
|
||||||
|
<MudSelect @bind-Value="_newTemplateType"
|
||||||
|
Label="نوع پیام"
|
||||||
|
Variant="Variant.Outlined">
|
||||||
|
<MudSelectItem Value="@MessageType.Announcement">اطلاعیه</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@MessageType.News">خبر</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@MessageType.Alert">هشدار</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@MessageType.Promotion">تبلیغات</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" md="2">
|
||||||
|
<MudNumericField @bind-Value="_newTemplatePriority"
|
||||||
|
Label="اولویت"
|
||||||
|
Min="1"
|
||||||
|
Max="5"
|
||||||
|
Variant="Variant.Outlined" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<MudTextField @bind-Value="_newTemplateContent"
|
||||||
|
Label="متن قالب"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Lines="4" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<MudButton Color="Color.Primary"
|
||||||
|
Variant="Variant.Filled"
|
||||||
|
OnClick="AddTemplate"
|
||||||
|
Disabled="string.IsNullOrWhiteSpace(_newTemplateTitle) || string.IsNullOrWhiteSpace(_newTemplateContent)">
|
||||||
|
افزودن قالب
|
||||||
|
</MudButton>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
|
||||||
|
<MudDivider Class="my-2" />
|
||||||
|
|
||||||
|
<MudDataGrid T="MessageTemplateModel"
|
||||||
|
Items="@_templates"
|
||||||
|
Hover="true"
|
||||||
|
Dense="true">
|
||||||
|
<Columns>
|
||||||
|
<PropertyColumn Property="x => x.Title" Title="عنوان" />
|
||||||
|
<TemplateColumn Title="نوع">
|
||||||
|
<CellTemplate>
|
||||||
|
<MudChip Color="Color.Info" Size="Size.Small">
|
||||||
|
@GetTypeText(context.Item.Type)
|
||||||
|
</MudChip>
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<PropertyColumn Property="x => x.Priority" Title="اولویت" />
|
||||||
|
<TemplateColumn Title="پیشنمایش متن">
|
||||||
|
<CellTemplate>
|
||||||
|
<MudTooltip Text="@context.Item.Content">
|
||||||
|
<MudText Typo="Typo.caption">
|
||||||
|
@(context.Item.Content.Length > 40
|
||||||
|
? context.Item.Content.Substring(0, 40) + "..."
|
||||||
|
: context.Item.Content)
|
||||||
|
</MudText>
|
||||||
|
</MudTooltip>
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<TemplateColumn Title="عملیات">
|
||||||
|
<CellTemplate>
|
||||||
|
<MudStack Row="true" Spacing="1">
|
||||||
|
<MudTooltip Text="استفاده از این قالب">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.ContentPaste"
|
||||||
|
Color="Color.Primary"
|
||||||
|
Size="Size.Small"
|
||||||
|
OnClick="@(() => UseTemplate(context.Item))" />
|
||||||
|
</MudTooltip>
|
||||||
|
<MudTooltip Text="حذف">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||||
|
Color="Color.Error"
|
||||||
|
Size="Size.Small"
|
||||||
|
OnClick="@(() => DeleteTemplate(context.Item))" />
|
||||||
|
</MudTooltip>
|
||||||
|
</MudStack>
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
</Columns>
|
||||||
|
</MudDataGrid>
|
||||||
|
</MudStack>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<MudButton OnClick="Close" Variant="Variant.Text">بستن</MudButton>
|
||||||
|
</DialogActions>
|
||||||
|
</MudDialog>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[CascadingParameter] MudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
[Parameter] public EventCallback<MessageFormModel> OnTemplateSelected { get; set; }
|
||||||
|
|
||||||
|
private const string StorageKey = "PublicMessageTemplates";
|
||||||
|
|
||||||
|
private readonly List<MessageTemplateModel> _templates = new();
|
||||||
|
private string _newTemplateTitle = string.Empty;
|
||||||
|
private string _newTemplateContent = string.Empty;
|
||||||
|
private MessageType _newTemplateType = MessageType.Announcement;
|
||||||
|
private int _newTemplatePriority = 1;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
var stored = await LocalStorage.GetItemAsync<List<MessageTemplateModel>>(StorageKey);
|
||||||
|
if (stored != null)
|
||||||
|
{
|
||||||
|
_templates.AddRange(stored);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PersistAsync()
|
||||||
|
{
|
||||||
|
await LocalStorage.SetItemAsync(StorageKey, _templates);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AddTemplate()
|
||||||
|
{
|
||||||
|
var template = new MessageTemplateModel
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Title = _newTemplateTitle.Trim(),
|
||||||
|
Content = _newTemplateContent.Trim(),
|
||||||
|
Type = _newTemplateType,
|
||||||
|
Priority = _newTemplatePriority
|
||||||
|
};
|
||||||
|
|
||||||
|
_templates.Add(template);
|
||||||
|
await PersistAsync();
|
||||||
|
|
||||||
|
_newTemplateTitle = string.Empty;
|
||||||
|
_newTemplateContent = string.Empty;
|
||||||
|
_newTemplateType = MessageType.Announcement;
|
||||||
|
_newTemplatePriority = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeleteTemplate(MessageTemplateModel template)
|
||||||
|
{
|
||||||
|
_templates.Remove(template);
|
||||||
|
await PersistAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UseTemplate(MessageTemplateModel template)
|
||||||
|
{
|
||||||
|
if (OnTemplateSelected.HasDelegate)
|
||||||
|
{
|
||||||
|
var model = new MessageFormModel
|
||||||
|
{
|
||||||
|
Title = template.Title,
|
||||||
|
Content = template.Content,
|
||||||
|
Type = template.Type,
|
||||||
|
Priority = template.Priority
|
||||||
|
};
|
||||||
|
|
||||||
|
await OnTemplateSelected.InvokeAsync(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Close()
|
||||||
|
{
|
||||||
|
MudDialog.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetTypeText(MessageType type)
|
||||||
|
{
|
||||||
|
return type switch
|
||||||
|
{
|
||||||
|
MessageType.Announcement => "اطلاعیه",
|
||||||
|
MessageType.News => "خبر",
|
||||||
|
MessageType.Alert => "هشدار",
|
||||||
|
MessageType.Promotion => "تبلیغات",
|
||||||
|
_ => "نامشخص"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MessageTemplateModel
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string Content { get; set; } = string.Empty;
|
||||||
|
public MessageType Type { get; set; } = MessageType.Announcement;
|
||||||
|
public int Priority { get; set; } = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -60,6 +60,15 @@
|
|||||||
پیام جدید
|
پیام جدید
|
||||||
</MudButton>
|
</MudButton>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6" md="2">
|
||||||
|
<MudButton Variant="Variant.Outlined"
|
||||||
|
Color="Color.Secondary"
|
||||||
|
FullWidth="true"
|
||||||
|
StartIcon="@Icons.Material.Filled.ContentPasteGo"
|
||||||
|
OnClick="OpenTemplatesDialog">
|
||||||
|
قالبها
|
||||||
|
</MudButton>
|
||||||
|
</MudItem>
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
|
|
||||||
<MudDataGrid @ref="_dataGrid" T="PublicMessageDto"
|
<MudDataGrid @ref="_dataGrid" T="PublicMessageDto"
|
||||||
@@ -162,6 +171,21 @@
|
|||||||
private MessageStatus? _statusFilter;
|
private MessageStatus? _statusFilter;
|
||||||
private MessageType? _typeFilter;
|
private MessageType? _typeFilter;
|
||||||
|
|
||||||
|
private async Task OpenTemplatesDialog()
|
||||||
|
{
|
||||||
|
var options = new DialogOptions
|
||||||
|
{
|
||||||
|
CloseButton = true,
|
||||||
|
MaxWidth = MaxWidth.Medium,
|
||||||
|
FullWidth = true
|
||||||
|
};
|
||||||
|
|
||||||
|
await DialogService.ShowAsync<BackOffice.Pages.PublicMessages.Components.MessageTemplatesDialog>(
|
||||||
|
"قالبهای پیام",
|
||||||
|
new DialogParameters(),
|
||||||
|
options);
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
await LoadMessages();
|
await LoadMessages();
|
||||||
|
|||||||
56
src/BackOffice/Pages/Tag/Components/AssignTagsDialog.razor
Normal file
56
src/BackOffice/Pages/Tag/Components/AssignTagsDialog.razor
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
@using BackOffice.Services.Tag
|
||||||
|
|
||||||
|
<MudDialog>
|
||||||
|
<DialogContent>
|
||||||
|
<MudStack Spacing="2">
|
||||||
|
<MudText Typo="Typo.h6">تگهای محصول: @ProductTitle</MudText>
|
||||||
|
|
||||||
|
@if (_currentTags.Any())
|
||||||
|
{
|
||||||
|
<MudStack Row="true" Spacing="1">
|
||||||
|
@foreach (var tag in _currentTags)
|
||||||
|
{
|
||||||
|
<MudChip Color="Color.Info"
|
||||||
|
Size="Size.Small"
|
||||||
|
OnClose="@(() => RemoveAsync(tag))"
|
||||||
|
CloseIcon="@Icons.Material.Filled.Close">
|
||||||
|
@($"{tag.TagTitle} (#{tag.TagId})")
|
||||||
|
</MudChip>
|
||||||
|
}
|
||||||
|
</MudStack>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.caption" Color="Color.TextSecondary">
|
||||||
|
هیچ تگی برای این محصول ثبت نشده است.
|
||||||
|
</MudText>
|
||||||
|
}
|
||||||
|
|
||||||
|
<MudSelect T="long"
|
||||||
|
Label="انتخاب تگ"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense"
|
||||||
|
@bind-Value="_selectedTagId">
|
||||||
|
@if (_tags != null)
|
||||||
|
{
|
||||||
|
@foreach (var tag in _tags)
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@tag.Id">@tag.Title (@tag.Name)</MudSelectItem>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
|
||||||
|
<MudButton Variant="Variant.Filled"
|
||||||
|
Color="Color.Primary"
|
||||||
|
Disabled="_selectedTagId == 0"
|
||||||
|
OnClick="AssignAsync">
|
||||||
|
افزودن تگ به محصول
|
||||||
|
</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<MudButton Color="Color.Default" Variant="Variant.Text" OnClick="Close">
|
||||||
|
بستن
|
||||||
|
</MudButton>
|
||||||
|
</DialogActions>
|
||||||
|
</MudDialog>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using BackOffice.Services.Tag;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using MudBlazor;
|
||||||
|
|
||||||
|
namespace BackOffice.Pages.Tag.Components;
|
||||||
|
|
||||||
|
public partial class AssignTagsDialog
|
||||||
|
{
|
||||||
|
[CascadingParameter] MudDialogInstance DialogInstance { get; set; } = default!;
|
||||||
|
[Inject] public ITagService TagService { get; set; } = default!;
|
||||||
|
|
||||||
|
[Parameter] public long ProductId { get; set; }
|
||||||
|
[Parameter] public string ProductTitle { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
private List<TagListItemDto> _tags = new();
|
||||||
|
private List<ProductTagViewDto> _currentTags = new();
|
||||||
|
private long _selectedTagId;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
var tagsResult = await TagService.GetTagsAsync(new TagFilterDto
|
||||||
|
{
|
||||||
|
PageNumber = 1,
|
||||||
|
PageSize = 100,
|
||||||
|
IsActive = true
|
||||||
|
});
|
||||||
|
|
||||||
|
_tags = tagsResult.Items;
|
||||||
|
|
||||||
|
_currentTags = await TagService.GetProductTagsAsync(ProductId);
|
||||||
|
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AssignAsync()
|
||||||
|
{
|
||||||
|
if (_selectedTagId == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await TagService.AssignToProductAsync(ProductId, _selectedTagId);
|
||||||
|
Snackbar.Add("تگ به محصول اضافه شد", Severity.Success);
|
||||||
|
|
||||||
|
_currentTags = await TagService.GetProductTagsAsync(ProductId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RemoveAsync(ProductTagViewDto tag)
|
||||||
|
{
|
||||||
|
await TagService.RemoveProductTagAsync(tag.ProductTagId);
|
||||||
|
Snackbar.Add("تگ از محصول حذف شد", Severity.Success);
|
||||||
|
_currentTags = await TagService.GetProductTagsAsync(ProductId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Close()
|
||||||
|
{
|
||||||
|
DialogInstance.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/BackOffice/Pages/Tag/Components/TagEditDialog.razor
Normal file
44
src/BackOffice/Pages/Tag/Components/TagEditDialog.razor
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
@using BackOffice.Services.Tag
|
||||||
|
|
||||||
|
<MudDialog>
|
||||||
|
<DialogContent>
|
||||||
|
<EditForm Model="@Model" OnValidSubmit="HandleValidSubmit">
|
||||||
|
<MudStack Spacing="2">
|
||||||
|
<MudTextField @bind-Value="Model.Name"
|
||||||
|
Label="نام (انگلیسی)"
|
||||||
|
Required="true"
|
||||||
|
Variant="Variant.Outlined" />
|
||||||
|
|
||||||
|
<MudTextField @bind-Value="Model.Title"
|
||||||
|
Label="عنوان (نمایش در UI)"
|
||||||
|
Required="true"
|
||||||
|
Variant="Variant.Outlined" />
|
||||||
|
|
||||||
|
<MudTextField @bind-Value="Model.Description"
|
||||||
|
Label="توضیحات"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Lines="3"
|
||||||
|
Text="@Model.Description" />
|
||||||
|
|
||||||
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||||
|
<MudSwitch @bind-Checked="Model.IsActive" Color="Color.Primary" />
|
||||||
|
<MudText Typo="Typo.body2">فعال</MudText>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
<MudNumericField T="int"
|
||||||
|
@bind-Value="Model.SortOrder"
|
||||||
|
Label="ترتیب نمایش"
|
||||||
|
Variant="Variant.Outlined" />
|
||||||
|
</MudStack>
|
||||||
|
</EditForm>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="SubmitAsync">
|
||||||
|
@(_isEditMode ? "ذخیره تغییرات" : "ایجاد تگ")
|
||||||
|
</MudButton>
|
||||||
|
<MudButton Color="Color.Default" Variant="Variant.Text" OnClick="Cancel">
|
||||||
|
انصراف
|
||||||
|
</MudButton>
|
||||||
|
</DialogActions>
|
||||||
|
</MudDialog>
|
||||||
|
|
||||||
42
src/BackOffice/Pages/Tag/Components/TagEditDialog.razor.cs
Normal file
42
src/BackOffice/Pages/Tag/Components/TagEditDialog.razor.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using BackOffice.Services.Tag;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using MudBlazor;
|
||||||
|
|
||||||
|
namespace BackOffice.Pages.Tag.Components;
|
||||||
|
|
||||||
|
public partial class TagEditDialog
|
||||||
|
{
|
||||||
|
[CascadingParameter] MudDialogInstance DialogInstance { get; set; } = default!;
|
||||||
|
[Inject] public ITagService TagService { get; set; } = default!;
|
||||||
|
|
||||||
|
[Parameter] public TagEditDto Model { get; set; } = new();
|
||||||
|
[Parameter] public long? TagId { get; set; }
|
||||||
|
[Parameter] public bool IsEditMode { get; set; }
|
||||||
|
|
||||||
|
private bool _isEditMode => IsEditMode && TagId.HasValue;
|
||||||
|
|
||||||
|
private async Task HandleValidSubmit()
|
||||||
|
{
|
||||||
|
await SubmitAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SubmitAsync()
|
||||||
|
{
|
||||||
|
if (_isEditMode && TagId.HasValue)
|
||||||
|
{
|
||||||
|
await TagService.UpdateAsync(TagId.Value, Model);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await TagService.CreateAsync(Model);
|
||||||
|
}
|
||||||
|
|
||||||
|
DialogInstance.Close(DialogResult.Ok(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Cancel()
|
||||||
|
{
|
||||||
|
DialogInstance.Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
95
src/BackOffice/Pages/Tag/TagManagementPage.razor
Normal file
95
src/BackOffice/Pages/Tag/TagManagementPage.razor
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
@page "/tags"
|
||||||
|
@attribute [Authorize(Roles = "Administrator")]
|
||||||
|
|
||||||
|
@using BackOffice.Services.Tag
|
||||||
|
|
||||||
|
@inject ITagService TagService
|
||||||
|
|
||||||
|
<MudContainer MaxWidth="MaxWidth.Medium" Class="mt-4">
|
||||||
|
<MudStack Spacing="2">
|
||||||
|
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||||
|
<MudText Typo="Typo.h5">مدیریت تگها</MudText>
|
||||||
|
<MudButton Variant="Variant.Filled"
|
||||||
|
Color="Color.Primary"
|
||||||
|
StartIcon="@Icons.Material.Filled.Add"
|
||||||
|
OnClick="CreateTag">
|
||||||
|
افزودن تگ
|
||||||
|
</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
|
||||||
|
<MudTextField @bind-Value="_search"
|
||||||
|
Label="جستجو بر اساس نام/عنوان"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense"
|
||||||
|
Adornment="Adornment.Start"
|
||||||
|
AdornmentIcon="@Icons.Material.Filled.Search"
|
||||||
|
Immediate="true"
|
||||||
|
OnBlur="ReloadAsync"
|
||||||
|
OnKeyDown="OnSearchKeyDown" />
|
||||||
|
|
||||||
|
<MudSelect @bind-Value="_isActive"
|
||||||
|
Label="وضعیت"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense"
|
||||||
|
Style="width: 160px;">
|
||||||
|
<MudSelectItem Value="@( (bool?)null )">همه</MudSelectItem>
|
||||||
|
<MudSelectItem Value="true">فعال</MudSelectItem>
|
||||||
|
<MudSelectItem Value="false">غیرفعال</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
|
||||||
|
<MudButton Variant="Variant.Outlined"
|
||||||
|
Color="Color.Primary"
|
||||||
|
OnClick="ReloadAsync">
|
||||||
|
اعمال فیلتر
|
||||||
|
</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
<MudDataGrid T="TagListItemDto"
|
||||||
|
@ref="_grid"
|
||||||
|
ServerData="LoadServerData"
|
||||||
|
Hover="true"
|
||||||
|
Elevation="1"
|
||||||
|
Dense="true"
|
||||||
|
Height="65vh">
|
||||||
|
<Columns>
|
||||||
|
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||||
|
<PropertyColumn Property="x => x.Name" Title="نام" />
|
||||||
|
<PropertyColumn Property="x => x.Title" Title="عنوان" />
|
||||||
|
<PropertyColumn Property="x => x.IsActive" Title="وضعیت">
|
||||||
|
<CellTemplate>
|
||||||
|
<MudChip Color="@ (context.Item.IsActive ? Color.Success : Color.Error)"
|
||||||
|
Size="Size.Small">
|
||||||
|
@(context.Item.IsActive ? "فعال" : "غیرفعال")
|
||||||
|
</MudChip>
|
||||||
|
</CellTemplate>
|
||||||
|
</PropertyColumn>
|
||||||
|
<PropertyColumn Property="x => x.SortOrder" Title="ترتیب" />
|
||||||
|
<TemplateColumn Title="عملیات" Sortable="false">
|
||||||
|
<CellTemplate>
|
||||||
|
<MudStack Row="true" Spacing="1">
|
||||||
|
<MudTooltip Text="ویرایش">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||||
|
Size="Size.Small"
|
||||||
|
Color="Color.Primary"
|
||||||
|
OnClick="@(() => EditTag(context.Item))" />
|
||||||
|
</MudTooltip>
|
||||||
|
<MudTooltip Text="حذف">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||||
|
Size="Size.Small"
|
||||||
|
Color="Color.Error"
|
||||||
|
OnClick="@(() => DeleteTag(context.Item))" />
|
||||||
|
</MudTooltip>
|
||||||
|
</MudStack>
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
</Columns>
|
||||||
|
<PagerContent>
|
||||||
|
<MudDataGridPager T="TagListItemDto"
|
||||||
|
PageSizeOptions="@(new int[] { 10, 25, 50 })"
|
||||||
|
RowsPerPageString="تعداد در صفحه" />
|
||||||
|
</PagerContent>
|
||||||
|
</MudDataGrid>
|
||||||
|
</MudStack>
|
||||||
|
</MudContainer>
|
||||||
|
|
||||||
114
src/BackOffice/Pages/Tag/TagManagementPage.razor.cs
Normal file
114
src/BackOffice/Pages/Tag/TagManagementPage.razor.cs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
using BackOffice.Services.Tag;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using MudBlazor;
|
||||||
|
|
||||||
|
namespace BackOffice.Pages.Tag;
|
||||||
|
|
||||||
|
public partial class TagManagementPage
|
||||||
|
{
|
||||||
|
[Inject] public ITagService TagService { get; set; } = default!;
|
||||||
|
|
||||||
|
private MudDataGrid<TagListItemDto> _grid = default!;
|
||||||
|
private string? _search;
|
||||||
|
private bool? _isActive;
|
||||||
|
|
||||||
|
private async Task<GridData<TagListItemDto>> LoadServerData(GridState<TagListItemDto> state)
|
||||||
|
{
|
||||||
|
var filter = new TagFilterDto
|
||||||
|
{
|
||||||
|
PageNumber = state.Page + 1,
|
||||||
|
PageSize = state.PageSize,
|
||||||
|
SearchTerm = _search,
|
||||||
|
IsActive = _isActive
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await TagService.GetTagsAsync(filter);
|
||||||
|
|
||||||
|
return new GridData<TagListItemDto>
|
||||||
|
{
|
||||||
|
Items = result.Items,
|
||||||
|
TotalItems = result.TotalCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReloadAsync()
|
||||||
|
{
|
||||||
|
if (_grid != null)
|
||||||
|
{
|
||||||
|
await _grid.ReloadServerData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSearchKeyDown(KeyboardEventArgs args)
|
||||||
|
{
|
||||||
|
if (args.Key == "Enter")
|
||||||
|
{
|
||||||
|
_ = ReloadAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateTag()
|
||||||
|
{
|
||||||
|
var parameters = new DialogParameters<TagEditDialog>
|
||||||
|
{
|
||||||
|
{ x => x.Model, new TagEditDto() },
|
||||||
|
{ x => x.IsEditMode, false }
|
||||||
|
};
|
||||||
|
|
||||||
|
var dialog = await DialogService.ShowAsync<TagEditDialog>("ایجاد تگ جدید", parameters,
|
||||||
|
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true });
|
||||||
|
|
||||||
|
var result = await dialog.Result;
|
||||||
|
if (!result.Canceled)
|
||||||
|
{
|
||||||
|
Snackbar.Add("تگ با موفقیت ایجاد شد", Severity.Success);
|
||||||
|
await ReloadAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EditTag(TagListItemDto item)
|
||||||
|
{
|
||||||
|
var dto = new TagEditDto
|
||||||
|
{
|
||||||
|
Name = item.Name,
|
||||||
|
Title = item.Title,
|
||||||
|
Description = item.Description,
|
||||||
|
IsActive = item.IsActive,
|
||||||
|
SortOrder = item.SortOrder
|
||||||
|
};
|
||||||
|
|
||||||
|
var parameters = new DialogParameters<TagEditDialog>
|
||||||
|
{
|
||||||
|
{ x => x.Model, dto },
|
||||||
|
{ x => x.TagId, item.Id },
|
||||||
|
{ x => x.IsEditMode, true }
|
||||||
|
};
|
||||||
|
|
||||||
|
var dialog = await DialogService.ShowAsync<TagEditDialog>("ویرایش تگ", parameters,
|
||||||
|
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true });
|
||||||
|
|
||||||
|
var result = await dialog.Result;
|
||||||
|
if (!result.Canceled)
|
||||||
|
{
|
||||||
|
Snackbar.Add("تگ با موفقیت بهروزرسانی شد", Severity.Success);
|
||||||
|
await ReloadAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeleteTag(TagListItemDto item)
|
||||||
|
{
|
||||||
|
var confirmed = await DialogService.ShowMessageBox(
|
||||||
|
"حذف تگ",
|
||||||
|
$"آیا از حذف تگ «{item.Title}» مطمئن هستید؟",
|
||||||
|
yesText: "حذف",
|
||||||
|
cancelText: "لغو");
|
||||||
|
|
||||||
|
if (confirmed == true)
|
||||||
|
{
|
||||||
|
await TagService.DeleteAsync(item.Id);
|
||||||
|
Snackbar.Add("تگ حذف شد", Severity.Success);
|
||||||
|
await ReloadAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
@using BackOffice.BFF.UserOrder.Protobuf.Protos.UserOrder
|
||||||
|
|
||||||
|
<MudDialog>
|
||||||
|
<DialogContent>
|
||||||
|
<MudStack Spacing="2">
|
||||||
|
<MudText Typo="Typo.h6">
|
||||||
|
اعمال تخفیف روی سفارش شماره @OrderId
|
||||||
|
</MudText>
|
||||||
|
|
||||||
|
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||||
|
مبلغ تخفیف بهصورت ریالی وارد شود.
|
||||||
|
</MudText>
|
||||||
|
|
||||||
|
<MudNumericField T="long?"
|
||||||
|
@bind-Value="_discountAmount"
|
||||||
|
Label="مبلغ تخفیف (ریال)"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
HideSpinButtons="true" />
|
||||||
|
|
||||||
|
<MudTextField T="string"
|
||||||
|
@bind-Value="_reason"
|
||||||
|
Label="دلیل تخفیف"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Lines="3" />
|
||||||
|
</MudStack>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<MudButton Variant="Variant.Text" Color="Color.Default" OnClick="Cancel">
|
||||||
|
انصراف
|
||||||
|
</MudButton>
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SubmitAsync" Disabled="_isSaving">
|
||||||
|
اعمال تخفیف
|
||||||
|
</MudButton>
|
||||||
|
</DialogActions>
|
||||||
|
</MudDialog>
|
||||||
|
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using BackOffice.BFF.UserOrder.Protobuf.Protos.UserOrder;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using MudBlazor;
|
||||||
|
|
||||||
|
namespace BackOffice.Pages.UserOrder.Components;
|
||||||
|
|
||||||
|
public partial class ApplyDiscountDialog
|
||||||
|
{
|
||||||
|
[CascadingParameter] public IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
[Inject] public UserOrderContract.UserOrderContractClient UserOrderContract { get; set; } = default!;
|
||||||
|
[Inject] public ISnackbar Snackbar { get; set; } = default!;
|
||||||
|
|
||||||
|
[Parameter] public long OrderId { get; set; }
|
||||||
|
|
||||||
|
private long? _discountAmount;
|
||||||
|
private string? _reason;
|
||||||
|
private bool _isSaving;
|
||||||
|
|
||||||
|
private async Task SubmitAsync()
|
||||||
|
{
|
||||||
|
if (!_discountAmount.HasValue || _discountAmount.Value <= 0)
|
||||||
|
{
|
||||||
|
Snackbar.Add("مبلغ تخفیف باید بزرگتر از صفر باشد.", Severity.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isSaving = true;
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var request = new ApplyDiscountToOrderRequest
|
||||||
|
{
|
||||||
|
OrderId = OrderId,
|
||||||
|
DiscountAmount = _discountAmount.Value,
|
||||||
|
Reason = _reason ?? string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await UserOrderContract.ApplyDiscountToOrderAsync(request);
|
||||||
|
|
||||||
|
if (response.Success)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"تخفیف با موفقیت اعمال شد. مبلغ نهایی: {response.FinalAmount.ToString("N0")} ریال", Severity.Success);
|
||||||
|
MudDialog.Close(DialogResult.Ok(true));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var message = string.IsNullOrWhiteSpace(response.Message)
|
||||||
|
? "اعمال تخفیف با خطا مواجه شد."
|
||||||
|
: response.Message;
|
||||||
|
Snackbar.Add(message, Severity.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"خطا در اعمال تخفیف: {ex.Message}", Severity.Error);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isSaving = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Cancel()
|
||||||
|
{
|
||||||
|
MudDialog.Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
@using BackOffice.BFF.UserOrder.Protobuf.Protos.UserOrder
|
||||||
|
|
||||||
|
<MudDialog>
|
||||||
|
<DialogContent>
|
||||||
|
<MudStack Spacing="2">
|
||||||
|
<MudText Typo="Typo.h6">
|
||||||
|
لغو سفارش شماره @OrderId
|
||||||
|
</MudText>
|
||||||
|
|
||||||
|
<MudAlert Severity="Severity.Warning">
|
||||||
|
<MudText>
|
||||||
|
با لغو سفارش، وضعیت ارسال سفارش به «لغو شده» تغییر میکند.
|
||||||
|
</MudText>
|
||||||
|
</MudAlert>
|
||||||
|
|
||||||
|
<MudTextField T="string"
|
||||||
|
@bind-Value="_cancelReason"
|
||||||
|
Label="دلیل لغو"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Required="true"
|
||||||
|
Lines="3" />
|
||||||
|
|
||||||
|
<MudSwitch @bind-Checked="_refundPayment" Color="Color.Primary" />
|
||||||
|
<MudText Typo="Typo.body2">
|
||||||
|
بازگشت وجه به کاربر (در صورت پرداخت موفق)
|
||||||
|
</MudText>
|
||||||
|
</MudStack>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<MudButton Variant="Variant.Text" Color="Color.Default" OnClick="Cancel">
|
||||||
|
انصراف
|
||||||
|
</MudButton>
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Error" OnClick="SubmitAsync" Disabled="_isSaving">
|
||||||
|
لغو سفارش
|
||||||
|
</MudButton>
|
||||||
|
</DialogActions>
|
||||||
|
</MudDialog>
|
||||||
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using BackOffice.BFF.UserOrder.Protobuf.Protos.UserOrder;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using MudBlazor;
|
||||||
|
|
||||||
|
namespace BackOffice.Pages.UserOrder.Components;
|
||||||
|
|
||||||
|
public partial class CancelOrderDialog
|
||||||
|
{
|
||||||
|
[CascadingParameter] public IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
[Inject] public UserOrderContract.UserOrderContractClient UserOrderContract { get; set; } = default!;
|
||||||
|
[Inject] public ISnackbar Snackbar { get; set; } = default!;
|
||||||
|
|
||||||
|
[Parameter] public long OrderId { get; set; }
|
||||||
|
|
||||||
|
private string? _cancelReason;
|
||||||
|
private bool _refundPayment;
|
||||||
|
private bool _isSaving;
|
||||||
|
|
||||||
|
private async Task SubmitAsync()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_cancelReason))
|
||||||
|
{
|
||||||
|
Snackbar.Add("لطفاً دلیل لغو سفارش را وارد کنید.", Severity.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isSaving = true;
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var request = new CancelOrderRequest
|
||||||
|
{
|
||||||
|
OrderId = OrderId,
|
||||||
|
CancelReason = _cancelReason ?? string.Empty,
|
||||||
|
RefundPayment = _refundPayment
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await UserOrderContract.CancelOrderAsync(request);
|
||||||
|
|
||||||
|
var message = string.IsNullOrWhiteSpace(response.Message)
|
||||||
|
? "سفارش با موفقیت لغو شد."
|
||||||
|
: response.Message;
|
||||||
|
|
||||||
|
Snackbar.Add(message, Severity.Success);
|
||||||
|
MudDialog.Close(DialogResult.Ok(true));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"خطا در لغو سفارش: {ex.Message}", Severity.Error);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isSaving = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Cancel()
|
||||||
|
{
|
||||||
|
MudDialog.Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
@using BackOffice.BFF.UserOrder.Protobuf.Protos.UserOrder
|
||||||
|
|
||||||
|
<MudDialog>
|
||||||
|
<DialogContent>
|
||||||
|
<MudStack Spacing="2">
|
||||||
|
<MudText Typo="Typo.h6">
|
||||||
|
تغییر وضعیت ارسال سفارش شماره @OrderId
|
||||||
|
</MudText>
|
||||||
|
|
||||||
|
<MudAlert Severity="Severity.Info" Dense="true">
|
||||||
|
وضعیت فعلی: <strong>@GetDeliveryStatusText(CurrentStatus)</strong>
|
||||||
|
</MudAlert>
|
||||||
|
|
||||||
|
<MudSelect T="int"
|
||||||
|
@bind-Value="_newStatus"
|
||||||
|
Label="وضعیت جدید"
|
||||||
|
Variant="Variant.Outlined">
|
||||||
|
<MudSelectItem T="int" Value="0">بدون ارسال / نامشخص</MudSelectItem>
|
||||||
|
<MudSelectItem T="int" Value="1">در انتظار ارسال</MudSelectItem>
|
||||||
|
<MudSelectItem T="int" Value="2">تحویل پست</MudSelectItem>
|
||||||
|
<MudSelectItem T="int" Value="3">تحویل به مشتری</MudSelectItem>
|
||||||
|
<MudSelectItem T="int" Value="4">مرجوع شده</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
</MudStack>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<MudButton Variant="Variant.Text" Color="Color.Default" OnClick="Cancel">
|
||||||
|
انصراف
|
||||||
|
</MudButton>
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SubmitAsync" Disabled="_isSaving">
|
||||||
|
ثبت وضعیت
|
||||||
|
</MudButton>
|
||||||
|
</DialogActions>
|
||||||
|
</MudDialog>
|
||||||
|
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
using BackOffice.BFF.UserOrder.Protobuf.Protos.UserOrder;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using MudBlazor;
|
||||||
|
|
||||||
|
namespace BackOffice.Pages.UserOrder.Components;
|
||||||
|
|
||||||
|
public partial class ChangeOrderStatusDialog
|
||||||
|
{
|
||||||
|
[CascadingParameter] public IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
[Inject] public UserOrderContract.UserOrderContractClient UserOrderContract { get; set; } = default!;
|
||||||
|
[Inject] public ISnackbar Snackbar { get; set; } = default!;
|
||||||
|
|
||||||
|
[Parameter] public long OrderId { get; set; }
|
||||||
|
[Parameter] public int CurrentStatus { get; set; }
|
||||||
|
|
||||||
|
private int _newStatus;
|
||||||
|
private bool _isSaving;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
_newStatus = CurrentStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetDeliveryStatusText(int status)
|
||||||
|
{
|
||||||
|
return status switch
|
||||||
|
{
|
||||||
|
1 => "در انتظار ارسال",
|
||||||
|
2 => "تحویل پست",
|
||||||
|
3 => "تحویل به مشتری",
|
||||||
|
4 => "مرجوع شده",
|
||||||
|
_ => "بدون ارسال / نامشخص"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SubmitAsync()
|
||||||
|
{
|
||||||
|
_isSaving = true;
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await UserOrderContract.UpdateOrderStatusAsync(new UpdateOrderStatusRequest
|
||||||
|
{
|
||||||
|
OrderId = OrderId,
|
||||||
|
NewStatus = _newStatus
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.Success)
|
||||||
|
{
|
||||||
|
Snackbar.Add("وضعیت ارسال سفارش با موفقیت بهروزرسانی شد.", Severity.Success);
|
||||||
|
MudDialog.Close(DialogResult.Ok(true));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var message = string.IsNullOrWhiteSpace(response.Message)
|
||||||
|
? "بهروزرسانی وضعیت سفارش با خطا مواجه شد."
|
||||||
|
: response.Message;
|
||||||
|
Snackbar.Add(message, Severity.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"خطا در تغییر وضعیت سفارش: {ex.Message}", Severity.Error);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isSaving = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Cancel()
|
||||||
|
{
|
||||||
|
MudDialog.Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
</MudText>
|
</MudText>
|
||||||
|
|
||||||
<MudPaper Class="pa-3">
|
<MudPaper Class="pa-3">
|
||||||
<MudStack Spacing="1">
|
<MudStack Spacing="2">
|
||||||
<MudText Typo="Typo.subtitle2">خلاصه سفارش</MudText>
|
<MudText Typo="Typo.subtitle2">خلاصه سفارش</MudText>
|
||||||
<MudDivider />
|
<MudDivider />
|
||||||
<MudText Typo="Typo.body2">مبلغ: @_model.Amount.ToString("N0") تومان</MudText>
|
<MudText Typo="Typo.body2">مبلغ: @_model.Amount.ToString("N0") تومان</MudText>
|
||||||
@@ -50,6 +50,23 @@
|
|||||||
<MudChip T="string" Color="Color.Error" Size="Size.Small">پرداخت نشده</MudChip>
|
<MudChip T="string" Color="Color.Error" Size="Size.Small">پرداخت نشده</MudChip>
|
||||||
}
|
}
|
||||||
</MudText>
|
</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">
|
||||||
|
<MudAvatar Color="@GetStepColor(step.State)"
|
||||||
|
Size="Size.Small">
|
||||||
|
@step.Icon
|
||||||
|
</MudAvatar>
|
||||||
|
<MudText Typo="Typo.caption">@step.Title</MudText>
|
||||||
|
</MudStack>
|
||||||
|
}
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
<MudDivider Class="my-2" />
|
||||||
<MudText Typo="Typo.body2">
|
<MudText Typo="Typo.body2">
|
||||||
وضعیت ارسال:
|
وضعیت ارسال:
|
||||||
<MudSelect T="int"
|
<MudSelect T="int"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public partial class UserOrderDetailsDialog
|
|||||||
private int _deliveryStatusValue;
|
private int _deliveryStatusValue;
|
||||||
private string? _trackingCode;
|
private string? _trackingCode;
|
||||||
private string? _deliveryDescription;
|
private string? _deliveryDescription;
|
||||||
|
private List<TimelineStep> _timelineSteps = new();
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
@@ -32,6 +33,8 @@ public partial class UserOrderDetailsDialog
|
|||||||
_deliveryStatusValue = _model.DeliveryStatus.GetHashCode();
|
_deliveryStatusValue = _model.DeliveryStatus.GetHashCode();
|
||||||
_trackingCode = _model.TrackingCode;
|
_trackingCode = _model.TrackingCode;
|
||||||
_deliveryDescription = _model.DeliveryDescription;
|
_deliveryDescription = _model.DeliveryDescription;
|
||||||
|
|
||||||
|
BuildTimeline();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -77,6 +80,86 @@ public partial class UserOrderDetailsDialog
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void BuildTimeline()
|
||||||
|
{
|
||||||
|
if (_model is null)
|
||||||
|
{
|
||||||
|
_timelineSteps = new List<TimelineStep>();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var steps = new List<TimelineStep>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Title = "ثبت سفارش",
|
||||||
|
State = _model.PaymentStatus == PaymentStatus.None && _model.DeliveryStatus == DeliveryStatus.None ? StepState.Active : StepState.Completed,
|
||||||
|
Icon = "1"
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Title = "پرداخت",
|
||||||
|
State = _model.PaymentStatus switch
|
||||||
|
{
|
||||||
|
PaymentStatus.Success => StepState.Completed,
|
||||||
|
PaymentStatus.Pending => StepState.Active,
|
||||||
|
_ => StepState.Inactive
|
||||||
|
},
|
||||||
|
Icon = "2"
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Title = "ارسال",
|
||||||
|
State = _model.DeliveryStatus switch
|
||||||
|
{
|
||||||
|
DeliveryStatus.Pending => StepState.Active,
|
||||||
|
DeliveryStatus.InTransit => StepState.Active,
|
||||||
|
DeliveryStatus.Delivered => StepState.Completed,
|
||||||
|
DeliveryStatus.Returned => StepState.Completed,
|
||||||
|
_ => StepState.Inactive
|
||||||
|
},
|
||||||
|
Icon = "3"
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Title = "تحویل / مرجوعی",
|
||||||
|
State = _model.DeliveryStatus switch
|
||||||
|
{
|
||||||
|
DeliveryStatus.Delivered => StepState.Completed,
|
||||||
|
DeliveryStatus.Returned => StepState.Completed,
|
||||||
|
_ => StepState.Inactive
|
||||||
|
},
|
||||||
|
Icon = "4"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_timelineSteps = steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Color GetStepColor(StepState state)
|
||||||
|
{
|
||||||
|
return state switch
|
||||||
|
{
|
||||||
|
StepState.Completed => Color.Success,
|
||||||
|
StepState.Active => Color.Info,
|
||||||
|
_ => Color.Default
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum StepState
|
||||||
|
{
|
||||||
|
Inactive = 0,
|
||||||
|
Active = 1,
|
||||||
|
Completed = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TimelineStep
|
||||||
|
{
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public StepState State { get; set; }
|
||||||
|
public string Icon { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task SaveAsync()
|
private async Task SaveAsync()
|
||||||
{
|
{
|
||||||
if (_model is null)
|
if (_model is null)
|
||||||
|
|||||||
@@ -9,6 +9,30 @@
|
|||||||
<BackOffice.Common.BaseComponents.BasePageComponent @ref="_basePage" OnClearFilterClick="OnFilterCleared"
|
<BackOffice.Common.BaseComponents.BasePageComponent @ref="_basePage" OnClearFilterClick="OnFilterCleared"
|
||||||
OnSubmitClick="OnFilterSubmit">
|
OnSubmitClick="OnFilterSubmit">
|
||||||
<Filters>
|
<Filters>
|
||||||
|
<MudNumericField HideSpinButtons="true"
|
||||||
|
T="long?"
|
||||||
|
Clearable="true"
|
||||||
|
Label="شناسه سفارش"
|
||||||
|
@bind-Value="@_orderIdFilter"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense"/>
|
||||||
|
|
||||||
|
<MudNumericField HideSpinButtons="true"
|
||||||
|
T="long?"
|
||||||
|
Clearable="true"
|
||||||
|
Label="شناسه کاربر"
|
||||||
|
@bind-Value="@_userIdFilter"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense"/>
|
||||||
|
|
||||||
|
<MudNumericField HideSpinButtons="true"
|
||||||
|
T="long?"
|
||||||
|
Clearable="true"
|
||||||
|
Label="شناسه تراکنش"
|
||||||
|
@bind-Value="@_transactionIdFilter"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense"/>
|
||||||
|
|
||||||
<MudNumericField HideSpinButtons="true"
|
<MudNumericField HideSpinButtons="true"
|
||||||
T="long?"
|
T="long?"
|
||||||
Clearable="true"
|
Clearable="true"
|
||||||
@@ -56,9 +80,43 @@
|
|||||||
</MudSelect>
|
</MudSelect>
|
||||||
</Filters>
|
</Filters>
|
||||||
<Content>
|
<Content>
|
||||||
|
<MudGrid Class="mb-2">
|
||||||
|
<MudItem xs="12" md="4">
|
||||||
|
<MudCard>
|
||||||
|
<MudCardContent>
|
||||||
|
<MudText Typo="Typo.subtitle2">جمع سفارشها در بازه فعلی</MudText>
|
||||||
|
<MudText Typo="Typo.h5">@_stats.TotalOrders.ToString("N0")</MudText>
|
||||||
|
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||||
|
مجموع مبلغ: @_stats.TotalAmount.ToString("N0") ریال
|
||||||
|
</MudText>
|
||||||
|
</MudCardContent>
|
||||||
|
</MudCard>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" md="8">
|
||||||
|
<MudCard>
|
||||||
|
<MudCardContent>
|
||||||
|
<MudText Typo="Typo.subtitle2" Class="mb-2">نمودار تعداد سفارشها بر اساس وضعیت ارسال</MudText>
|
||||||
|
@if (_statusChartLabels.Length == 0)
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||||
|
برای این فیلتر، سفارش فعالی یافت نشد.
|
||||||
|
</MudText>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudChart ChartType="ChartType.Bar"
|
||||||
|
ChartSeries="@_statusChartSeries"
|
||||||
|
XAxisLabels="@_statusChartLabels"
|
||||||
|
Height="200px" />
|
||||||
|
}
|
||||||
|
</MudCardContent>
|
||||||
|
</MudCard>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
|
||||||
<MudDataGrid T="DataModel"
|
<MudDataGrid T="DataModel"
|
||||||
ServerData="@(new Func<GridState<DataModel>, Task<GridData<DataModel>>>(ServerReload))"
|
ServerData="@(new Func<GridState<DataModel>, Task<GridData<DataModel>>>(ServerReload))"
|
||||||
Hover="true" @ref="_gridData" Height="72vh">
|
Hover="true" @ref="_gridData" Height="60vh">
|
||||||
<ColGroup>
|
<ColGroup>
|
||||||
<col/>
|
<col/>
|
||||||
<col/>
|
<col/>
|
||||||
@@ -143,6 +201,33 @@
|
|||||||
Style="cursor:pointer;"/>
|
Style="cursor:pointer;"/>
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
|
|
||||||
|
<MudTooltip Text="تغییر وضعیت ارسال">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.LocalShipping"
|
||||||
|
Size="Size.Small"
|
||||||
|
Color="Color.Primary"
|
||||||
|
ButtonType="ButtonType.Button"
|
||||||
|
OnClick="@(() => OpenChangeStatus(context.Item))"
|
||||||
|
Style="cursor:pointer;"/>
|
||||||
|
</MudTooltip>
|
||||||
|
|
||||||
|
<MudTooltip Text="اعمال تخفیف">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.PriceChange"
|
||||||
|
Size="Size.Small"
|
||||||
|
Color="Color.Secondary"
|
||||||
|
ButtonType="ButtonType.Button"
|
||||||
|
OnClick="@(() => OpenApplyDiscount(context.Item))"
|
||||||
|
Style="cursor:pointer;"/>
|
||||||
|
</MudTooltip>
|
||||||
|
|
||||||
|
<MudTooltip Text="لغو سفارش">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Cancel"
|
||||||
|
Size="Size.Small"
|
||||||
|
Color="Color.Error"
|
||||||
|
ButtonType="ButtonType.Button"
|
||||||
|
OnClick="@(() => OpenCancelOrder(context.Item))"
|
||||||
|
Style="cursor:pointer;"/>
|
||||||
|
</MudTooltip>
|
||||||
|
|
||||||
<MudTooltip Text="آرشیو">
|
<MudTooltip Text="آرشیو">
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.DeleteOutline" Size="Size.Small"
|
<MudIconButton Icon="@Icons.Material.Filled.DeleteOutline" Size="Size.Small"
|
||||||
ButtonType="ButtonType.Button" OnClick="@(() => OnDelete(context.Item))"
|
ButtonType="ButtonType.Button" OnClick="@(() => OnDelete(context.Item))"
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ public partial class UserOrderMainPage
|
|||||||
private MudDataGrid<DataModel> _gridData;
|
private MudDataGrid<DataModel> _gridData;
|
||||||
BasePageComponent _basePage;
|
BasePageComponent _basePage;
|
||||||
|
|
||||||
|
private long? _orderIdFilter;
|
||||||
|
private long? _userIdFilter;
|
||||||
|
private long? _transactionIdFilter;
|
||||||
private int? _paymentStatusFilter;
|
private int? _paymentStatusFilter;
|
||||||
private int? _deliveryStatusFilter;
|
private int? _deliveryStatusFilter;
|
||||||
private int? _paymentMethodFilter;
|
private int? _paymentMethodFilter;
|
||||||
@@ -23,6 +26,10 @@ public partial class UserOrderMainPage
|
|||||||
|
|
||||||
private GetAllUserOrderByFilterRequest _request = new() { Filter = new() };
|
private GetAllUserOrderByFilterRequest _request = new() { Filter = new() };
|
||||||
|
|
||||||
|
private OrderStatsViewModel _stats = new();
|
||||||
|
private string[] _statusChartLabels = Array.Empty<string>();
|
||||||
|
private List<ChartSeries> _statusChartSeries = new();
|
||||||
|
|
||||||
|
|
||||||
private async Task<GridData<DataModel>> ServerReload(GridState<DataModel> state)
|
private async Task<GridData<DataModel>> ServerReload(GridState<DataModel> state)
|
||||||
{
|
{
|
||||||
@@ -32,13 +39,24 @@ public partial class UserOrderMainPage
|
|||||||
_request.PaginationState.PageNumber = state.Page + 1;
|
_request.PaginationState.PageNumber = state.Page + 1;
|
||||||
_request.PaginationState.PageSize = state.PageSize;
|
_request.PaginationState.PageSize = state.PageSize;
|
||||||
|
|
||||||
|
if (_orderIdFilter.HasValue && _orderIdFilter.Value > 0)
|
||||||
|
{
|
||||||
|
_request.Filter.Id = _orderIdFilter.Value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_request.Filter.Id = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (UserId.HasValue && UserId.Value > 0)
|
if (UserId.HasValue && UserId.Value > 0)
|
||||||
{
|
{
|
||||||
_request.Filter.UserId = UserId.Value;
|
_request.Filter.UserId = UserId.Value;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_request.Filter.UserId = null;
|
_request.Filter.UserId = _userIdFilter.HasValue && _userIdFilter.Value > 0
|
||||||
|
? _userIdFilter.Value
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_paymentDateFrom.HasValue)
|
if (_paymentDateFrom.HasValue)
|
||||||
@@ -78,13 +96,29 @@ public partial class UserOrderMainPage
|
|||||||
_request.Filter.PaymentMethod = null;
|
_request.Filter.PaymentMethod = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_transactionIdFilter.HasValue && _transactionIdFilter.Value > 0)
|
||||||
|
{
|
||||||
|
_request.Filter.TransactionId = _transactionIdFilter.Value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_request.Filter.TransactionId = null;
|
||||||
|
}
|
||||||
|
|
||||||
var result = await UserOrderContract.GetAllUserOrderByFilterAsync(_request);
|
var result = await UserOrderContract.GetAllUserOrderByFilterAsync(_request);
|
||||||
if (result != null && result.Models != null && result.Models.Any())
|
if (result != null && result.Models != null && result.Models.Any())
|
||||||
{
|
{
|
||||||
return new GridData<DataModel>()
|
var items = result.Models.ToList();
|
||||||
{ Items = result.Models.ToList(), TotalItems = (int)result.MetaData.TotalCount };
|
UpdateStats(items);
|
||||||
|
|
||||||
|
return new GridData<DataModel>
|
||||||
|
{
|
||||||
|
Items = items,
|
||||||
|
TotalItems = (int)result.MetaData.TotalCount
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateStats(new List<DataModel>());
|
||||||
return new GridData<DataModel>();
|
return new GridData<DataModel>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,10 +179,16 @@ public partial class UserOrderMainPage
|
|||||||
_basePage.IsFiltered = false;
|
_basePage.IsFiltered = false;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
_request = new() { Filter = new() { } };
|
_request = new() { Filter = new() { } };
|
||||||
|
_orderIdFilter = null;
|
||||||
|
_userIdFilter = null;
|
||||||
|
_transactionIdFilter = null;
|
||||||
_paymentStatusFilter = null;
|
_paymentStatusFilter = null;
|
||||||
_deliveryStatusFilter = null;
|
_deliveryStatusFilter = null;
|
||||||
_paymentDateFrom = null;
|
_paymentDateFrom = null;
|
||||||
_paymentMethodFilter = null;
|
_paymentMethodFilter = null;
|
||||||
|
_stats = new OrderStatsViewModel();
|
||||||
|
_statusChartLabels = Array.Empty<string>();
|
||||||
|
_statusChartSeries = new List<ChartSeries>();
|
||||||
ReLoadData();
|
ReLoadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,4 +224,100 @@ public partial class UserOrderMainPage
|
|||||||
_ => "درگاه پرداخت"
|
_ => "درگاه پرداخت"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateStats(List<DataModel> items)
|
||||||
|
{
|
||||||
|
if (items == null || items.Count == 0)
|
||||||
|
{
|
||||||
|
_stats = new OrderStatsViewModel();
|
||||||
|
_statusChartLabels = Array.Empty<string>();
|
||||||
|
_statusChartSeries = new List<ChartSeries>();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_stats = new OrderStatsViewModel
|
||||||
|
{
|
||||||
|
TotalOrders = items.Count,
|
||||||
|
TotalAmount = items.Sum(x => x.Amount)
|
||||||
|
};
|
||||||
|
|
||||||
|
var groups = items
|
||||||
|
.GroupBy(x => x.DeliveryStatus.GetHashCode())
|
||||||
|
.OrderBy(g => g.Key)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
_statusChartLabels = groups
|
||||||
|
.Select(g => GetDeliveryStatusText(g.Key))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
_statusChartSeries = new List<ChartSeries>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "تعداد سفارشها",
|
||||||
|
Data = groups.Select(g => (double)g.Count()).ToArray()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private class OrderStatsViewModel
|
||||||
|
{
|
||||||
|
public int TotalOrders { get; set; }
|
||||||
|
public long TotalAmount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OpenChangeStatus(DataModel model)
|
||||||
|
{
|
||||||
|
var parameters = new DialogParameters
|
||||||
|
{
|
||||||
|
{ nameof(BackOffice.Pages.UserOrder.Components.ChangeOrderStatusDialog.OrderId), model.Id },
|
||||||
|
{ nameof(BackOffice.Pages.UserOrder.Components.ChangeOrderStatusDialog.CurrentStatus), model.DeliveryStatus.GetHashCode() }
|
||||||
|
};
|
||||||
|
|
||||||
|
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||||
|
var dialog = await DialogService.ShowAsync<BackOffice.Pages.UserOrder.Components.ChangeOrderStatusDialog>(
|
||||||
|
"تغییر وضعیت ارسال سفارش", parameters, options);
|
||||||
|
var result = await dialog.Result;
|
||||||
|
|
||||||
|
if (!result.Canceled)
|
||||||
|
{
|
||||||
|
ReLoadData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OpenApplyDiscount(DataModel model)
|
||||||
|
{
|
||||||
|
var parameters = new DialogParameters
|
||||||
|
{
|
||||||
|
{ nameof(BackOffice.Pages.UserOrder.Components.ApplyDiscountDialog.OrderId), model.Id }
|
||||||
|
};
|
||||||
|
|
||||||
|
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||||
|
var dialog = await DialogService.ShowAsync<BackOffice.Pages.UserOrder.Components.ApplyDiscountDialog>(
|
||||||
|
"اعمال تخفیف روی سفارش", parameters, options);
|
||||||
|
var result = await dialog.Result;
|
||||||
|
|
||||||
|
if (!result.Canceled)
|
||||||
|
{
|
||||||
|
ReLoadData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OpenCancelOrder(DataModel model)
|
||||||
|
{
|
||||||
|
var parameters = new DialogParameters
|
||||||
|
{
|
||||||
|
{ nameof(BackOffice.Pages.UserOrder.Components.CancelOrderDialog.OrderId), model.Id }
|
||||||
|
};
|
||||||
|
|
||||||
|
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||||
|
var dialog = await DialogService.ShowAsync<BackOffice.Pages.UserOrder.Components.CancelOrderDialog>(
|
||||||
|
"لغو سفارش", parameters, options);
|
||||||
|
var result = await dialog.Result;
|
||||||
|
|
||||||
|
if (!result.Canceled)
|
||||||
|
{
|
||||||
|
ReLoadData();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
58
src/BackOffice/Services/Tag/ITagService.cs
Normal file
58
src/BackOffice/Services/Tag/ITagService.cs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
namespace BackOffice.Services.Tag;
|
||||||
|
|
||||||
|
public interface ITagService
|
||||||
|
{
|
||||||
|
Task<TagListResultDto> GetTagsAsync(TagFilterDto filter);
|
||||||
|
Task<TagDetailsDto?> GetByIdAsync(long id);
|
||||||
|
Task<long> CreateAsync(TagEditDto dto);
|
||||||
|
Task UpdateAsync(long id, TagEditDto dto);
|
||||||
|
Task DeleteAsync(long id);
|
||||||
|
Task AssignToProductAsync(long productId, long tagId);
|
||||||
|
Task<List<ProductTagViewDto>> GetProductTagsAsync(long productId);
|
||||||
|
Task RemoveProductTagAsync(long productTagId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TagFilterDto
|
||||||
|
{
|
||||||
|
public int PageNumber { get; set; } = 1;
|
||||||
|
public int PageSize { get; set; } = 20;
|
||||||
|
public string? SearchTerm { get; set; }
|
||||||
|
public bool? IsActive { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TagListResultDto
|
||||||
|
{
|
||||||
|
public List<TagListItemDto> Items { get; set; } = new();
|
||||||
|
public int TotalCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TagListItemDto
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public bool IsActive { get; set; }
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TagDetailsDto : TagListItemDto
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TagEditDto
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ProductTagViewDto
|
||||||
|
{
|
||||||
|
public long ProductTagId { get; set; }
|
||||||
|
public long TagId { get; set; }
|
||||||
|
public string TagTitle { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
176
src/BackOffice/Services/Tag/TagService.cs
Normal file
176
src/BackOffice/Services/Tag/TagService.cs
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
namespace BackOffice.Services.Tag;
|
||||||
|
|
||||||
|
public class TagService : ITagService
|
||||||
|
{
|
||||||
|
private readonly BackOffice.BFF.Tag.Protobuf.Protos.Tag.TagContract.TagContractClient _tagClient;
|
||||||
|
private readonly BackOffice.BFF.ProductTag.Protobuf.Protos.ProductTag.ProductTagContract.ProductTagContractClient _productTagClient;
|
||||||
|
private readonly BackOffice.BFF.Products.Protobuf.Protos.Products.ProductsContract.ProductsContractClient _productsClient;
|
||||||
|
public TagService(
|
||||||
|
BackOffice.BFF.Tag.Protobuf.Protos.Tag.TagContract.TagContractClient tagClient,
|
||||||
|
BackOffice.BFF.ProductTag.Protobuf.Protos.ProductTag.ProductTagContract.ProductTagContractClient productTagClient,
|
||||||
|
BackOffice.BFF.Products.Protobuf.Protos.Products.ProductsContract.ProductsContractClient productsClient)
|
||||||
|
{
|
||||||
|
_tagClient = tagClient;
|
||||||
|
_productTagClient = productTagClient;
|
||||||
|
_productsClient = productsClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TagListResultDto> GetTagsAsync(TagFilterDto filter)
|
||||||
|
{
|
||||||
|
var request = new BackOffice.BFF.Tag.Protobuf.Protos.Tag.GetAllTagByFilterRequest
|
||||||
|
{
|
||||||
|
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
|
||||||
|
{
|
||||||
|
PageNumber = filter.PageNumber,
|
||||||
|
PageSize = filter.PageSize
|
||||||
|
},
|
||||||
|
Filter = new BackOffice.BFF.Tag.Protobuf.Protos.Tag.GetAllTagByFilterFilter()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter.SearchTerm))
|
||||||
|
{
|
||||||
|
request.Filter.Name = filter.SearchTerm;
|
||||||
|
request.Filter.Title = filter.SearchTerm;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.IsActive.HasValue)
|
||||||
|
{
|
||||||
|
request.Filter.IsActive = filter.IsActive.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await _tagClient.GetAllTagByFilterAsync(request);
|
||||||
|
|
||||||
|
var result = new TagListResultDto
|
||||||
|
{
|
||||||
|
TotalCount = response.MetaData != null ? (int)response.MetaData.TotalCount : response.Models.Count
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var model in response.Models)
|
||||||
|
{
|
||||||
|
result.Items.Add(new TagListItemDto
|
||||||
|
{
|
||||||
|
Id = model.Id,
|
||||||
|
Name = model.Name,
|
||||||
|
Title = model.Title,
|
||||||
|
Description = model.Description,
|
||||||
|
IsActive = model.IsActive,
|
||||||
|
SortOrder = model.SortOrder
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TagDetailsDto?> GetByIdAsync(long id)
|
||||||
|
{
|
||||||
|
var response = await _tagClient.GetTagAsync(new BackOffice.BFF.Tag.Protobuf.Protos.Tag.GetTagRequest { Id = id });
|
||||||
|
|
||||||
|
if (response == null || response.Id <= 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TagDetailsDto
|
||||||
|
{
|
||||||
|
Id = response.Id,
|
||||||
|
Name = response.Name,
|
||||||
|
Title = response.Title,
|
||||||
|
Description = response.Description,
|
||||||
|
IsActive = response.IsActive,
|
||||||
|
SortOrder = response.SortOrder
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<long> CreateAsync(TagEditDto dto)
|
||||||
|
{
|
||||||
|
var request = new BackOffice.BFF.Tag.Protobuf.Protos.Tag.CreateNewTagRequest
|
||||||
|
{
|
||||||
|
Name = dto.Name,
|
||||||
|
Title = dto.Title,
|
||||||
|
Description = dto.Description ?? string.Empty,
|
||||||
|
IsActive = dto.IsActive,
|
||||||
|
SortOrder = dto.SortOrder
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await _tagClient.CreateNewTagAsync(request);
|
||||||
|
return response.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(long id, TagEditDto dto)
|
||||||
|
{
|
||||||
|
var request = new BackOffice.BFF.Tag.Protobuf.Protos.Tag.UpdateTagRequest
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Name = dto.Name,
|
||||||
|
Title = dto.Title,
|
||||||
|
Description = dto.Description ?? string.Empty,
|
||||||
|
IsActive = dto.IsActive,
|
||||||
|
SortOrder = dto.SortOrder
|
||||||
|
};
|
||||||
|
|
||||||
|
await _tagClient.UpdateTagAsync(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(long id)
|
||||||
|
{
|
||||||
|
await _tagClient.DeleteTagAsync(new BackOffice.BFF.Tag.Protobuf.Protos.Tag.DeleteTagRequest { Id = id });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AssignToProductAsync(long productId, long tagId)
|
||||||
|
{
|
||||||
|
var request = new BackOffice.BFF.ProductTag.Protobuf.Protos.ProductTag.CreateNewProductTagRequest
|
||||||
|
{
|
||||||
|
ProductId = productId,
|
||||||
|
TagId = tagId
|
||||||
|
};
|
||||||
|
|
||||||
|
await _productTagClient.CreateNewProductTagAsync(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<ProductTagViewDto>> GetProductTagsAsync(long productId)
|
||||||
|
{
|
||||||
|
var request = new BackOffice.BFF.ProductTag.Protobuf.Protos.ProductTag.GetAllProductTagByFilterRequest
|
||||||
|
{
|
||||||
|
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
|
||||||
|
{
|
||||||
|
PageNumber = 1,
|
||||||
|
PageSize = 100
|
||||||
|
},
|
||||||
|
Filter = new BackOffice.BFF.ProductTag.Protobuf.Protos.ProductTag.GetAllProductTagByFilterFilter
|
||||||
|
{
|
||||||
|
ProductId = productId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await _productTagClient.GetAllProductTagByFilterAsync(request);
|
||||||
|
|
||||||
|
var tagIds = response.Models.Select(m => m.TagId).Distinct().ToList();
|
||||||
|
|
||||||
|
var tagsResult = new List<ProductTagViewDto>();
|
||||||
|
|
||||||
|
if (tagIds.Any())
|
||||||
|
{
|
||||||
|
// از ProductsContract برای گرفتن اطلاعات تگها نیست، بنابراین فقط TagId را نگه میداریم
|
||||||
|
foreach (var model in response.Models)
|
||||||
|
{
|
||||||
|
tagsResult.Add(new ProductTagViewDto
|
||||||
|
{
|
||||||
|
ProductTagId = model.Id,
|
||||||
|
TagId = model.TagId,
|
||||||
|
TagTitle = model.TagId.ToString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tagsResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveProductTagAsync(long productTagId)
|
||||||
|
{
|
||||||
|
await _productTagClient.DeleteProductTagAsync(
|
||||||
|
new BackOffice.BFF.ProductTag.Protobuf.Protos.ProductTag.DeleteProductTagRequest
|
||||||
|
{
|
||||||
|
Id = productTagId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -107,6 +107,12 @@
|
|||||||
مدیریت دستهبندی
|
مدیریت دستهبندی
|
||||||
</MudNavLink>
|
</MudNavLink>
|
||||||
|
|
||||||
|
<MudNavLink Match="NavLinkMatch.Prefix"
|
||||||
|
Href="/tags"
|
||||||
|
Icon="@Icons.Material.Filled.Label">
|
||||||
|
مدیریت تگها
|
||||||
|
</MudNavLink>
|
||||||
|
|
||||||
<MudNavLink Match="NavLinkMatch.Prefix"
|
<MudNavLink Match="NavLinkMatch.Prefix"
|
||||||
Href="@(RouteConstance.UserPage)"
|
Href="@(RouteConstance.UserPage)"
|
||||||
Icon="@Icons.Material.Filled.People">
|
Icon="@Icons.Material.Filled.People">
|
||||||
@@ -144,6 +150,12 @@
|
|||||||
Icon="@Icons.Material.Filled.ShoppingCart">
|
Icon="@Icons.Material.Filled.ShoppingCart">
|
||||||
سفارشات فروشگاه
|
سفارشات فروشگاه
|
||||||
</MudNavLink>
|
</MudNavLink>
|
||||||
|
|
||||||
|
<MudNavLink Match="NavLinkMatch.Prefix"
|
||||||
|
Href="/discount-sales-reports"
|
||||||
|
Icon="@Icons.Material.Filled.BarChart">
|
||||||
|
گزارش فروش
|
||||||
|
</MudNavLink>
|
||||||
</MudNavGroup>
|
</MudNavGroup>
|
||||||
|
|
||||||
<MudNavLink Match="NavLinkMatch.Prefix"
|
<MudNavLink Match="NavLinkMatch.Prefix"
|
||||||
|
|||||||
Reference in New Issue
Block a user