feat: Implement Public Messages Management Page with CRUD operations
- Added PublicMessagesMainPage.razor for managing public messages. - Integrated search and filter functionality for messages. - Implemented create, edit, view, publish, archive, and delete operations for public messages. - Added necessary services and DTOs for handling public messages. - Created DiscountCategory and DiscountOrder services with CRUD operations. - Implemented DiscountProduct service for managing discount products. - Defined interfaces for all services to ensure proper abstraction and testing.
This commit is contained in:
@@ -12,7 +12,15 @@ using BackOffice.BFF.NetworkMembership.Protobuf;
|
||||
using BackOffice.BFF.ClubMembership.Protobuf;
|
||||
using BackOffice.BFF.Configuration.Protobuf;
|
||||
using BackOffice.BFF.Health.Protobuf;
|
||||
using BackOffice.BFF.DiscountProduct.Protobuf.Protos.DiscountProduct;
|
||||
using BackOffice.BFF.DiscountCategory.Protobuf.Protos.DiscountCategory;
|
||||
using BackOffice.BFF.DiscountOrder.Protobuf.Protos.DiscountOrder;
|
||||
using BackOffice.BFF.PublicMessage.Protobuf.Protos.PublicMessage;
|
||||
using BackOffice.Common.Utilities;
|
||||
using BackOffice.Services.DiscountProduct;
|
||||
using BackOffice.Services.DiscountCategory;
|
||||
using BackOffice.Services.DiscountOrder;
|
||||
using BackOffice.Services.PublicMessage;
|
||||
using Blazored.LocalStorage;
|
||||
using Grpc.Core;
|
||||
using Grpc.Core.Interceptors;
|
||||
@@ -46,6 +54,13 @@ public static class ConfigureServices
|
||||
services.AddSingleton<ITokenHandler, TokenHandler>();
|
||||
services.AddMudServices();
|
||||
services.AddGrpcServices(configuration);
|
||||
|
||||
// Application Services
|
||||
services.AddScoped<IDiscountProductService, DiscountProductService>();
|
||||
services.AddScoped<IDiscountCategoryService, DiscountCategoryService>();
|
||||
services.AddScoped<IDiscountOrderService, DiscountOrderService>();
|
||||
services.AddScoped<IPublicMessageService, PublicMessageService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -88,6 +103,14 @@ public static class ConfigureServices
|
||||
services.AddTransient(sp => new ConfigurationContract.ConfigurationContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new HealthContract.HealthContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
|
||||
// Discount Shop Services
|
||||
services.AddTransient(sp => new DiscountProductsContract.DiscountProductsContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new DiscountCategoriesContract.DiscountCategoriesContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new DiscountOrdersContract.DiscountOrdersContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
|
||||
// Public Message Service
|
||||
services.AddTransient(sp => new PublicMessagesContract.PublicMessagesContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
|
||||
309
src/BackOffice/Pages/Commission/WithdrawalReports.razor
Normal file
309
src/BackOffice/Pages/Commission/WithdrawalReports.razor
Normal file
@@ -0,0 +1,309 @@
|
||||
@page "/commission/withdrawal-reports"
|
||||
@attribute [Authorize]
|
||||
|
||||
@using MudBlazor
|
||||
@using BackOffice.BFF.Commission.Protobuf
|
||||
@using Google.Protobuf.WellKnownTypes
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<MudText Typo="Typo.h4" Class="mb-1">گزارش برداشتها</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary" Class="mb-4">
|
||||
گزارش تجمیعی برداشتهای کمیسیون بر اساس بازه تاریخ و نوع دوره
|
||||
</MudText>
|
||||
|
||||
<MudCard Class="mb-4">
|
||||
<MudCardContent>
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="3">
|
||||
<MudDatePicker @bind-Date="_startDate"
|
||||
Label="از تاریخ"
|
||||
DateFormat="yyyy/MM/dd"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="3">
|
||||
<MudDatePicker @bind-Date="_endDate"
|
||||
Label="تا تاریخ"
|
||||
DateFormat="yyyy/MM/dd"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="3">
|
||||
<MudSelect T="PeriodType" @bind-Value="_periodType"
|
||||
Label="نوع دوره"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense">
|
||||
<MudSelectItem Value="PeriodType.Daily">روزانه</MudSelectItem>
|
||||
<MudSelectItem Value="PeriodType.Weekly">هفتگی</MudSelectItem>
|
||||
<MudSelectItem Value="PeriodType.Monthly">ماهانه</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="3">
|
||||
<MudNumericField TValue="long?" @bind-Value="_userId"
|
||||
Label="شناسه کاربر (اختیاری)"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" />
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudGrid Class="mt-2">
|
||||
<MudItem xs="12" md="4">
|
||||
<MudSelect T="int?" @bind-Value="_status"
|
||||
Label="وضعیت (اختیاری)"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense">
|
||||
<MudSelectItem Value="@(null as int?)">همه وضعیتها</MudSelectItem>
|
||||
<MudSelectItem Value="0">در انتظار واریز به کیف پول</MudSelectItem>
|
||||
<MudSelectItem Value="1">واریز شده به کیف پول طلایی</MudSelectItem>
|
||||
<MudSelectItem Value="2">درخواست برداشت ثبت شده</MudSelectItem>
|
||||
<MudSelectItem Value="3">برداشت انجام شده</MudSelectItem>
|
||||
<MudSelectItem Value="4">خطا در پرداخت بانکی</MudSelectItem>
|
||||
<MudSelectItem Value="5">لغو شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4" Class="d-flex align-end">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
OnClick="LoadReports"
|
||||
StartIcon="@Icons.Material.Filled.Search">
|
||||
جستجو
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
|
||||
@if (_isLoading)
|
||||
{
|
||||
<MudPaper Class="pa-4">
|
||||
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
|
||||
</MudPaper>
|
||||
}
|
||||
else if (_periodReports.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">@_summary.TotalAmount.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">@_summary.TotalPaid.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">@_summary.TotalPending.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">@_summary.SuccessRate.ToString("0.##")%</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||
@($"{_summary.TotalRequests} درخواست، {_summary.UniqueUsers} کاربر")
|
||||
</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudCard>
|
||||
<MudCardContent>
|
||||
<MudDataGrid T="PeriodReportViewModel"
|
||||
Items="_periodReports"
|
||||
Hover="true"
|
||||
Filterable="true"
|
||||
SortMode="SortMode.Multiple">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.PeriodLabel" Title="دوره" />
|
||||
<PropertyColumn Property="x => x.StartDate" Title="از تاریخ">
|
||||
<CellTemplate>
|
||||
@context.Item.StartDate.ToString("yyyy/MM/dd")
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
<PropertyColumn Property="x => x.EndDate" Title="تا تاریخ">
|
||||
<CellTemplate>
|
||||
@context.Item.EndDate.ToString("yyyy/MM/dd")
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
<PropertyColumn Property="x => x.TotalRequests" Title="تعداد درخواستها" />
|
||||
<PropertyColumn Property="x => x.CompletedCount" Title="موفق" />
|
||||
<PropertyColumn Property="x => x.FailedCount" Title="ناموفق" />
|
||||
<PropertyColumn Property="x => x.PendingCount" Title="در انتظار" />
|
||||
<PropertyColumn Property="x => x.TotalAmount" Title="مبلغ کل" Format="N0">
|
||||
<CellTemplate>
|
||||
@context.Item.TotalAmount.ToString("N0") ریال
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
<PropertyColumn Property="x => x.PaidAmount" Title="پرداخت شده" Format="N0">
|
||||
<CellTemplate>
|
||||
@context.Item.PaidAmount.ToString("N0") ریال
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
<PropertyColumn Property="x => x.PendingAmount" Title="در انتظار پرداخت" Format="N0">
|
||||
<CellTemplate>
|
||||
@context.Item.PendingAmount.ToString("N0") ریال
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
</Columns>
|
||||
</MudDataGrid>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
}
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
[Inject] public CommissionContract.CommissionContractClient CommissionClient { get; set; }
|
||||
|
||||
private DateTime? _startDate = DateTime.Today.AddDays(-30);
|
||||
private DateTime? _endDate = DateTime.Today;
|
||||
private PeriodType _periodType = PeriodType.Daily;
|
||||
private int? _status;
|
||||
private long? _userId;
|
||||
|
||||
private bool _isLoading;
|
||||
private WithdrawalSummaryViewModel _summary = new();
|
||||
private List<PeriodReportViewModel> _periodReports = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadReports();
|
||||
}
|
||||
|
||||
private async Task LoadReports()
|
||||
{
|
||||
if (!_startDate.HasValue || !_endDate.HasValue)
|
||||
{
|
||||
Snackbar.Add("لطفاً بازه تاریخ را مشخص کنید.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_startDate.Value.Date > _endDate.Value.Date)
|
||||
{
|
||||
Snackbar.Add("تاریخ شروع نباید بعد از تاریخ پایان باشد.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
_periodReports.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
var request = new GetWithdrawalReportsRequest
|
||||
{
|
||||
PeriodType = (int)_periodType
|
||||
};
|
||||
|
||||
var start = DateTime.SpecifyKind(_startDate.Value.Date, DateTimeKind.Local).ToUniversalTime();
|
||||
var end = DateTime.SpecifyKind(_endDate.Value.Date.AddDays(1).AddTicks(-1), DateTimeKind.Local).ToUniversalTime();
|
||||
|
||||
request.StartDate = Timestamp.FromDateTime(start);
|
||||
request.EndDate = Timestamp.FromDateTime(end);
|
||||
|
||||
if (_status.HasValue)
|
||||
{
|
||||
request.Status = new Int32Value { Value = _status.Value };
|
||||
}
|
||||
|
||||
if (_userId.HasValue)
|
||||
{
|
||||
request.UserId = new Int64Value { Value = _userId.Value };
|
||||
}
|
||||
|
||||
var response = await CommissionClient.GetWithdrawalReportsAsync(request);
|
||||
|
||||
if (response?.Summary != null)
|
||||
{
|
||||
_summary = new WithdrawalSummaryViewModel
|
||||
{
|
||||
TotalRequests = response.Summary.TotalRequests,
|
||||
TotalAmount = response.Summary.TotalAmount,
|
||||
TotalPaid = response.Summary.TotalPaid,
|
||||
TotalPending = response.Summary.TotalPending,
|
||||
TotalRejected = response.Summary.TotalRejected,
|
||||
AverageAmount = response.Summary.AverageAmount,
|
||||
UniqueUsers = response.Summary.UniqueUsers,
|
||||
SuccessRate = (decimal)response.Summary.SuccessRate
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
_summary = new WithdrawalSummaryViewModel();
|
||||
}
|
||||
|
||||
_periodReports = response?.PeriodReports
|
||||
.Select(p => new PeriodReportViewModel
|
||||
{
|
||||
PeriodLabel = p.PeriodLabel,
|
||||
StartDate = p.StartDate.ToDateTime().ToLocalTime().Date,
|
||||
EndDate = p.EndDate.ToDateTime().ToLocalTime().Date,
|
||||
TotalRequests = p.TotalRequests,
|
||||
PendingCount = p.PendingCount,
|
||||
ApprovedCount = p.ApprovedCount,
|
||||
RejectedCount = p.RejectedCount,
|
||||
CompletedCount = p.CompletedCount,
|
||||
FailedCount = p.FailedCount,
|
||||
TotalAmount = p.TotalAmount,
|
||||
PaidAmount = p.PaidAmount,
|
||||
PendingAmount = p.PendingAmount
|
||||
})
|
||||
.ToList() ?? new List<PeriodReportViewModel>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری گزارشها: {ex.Message}", Severity.Error);
|
||||
_summary = new WithdrawalSummaryViewModel();
|
||||
_periodReports.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private enum PeriodType
|
||||
{
|
||||
Daily = 1,
|
||||
Weekly = 2,
|
||||
Monthly = 3
|
||||
}
|
||||
|
||||
private class WithdrawalSummaryViewModel
|
||||
{
|
||||
public int TotalRequests { get; set; }
|
||||
public long TotalAmount { get; set; }
|
||||
public long TotalPaid { get; set; }
|
||||
public long TotalPending { get; set; }
|
||||
public long TotalRejected { get; set; }
|
||||
public long AverageAmount { get; set; }
|
||||
public int UniqueUsers { get; set; }
|
||||
public decimal SuccessRate { get; set; }
|
||||
}
|
||||
|
||||
private class PeriodReportViewModel
|
||||
{
|
||||
public string PeriodLabel { get; set; } = string.Empty;
|
||||
public DateTime StartDate { get; set; }
|
||||
public DateTime EndDate { get; set; }
|
||||
public int TotalRequests { get; set; }
|
||||
public int PendingCount { get; set; }
|
||||
public int ApprovedCount { get; set; }
|
||||
public int RejectedCount { get; set; }
|
||||
public int CompletedCount { get; set; }
|
||||
public int FailedCount { get; set; }
|
||||
public long TotalAmount { get; set; }
|
||||
public long PaidAmount { get; set; }
|
||||
public long PendingAmount { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
@using BackOffice.Services.DiscountCategory
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudForm @ref="_form" @bind-IsValid="@_isValid">
|
||||
<MudGrid>
|
||||
<MudItem xs="12">
|
||||
<MudTextField @bind-Value="Model.Title"
|
||||
Label="عنوان دستهبندی *"
|
||||
Required="true"
|
||||
RequiredError="عنوان الزامی است"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12">
|
||||
<MudTextField @bind-Value="Model.Description"
|
||||
Label="توضیحات"
|
||||
Lines="3"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudSelect @bind-Value="Model.ParentCategoryId"
|
||||
Label="دستهبندی والد"
|
||||
Variant="Variant.Outlined"
|
||||
Clearable="true"
|
||||
HelperText="برای دسته اصلی خالی بگذارید">
|
||||
@foreach (var category in _availableParents)
|
||||
{
|
||||
<MudSelectItem Value="@category.CategoryId">@GetCategoryPath(category)</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudNumericField @bind-Value="Model.DisplayOrder"
|
||||
Label="ترتیب نمایش"
|
||||
Min="0"
|
||||
Variant="Variant.Outlined"
|
||||
HelperText="عدد کمتر = اولویت بالاتر" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12">
|
||||
<MudSwitch @bind-Checked="Model.IsActive"
|
||||
Label="دستهبندی فعال"
|
||||
Color="Color.Success" />
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel" Variant="Variant.Text">انصراف</MudButton>
|
||||
<MudButton OnClick="Submit"
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
Disabled="@(!_isValid || _loading)">
|
||||
@if (_loading)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>@(IsEditMode ? "ذخیره تغییرات" : "ایجاد دستهبندی")</text>
|
||||
}
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] MudDialogInstance MudDialog { get; set; } = null!;
|
||||
[Parameter] public CategoryFormModel Model { get; set; } = new();
|
||||
[Parameter] public bool IsEditMode { get; set; }
|
||||
[Parameter] public long? ExcludeCategoryId { get; set; }
|
||||
[Inject] private IDiscountCategoryService CategoryService { get; set; } = null!;
|
||||
|
||||
private MudForm? _form;
|
||||
private bool _isValid;
|
||||
private bool _loading;
|
||||
private List<DiscountCategoryDto> _availableParents = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadCategories();
|
||||
}
|
||||
|
||||
private async Task LoadCategories()
|
||||
{
|
||||
try
|
||||
{
|
||||
var tree = await CategoryService.GetCategoriesAsync(isActive: null);
|
||||
var flat = FlattenCategories(tree);
|
||||
|
||||
// در حالت Edit، دسته جاری و فرزندانش را نمایش نده
|
||||
if (ExcludeCategoryId.HasValue)
|
||||
{
|
||||
_availableParents = flat.Where(c => c.CategoryId != ExcludeCategoryId.Value).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
_availableParents = flat;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری دستهبندیها: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private List<DiscountCategoryDto> FlattenCategories(List<DiscountCategoryDto> categories, string prefix = "")
|
||||
{
|
||||
var result = new List<DiscountCategoryDto>();
|
||||
foreach (var category in categories)
|
||||
{
|
||||
var catCopy = new DiscountCategoryDto
|
||||
{
|
||||
CategoryId = category.CategoryId,
|
||||
Title = prefix + category.Title,
|
||||
ParentCategoryId = category.ParentCategoryId
|
||||
};
|
||||
result.Add(catCopy);
|
||||
|
||||
if (category.Children.Any())
|
||||
{
|
||||
result.AddRange(FlattenCategories(category.Children.ToList(), prefix + " "));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private string GetCategoryPath(DiscountCategoryDto category)
|
||||
{
|
||||
return category.Title;
|
||||
}
|
||||
|
||||
private async Task Submit()
|
||||
{
|
||||
if (_form != null)
|
||||
{
|
||||
await _form.Validate();
|
||||
if (!_isValid) return;
|
||||
}
|
||||
|
||||
_loading = true;
|
||||
MudDialog.Close(DialogResult.Ok(Model));
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
MudDialog.Cancel();
|
||||
}
|
||||
|
||||
public class CategoryFormModel
|
||||
{
|
||||
public long? ParentCategoryId { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public int DisplayOrder { get; set; } = 0;
|
||||
public bool IsActive { get; set; } = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
@using BackOffice.Services.DiscountOrder
|
||||
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Edit" Class="ml-2" />
|
||||
تغییر وضعیت سفارش #@OrderId
|
||||
</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
<MudForm @ref="_form" @bind-IsValid="@_isValid">
|
||||
<MudGrid>
|
||||
<MudItem xs="12">
|
||||
<MudAlert Severity="Severity.Info" Dense="true">
|
||||
وضعیت فعلی: <strong>@GetStatusText(CurrentStatus)</strong>
|
||||
</MudAlert>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12">
|
||||
<MudSelect @bind-Value="Model.Status"
|
||||
Label="وضعیت جدید *"
|
||||
Required="true"
|
||||
Variant="Variant.Outlined">
|
||||
<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">
|
||||
<MudTextField @bind-Value="Model.AdminNote"
|
||||
Label="یادداشت ادمین"
|
||||
Lines="4"
|
||||
Variant="Variant.Outlined"
|
||||
HelperText="دلیل تغییر وضعیت یا توضیحات اضافی" />
|
||||
</MudItem>
|
||||
|
||||
@if (Model.Status == OrderStatus.Cancelled || Model.Status == OrderStatus.Returned)
|
||||
{
|
||||
<MudItem xs="12">
|
||||
<MudAlert Severity="Severity.Warning">
|
||||
<MudText>توجه: در صورت لغو یا مرجوعی سفارش، موجودی محصولات به انبار بازگردانده میشود.</MudText>
|
||||
</MudAlert>
|
||||
</MudItem>
|
||||
}
|
||||
|
||||
@if (Model.Status == OrderStatus.Shipped)
|
||||
{
|
||||
<MudItem xs="12">
|
||||
<MudAlert Severity="Severity.Info">
|
||||
<MudText>پس از تغییر وضعیت به "ارسال شده"، اطلاعرسانی به کاربر ارسال خواهد شد.</MudText>
|
||||
</MudAlert>
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel" Variant="Variant.Text">انصراف</MudButton>
|
||||
<MudButton OnClick="Submit"
|
||||
Variant="Variant.Filled"
|
||||
Color="@GetStatusColor(Model.Status)"
|
||||
Disabled="@(!_isValid || _loading)">
|
||||
@if (_loading)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>تغییر وضعیت</text>
|
||||
}
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] MudDialogInstance MudDialog { get; set; } = null!;
|
||||
[Parameter] public long OrderId { get; set; }
|
||||
[Parameter] public OrderStatus CurrentStatus { get; set; }
|
||||
|
||||
private MudForm? _form;
|
||||
private bool _isValid;
|
||||
private bool _loading;
|
||||
private UpdateOrderStatusDto Model = new();
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Model.Status = CurrentStatus;
|
||||
}
|
||||
|
||||
private async Task Submit()
|
||||
{
|
||||
if (_form != null)
|
||||
{
|
||||
await _form.Validate();
|
||||
if (!_isValid) return;
|
||||
}
|
||||
|
||||
_loading = true;
|
||||
MudDialog.Close(DialogResult.Ok(Model));
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
MudDialog.Cancel();
|
||||
}
|
||||
|
||||
private Color GetStatusColor(OrderStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
OrderStatus.Pending => Color.Warning,
|
||||
OrderStatus.Paid => Color.Info,
|
||||
OrderStatus.Processing => Color.Primary,
|
||||
OrderStatus.Shipped => Color.Secondary,
|
||||
OrderStatus.Delivered => Color.Success,
|
||||
OrderStatus.Cancelled => Color.Error,
|
||||
OrderStatus.Returned => Color.Dark,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
private string GetStatusText(OrderStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
OrderStatus.Pending => "در انتظار پرداخت",
|
||||
OrderStatus.Paid => "پرداخت شده",
|
||||
OrderStatus.Processing => "در حال آمادهسازی",
|
||||
OrderStatus.Shipped => "ارسال شده",
|
||||
OrderStatus.Delivered => "تحویل داده شده",
|
||||
OrderStatus.Cancelled => "لغو شده",
|
||||
OrderStatus.Returned => "مرجوع شده",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
@using BackOffice.Services.DiscountOrder
|
||||
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ShoppingCart" Class="ml-2" />
|
||||
جزئیات سفارش #@Order.OrderId
|
||||
</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
<MudGrid>
|
||||
<!-- اطلاعات کاربر -->
|
||||
<MudItem xs="12">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.h6" GutterBottom="true">اطلاعات خریدار</MudText>
|
||||
<MudGrid>
|
||||
<MudItem xs="6">
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">نام و نام خانوادگی:</MudText>
|
||||
<MudText Typo="Typo.body1"><strong>@Order.UserFullName</strong></MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="6">
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">شناسه کاربر:</MudText>
|
||||
<MudText Typo="Typo.body1">@Order.UserId</MudText>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<!-- وضعیت سفارش -->
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.h6" GutterBottom="true">وضعیت سفارش</MudText>
|
||||
<MudChip Color="@GetStatusColor(Order.Status)" Size="Size.Large">
|
||||
@GetStatusText(Order.Status)
|
||||
</MudChip>
|
||||
<MudText Typo="Typo.body2" Class="mt-2">
|
||||
تاریخ ثبت: @Order.CreatedAt.ToString("yyyy/MM/dd HH:mm")
|
||||
</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<!-- وضعیت پرداخت -->
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.h6" GutterBottom="true">وضعیت پرداخت</MudText>
|
||||
@if (Order.IsPaid)
|
||||
{
|
||||
<MudChip Color="Color.Success" Size="Size.Large" Icon="@Icons.Material.Filled.CheckCircle">
|
||||
پرداخت شده
|
||||
</MudChip>
|
||||
@if (Order.PaidAt.HasValue)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mt-2">
|
||||
تاریخ پرداخت: @Order.PaidAt.Value.ToString("yyyy/MM/dd HH:mm")
|
||||
</MudText>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Order.PaymentTransactionCode))
|
||||
{
|
||||
<MudText Typo="Typo.body2">
|
||||
کد تراکنش: @Order.PaymentTransactionCode
|
||||
</MudText>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChip Color="Color.Warning" Size="Size.Large" Icon="@Icons.Material.Filled.Schedule">
|
||||
در انتظار پرداخت
|
||||
</MudChip>
|
||||
}
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<!-- آدرس ارسال -->
|
||||
@if (!string.IsNullOrEmpty(Order.ShippingAddress))
|
||||
{
|
||||
<MudItem xs="12">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.h6" GutterBottom="true">
|
||||
<MudIcon Icon="@Icons.Material.Filled.LocationOn" Size="Size.Small" />
|
||||
آدرس ارسال
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body1">@Order.ShippingAddress</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
}
|
||||
|
||||
<!-- آیتمهای سفارش -->
|
||||
<MudItem xs="12">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.h6" GutterBottom="true">آیتمهای سفارش</MudText>
|
||||
<MudTable Items="@Order.Items" Hover="true" Dense="true" Elevation="0">
|
||||
<HeaderContent>
|
||||
<MudTh>محصول</MudTh>
|
||||
<MudTh>تعداد</MudTh>
|
||||
<MudTh>قیمت واحد</MudTh>
|
||||
<MudTh>تخفیف</MudTh>
|
||||
<MudTh>قیمت نهایی</MudTh>
|
||||
<MudTh>جمع</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>
|
||||
<div class="d-flex align-center">
|
||||
@if (!string.IsNullOrEmpty(context.ProductThumbnail))
|
||||
{
|
||||
<MudAvatar Image="@context.ProductThumbnail" Size="Size.Small" Class="ml-2" />
|
||||
}
|
||||
<MudText>@context.ProductTitle</MudText>
|
||||
</div>
|
||||
</MudTd>
|
||||
<MudTd>@context.Quantity</MudTd>
|
||||
<MudTd>@context.UnitPrice.ToString("N0") ریال</MudTd>
|
||||
<MudTd>
|
||||
<MudChip Size="Size.Small" Color="Color.Success">@context.DiscountPercent%</MudChip>
|
||||
</MudTd>
|
||||
<MudTd>@context.DiscountedPrice.ToString("N0") ریال</MudTd>
|
||||
<MudTd><strong>@context.TotalPrice.ToString("N0") ریال</strong></MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<!-- خلاصه مالی -->
|
||||
<MudItem xs="12">
|
||||
<MudPaper Class="pa-4" Elevation="2">
|
||||
<MudText Typo="Typo.h6" GutterBottom="true">خلاصه مالی</MudText>
|
||||
<MudGrid>
|
||||
<MudItem xs="6">
|
||||
<MudText Color="Color.Secondary">مبلغ کل:</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="6" Class="text-left">
|
||||
<MudText>@Order.TotalAmount.ToString("N0") ریال</MudText>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="6">
|
||||
<MudText Color="Color.Success">تخفیف:</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="6" Class="text-left">
|
||||
<MudText Color="Color.Success">@Order.TotalDiscount.ToString("N0") ریال</MudText>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12"><MudDivider /></MudItem>
|
||||
|
||||
<MudItem xs="6">
|
||||
<MudText Typo="Typo.h6">مبلغ قابل پرداخت:</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="6" Class="text-left">
|
||||
<MudText Typo="Typo.h6" Color="Color.Primary">
|
||||
<strong>@Order.FinalAmount.ToString("N0") ریال</strong>
|
||||
</MudText>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<!-- یادداشت ادمین -->
|
||||
@if (!string.IsNullOrEmpty(Order.AdminNote))
|
||||
{
|
||||
<MudItem xs="12">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.h6" GutterBottom="true">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Note" Size="Size.Small" />
|
||||
یادداشت ادمین
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body1">@Order.AdminNote</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Close" Variant="Variant.Filled" Color="Color.Primary">بستن</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] MudDialogInstance MudDialog { get; set; } = null!;
|
||||
[Parameter] public DiscountOrderDetailsDto Order { get; set; } = null!;
|
||||
|
||||
private void Close()
|
||||
{
|
||||
MudDialog.Close();
|
||||
}
|
||||
|
||||
private Color GetStatusColor(OrderStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
OrderStatus.Pending => Color.Warning,
|
||||
OrderStatus.Paid => Color.Info,
|
||||
OrderStatus.Processing => Color.Primary,
|
||||
OrderStatus.Shipped => Color.Secondary,
|
||||
OrderStatus.Delivered => Color.Success,
|
||||
OrderStatus.Cancelled => Color.Error,
|
||||
OrderStatus.Returned => Color.Dark,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
private string GetStatusText(OrderStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
OrderStatus.Pending => "در انتظار پرداخت",
|
||||
OrderStatus.Paid => "پرداخت شده",
|
||||
OrderStatus.Processing => "در حال آمادهسازی",
|
||||
OrderStatus.Shipped => "ارسال شده",
|
||||
OrderStatus.Delivered => "تحویل داده شده",
|
||||
OrderStatus.Cancelled => "لغو شده",
|
||||
OrderStatus.Returned => "مرجوع شده",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
@using BackOffice.Services.DiscountProduct
|
||||
@using BackOffice.Services.DiscountCategory
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudForm @ref="_form" @bind-IsValid="@_isValid">
|
||||
<MudGrid>
|
||||
<MudItem xs="12">
|
||||
<MudTextField @bind-Value="Model.Title"
|
||||
Label="عنوان محصول *"
|
||||
Required="true"
|
||||
RequiredError="عنوان الزامی است"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12">
|
||||
<MudTextField @bind-Value="Model.Description"
|
||||
Label="توضیحات"
|
||||
Lines="4"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudNumericField @bind-Value="Model.Price"
|
||||
Label="قیمت (ریال) *"
|
||||
Required="true"
|
||||
Min="0"
|
||||
Format="N0"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudNumericField @bind-Value="Model.MaxDiscountPercent"
|
||||
Label="حداکثر درصد تخفیف *"
|
||||
Required="true"
|
||||
Min="0"
|
||||
Max="100"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudNumericField @bind-Value="Model.Stock"
|
||||
Label="موجودی *"
|
||||
Required="true"
|
||||
Min="0"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudSelect @bind-Value="Model.CategoryId"
|
||||
Label="دستهبندی"
|
||||
Variant="Variant.Outlined"
|
||||
Clearable="true">
|
||||
@foreach (var category in _categories)
|
||||
{
|
||||
<MudSelectItem Value="@category.CategoryId">@GetCategoryPath(category)</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12">
|
||||
<MudTextField @bind-Value="Model.ThumbnailPath"
|
||||
Label="مسیر تصویر"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.End"
|
||||
AdornmentIcon="@Icons.Material.Filled.Image" />
|
||||
</MudItem>
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model.ThumbnailPath))
|
||||
{
|
||||
<MudItem xs="12">
|
||||
<MudImage Src="@Model.ThumbnailPath"
|
||||
Alt="پیشنمایش"
|
||||
Height="150"
|
||||
ObjectFit="ObjectFit.Contain"
|
||||
Class="rounded" />
|
||||
</MudItem>
|
||||
}
|
||||
|
||||
<MudItem xs="12">
|
||||
<MudTextField @bind-Value="_tagsInput"
|
||||
Label="تگها (با کاما جدا کنید)"
|
||||
Variant="Variant.Outlined"
|
||||
HelperText="مثال: الکترونیک, موبایل, سامسونگ" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12">
|
||||
<MudSwitch @bind-Checked="Model.IsActive"
|
||||
Label="محصول فعال"
|
||||
Color="Color.Success" />
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel" Variant="Variant.Text">انصراف</MudButton>
|
||||
<MudButton OnClick="Submit"
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
Disabled="@(!_isValid || _loading)">
|
||||
@if (_loading)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>@(IsEditMode ? "ذخیره تغییرات" : "ایجاد محصول")</text>
|
||||
}
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] MudDialogInstance MudDialog { get; set; } = null!;
|
||||
[Parameter] public ProductFormModel Model { get; set; } = new();
|
||||
[Parameter] public bool IsEditMode { get; set; }
|
||||
[Inject] private IDiscountCategoryService CategoryService { get; set; } = null!;
|
||||
|
||||
private MudForm? _form;
|
||||
private bool _isValid;
|
||||
private bool _loading;
|
||||
private List<DiscountCategoryDto> _categories = new();
|
||||
private string _tagsInput = string.Empty;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadCategories();
|
||||
|
||||
if (Model.Tags?.Any() == true)
|
||||
{
|
||||
_tagsInput = string.Join(", ", Model.Tags);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadCategories()
|
||||
{
|
||||
try
|
||||
{
|
||||
var tree = await CategoryService.GetCategoriesAsync(isActive: true);
|
||||
_categories = FlattenCategories(tree);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری دستهبندیها: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private List<DiscountCategoryDto> FlattenCategories(List<DiscountCategoryDto> categories, string prefix = "")
|
||||
{
|
||||
var result = new List<DiscountCategoryDto>();
|
||||
foreach (var category in categories)
|
||||
{
|
||||
var catCopy = new DiscountCategoryDto
|
||||
{
|
||||
CategoryId = category.CategoryId,
|
||||
Title = prefix + category.Title,
|
||||
ParentCategoryId = category.ParentCategoryId
|
||||
};
|
||||
result.Add(catCopy);
|
||||
|
||||
if (category.Children.Any())
|
||||
{
|
||||
result.AddRange(FlattenCategories(category.Children.ToList(), prefix + " "));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private string GetCategoryPath(DiscountCategoryDto category)
|
||||
{
|
||||
return category.Title;
|
||||
}
|
||||
|
||||
private async Task Submit()
|
||||
{
|
||||
if (_form != null)
|
||||
{
|
||||
await _form.Validate();
|
||||
if (!_isValid) return;
|
||||
}
|
||||
|
||||
_loading = true;
|
||||
|
||||
// Parse tags
|
||||
if (!string.IsNullOrWhiteSpace(_tagsInput))
|
||||
{
|
||||
Model.Tags = _tagsInput.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(t => t.Trim())
|
||||
.Where(t => !string.IsNullOrEmpty(t))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
MudDialog.Close(DialogResult.Ok(Model));
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
MudDialog.Cancel();
|
||||
}
|
||||
|
||||
public class ProductFormModel
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public string? ThumbnailPath { get; set; }
|
||||
public long Price { get; set; }
|
||||
public int MaxDiscountPercent { get; set; }
|
||||
public int Stock { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public long? CategoryId { get; set; }
|
||||
public List<string>? Tags { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
@page "/discount-categories"
|
||||
@using BackOffice.Services.DiscountCategory
|
||||
@using BackOffice.Pages.DiscountShop.Components
|
||||
@using static BackOffice.Pages.DiscountShop.Components.CategoryFormDialog
|
||||
@inject IDiscountCategoryService DiscountCategoryService
|
||||
@inject IDialogService DialogService
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<PageTitle>مدیریت دستهبندی محصولات تخفیفی</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Category" Class="ml-2" />
|
||||
مدیریت دستهبندیهای فروشگاه تخفیفی
|
||||
</MudText>
|
||||
|
||||
<MudPaper Class="pa-4 mt-4">
|
||||
<MudGrid>
|
||||
<MudItem xs="12" sm="8">
|
||||
<MudTextField @bind-Value="_searchQuery"
|
||||
Label="جستجو در دستهبندیها"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search"
|
||||
OnKeyUp="@(() => FilterCategories())" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="4">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
FullWidth="true"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="@(() => OpenCreateDialog())">
|
||||
دستهبندی جدید
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mt-4" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTreeView T="DiscountCategoryDto"
|
||||
Items="@_filteredCategories"
|
||||
Hover="true"
|
||||
Dense="true"
|
||||
Class="mt-4">
|
||||
<ItemTemplate Context="category">
|
||||
<MudTreeViewItem @bind-Expanded="@category.IsExpanded"
|
||||
Value="@category"
|
||||
Icon="@Icons.Material.Filled.Folder"
|
||||
Items="@category.Children">
|
||||
<Content>
|
||||
<MudGrid Justify="Justify.SpaceBetween" Style="width: 100%;">
|
||||
<MudItem xs="6">
|
||||
<MudText>
|
||||
<strong>@category.Title</strong>
|
||||
@if (!string.IsNullOrEmpty(category.Description))
|
||||
{
|
||||
<MudText Typo="Typo.caption" Class="mr-2">@category.Description</MudText>
|
||||
}
|
||||
</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="3">
|
||||
<MudChip Color="@(category.IsActive ? Color.Success : Color.Default)"
|
||||
Size="Size.Small">
|
||||
@(category.IsActive ? "فعال" : "غیرفعال")
|
||||
</MudChip>
|
||||
<MudChip Color="Color.Info" Size="Size.Small" Class="mr-1">
|
||||
ترتیب: @category.DisplayOrder
|
||||
</MudChip>
|
||||
</MudItem>
|
||||
<MudItem xs="3" Class="d-flex justify-end">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Add"
|
||||
Color="Color.Success"
|
||||
Size="Size.Small"
|
||||
Title="افزودن زیردسته"
|
||||
OnClick="@(() => OpenCreateDialog(category.CategoryId))" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Small"
|
||||
Title="ویرایش"
|
||||
OnClick="@(() => OpenEditDialog(category.CategoryId))" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||
Color="Color.Error"
|
||||
Size="Size.Small"
|
||||
Title="حذف"
|
||||
OnClick="@(() => DeleteCategory(category.CategoryId))" />
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</Content>
|
||||
</MudTreeViewItem>
|
||||
</ItemTemplate>
|
||||
</MudTreeView>
|
||||
|
||||
@if (!_filteredCategories.Any())
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Class="mt-4">
|
||||
@if (string.IsNullOrEmpty(_searchQuery))
|
||||
{
|
||||
<text>هنوز دستهبندیای ایجاد نشده است.</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>دستهبندی با عبارت "@_searchQuery" یافت نشد.</text>
|
||||
}
|
||||
</MudAlert>
|
||||
}
|
||||
}
|
||||
</MudPaper>
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
private List<DiscountCategoryDto> _categories = new();
|
||||
private HashSet<DiscountCategoryDto> _filteredCategories = new();
|
||||
private bool _loading = false;
|
||||
private string? _searchQuery;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadCategories();
|
||||
}
|
||||
|
||||
private async Task LoadCategories()
|
||||
{
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
var allCategories = await DiscountCategoryService.GetCategoriesAsync();
|
||||
_categories = FlattenCategories(allCategories);
|
||||
FilterCategories();
|
||||
Snackbar.Add("دستهبندیها بارگذاری شدند", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری دستهبندیها: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private List<DiscountCategoryDto> FlattenCategories(List<DiscountCategoryDto> categories)
|
||||
{
|
||||
var result = new List<DiscountCategoryDto>();
|
||||
foreach (var category in categories)
|
||||
{
|
||||
result.Add(category);
|
||||
if (category.Children.Any())
|
||||
{
|
||||
result.AddRange(FlattenCategories(category.Children.ToList()));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void FilterCategories()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_searchQuery))
|
||||
{
|
||||
_filteredCategories = _categories.Where(c => c.ParentCategoryId == null).ToHashSet();
|
||||
}
|
||||
else
|
||||
{
|
||||
var query = _searchQuery.ToLower();
|
||||
_filteredCategories = _categories
|
||||
.Where(c => c.Title.ToLower().Contains(query) ||
|
||||
(c.Description?.ToLower().Contains(query) ?? false))
|
||||
.ToHashSet();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenCreateDialog(long? parentId = null)
|
||||
{
|
||||
var model = new CategoryFormModel { ParentCategoryId = parentId };
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
{ "Model", model },
|
||||
{ "IsEditMode", false },
|
||||
{ "ExcludeCategoryId", (long?)null }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<CategoryFormDialog>(
|
||||
parentId.HasValue ? "ایجاد زیردسته" : "ایجاد دستهبندی جدید",
|
||||
parameters,
|
||||
options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (!result.Canceled && result.Data is CategoryFormModel formData)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dto = new CreateDiscountCategoryDto
|
||||
{
|
||||
ParentCategoryId = formData.ParentCategoryId,
|
||||
Title = formData.Title,
|
||||
Description = formData.Description,
|
||||
DisplayOrder = formData.DisplayOrder,
|
||||
IsActive = formData.IsActive
|
||||
};
|
||||
|
||||
await DiscountCategoryService.CreateAsync(dto);
|
||||
Snackbar.Add("دستهبندی با موفقیت ایجاد شد", Severity.Success);
|
||||
await LoadCategories();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ایجاد دستهبندی: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenEditDialog(long categoryId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var category = await DiscountCategoryService.GetByIdAsync(categoryId);
|
||||
if (category == null)
|
||||
{
|
||||
Snackbar.Add("دستهبندی یافت نشد", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var model = new CategoryFormModel
|
||||
{
|
||||
ParentCategoryId = category.ParentCategoryId,
|
||||
Title = category.Title,
|
||||
Description = category.Description,
|
||||
DisplayOrder = category.DisplayOrder,
|
||||
IsActive = category.IsActive
|
||||
};
|
||||
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
{ "Model", model },
|
||||
{ "IsEditMode", true },
|
||||
{ "ExcludeCategoryId", categoryId }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<CategoryFormDialog>("ویرایش دستهبندی", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (!result.Canceled && result.Data is CategoryFormModel formData)
|
||||
{
|
||||
var dto = new UpdateDiscountCategoryDto
|
||||
{
|
||||
ParentCategoryId = formData.ParentCategoryId,
|
||||
Title = formData.Title,
|
||||
Description = formData.Description,
|
||||
DisplayOrder = formData.DisplayOrder,
|
||||
IsActive = formData.IsActive
|
||||
};
|
||||
|
||||
await DiscountCategoryService.UpdateAsync(categoryId, dto);
|
||||
Snackbar.Add("دستهبندی با موفقیت ویرایش شد", Severity.Success);
|
||||
await LoadCategories();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ویرایش دستهبندی: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteCategory(long categoryId)
|
||||
{
|
||||
var category = _categories.FirstOrDefault(c => c.CategoryId == categoryId);
|
||||
if (category?.Children.Any() == true)
|
||||
{
|
||||
Snackbar.Add("ابتدا زیردستههای این دستهبندی را حذف کنید", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await DialogService.ShowMessageBox(
|
||||
"تأیید حذف",
|
||||
"آیا از حذف این دستهبندی اطمینان دارید؟",
|
||||
yesText: "بله", cancelText: "خیر");
|
||||
|
||||
if (result == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await DiscountCategoryService.DeleteAsync(categoryId);
|
||||
Snackbar.Add("دستهبندی با موفقیت حذف شد", Severity.Success);
|
||||
await LoadCategories();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در حذف دستهبندی: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
272
src/BackOffice/Pages/DiscountShop/DiscountOrdersMainPage.razor
Normal file
272
src/BackOffice/Pages/DiscountShop/DiscountOrdersMainPage.razor
Normal file
@@ -0,0 +1,272 @@
|
||||
@page "/discount-orders"
|
||||
@using BackOffice.Services.DiscountOrder
|
||||
@using BackOffice.Pages.DiscountShop.Components
|
||||
@inject IDiscountOrderService DiscountOrderService
|
||||
@inject IDialogService DialogService
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<PageTitle>مدیریت سفارشات فروشگاه تخفیفی</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-4">
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ShoppingCart" Class="ml-2" />
|
||||
مدیریت سفارشات فروشگاه تخفیفی
|
||||
</MudText>
|
||||
|
||||
<MudPaper Class="pa-4 mt-4">
|
||||
<MudGrid>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudTextField @bind-Value="_searchQuery"
|
||||
Label="جستجو (شماره سفارش، کاربر)"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search"
|
||||
OnKeyUp="@(() => OnSearch())" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect @bind-Value="_statusFilter"
|
||||
Label="وضعیت سفارش"
|
||||
Variant="Variant.Outlined"
|
||||
T="OrderStatus?"
|
||||
Clearable="true"
|
||||
OnClearButtonClick="@(() => OnSearch())">
|
||||
<MudSelectItem Value="@((OrderStatus?)null)">همه</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">
|
||||
<MudDateRangePicker @bind-DateRange="_dateRange"
|
||||
Label="بازه تاریخ"
|
||||
Variant="Variant.Outlined"
|
||||
AutoClose="true"
|
||||
DateFormat="yyyy/MM/dd" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
FullWidth="true"
|
||||
StartIcon="@Icons.Material.Filled.Search"
|
||||
OnClick="@(() => OnSearch())">
|
||||
جستجو
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudDataGrid @ref="_dataGrid" T="DiscountOrderDto"
|
||||
Items="@_orders"
|
||||
Loading="@_loading"
|
||||
Hover="true"
|
||||
Dense="true"
|
||||
Class="mt-4">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.OrderId" Title="شماره سفارش" />
|
||||
|
||||
<PropertyColumn Property="x => x.UserFullName" Title="کاربر" />
|
||||
|
||||
<PropertyColumn Property="x => x.CreatedAt" Title="تاریخ ثبت" Format="yyyy/MM/dd HH:mm" />
|
||||
|
||||
<TemplateColumn Title="مبلغ کل">
|
||||
<CellTemplate>
|
||||
<MudText>@context.Item.TotalAmount.ToString("N0") ریال</MudText>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="تخفیف">
|
||||
<CellTemplate>
|
||||
<MudText Color="Color.Success">@context.Item.TotalDiscount.ToString("N0") ریال</MudText>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="قابل پرداخت">
|
||||
<CellTemplate>
|
||||
<MudText Typo="Typo.body2">
|
||||
<strong>@context.Item.FinalAmount.ToString("N0") ریال</strong>
|
||||
</MudText>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="وضعیت">
|
||||
<CellTemplate>
|
||||
<MudChip Color="@GetStatusColor(context.Item.Status)" Size="Size.Small">
|
||||
@GetStatusText(context.Item.Status)
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="پرداخت">
|
||||
<CellTemplate>
|
||||
@if (context.Item.IsPaid)
|
||||
{
|
||||
<MudChip Color="Color.Success" Size="Size.Small" Icon="@Icons.Material.Filled.CheckCircle">
|
||||
پرداخت شده
|
||||
</MudChip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChip Color="Color.Warning" Size="Size.Small" Icon="@Icons.Material.Filled.Schedule">
|
||||
در انتظار
|
||||
</MudChip>
|
||||
}
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="عملیات" Sortable="false">
|
||||
<CellTemplate>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Visibility"
|
||||
Color="Color.Info"
|
||||
Size="Size.Small"
|
||||
Title="مشاهده جزئیات"
|
||||
OnClick="@(() => OpenOrderDetails(context.Item.OrderId))" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Small"
|
||||
Title="تغییر وضعیت"
|
||||
OnClick="@(() => OpenChangeStatusDialog(context.Item.OrderId))" />
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="DiscountOrderDto" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudPaper>
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
private MudDataGrid<DiscountOrderDto>? _dataGrid;
|
||||
private List<DiscountOrderDto> _orders = new();
|
||||
private bool _loading = false;
|
||||
|
||||
private string? _searchQuery;
|
||||
private OrderStatus? _statusFilter;
|
||||
private DateRange? _dateRange;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadOrders();
|
||||
}
|
||||
|
||||
private async Task LoadOrders()
|
||||
{
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
var filter = new OrderFilterDto
|
||||
{
|
||||
SearchQuery = _searchQuery,
|
||||
Status = _statusFilter,
|
||||
FromDate = _dateRange?.Start,
|
||||
ToDate = _dateRange?.End
|
||||
};
|
||||
|
||||
_orders = await DiscountOrderService.GetOrdersAsync(filter);
|
||||
Snackbar.Add("سفارشات بارگذاری شدند", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری سفارشات: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnSearch()
|
||||
{
|
||||
await LoadOrders();
|
||||
}
|
||||
|
||||
private async Task OpenOrderDetails(long orderId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var order = await DiscountOrderService.GetByIdAsync(orderId);
|
||||
if (order == null)
|
||||
{
|
||||
Snackbar.Add("سفارش یافت نشد", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var parameters = new DialogParameters { { "Order", order } };
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Large, FullWidth = true };
|
||||
await DialogService.ShowAsync<OrderDetailsDialog>("جزئیات سفارش", parameters, options);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری جزئیات: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenChangeStatusDialog(long orderId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var order = _orders.FirstOrDefault(o => o.OrderId == orderId);
|
||||
if (order == null)
|
||||
{
|
||||
Snackbar.Add("سفارش یافت نشد", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
{ "OrderId", orderId },
|
||||
{ "CurrentStatus", order.Status }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<ChangeOrderStatusDialog>("تغییر وضعیت سفارش", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (!result.Canceled && result.Data is UpdateOrderStatusDto dto)
|
||||
{
|
||||
await DiscountOrderService.UpdateStatusAsync(orderId, dto);
|
||||
Snackbar.Add("وضعیت سفارش با موفقیت تغییر یافت", Severity.Success);
|
||||
await LoadOrders();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در تغییر وضعیت: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private Color GetStatusColor(OrderStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
OrderStatus.Pending => Color.Warning,
|
||||
OrderStatus.Paid => Color.Info,
|
||||
OrderStatus.Processing => Color.Primary,
|
||||
OrderStatus.Shipped => Color.Secondary,
|
||||
OrderStatus.Delivered => Color.Success,
|
||||
OrderStatus.Cancelled => Color.Error,
|
||||
OrderStatus.Returned => Color.Dark,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
private string GetStatusText(OrderStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
OrderStatus.Pending => "در انتظار پرداخت",
|
||||
OrderStatus.Paid => "پرداخت شده",
|
||||
OrderStatus.Processing => "در حال آمادهسازی",
|
||||
OrderStatus.Shipped => "ارسال شده",
|
||||
OrderStatus.Delivered => "تحویل داده شده",
|
||||
OrderStatus.Cancelled => "لغو شده",
|
||||
OrderStatus.Returned => "مرجوع شده",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
340
src/BackOffice/Pages/DiscountShop/DiscountProductsMainPage.razor
Normal file
340
src/BackOffice/Pages/DiscountShop/DiscountProductsMainPage.razor
Normal file
@@ -0,0 +1,340 @@
|
||||
@page "/discount-products"
|
||||
@using BackOffice.Services.DiscountProduct
|
||||
@using BackOffice.Pages.DiscountShop.Components
|
||||
@using static BackOffice.Pages.DiscountShop.Components.ProductFormDialog
|
||||
@inject IDiscountProductService DiscountProductService
|
||||
@inject IDialogService DialogService
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<PageTitle>مدیریت محصولات تخفیفی</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-4">
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Discount" Class="ml-2" />
|
||||
مدیریت محصولات تخفیفی
|
||||
</MudText>
|
||||
|
||||
<MudPaper Class="pa-4 mt-4">
|
||||
<MudGrid>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudTextField @bind-Value="_searchQuery"
|
||||
Label="جستجو"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search"
|
||||
OnKeyUp="@(() => OnSearch())" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect @bind-Value="_categoryFilter"
|
||||
Label="دستهبندی"
|
||||
Variant="Variant.Outlined"
|
||||
T="long?"
|
||||
Clearable="true"
|
||||
OnClearButtonClick="@(() => OnCategoryFilterChanged())">
|
||||
<MudSelectItem Value="@((long?)null)">همه</MudSelectItem>
|
||||
@* TODO: Load categories *@
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="2">
|
||||
<MudSelect @bind-Value="_statusFilter"
|
||||
Label="وضعیت"
|
||||
Variant="Variant.Outlined"
|
||||
T="bool?"
|
||||
Clearable="true">
|
||||
<MudSelectItem Value="@((bool?)null)">همه</MudSelectItem>
|
||||
<MudSelectItem Value="@((bool?)true)">فعال</MudSelectItem>
|
||||
<MudSelectItem Value="@((bool?)false)">غیرفعال</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="2">
|
||||
<MudSelect @bind-Value="_stockFilter"
|
||||
Label="موجودی"
|
||||
Variant="Variant.Outlined"
|
||||
T="bool?"
|
||||
Clearable="true">
|
||||
<MudSelectItem Value="@((bool?)null)">همه</MudSelectItem>
|
||||
<MudSelectItem Value="@((bool?)true)">موجود</MudSelectItem>
|
||||
<MudSelectItem Value="@((bool?)false)">ناموجود</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="12" md="2">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
FullWidth="true"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="@(() => OpenCreateDialog())">
|
||||
محصول جدید
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudDataGrid @ref="_dataGrid" T="DiscountProductDto"
|
||||
Items="@_products"
|
||||
Loading="@_loading"
|
||||
Hover="true"
|
||||
Dense="true"
|
||||
Class="mt-4">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.ProductId" Title="شناسه" />
|
||||
|
||||
<TemplateColumn Title="تصویر">
|
||||
<CellTemplate>
|
||||
@if (!string.IsNullOrEmpty(context.Item.ThumbnailPath))
|
||||
{
|
||||
<MudAvatar Image="@context.Item.ThumbnailPath" Size="Size.Medium" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudAvatar Color="Color.Secondary" Size="Size.Medium">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Image" />
|
||||
</MudAvatar>
|
||||
}
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<PropertyColumn Property="x => x.Title" Title="عنوان" />
|
||||
|
||||
<TemplateColumn Title="قیمت">
|
||||
<CellTemplate>
|
||||
<MudText>@context.Item.Price.ToString("N0") ریال</MudText>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="حداکثر تخفیف">
|
||||
<CellTemplate>
|
||||
<MudChip Color="Color.Success" Size="Size.Small">@context.Item.MaxDiscountPercent%</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="موجودی">
|
||||
<CellTemplate>
|
||||
<MudChip Color="@(context.Item.Stock > 0 ? Color.Info : Color.Error)" Size="Size.Small">
|
||||
@context.Item.Stock
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="فروش">
|
||||
<CellTemplate>
|
||||
<MudText>@context.Item.SaleCount</MudText>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="وضعیت">
|
||||
<CellTemplate>
|
||||
<MudChip Color="@(context.Item.IsActive ? Color.Success : Color.Default)"
|
||||
Size="Size.Small">
|
||||
@(context.Item.IsActive ? "فعال" : "غیرفعال")
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="عملیات" Sortable="false">
|
||||
<CellTemplate>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Small"
|
||||
OnClick="@(() => OpenEditDialog(context.Item.ProductId))" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||
Color="Color.Error"
|
||||
Size="Size.Small"
|
||||
OnClick="@(() => DeleteProduct(context.Item.ProductId))" />
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="DiscountProductDto" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudPaper>
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
private MudDataGrid<DiscountProductDto>? _dataGrid;
|
||||
private List<DiscountProductDto> _products = new();
|
||||
private bool _loading = false;
|
||||
|
||||
private string? _searchQuery;
|
||||
private long? _categoryFilter;
|
||||
private bool? _statusFilter;
|
||||
private bool? _stockFilter;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadProducts();
|
||||
}
|
||||
|
||||
private async Task LoadProducts()
|
||||
{
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
var filter = new ProductFilterDto
|
||||
{
|
||||
SearchQuery = _searchQuery,
|
||||
CategoryId = _categoryFilter,
|
||||
IsActive = _statusFilter,
|
||||
InStock = _stockFilter
|
||||
};
|
||||
|
||||
_products = await DiscountProductService.GetProductsAsync(filter);
|
||||
Snackbar.Add("محصولات بارگذاری شدند", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری محصولات: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnSearch()
|
||||
{
|
||||
await LoadProducts();
|
||||
}
|
||||
|
||||
private async Task OnCategoryFilterChanged()
|
||||
{
|
||||
await LoadProducts();
|
||||
}
|
||||
|
||||
private async Task OpenCreateDialog()
|
||||
{
|
||||
var model = new ProductFormModel();
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
{ "Model", model },
|
||||
{ "IsEditMode", false }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<ProductFormDialog>("ایجاد محصول جدید", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (!result.Canceled && result.Data is ProductFormModel formData)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dto = new CreateDiscountProductDto
|
||||
{
|
||||
Title = formData.Title,
|
||||
Description = formData.Description,
|
||||
ThumbnailPath = formData.ThumbnailPath,
|
||||
Price = formData.Price,
|
||||
MaxDiscountPercent = formData.MaxDiscountPercent,
|
||||
Stock = formData.Stock,
|
||||
IsActive = formData.IsActive,
|
||||
CategoryId = formData.CategoryId,
|
||||
Tags = formData.Tags
|
||||
};
|
||||
|
||||
await DiscountProductService.CreateAsync(dto);
|
||||
Snackbar.Add("محصول با موفقیت ایجاد شد", Severity.Success);
|
||||
await LoadProducts();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ایجاد محصول: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenEditDialog(long productId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var product = await DiscountProductService.GetByIdAsync(productId);
|
||||
if (product == null)
|
||||
{
|
||||
Snackbar.Add("محصول یافت نشد", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var model = new ProductFormModel
|
||||
{
|
||||
Title = product.Title,
|
||||
Description = product.Description,
|
||||
ThumbnailPath = product.ThumbnailPath,
|
||||
Price = product.Price,
|
||||
MaxDiscountPercent = product.MaxDiscountPercent,
|
||||
Stock = product.Stock,
|
||||
IsActive = product.IsActive,
|
||||
CategoryId = product.CategoryId
|
||||
};
|
||||
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
{ "Model", model },
|
||||
{ "IsEditMode", true }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<ProductFormDialog>("ویرایش محصول", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (!result.Canceled && result.Data is ProductFormModel formData)
|
||||
{
|
||||
var dto = new UpdateDiscountProductDto
|
||||
{
|
||||
Title = formData.Title,
|
||||
Description = formData.Description,
|
||||
ThumbnailPath = formData.ThumbnailPath,
|
||||
Price = formData.Price,
|
||||
MaxDiscountPercent = formData.MaxDiscountPercent,
|
||||
Stock = formData.Stock,
|
||||
IsActive = formData.IsActive,
|
||||
CategoryId = formData.CategoryId,
|
||||
Tags = formData.Tags
|
||||
};
|
||||
|
||||
await DiscountProductService.UpdateAsync(productId, dto);
|
||||
Snackbar.Add("محصول با موفقیت ویرایش شد", Severity.Success);
|
||||
await LoadProducts();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ویرایش محصول: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteProduct(long productId)
|
||||
{
|
||||
var result = await DialogService.ShowMessageBox(
|
||||
"تأیید حذف",
|
||||
"آیا از حذف این محصول اطمینان دارید؟",
|
||||
yesText: "بله", cancelText: "خیر");
|
||||
|
||||
if (result == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await DiscountProductService.DeleteAsync(productId);
|
||||
Snackbar.Add("محصول با موفقیت حذف شد", Severity.Success);
|
||||
await LoadProducts();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در حذف محصول: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Temporary DTO - will be removed when using service DTOs
|
||||
/*
|
||||
public class DiscountProductDto
|
||||
{
|
||||
public long ProductId { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string? ThumbnailPath { get; set; }
|
||||
public long Price { get; set; }
|
||||
public int MaxDiscountPercent { get; set; }
|
||||
public int Stock { get; set; }
|
||||
public int SaleCount { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
@using BackOffice.Services.PublicMessage
|
||||
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Campaign" Class="ml-2" />
|
||||
@(IsEditMode ? "ویرایش پیام" : "ایجاد پیام جدید")
|
||||
</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
<MudForm @ref="_form" @bind-IsValid="@_isValid">
|
||||
<MudGrid>
|
||||
<MudItem xs="12">
|
||||
<MudTextField @bind-Value="Model.Title"
|
||||
Label="عنوان پیام *"
|
||||
Required="true"
|
||||
RequiredError="عنوان الزامی است"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudSelect @bind-Value="Model.Type"
|
||||
Label="نوع پیام *"
|
||||
Required="true"
|
||||
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" sm="6">
|
||||
<MudNumericField @bind-Value="Model.Priority"
|
||||
Label="اولویت"
|
||||
Min="1"
|
||||
Max="5"
|
||||
Variant="Variant.Outlined"
|
||||
HelperText="1 = کم، 5 = زیاد" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12">
|
||||
<MudTextField @bind-Value="Model.Content"
|
||||
Label="متن پیام *"
|
||||
Required="true"
|
||||
Lines="6"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12">
|
||||
<MudTextField @bind-Value="Model.ImageUrl"
|
||||
Label="آدرس تصویر"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.End"
|
||||
AdornmentIcon="@Icons.Material.Filled.Image" />
|
||||
</MudItem>
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model.ImageUrl))
|
||||
{
|
||||
<MudItem xs="12">
|
||||
<MudImage Src="@Model.ImageUrl"
|
||||
Alt="پیشنمایش"
|
||||
Height="150"
|
||||
ObjectFit="ObjectFit.Contain"
|
||||
Class="rounded" />
|
||||
</MudItem>
|
||||
}
|
||||
|
||||
<MudItem xs="12" sm="8">
|
||||
<MudTextField @bind-Value="Model.ActionUrl"
|
||||
Label="لینک اکشن (اختیاری)"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.End"
|
||||
AdornmentIcon="@Icons.Material.Filled.Link" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="4">
|
||||
<MudTextField @bind-Value="Model.ActionText"
|
||||
Label="متن دکمه اکشن"
|
||||
Variant="Variant.Outlined"
|
||||
Disabled="@string.IsNullOrEmpty(Model.ActionUrl)" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12">
|
||||
<MudDatePicker @bind-Date="Model.ExpiresAt"
|
||||
Label="تاریخ انقضا (اختیاری)"
|
||||
Variant="Variant.Outlined"
|
||||
Clearable="true"
|
||||
DateFormat="yyyy/MM/dd" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12">
|
||||
<MudTextField @bind-Value="_tagsInput"
|
||||
Label="تگها (با کاما جدا کنید)"
|
||||
Variant="Variant.Outlined"
|
||||
HelperText="مثال: مهم, فوری, اطلاعیه" />
|
||||
</MudItem>
|
||||
|
||||
@if (!IsEditMode)
|
||||
{
|
||||
<MudItem xs="12">
|
||||
<MudSwitch @bind-Checked="Model.PublishImmediately"
|
||||
Label="انتشار فوری پس از ایجاد"
|
||||
Color="Color.Success" />
|
||||
@if (!Model.PublishImmediately)
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||
پیام به صورت پیشنویس ذخیره میشود
|
||||
</MudText>
|
||||
}
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel" Variant="Variant.Text">انصراف</MudButton>
|
||||
<MudButton OnClick="Submit"
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
Disabled="@(!_isValid || _loading)">
|
||||
@if (_loading)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>@(IsEditMode ? "ذخیره تغییرات" : Model.PublishImmediately ? "ایجاد و انتشار" : "ذخیره پیشنویس")</text>
|
||||
}
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] MudDialogInstance MudDialog { get; set; } = null!;
|
||||
[Parameter] public MessageFormModel Model { get; set; } = new();
|
||||
[Parameter] public bool IsEditMode { get; set; }
|
||||
|
||||
private MudForm? _form;
|
||||
private bool _isValid;
|
||||
private bool _loading;
|
||||
private string _tagsInput = string.Empty;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
if (Model.Tags?.Any() == true)
|
||||
{
|
||||
_tagsInput = string.Join(", ", Model.Tags);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Submit()
|
||||
{
|
||||
if (_form != null)
|
||||
{
|
||||
await _form.Validate();
|
||||
if (!_isValid) return;
|
||||
}
|
||||
|
||||
_loading = true;
|
||||
|
||||
// Parse tags
|
||||
if (!string.IsNullOrWhiteSpace(_tagsInput))
|
||||
{
|
||||
Model.Tags = _tagsInput.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(t => t.Trim())
|
||||
.Where(t => !string.IsNullOrEmpty(t))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
MudDialog.Close(DialogResult.Ok(Model));
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
MudDialog.Cancel();
|
||||
}
|
||||
|
||||
public class MessageFormModel
|
||||
{
|
||||
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;
|
||||
public string? ImageUrl { get; set; }
|
||||
public string? ActionUrl { get; set; }
|
||||
public string? ActionText { get; set; }
|
||||
public DateTime? ExpiresAt { get; set; }
|
||||
public List<string>? Tags { get; set; }
|
||||
public bool PublishImmediately { get; set; } = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
@using BackOffice.Services.PublicMessage
|
||||
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6">
|
||||
<MudIcon Icon="@GetTypeIcon(Message.Type)" Class="ml-2" />
|
||||
@Message.Title
|
||||
</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
<MudGrid>
|
||||
<!-- Header با اطلاعات کلی -->
|
||||
<MudItem xs="12">
|
||||
<MudPaper Class="pa-3" Elevation="1">
|
||||
<MudGrid>
|
||||
<MudItem xs="12" sm="3">
|
||||
<MudChip Color="@GetTypeColor(Message.Type)" Size="Size.Small">
|
||||
@GetTypeText(Message.Type)
|
||||
</MudChip>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="3">
|
||||
<MudChip Color="@GetStatusColor(Message.Status)" Size="Size.Small">
|
||||
@GetStatusText(Message.Status)
|
||||
</MudChip>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="3">
|
||||
<MudRating SelectedValue="@Message.Priority"
|
||||
ReadOnly="true"
|
||||
MaxValue="5"
|
||||
Size="Size.Small" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="3">
|
||||
<MudChip Color="Color.Info" Size="Size.Small" Icon="@Icons.Material.Filled.Visibility">
|
||||
@Message.ViewCount بازدید
|
||||
</MudChip>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<!-- تصویر -->
|
||||
@if (!string.IsNullOrEmpty(Message.ImageUrl))
|
||||
{
|
||||
<MudItem xs="12">
|
||||
<MudImage Src="@Message.ImageUrl"
|
||||
Alt="تصویر پیام"
|
||||
ObjectFit="ObjectFit.Contain"
|
||||
Height="300"
|
||||
Class="rounded" />
|
||||
</MudItem>
|
||||
}
|
||||
|
||||
<!-- محتوا -->
|
||||
<MudItem xs="12">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.h6" GutterBottom="true">محتوای پیام</MudText>
|
||||
<MudText Typo="Typo.body1" Style="white-space: pre-wrap;">@Message.Content</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<!-- اکشن -->
|
||||
@if (!string.IsNullOrEmpty(Message.ActionUrl))
|
||||
{
|
||||
<MudItem xs="12">
|
||||
<MudPaper Class="pa-3" Elevation="1">
|
||||
<MudButton Href="@Message.ActionUrl"
|
||||
Target="_blank"
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Link">
|
||||
@(string.IsNullOrEmpty(Message.ActionText) ? "مشاهده بیشتر" : Message.ActionText)
|
||||
</MudButton>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
}
|
||||
|
||||
<!-- تگها -->
|
||||
@if (Message.Tags?.Any() == true)
|
||||
{
|
||||
<MudItem xs="12">
|
||||
<MudPaper Class="pa-3" Elevation="1">
|
||||
<MudText Typo="Typo.subtitle2" GutterBottom="true">تگها:</MudText>
|
||||
@foreach (var tag in Message.Tags)
|
||||
{
|
||||
<MudChip Size="Size.Small" Color="Color.Default">@tag</MudChip>
|
||||
}
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
}
|
||||
|
||||
<!-- اطلاعات تاریخ -->
|
||||
<MudItem xs="12">
|
||||
<MudPaper Class="pa-3" Elevation="1">
|
||||
<MudGrid>
|
||||
<MudItem xs="12" sm="4">
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">تاریخ ایجاد:</MudText>
|
||||
<MudText Typo="Typo.body1">@Message.CreatedAt.ToString("yyyy/MM/dd HH:mm")</MudText>
|
||||
</MudItem>
|
||||
@if (Message.PublishedAt.HasValue)
|
||||
{
|
||||
<MudItem xs="12" sm="4">
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">تاریخ انتشار:</MudText>
|
||||
<MudText Typo="Typo.body1">@Message.PublishedAt.Value.ToString("yyyy/MM/dd HH:mm")</MudText>
|
||||
</MudItem>
|
||||
}
|
||||
@if (Message.ExpiresAt.HasValue)
|
||||
{
|
||||
<MudItem xs="12" sm="4">
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">تاریخ انقضا:</MudText>
|
||||
<MudText Typo="Typo.body1" Color="@(Message.ExpiresAt.Value < DateTime.Now ? Color.Error : Color.Default)">
|
||||
@Message.ExpiresAt.Value.ToString("yyyy/MM/dd")
|
||||
@if (Message.ExpiresAt.Value < DateTime.Now)
|
||||
{
|
||||
<MudChip Size="Size.Small" Color="Color.Error">منقضی شده</MudChip>
|
||||
}
|
||||
</MudText>
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Close" Variant="Variant.Filled" Color="Color.Primary">بستن</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] MudDialogInstance MudDialog { get; set; } = null!;
|
||||
[Parameter] public PublicMessageDetailsDto Message { get; set; } = null!;
|
||||
|
||||
private void Close()
|
||||
{
|
||||
MudDialog.Close();
|
||||
}
|
||||
|
||||
private Color GetTypeColor(MessageType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
MessageType.Announcement => Color.Primary,
|
||||
MessageType.News => Color.Info,
|
||||
MessageType.Alert => Color.Warning,
|
||||
MessageType.Promotion => Color.Success,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
private string GetTypeText(MessageType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
MessageType.Announcement => "اطلاعیه",
|
||||
MessageType.News => "خبر",
|
||||
MessageType.Alert => "هشدار",
|
||||
MessageType.Promotion => "تبلیغات",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
}
|
||||
|
||||
private string GetTypeIcon(MessageType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
MessageType.Announcement => Icons.Material.Filled.Announcement,
|
||||
MessageType.News => Icons.Material.Filled.Newspaper,
|
||||
MessageType.Alert => Icons.Material.Filled.Warning,
|
||||
MessageType.Promotion => Icons.Material.Filled.LocalOffer,
|
||||
_ => Icons.Material.Filled.Message
|
||||
};
|
||||
}
|
||||
|
||||
private Color GetStatusColor(MessageStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
MessageStatus.Draft => Color.Default,
|
||||
MessageStatus.Published => Color.Success,
|
||||
MessageStatus.Archived => Color.Dark,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
private string GetStatusText(MessageStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
MessageStatus.Draft => "پیشنویس",
|
||||
MessageStatus.Published => "منتشر شده",
|
||||
MessageStatus.Archived => "بایگانی شده",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
}
|
||||
}
|
||||
429
src/BackOffice/Pages/PublicMessages/PublicMessagesMainPage.razor
Normal file
429
src/BackOffice/Pages/PublicMessages/PublicMessagesMainPage.razor
Normal file
@@ -0,0 +1,429 @@
|
||||
@page "/public-messages"
|
||||
@using BackOffice.Services.PublicMessage
|
||||
@using BackOffice.Pages.PublicMessages.Components
|
||||
@using static BackOffice.Pages.PublicMessages.Components.MessageFormDialog
|
||||
@inject IPublicMessageService PublicMessageService
|
||||
@inject IDialogService DialogService
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<PageTitle>مدیریت پیامهای عمومی</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Campaign" Class="ml-2" />
|
||||
مدیریت پیامهای عمومی
|
||||
</MudText>
|
||||
|
||||
<MudPaper Class="pa-4 mt-4">
|
||||
<MudGrid>
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<MudTextField @bind-Value="_searchQuery"
|
||||
Label="جستجو"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search"
|
||||
OnKeyUp="@(() => OnSearch())" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect @bind-Value="_statusFilter"
|
||||
Label="وضعیت"
|
||||
Variant="Variant.Outlined"
|
||||
T="MessageStatus?"
|
||||
Clearable="true"
|
||||
OnClearButtonClick="@(() => OnSearch())">
|
||||
<MudSelectItem Value="@((MessageStatus?)null)">همه</MudSelectItem>
|
||||
<MudSelectItem Value="@MessageStatus.Draft">پیشنویس</MudSelectItem>
|
||||
<MudSelectItem Value="@MessageStatus.Published">منتشر شده</MudSelectItem>
|
||||
<MudSelectItem Value="@MessageStatus.Archived">بایگانی شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect @bind-Value="_typeFilter"
|
||||
Label="نوع"
|
||||
Variant="Variant.Outlined"
|
||||
T="MessageType?"
|
||||
Clearable="true"
|
||||
OnClearButtonClick="@(() => OnSearch())">
|
||||
<MudSelectItem Value="@((MessageType?)null)">همه</MudSelectItem>
|
||||
<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" sm="6" md="2">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
FullWidth="true"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="@(() => OpenCreateDialog())">
|
||||
پیام جدید
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudDataGrid @ref="_dataGrid" T="PublicMessageDto"
|
||||
Items="@_messages"
|
||||
Loading="@_loading"
|
||||
Hover="true"
|
||||
Dense="true"
|
||||
Class="mt-4">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.MessageId" Title="شناسه" />
|
||||
|
||||
<PropertyColumn Property="x => x.Title" Title="عنوان" />
|
||||
|
||||
<TemplateColumn Title="نوع">
|
||||
<CellTemplate>
|
||||
<MudChip Color="@GetTypeColor(context.Item.Type)" Size="Size.Small">
|
||||
@GetTypeText(context.Item.Type)
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="اولویت">
|
||||
<CellTemplate>
|
||||
<MudRating SelectedValue="@context.Item.Priority"
|
||||
ReadOnly="true"
|
||||
MaxValue="5"
|
||||
Size="Size.Small" />
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="وضعیت">
|
||||
<CellTemplate>
|
||||
<MudChip Color="@GetStatusColor(context.Item.Status)" Size="Size.Small">
|
||||
@GetStatusText(context.Item.Status)
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<PropertyColumn Property="x => x.PublishedAt" Title="تاریخ انتشار" Format="yyyy/MM/dd HH:mm" />
|
||||
|
||||
<PropertyColumn Property="x => x.ExpiresAt" Title="تاریخ انقضا" Format="yyyy/MM/dd" />
|
||||
|
||||
<TemplateColumn Title="بازدید">
|
||||
<CellTemplate>
|
||||
<MudChip Color="Color.Info" Size="Size.Small" Icon="@Icons.Material.Filled.Visibility">
|
||||
@context.Item.ViewCount
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="عملیات" Sortable="false">
|
||||
<CellTemplate>
|
||||
<MudMenu Icon="@Icons.Material.Filled.MoreVert" Size="Size.Small">
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Visibility"
|
||||
OnClick="@(() => ViewMessage(context.Item.MessageId))">
|
||||
مشاهده
|
||||
</MudMenuItem>
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Edit"
|
||||
OnClick="@(() => OpenEditDialog(context.Item.MessageId))">
|
||||
ویرایش
|
||||
</MudMenuItem>
|
||||
@if (context.Item.Status == MessageStatus.Draft)
|
||||
{
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Publish"
|
||||
OnClick="@(() => PublishMessage(context.Item.MessageId))">
|
||||
انتشار
|
||||
</MudMenuItem>
|
||||
}
|
||||
@if (context.Item.Status == MessageStatus.Published)
|
||||
{
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Archive"
|
||||
OnClick="@(() => ArchiveMessage(context.Item.MessageId))">
|
||||
بایگانی
|
||||
</MudMenuItem>
|
||||
}
|
||||
<MudDivider />
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Delete"
|
||||
IconColor="Color.Error"
|
||||
OnClick="@(() => DeleteMessage(context.Item.MessageId))">
|
||||
حذف
|
||||
</MudMenuItem>
|
||||
</MudMenu>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="PublicMessageDto" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudPaper>
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
private MudDataGrid<PublicMessageDto>? _dataGrid;
|
||||
private List<PublicMessageDto> _messages = new();
|
||||
private bool _loading = false;
|
||||
|
||||
private string? _searchQuery;
|
||||
private MessageStatus? _statusFilter;
|
||||
private MessageType? _typeFilter;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadMessages();
|
||||
}
|
||||
|
||||
private async Task LoadMessages()
|
||||
{
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
var filter = new MessageFilterDto
|
||||
{
|
||||
SearchQuery = _searchQuery,
|
||||
Status = _statusFilter,
|
||||
Type = _typeFilter
|
||||
};
|
||||
|
||||
_messages = await PublicMessageService.GetMessagesAsync(filter);
|
||||
Snackbar.Add("پیامها بارگذاری شدند", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری پیامها: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnSearch()
|
||||
{
|
||||
await LoadMessages();
|
||||
}
|
||||
|
||||
private async Task OpenCreateDialog()
|
||||
{
|
||||
var model = new MessageFormModel();
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
{ "Model", model },
|
||||
{ "IsEditMode", false }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<MessageFormDialog>("ایجاد پیام جدید", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (!result.Canceled && result.Data is MessageFormModel formData)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dto = new CreatePublicMessageDto
|
||||
{
|
||||
Title = formData.Title,
|
||||
Content = formData.Content,
|
||||
Type = formData.Type,
|
||||
Priority = formData.Priority,
|
||||
ImageUrl = formData.ImageUrl,
|
||||
ActionUrl = formData.ActionUrl,
|
||||
ActionText = formData.ActionText,
|
||||
ExpiresAt = formData.ExpiresAt,
|
||||
Tags = formData.Tags,
|
||||
PublishImmediately = formData.PublishImmediately
|
||||
};
|
||||
|
||||
await PublicMessageService.CreateAsync(dto);
|
||||
Snackbar.Add(
|
||||
formData.PublishImmediately ? "پیام با موفقیت ایجاد و منتشر شد" : "پیام به صورت پیشنویس ذخیره شد",
|
||||
Severity.Success);
|
||||
await LoadMessages();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ایجاد پیام: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenEditDialog(long messageId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var message = await PublicMessageService.GetByIdAsync(messageId);
|
||||
if (message == null)
|
||||
{
|
||||
Snackbar.Add("پیام یافت نشد", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var model = new MessageFormModel
|
||||
{
|
||||
Title = message.Title,
|
||||
Content = message.Content,
|
||||
Type = message.Type,
|
||||
Priority = message.Priority,
|
||||
ImageUrl = message.ImageUrl,
|
||||
ActionUrl = message.ActionUrl,
|
||||
ActionText = message.ActionText,
|
||||
ExpiresAt = message.ExpiresAt,
|
||||
Tags = message.Tags
|
||||
};
|
||||
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
{ "Model", model },
|
||||
{ "IsEditMode", true }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<MessageFormDialog>("ویرایش پیام", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (!result.Canceled && result.Data is MessageFormModel formData)
|
||||
{
|
||||
var dto = new UpdatePublicMessageDto
|
||||
{
|
||||
Title = formData.Title,
|
||||
Content = formData.Content,
|
||||
Type = formData.Type,
|
||||
Priority = formData.Priority,
|
||||
ImageUrl = formData.ImageUrl,
|
||||
ActionUrl = formData.ActionUrl,
|
||||
ActionText = formData.ActionText,
|
||||
ExpiresAt = formData.ExpiresAt,
|
||||
Tags = formData.Tags
|
||||
};
|
||||
|
||||
await PublicMessageService.UpdateAsync(messageId, dto);
|
||||
Snackbar.Add("پیام با موفقیت ویرایش شد", Severity.Success);
|
||||
await LoadMessages();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ویرایش پیام: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ViewMessage(long messageId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var message = await PublicMessageService.GetByIdAsync(messageId);
|
||||
if (message == null)
|
||||
{
|
||||
Snackbar.Add("پیام یافت نشد", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var parameters = new DialogParameters { { "Message", message } };
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Large, FullWidth = true };
|
||||
await DialogService.ShowAsync<MessageViewDialog>("مشاهده پیام", parameters, options);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری پیام: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PublishMessage(long messageId)
|
||||
{
|
||||
var result = await DialogService.ShowMessageBox(
|
||||
"تأیید انتشار",
|
||||
"آیا از انتشار این پیام اطمینان دارید؟",
|
||||
yesText: "بله", cancelText: "خیر");
|
||||
|
||||
if (result == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await PublicMessageService.PublishAsync(messageId);
|
||||
Snackbar.Add("پیام با موفقیت منتشر شد", Severity.Success);
|
||||
await LoadMessages();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در انتشار پیام: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ArchiveMessage(long messageId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await PublicMessageService.ArchiveAsync(messageId);
|
||||
Snackbar.Add("پیام بایگانی شد", Severity.Success);
|
||||
await LoadMessages();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بایگانی پیام: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteMessage(long messageId)
|
||||
{
|
||||
var result = await DialogService.ShowMessageBox(
|
||||
"تأیید حذف",
|
||||
"آیا از حذف این پیام اطمینان دارید؟",
|
||||
yesText: "بله", cancelText: "خیر");
|
||||
|
||||
if (result == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await PublicMessageService.DeleteAsync(messageId);
|
||||
Snackbar.Add("پیام با موفقیت حذف شد", Severity.Success);
|
||||
await LoadMessages();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در حذف پیام: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Color GetTypeColor(MessageType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
MessageType.Announcement => Color.Primary,
|
||||
MessageType.News => Color.Info,
|
||||
MessageType.Alert => Color.Warning,
|
||||
MessageType.Promotion => Color.Success,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
private string GetTypeText(MessageType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
MessageType.Announcement => "اطلاعیه",
|
||||
MessageType.News => "خبر",
|
||||
MessageType.Alert => "هشدار",
|
||||
MessageType.Promotion => "تبلیغات",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
}
|
||||
|
||||
private Color GetStatusColor(MessageStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
MessageStatus.Draft => Color.Default,
|
||||
MessageStatus.Published => Color.Success,
|
||||
MessageStatus.Archived => Color.Dark,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
private string GetStatusText(MessageStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
MessageStatus.Draft => "پیشنویس",
|
||||
MessageStatus.Published => "منتشر شده",
|
||||
MessageStatus.Archived => "بایگانی شده",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using BackOffice.BFF.DiscountCategory.Protobuf.Protos.DiscountCategory;
|
||||
|
||||
namespace BackOffice.Services.DiscountCategory;
|
||||
|
||||
public class DiscountCategoryService : IDiscountCategoryService
|
||||
{
|
||||
private readonly DiscountCategoriesContract.DiscountCategoriesContractClient _client;
|
||||
|
||||
public DiscountCategoryService(DiscountCategoriesContract.DiscountCategoriesContractClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public async Task<List<DiscountCategoryDto>> GetCategoriesAsync(bool? isActive = null)
|
||||
{
|
||||
var request = new GetDiscountCategoriesRequest
|
||||
{
|
||||
IsActive = isActive
|
||||
};
|
||||
|
||||
var response = await _client.GetDiscountCategoriesAsync(request);
|
||||
|
||||
var categories = response.Categories.Select(c => new DiscountCategoryDto
|
||||
{
|
||||
CategoryId = c.CategoryId,
|
||||
ParentCategoryId = c.ParentCategoryId > 0 ? c.ParentCategoryId : null,
|
||||
Title = c.Title,
|
||||
Description = c.Description,
|
||||
DisplayOrder = c.DisplayOrder,
|
||||
IsActive = c.IsActive,
|
||||
CreatedAt = c.CreatedAt.ToDateTime(),
|
||||
UpdatedAt = c.UpdatedAt?.ToDateTime()
|
||||
}).ToList();
|
||||
|
||||
// Build tree structure
|
||||
return BuildCategoryTree(categories);
|
||||
}
|
||||
|
||||
public async Task<DiscountCategoryDto?> GetByIdAsync(long id)
|
||||
{
|
||||
// Note: Proto doesn't have GetById yet, so we'll get all and filter
|
||||
var categories = await GetCategoriesAsync();
|
||||
return FindCategoryById(categories, id);
|
||||
}
|
||||
|
||||
public async Task<long> CreateAsync(CreateDiscountCategoryDto dto)
|
||||
{
|
||||
var request = new CreateDiscountCategoryRequest
|
||||
{
|
||||
ParentCategoryId = dto.ParentCategoryId ?? 0,
|
||||
Title = dto.Title,
|
||||
Description = dto.Description ?? string.Empty,
|
||||
DisplayOrder = dto.DisplayOrder,
|
||||
IsActive = dto.IsActive
|
||||
};
|
||||
|
||||
var response = await _client.CreateDiscountCategoryAsync(request);
|
||||
return response.CategoryId;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(long id, UpdateDiscountCategoryDto dto)
|
||||
{
|
||||
var request = new UpdateDiscountCategoryRequest
|
||||
{
|
||||
CategoryId = id,
|
||||
ParentCategoryId = dto.ParentCategoryId ?? 0,
|
||||
Title = dto.Title,
|
||||
Description = dto.Description ?? string.Empty,
|
||||
DisplayOrder = dto.DisplayOrder,
|
||||
IsActive = dto.IsActive
|
||||
};
|
||||
|
||||
await _client.UpdateDiscountCategoryAsync(request);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(long id)
|
||||
{
|
||||
var request = new DeleteDiscountCategoryRequest { CategoryId = id };
|
||||
await _client.DeleteDiscountCategoryAsync(request);
|
||||
}
|
||||
|
||||
private List<DiscountCategoryDto> BuildCategoryTree(List<DiscountCategoryDto> categories)
|
||||
{
|
||||
var categoryDict = categories.ToDictionary(c => c.CategoryId);
|
||||
|
||||
foreach (var category in categories)
|
||||
{
|
||||
if (category.ParentCategoryId.HasValue &&
|
||||
categoryDict.TryGetValue(category.ParentCategoryId.Value, out var parent))
|
||||
{
|
||||
parent.Children.Add(category);
|
||||
}
|
||||
}
|
||||
|
||||
return categories.Where(c => !c.ParentCategoryId.HasValue).ToList();
|
||||
}
|
||||
|
||||
private DiscountCategoryDto? FindCategoryById(List<DiscountCategoryDto> categories, long id)
|
||||
{
|
||||
foreach (var category in categories)
|
||||
{
|
||||
if (category.CategoryId == id)
|
||||
return category;
|
||||
|
||||
var found = FindCategoryById(category.Children.ToList(), id);
|
||||
if (found != null)
|
||||
return found;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
namespace BackOffice.Services.DiscountCategory;
|
||||
|
||||
public interface IDiscountCategoryService
|
||||
{
|
||||
Task<List<DiscountCategoryDto>> GetCategoriesAsync(bool? isActive = null);
|
||||
Task<DiscountCategoryDto?> GetByIdAsync(long id);
|
||||
Task<long> CreateAsync(CreateDiscountCategoryDto dto);
|
||||
Task UpdateAsync(long id, UpdateDiscountCategoryDto dto);
|
||||
Task DeleteAsync(long id);
|
||||
}
|
||||
|
||||
public class DiscountCategoryDto
|
||||
{
|
||||
public long CategoryId { get; set; }
|
||||
public long? ParentCategoryId { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public int DisplayOrder { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
// For UI tree view
|
||||
public bool IsExpanded { get; set; } = false;
|
||||
public HashSet<DiscountCategoryDto> Children { get; set; } = new();
|
||||
}
|
||||
|
||||
public class CreateDiscountCategoryDto
|
||||
{
|
||||
public long? ParentCategoryId { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public int DisplayOrder { get; set; } = 0;
|
||||
public bool IsActive { get; set; } = true;
|
||||
}
|
||||
|
||||
public class UpdateDiscountCategoryDto
|
||||
{
|
||||
public long? ParentCategoryId { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public int DisplayOrder { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
119
src/BackOffice/Services/DiscountOrder/DiscountOrderService.cs
Normal file
119
src/BackOffice/Services/DiscountOrder/DiscountOrderService.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using BackOffice.BFF.DiscountOrder.Protobuf.Protos.DiscountOrder;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
|
||||
namespace BackOffice.Services.DiscountOrder;
|
||||
|
||||
public class DiscountOrderService : IDiscountOrderService
|
||||
{
|
||||
private readonly DiscountOrdersContract.DiscountOrdersContractClient _client;
|
||||
|
||||
public DiscountOrderService(DiscountOrdersContract.DiscountOrdersContractClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public async Task<List<DiscountOrderDto>> GetOrdersAsync(OrderFilterDto? filter = null)
|
||||
{
|
||||
filter ??= new OrderFilterDto();
|
||||
|
||||
var request = new GetUserOrdersRequest
|
||||
{
|
||||
UserId = 0, // Admin gets all orders
|
||||
PageNumber = filter.PageNumber,
|
||||
PageSize = filter.PageSize
|
||||
};
|
||||
|
||||
// Note: Current proto may not have all filter fields, adjust as needed
|
||||
var response = await _client.GetUserOrdersAsync(request);
|
||||
|
||||
var orders = response.Orders.Select(o => new DiscountOrderDto
|
||||
{
|
||||
OrderId = o.OrderId,
|
||||
UserId = o.UserId,
|
||||
UserFullName = o.UserFullName ?? "N/A",
|
||||
CreatedAt = o.CreatedAt.ToDateTime(),
|
||||
TotalAmount = o.TotalAmount,
|
||||
TotalDiscount = o.TotalDiscount,
|
||||
FinalAmount = o.FinalAmount,
|
||||
Status = (OrderStatus)o.Status,
|
||||
IsPaid = o.IsPaid,
|
||||
PaidAt = o.PaidAt?.ToDateTime()
|
||||
}).ToList();
|
||||
|
||||
// Apply client-side filters (until proto supports them)
|
||||
if (!string.IsNullOrEmpty(filter.SearchQuery))
|
||||
{
|
||||
var query = filter.SearchQuery.ToLower();
|
||||
orders = orders.Where(o =>
|
||||
o.OrderId.ToString().Contains(query) ||
|
||||
o.UserFullName.ToLower().Contains(query)
|
||||
).ToList();
|
||||
}
|
||||
|
||||
if (filter.Status.HasValue)
|
||||
{
|
||||
orders = orders.Where(o => o.Status == filter.Status.Value).ToList();
|
||||
}
|
||||
|
||||
if (filter.FromDate.HasValue)
|
||||
{
|
||||
orders = orders.Where(o => o.CreatedAt >= filter.FromDate.Value).ToList();
|
||||
}
|
||||
|
||||
if (filter.ToDate.HasValue)
|
||||
{
|
||||
orders = orders.Where(o => o.CreatedAt <= filter.ToDate.Value).ToList();
|
||||
}
|
||||
|
||||
return orders;
|
||||
}
|
||||
|
||||
public async Task<DiscountOrderDetailsDto?> GetByIdAsync(long id)
|
||||
{
|
||||
var request = new GetOrderByIdRequest { OrderId = id };
|
||||
var response = await _client.GetOrderByIdAsync(request);
|
||||
|
||||
if (response.Order == null)
|
||||
return null;
|
||||
|
||||
return new DiscountOrderDetailsDto
|
||||
{
|
||||
OrderId = response.Order.OrderId,
|
||||
UserId = response.Order.UserId,
|
||||
UserFullName = response.Order.UserFullName ?? "N/A",
|
||||
CreatedAt = response.Order.CreatedAt.ToDateTime(),
|
||||
TotalAmount = response.Order.TotalAmount,
|
||||
TotalDiscount = response.Order.TotalDiscount,
|
||||
FinalAmount = response.Order.FinalAmount,
|
||||
Status = (OrderStatus)response.Order.Status,
|
||||
IsPaid = response.Order.IsPaid,
|
||||
PaidAt = response.Order.PaidAt?.ToDateTime(),
|
||||
ShippingAddress = response.Order.ShippingAddress,
|
||||
PaymentTransactionCode = response.Order.PaymentTransactionCode,
|
||||
AdminNote = response.Order.AdminNote,
|
||||
Items = response.Order.Items.Select(item => new OrderItemDto
|
||||
{
|
||||
ProductId = item.ProductId,
|
||||
ProductTitle = item.ProductTitle,
|
||||
ProductThumbnail = item.ProductThumbnail,
|
||||
Quantity = item.Quantity,
|
||||
UnitPrice = item.UnitPrice,
|
||||
DiscountPercent = item.DiscountPercent,
|
||||
DiscountedPrice = item.DiscountedPrice,
|
||||
TotalPrice = item.TotalPrice
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task UpdateStatusAsync(long id, UpdateOrderStatusDto dto)
|
||||
{
|
||||
var request = new UpdateOrderStatusRequest
|
||||
{
|
||||
OrderId = id,
|
||||
Status = (int)dto.Status,
|
||||
AdminNote = dto.AdminNote ?? string.Empty
|
||||
};
|
||||
|
||||
await _client.UpdateOrderStatusAsync(request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
namespace BackOffice.Services.DiscountOrder;
|
||||
|
||||
public interface IDiscountOrderService
|
||||
{
|
||||
Task<List<DiscountOrderDto>> GetOrdersAsync(OrderFilterDto? filter = null);
|
||||
Task<DiscountOrderDetailsDto?> GetByIdAsync(long id);
|
||||
Task UpdateStatusAsync(long id, UpdateOrderStatusDto dto);
|
||||
}
|
||||
|
||||
public class OrderFilterDto
|
||||
{
|
||||
public string? SearchQuery { get; set; }
|
||||
public OrderStatus? Status { get; set; }
|
||||
public DateTime? FromDate { get; set; }
|
||||
public DateTime? ToDate { get; set; }
|
||||
public int PageNumber { get; set; } = 1;
|
||||
public int PageSize { get; set; } = 20;
|
||||
}
|
||||
|
||||
public enum OrderStatus
|
||||
{
|
||||
Pending = 0,
|
||||
Paid = 1,
|
||||
Processing = 2,
|
||||
Shipped = 3,
|
||||
Delivered = 4,
|
||||
Cancelled = 5,
|
||||
Returned = 6
|
||||
}
|
||||
|
||||
public class DiscountOrderDto
|
||||
{
|
||||
public long OrderId { get; set; }
|
||||
public long UserId { get; set; }
|
||||
public string UserFullName { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public long TotalAmount { get; set; }
|
||||
public long TotalDiscount { get; set; }
|
||||
public long FinalAmount { get; set; }
|
||||
public OrderStatus Status { get; set; }
|
||||
public bool IsPaid { get; set; }
|
||||
public DateTime? PaidAt { get; set; }
|
||||
}
|
||||
|
||||
public class DiscountOrderDetailsDto : DiscountOrderDto
|
||||
{
|
||||
public string? ShippingAddress { get; set; }
|
||||
public string? PaymentTransactionCode { get; set; }
|
||||
public string? AdminNote { get; set; }
|
||||
public List<OrderItemDto> Items { get; set; } = new();
|
||||
}
|
||||
|
||||
public class OrderItemDto
|
||||
{
|
||||
public long ProductId { get; set; }
|
||||
public string ProductTitle { get; set; } = string.Empty;
|
||||
public string? ProductThumbnail { get; set; }
|
||||
public int Quantity { get; set; }
|
||||
public long UnitPrice { get; set; }
|
||||
public int DiscountPercent { get; set; }
|
||||
public long DiscountedPrice { get; set; }
|
||||
public long TotalPrice { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateOrderStatusDto
|
||||
{
|
||||
public OrderStatus Status { get; set; }
|
||||
public string? AdminNote { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
using BackOffice.BFF.DiscountProduct.Protobuf.Protos.DiscountProduct;
|
||||
|
||||
namespace BackOffice.Services.DiscountProduct;
|
||||
|
||||
public class DiscountProductService : IDiscountProductService
|
||||
{
|
||||
private readonly DiscountProductsContract.DiscountProductsContractClient _client;
|
||||
|
||||
public DiscountProductService(DiscountProductsContract.DiscountProductsContractClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public async Task<List<DiscountProductDto>> GetProductsAsync(ProductFilterDto? filter = null)
|
||||
{
|
||||
filter ??= new ProductFilterDto();
|
||||
|
||||
var request = new GetDiscountProductsRequest
|
||||
{
|
||||
SearchQuery = filter.SearchQuery ?? string.Empty,
|
||||
CategoryId = filter.CategoryId ?? 0,
|
||||
IsActive = filter.IsActive,
|
||||
InStock = filter.InStock,
|
||||
PageNumber = filter.PageNumber,
|
||||
PageSize = filter.PageSize
|
||||
};
|
||||
|
||||
var response = await _client.GetDiscountProductsAsync(request);
|
||||
|
||||
return response.Products.Select(p => new DiscountProductDto
|
||||
{
|
||||
ProductId = p.ProductId,
|
||||
Title = p.Title,
|
||||
Description = p.Description,
|
||||
ThumbnailPath = p.ThumbnailPath,
|
||||
Price = p.Price,
|
||||
MaxDiscountPercent = p.MaxDiscountPercent,
|
||||
Stock = p.Stock,
|
||||
SaleCount = p.SaleCount,
|
||||
IsActive = p.IsActive,
|
||||
CategoryId = p.CategoryId > 0 ? p.CategoryId : null,
|
||||
CategoryTitle = p.CategoryTitle,
|
||||
CreatedAt = p.CreatedAt.ToDateTime(),
|
||||
UpdatedAt = p.UpdatedAt?.ToDateTime()
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public async Task<DiscountProductDto?> GetByIdAsync(long id)
|
||||
{
|
||||
var request = new GetDiscountProductByIdRequest { ProductId = id };
|
||||
var response = await _client.GetDiscountProductByIdAsync(request);
|
||||
|
||||
if (response.Product == null)
|
||||
return null;
|
||||
|
||||
return new DiscountProductDto
|
||||
{
|
||||
ProductId = response.Product.ProductId,
|
||||
Title = response.Product.Title,
|
||||
Description = response.Product.Description,
|
||||
ThumbnailPath = response.Product.ThumbnailPath,
|
||||
Price = response.Product.Price,
|
||||
MaxDiscountPercent = response.Product.MaxDiscountPercent,
|
||||
Stock = response.Product.Stock,
|
||||
SaleCount = response.Product.SaleCount,
|
||||
IsActive = response.Product.IsActive,
|
||||
CategoryId = response.Product.CategoryId > 0 ? response.Product.CategoryId : null,
|
||||
CategoryTitle = response.Product.CategoryTitle,
|
||||
CreatedAt = response.Product.CreatedAt.ToDateTime(),
|
||||
UpdatedAt = response.Product.UpdatedAt?.ToDateTime()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<long> CreateAsync(CreateDiscountProductDto dto)
|
||||
{
|
||||
var request = new CreateDiscountProductRequest
|
||||
{
|
||||
Title = dto.Title,
|
||||
Description = dto.Description ?? string.Empty,
|
||||
ThumbnailPath = dto.ThumbnailPath ?? string.Empty,
|
||||
Price = dto.Price,
|
||||
MaxDiscountPercent = dto.MaxDiscountPercent,
|
||||
Stock = dto.Stock,
|
||||
IsActive = dto.IsActive,
|
||||
CategoryId = dto.CategoryId ?? 0
|
||||
};
|
||||
|
||||
if (dto.Tags != null)
|
||||
{
|
||||
request.Tags.AddRange(dto.Tags);
|
||||
}
|
||||
|
||||
var response = await _client.CreateDiscountProductAsync(request);
|
||||
return response.ProductId;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(long id, UpdateDiscountProductDto dto)
|
||||
{
|
||||
var request = new UpdateDiscountProductRequest
|
||||
{
|
||||
ProductId = id,
|
||||
Title = dto.Title,
|
||||
Description = dto.Description ?? string.Empty,
|
||||
ThumbnailPath = dto.ThumbnailPath ?? string.Empty,
|
||||
Price = dto.Price,
|
||||
MaxDiscountPercent = dto.MaxDiscountPercent,
|
||||
Stock = dto.Stock,
|
||||
IsActive = dto.IsActive,
|
||||
CategoryId = dto.CategoryId ?? 0
|
||||
};
|
||||
|
||||
if (dto.Tags != null)
|
||||
{
|
||||
request.Tags.AddRange(dto.Tags);
|
||||
}
|
||||
|
||||
await _client.UpdateDiscountProductAsync(request);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(long id)
|
||||
{
|
||||
var request = new DeleteDiscountProductRequest { ProductId = id };
|
||||
await _client.DeleteDiscountProductAsync(request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
namespace BackOffice.Services.DiscountProduct;
|
||||
|
||||
public interface IDiscountProductService
|
||||
{
|
||||
Task<List<DiscountProductDto>> GetProductsAsync(ProductFilterDto? filter = null);
|
||||
Task<DiscountProductDto?> GetByIdAsync(long id);
|
||||
Task<long> CreateAsync(CreateDiscountProductDto dto);
|
||||
Task UpdateAsync(long id, UpdateDiscountProductDto dto);
|
||||
Task DeleteAsync(long id);
|
||||
}
|
||||
|
||||
public class ProductFilterDto
|
||||
{
|
||||
public string? SearchQuery { get; set; }
|
||||
public long? CategoryId { get; set; }
|
||||
public bool? IsActive { get; set; }
|
||||
public bool? InStock { get; set; }
|
||||
public int PageNumber { get; set; } = 1;
|
||||
public int PageSize { get; set; } = 20;
|
||||
}
|
||||
|
||||
public class DiscountProductDto
|
||||
{
|
||||
public long ProductId { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public string? ThumbnailPath { get; set; }
|
||||
public long Price { get; set; }
|
||||
public int MaxDiscountPercent { get; set; }
|
||||
public int Stock { get; set; }
|
||||
public int SaleCount { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public long? CategoryId { get; set; }
|
||||
public string? CategoryTitle { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class CreateDiscountProductDto
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public string? ThumbnailPath { get; set; }
|
||||
public long Price { get; set; }
|
||||
public int MaxDiscountPercent { get; set; }
|
||||
public int Stock { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public long? CategoryId { get; set; }
|
||||
public List<string>? Tags { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateDiscountProductDto
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public string? ThumbnailPath { get; set; }
|
||||
public long Price { get; set; }
|
||||
public int MaxDiscountPercent { get; set; }
|
||||
public int Stock { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public long? CategoryId { get; set; }
|
||||
public List<string>? Tags { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
namespace BackOffice.Services.PublicMessage;
|
||||
|
||||
public interface IPublicMessageService
|
||||
{
|
||||
Task<List<PublicMessageDto>> GetMessagesAsync(MessageFilterDto? filter = null);
|
||||
Task<PublicMessageDetailsDto?> GetByIdAsync(long id);
|
||||
Task<long> CreateAsync(CreatePublicMessageDto dto);
|
||||
Task UpdateAsync(long id, UpdatePublicMessageDto dto);
|
||||
Task DeleteAsync(long id);
|
||||
Task PublishAsync(long id);
|
||||
Task ArchiveAsync(long id);
|
||||
}
|
||||
|
||||
public class MessageFilterDto
|
||||
{
|
||||
public string? SearchQuery { get; set; }
|
||||
public MessageStatus? Status { get; set; }
|
||||
public MessageType? Type { get; set; }
|
||||
public int PageNumber { get; set; } = 1;
|
||||
public int PageSize { get; set; } = 20;
|
||||
}
|
||||
|
||||
public enum MessageType
|
||||
{
|
||||
Announcement = 0,
|
||||
News = 1,
|
||||
Alert = 2,
|
||||
Promotion = 3
|
||||
}
|
||||
|
||||
public enum MessageStatus
|
||||
{
|
||||
Draft = 0,
|
||||
Published = 1,
|
||||
Archived = 2
|
||||
}
|
||||
|
||||
public class PublicMessageDto
|
||||
{
|
||||
public long MessageId { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public MessageType Type { get; set; }
|
||||
public int Priority { get; set; }
|
||||
public MessageStatus Status { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? PublishedAt { get; set; }
|
||||
public DateTime? ExpiresAt { get; set; }
|
||||
public int ViewCount { get; set; }
|
||||
}
|
||||
|
||||
public class PublicMessageDetailsDto : PublicMessageDto
|
||||
{
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public string? ImageUrl { get; set; }
|
||||
public string? ActionUrl { get; set; }
|
||||
public string? ActionText { get; set; }
|
||||
public List<string> Tags { get; set; } = new();
|
||||
}
|
||||
|
||||
public class CreatePublicMessageDto
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public MessageType Type { get; set; }
|
||||
public int Priority { get; set; } = 1;
|
||||
public string? ImageUrl { get; set; }
|
||||
public string? ActionUrl { get; set; }
|
||||
public string? ActionText { get; set; }
|
||||
public DateTime? ExpiresAt { get; set; }
|
||||
public List<string>? Tags { get; set; }
|
||||
public bool PublishImmediately { get; set; } = false;
|
||||
}
|
||||
|
||||
public class UpdatePublicMessageDto
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public MessageType Type { get; set; }
|
||||
public int Priority { get; set; }
|
||||
public string? ImageUrl { get; set; }
|
||||
public string? ActionUrl { get; set; }
|
||||
public string? ActionText { get; set; }
|
||||
public DateTime? ExpiresAt { get; set; }
|
||||
public List<string>? Tags { get; set; }
|
||||
}
|
||||
156
src/BackOffice/Services/PublicMessage/PublicMessageService.cs
Normal file
156
src/BackOffice/Services/PublicMessage/PublicMessageService.cs
Normal file
@@ -0,0 +1,156 @@
|
||||
using BackOffice.BFF.PublicMessage.Protobuf.Protos.PublicMessage;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
|
||||
namespace BackOffice.Services.PublicMessage;
|
||||
|
||||
public class PublicMessageService : IPublicMessageService
|
||||
{
|
||||
private readonly PublicMessagesContract.PublicMessagesContractClient _client;
|
||||
|
||||
public PublicMessageService(PublicMessagesContract.PublicMessagesContractClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public async Task<List<PublicMessageDto>> GetMessagesAsync(MessageFilterDto? filter = null)
|
||||
{
|
||||
filter ??= new MessageFilterDto();
|
||||
|
||||
var request = new GetPublicMessagesRequest
|
||||
{
|
||||
PageNumber = filter.PageNumber,
|
||||
PageSize = filter.PageSize
|
||||
};
|
||||
|
||||
var response = await _client.GetPublicMessagesAsync(request);
|
||||
|
||||
var messages = response.Messages.Select(m => new PublicMessageDto
|
||||
{
|
||||
MessageId = m.MessageId,
|
||||
Title = m.Title,
|
||||
Type = (MessageType)m.Type,
|
||||
Priority = m.Priority,
|
||||
Status = (MessageStatus)m.Status,
|
||||
CreatedAt = m.CreatedAt.ToDateTime(),
|
||||
PublishedAt = m.PublishedAt?.ToDateTime(),
|
||||
ExpiresAt = m.ExpiresAt?.ToDateTime(),
|
||||
ViewCount = m.ViewCount
|
||||
}).ToList();
|
||||
|
||||
// Apply client-side filters
|
||||
if (!string.IsNullOrEmpty(filter.SearchQuery))
|
||||
{
|
||||
var query = filter.SearchQuery.ToLower();
|
||||
messages = messages.Where(m => m.Title.ToLower().Contains(query)).ToList();
|
||||
}
|
||||
|
||||
if (filter.Status.HasValue)
|
||||
{
|
||||
messages = messages.Where(m => m.Status == filter.Status.Value).ToList();
|
||||
}
|
||||
|
||||
if (filter.Type.HasValue)
|
||||
{
|
||||
messages = messages.Where(m => m.Type == filter.Type.Value).ToList();
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
public async Task<PublicMessageDetailsDto?> GetByIdAsync(long id)
|
||||
{
|
||||
var request = new GetPublicMessageByIdRequest { MessageId = id };
|
||||
var response = await _client.GetPublicMessageByIdAsync(request);
|
||||
|
||||
if (response.Message == null)
|
||||
return null;
|
||||
|
||||
return new PublicMessageDetailsDto
|
||||
{
|
||||
MessageId = response.Message.MessageId,
|
||||
Title = response.Message.Title,
|
||||
Content = response.Message.Content,
|
||||
Type = (MessageType)response.Message.Type,
|
||||
Priority = response.Message.Priority,
|
||||
Status = (MessageStatus)response.Message.Status,
|
||||
ImageUrl = response.Message.ImageUrl,
|
||||
ActionUrl = response.Message.ActionUrl,
|
||||
ActionText = response.Message.ActionText,
|
||||
CreatedAt = response.Message.CreatedAt.ToDateTime(),
|
||||
PublishedAt = response.Message.PublishedAt?.ToDateTime(),
|
||||
ExpiresAt = response.Message.ExpiresAt?.ToDateTime(),
|
||||
ViewCount = response.Message.ViewCount,
|
||||
Tags = response.Message.Tags.ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<long> CreateAsync(CreatePublicMessageDto dto)
|
||||
{
|
||||
var request = new CreatePublicMessageRequest
|
||||
{
|
||||
Title = dto.Title,
|
||||
Content = dto.Content,
|
||||
Type = (int)dto.Type,
|
||||
Priority = dto.Priority,
|
||||
ImageUrl = dto.ImageUrl ?? string.Empty,
|
||||
ActionUrl = dto.ActionUrl ?? string.Empty,
|
||||
ActionText = dto.ActionText ?? string.Empty,
|
||||
ExpiresAt = dto.ExpiresAt.HasValue ? Timestamp.FromDateTime(dto.ExpiresAt.Value.ToUniversalTime()) : null
|
||||
};
|
||||
|
||||
if (dto.Tags != null)
|
||||
{
|
||||
request.Tags.AddRange(dto.Tags);
|
||||
}
|
||||
|
||||
var response = await _client.CreatePublicMessageAsync(request);
|
||||
|
||||
if (dto.PublishImmediately && response.MessageId > 0)
|
||||
{
|
||||
await PublishAsync(response.MessageId);
|
||||
}
|
||||
|
||||
return response.MessageId;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(long id, UpdatePublicMessageDto dto)
|
||||
{
|
||||
var request = new UpdatePublicMessageRequest
|
||||
{
|
||||
MessageId = id,
|
||||
Title = dto.Title,
|
||||
Content = dto.Content,
|
||||
Type = (int)dto.Type,
|
||||
Priority = dto.Priority,
|
||||
ImageUrl = dto.ImageUrl ?? string.Empty,
|
||||
ActionUrl = dto.ActionUrl ?? string.Empty,
|
||||
ActionText = dto.ActionText ?? string.Empty,
|
||||
ExpiresAt = dto.ExpiresAt.HasValue ? Timestamp.FromDateTime(dto.ExpiresAt.Value.ToUniversalTime()) : null
|
||||
};
|
||||
|
||||
if (dto.Tags != null)
|
||||
{
|
||||
request.Tags.AddRange(dto.Tags);
|
||||
}
|
||||
|
||||
await _client.UpdatePublicMessageAsync(request);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(long id)
|
||||
{
|
||||
var request = new DeletePublicMessageRequest { MessageId = id };
|
||||
await _client.DeletePublicMessageAsync(request);
|
||||
}
|
||||
|
||||
public async Task PublishAsync(long id)
|
||||
{
|
||||
var request = new PublishPublicMessageRequest { MessageId = id };
|
||||
await _client.PublishPublicMessageAsync(request);
|
||||
}
|
||||
|
||||
public async Task ArchiveAsync(long id)
|
||||
{
|
||||
var request = new ArchivePublicMessageRequest { MessageId = id };
|
||||
await _client.ArchivePublicMessageAsync(request);
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,11 @@
|
||||
Icon="@Icons.Material.Filled.RequestQuote">
|
||||
درخواستهای برداشت
|
||||
</MudNavLink>
|
||||
<MudNavLink Match="NavLinkMatch.Prefix"
|
||||
Href="/commission/withdrawal-reports"
|
||||
Icon="@Icons.Material.Filled.BarChart">
|
||||
گزارش برداشتها
|
||||
</MudNavLink>
|
||||
</MudNavGroup>
|
||||
|
||||
<MudNavGroup Title="شبکه" Icon="@Icons.Material.Filled.AccountTree" Expanded="false">
|
||||
@@ -116,6 +121,39 @@
|
||||
</Authorized>
|
||||
</AuthorizeView>
|
||||
|
||||
<MudDivider Class="my-2" />
|
||||
<MudText Class="nav-menu__title" Typo="Typo.subtitle2">فروشگاه تخفیفی</MudText>
|
||||
|
||||
<AuthorizeView Roles="Administrator">
|
||||
<Authorized>
|
||||
<MudNavGroup Title="فروشگاه تخفیفی" Icon="@Icons.Material.Filled.Discount" Expanded="false">
|
||||
<MudNavLink Match="NavLinkMatch.Prefix"
|
||||
Href="/discount-products"
|
||||
Icon="@Icons.Material.Filled.ShoppingBag">
|
||||
محصولات تخفیفی
|
||||
</MudNavLink>
|
||||
|
||||
<MudNavLink Match="NavLinkMatch.Prefix"
|
||||
Href="/discount-categories"
|
||||
Icon="@Icons.Material.Filled.Category">
|
||||
دستهبندیهای فروشگاه
|
||||
</MudNavLink>
|
||||
|
||||
<MudNavLink Match="NavLinkMatch.Prefix"
|
||||
Href="/discount-orders"
|
||||
Icon="@Icons.Material.Filled.ShoppingCart">
|
||||
سفارشات فروشگاه
|
||||
</MudNavLink>
|
||||
</MudNavGroup>
|
||||
|
||||
<MudNavLink Match="NavLinkMatch.Prefix"
|
||||
Href="/public-messages"
|
||||
Icon="@Icons.Material.Filled.Campaign">
|
||||
پیامهای عمومی
|
||||
</MudNavLink>
|
||||
</Authorized>
|
||||
</AuthorizeView>
|
||||
|
||||
<MudDivider Class="my-2" />
|
||||
<MudText Class="nav-menu__title" Typo="Typo.subtitle2">سیستم</MudText>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user