diff --git a/src/BackOffice/Common/Configure/ConfigureService.cs b/src/BackOffice/Common/Configure/ConfigureService.cs index 30825eb..455200a 100644 --- a/src/BackOffice/Common/Configure/ConfigureService.cs +++ b/src/BackOffice/Common/Configure/ConfigureService.cs @@ -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(); services.AddMudServices(); services.AddGrpcServices(configuration); + + // Application Services + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + return services; } @@ -88,6 +103,14 @@ public static class ConfigureServices services.AddTransient(sp => new ConfigurationContract.ConfigurationContractClient(sp.GetRequiredService())); services.AddTransient(sp => new HealthContract.HealthContractClient(sp.GetRequiredService())); + // Discount Shop Services + services.AddTransient(sp => new DiscountProductsContract.DiscountProductsContractClient(sp.GetRequiredService())); + services.AddTransient(sp => new DiscountCategoriesContract.DiscountCategoriesContractClient(sp.GetRequiredService())); + services.AddTransient(sp => new DiscountOrdersContract.DiscountOrdersContractClient(sp.GetRequiredService())); + + // Public Message Service + services.AddTransient(sp => new PublicMessagesContract.PublicMessagesContractClient(sp.GetRequiredService())); + return services; } diff --git a/src/BackOffice/Pages/Commission/WithdrawalReports.razor b/src/BackOffice/Pages/Commission/WithdrawalReports.razor new file mode 100644 index 0000000..f83d741 --- /dev/null +++ b/src/BackOffice/Pages/Commission/WithdrawalReports.razor @@ -0,0 +1,309 @@ +@page "/commission/withdrawal-reports" +@attribute [Authorize] + +@using MudBlazor +@using BackOffice.BFF.Commission.Protobuf +@using Google.Protobuf.WellKnownTypes + + + گزارش برداشت‌ها + + گزارش تجمیعی برداشت‌های کمیسیون بر اساس بازه تاریخ و نوع دوره + + + + + + + + + + + + + + روزانه + هفتگی + ماهانه + + + + + + + + + + + همه وضعیت‌ها + در انتظار واریز به کیف پول + واریز شده به کیف پول طلایی + درخواست برداشت ثبت شده + برداشت انجام شده + خطا در پرداخت بانکی + لغو شده + + + + + جستجو + + + + + + + @if (_isLoading) + { + + + + } + else if (_periodReports.Count == 0) + { + + هیچ داده‌ای برای بازه و فیلترهای انتخاب‌شده یافت نشد. + + } + else + { + + + + مجموع مبلغ برداشت‌ها + @_summary.TotalAmount.ToString("N0") ریال + + + + + مبلغ پرداخت شده + @_summary.TotalPaid.ToString("N0") ریال + + + + + مبلغ در انتظار + @_summary.TotalPending.ToString("N0") ریال + + + + + نرخ موفقیت + @_summary.SuccessRate.ToString("0.##")% + + @($"{_summary.TotalRequests} درخواست، {_summary.UniqueUsers} کاربر") + + + + + + + + + + + + + @context.Item.StartDate.ToString("yyyy/MM/dd") + + + + + @context.Item.EndDate.ToString("yyyy/MM/dd") + + + + + + + + + @context.Item.TotalAmount.ToString("N0") ریال + + + + + @context.Item.PaidAmount.ToString("N0") ریال + + + + + @context.Item.PendingAmount.ToString("N0") ریال + + + + + + + } + + +@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 _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(); + } + 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; } + } +} + diff --git a/src/BackOffice/Pages/DiscountShop/Components/CategoryFormDialog.razor b/src/BackOffice/Pages/DiscountShop/Components/CategoryFormDialog.razor new file mode 100644 index 0000000..05afea1 --- /dev/null +++ b/src/BackOffice/Pages/DiscountShop/Components/CategoryFormDialog.razor @@ -0,0 +1,162 @@ +@using BackOffice.Services.DiscountCategory +@inject ISnackbar Snackbar + + + + + + + + + + + + + + + + @foreach (var category in _availableParents) + { + @GetCategoryPath(category) + } + + + + + + + + + + + + + + + انصراف + + @if (_loading) + { + + } + else + { + @(IsEditMode ? "ذخیره تغییرات" : "ایجاد دسته‌بندی") + } + + + + +@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 _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 FlattenCategories(List categories, string prefix = "") + { + var result = new List(); + 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; + } +} diff --git a/src/BackOffice/Pages/DiscountShop/Components/ChangeOrderStatusDialog.razor b/src/BackOffice/Pages/DiscountShop/Components/ChangeOrderStatusDialog.razor new file mode 100644 index 0000000..a61c2c0 --- /dev/null +++ b/src/BackOffice/Pages/DiscountShop/Components/ChangeOrderStatusDialog.razor @@ -0,0 +1,142 @@ +@using BackOffice.Services.DiscountOrder + + + + + + تغییر وضعیت سفارش #@OrderId + + + + + + + + وضعیت فعلی: @GetStatusText(CurrentStatus) + + + + + + در انتظار پرداخت + پرداخت شده + در حال آماده‌سازی + ارسال شده + تحویل داده شده + لغو شده + مرجوع شده + + + + + + + + @if (Model.Status == OrderStatus.Cancelled || Model.Status == OrderStatus.Returned) + { + + + توجه: در صورت لغو یا مرجوعی سفارش، موجودی محصولات به انبار بازگردانده می‌شود. + + + } + + @if (Model.Status == OrderStatus.Shipped) + { + + + پس از تغییر وضعیت به "ارسال شده"، اطلاع‌رسانی به کاربر ارسال خواهد شد. + + + } + + + + + انصراف + + @if (_loading) + { + + } + else + { + تغییر وضعیت + } + + + + +@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 => "مرجوع شده", + _ => "نامشخص" + }; + } +} diff --git a/src/BackOffice/Pages/DiscountShop/Components/OrderDetailsDialog.razor b/src/BackOffice/Pages/DiscountShop/Components/OrderDetailsDialog.razor new file mode 100644 index 0000000..1a2a91d --- /dev/null +++ b/src/BackOffice/Pages/DiscountShop/Components/OrderDetailsDialog.razor @@ -0,0 +1,213 @@ +@using BackOffice.Services.DiscountOrder + + + + + + جزئیات سفارش #@Order.OrderId + + + + + + + + اطلاعات خریدار + + + نام و نام خانوادگی: + @Order.UserFullName + + + شناسه کاربر: + @Order.UserId + + + + + + + + + وضعیت سفارش + + @GetStatusText(Order.Status) + + + تاریخ ثبت: @Order.CreatedAt.ToString("yyyy/MM/dd HH:mm") + + + + + + + + وضعیت پرداخت + @if (Order.IsPaid) + { + + پرداخت شده + + @if (Order.PaidAt.HasValue) + { + + تاریخ پرداخت: @Order.PaidAt.Value.ToString("yyyy/MM/dd HH:mm") + + } + @if (!string.IsNullOrEmpty(Order.PaymentTransactionCode)) + { + + کد تراکنش: @Order.PaymentTransactionCode + + } + } + else + { + + در انتظار پرداخت + + } + + + + + @if (!string.IsNullOrEmpty(Order.ShippingAddress)) + { + + + + + آدرس ارسال + + @Order.ShippingAddress + + + } + + + + + آیتم‌های سفارش + + + محصول + تعداد + قیمت واحد + تخفیف + قیمت نهایی + جمع + + + +
+ @if (!string.IsNullOrEmpty(context.ProductThumbnail)) + { + + } + @context.ProductTitle +
+
+ @context.Quantity + @context.UnitPrice.ToString("N0") ریال + + @context.DiscountPercent% + + @context.DiscountedPrice.ToString("N0") ریال + @context.TotalPrice.ToString("N0") ریال +
+
+
+
+ + + + + خلاصه مالی + + + مبلغ کل: + + + @Order.TotalAmount.ToString("N0") ریال + + + + تخفیف: + + + @Order.TotalDiscount.ToString("N0") ریال + + + + + + مبلغ قابل پرداخت: + + + + @Order.FinalAmount.ToString("N0") ریال + + + + + + + + @if (!string.IsNullOrEmpty(Order.AdminNote)) + { + + + + + یادداشت ادمین + + @Order.AdminNote + + + } +
+
+ + بستن + +
+ +@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 => "مرجوع شده", + _ => "نامشخص" + }; + } +} diff --git a/src/BackOffice/Pages/DiscountShop/Components/ProductFormDialog.razor b/src/BackOffice/Pages/DiscountShop/Components/ProductFormDialog.razor new file mode 100644 index 0000000..162d285 --- /dev/null +++ b/src/BackOffice/Pages/DiscountShop/Components/ProductFormDialog.razor @@ -0,0 +1,215 @@ +@using BackOffice.Services.DiscountProduct +@using BackOffice.Services.DiscountCategory +@inject ISnackbar Snackbar + + + + + + + + + + + + + + + + + + + + + + + + + + + + @foreach (var category in _categories) + { + @GetCategoryPath(category) + } + + + + + + + + @if (!string.IsNullOrEmpty(Model.ThumbnailPath)) + { + + + + } + + + + + + + + + + + + + انصراف + + @if (_loading) + { + + } + else + { + @(IsEditMode ? "ذخیره تغییرات" : "ایجاد محصول") + } + + + + +@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 _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 FlattenCategories(List categories, string prefix = "") + { + var result = new List(); + 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? Tags { get; set; } + } +} diff --git a/src/BackOffice/Pages/DiscountShop/DiscountCategoriesMainPage.razor b/src/BackOffice/Pages/DiscountShop/DiscountCategoriesMainPage.razor new file mode 100644 index 0000000..134f51e --- /dev/null +++ b/src/BackOffice/Pages/DiscountShop/DiscountCategoriesMainPage.razor @@ -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 + +مدیریت دسته‌بندی محصولات تخفیفی + + + + + مدیریت دسته‌بندی‌های فروشگاه تخفیفی + + + + + + + + + + دسته‌بندی جدید + + + + + @if (_loading) + { + + } + else + { + + + + + + + + @category.Title + @if (!string.IsNullOrEmpty(category.Description)) + { + @category.Description + } + + + + + @(category.IsActive ? "فعال" : "غیرفعال") + + + ترتیب: @category.DisplayOrder + + + + + + + + + + + + + + @if (!_filteredCategories.Any()) + { + + @if (string.IsNullOrEmpty(_searchQuery)) + { + هنوز دسته‌بندی‌ای ایجاد نشده است. + } + else + { + دسته‌بندی با عبارت "@_searchQuery" یافت نشد. + } + + } + } + + + +@code { + private List _categories = new(); + private HashSet _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 FlattenCategories(List categories) + { + var result = new List(); + 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( + 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("ویرایش دسته‌بندی", 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); + } + } + } + +} diff --git a/src/BackOffice/Pages/DiscountShop/DiscountOrdersMainPage.razor b/src/BackOffice/Pages/DiscountShop/DiscountOrdersMainPage.razor new file mode 100644 index 0000000..d9acbc0 --- /dev/null +++ b/src/BackOffice/Pages/DiscountShop/DiscountOrdersMainPage.razor @@ -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 + +مدیریت سفارشات فروشگاه تخفیفی + + + + + مدیریت سفارشات فروشگاه تخفیفی + + + + + + + + + + همه + در انتظار پرداخت + پرداخت شده + در حال آماده‌سازی + ارسال شده + تحویل داده شده + لغو شده + مرجوع شده + + + + + + + + جستجو + + + + + + + + + + + + + + + @context.Item.TotalAmount.ToString("N0") ریال + + + + + + @context.Item.TotalDiscount.ToString("N0") ریال + + + + + + + @context.Item.FinalAmount.ToString("N0") ریال + + + + + + + + @GetStatusText(context.Item.Status) + + + + + + + @if (context.Item.IsPaid) + { + + پرداخت شده + + } + else + { + + در انتظار + + } + + + + + + + + + + + + + + + + + + +@code { + private MudDataGrid? _dataGrid; + private List _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("جزئیات سفارش", 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("تغییر وضعیت سفارش", 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 => "مرجوع شده", + _ => "نامشخص" + }; + } + +} diff --git a/src/BackOffice/Pages/DiscountShop/DiscountProductsMainPage.razor b/src/BackOffice/Pages/DiscountShop/DiscountProductsMainPage.razor new file mode 100644 index 0000000..1e98803 --- /dev/null +++ b/src/BackOffice/Pages/DiscountShop/DiscountProductsMainPage.razor @@ -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 + +مدیریت محصولات تخفیفی + + + + + مدیریت محصولات تخفیفی + + + + + + + + + + همه + @* TODO: Load categories *@ + + + + + همه + فعال + غیرفعال + + + + + همه + موجود + ناموجود + + + + + محصول جدید + + + + + + + + + + + @if (!string.IsNullOrEmpty(context.Item.ThumbnailPath)) + { + + } + else + { + + + + } + + + + + + + + @context.Item.Price.ToString("N0") ریال + + + + + + @context.Item.MaxDiscountPercent% + + + + + + + @context.Item.Stock + + + + + + + @context.Item.SaleCount + + + + + + + @(context.Item.IsActive ? "فعال" : "غیرفعال") + + + + + + + + + + + + + + + + + + + +@code { + private MudDataGrid? _dataGrid; + private List _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("ایجاد محصول جدید", 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("ویرایش محصول", 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; } + } + */ +} diff --git a/src/BackOffice/Pages/PublicMessages/Components/MessageFormDialog.razor b/src/BackOffice/Pages/PublicMessages/Components/MessageFormDialog.razor new file mode 100644 index 0000000..289679c --- /dev/null +++ b/src/BackOffice/Pages/PublicMessages/Components/MessageFormDialog.razor @@ -0,0 +1,193 @@ +@using BackOffice.Services.PublicMessage + + + + + + @(IsEditMode ? "ویرایش پیام" : "ایجاد پیام جدید") + + + + + + + + + + + + اطلاعیه + خبر + هشدار + تبلیغات + + + + + + + + + + + + + + + + @if (!string.IsNullOrEmpty(Model.ImageUrl)) + { + + + + } + + + + + + + + + + + + + + + + + + @if (!IsEditMode) + { + + + @if (!Model.PublishImmediately) + { + + پیام به صورت پیش‌نویس ذخیره می‌شود + + } + + } + + + + + انصراف + + @if (_loading) + { + + } + else + { + @(IsEditMode ? "ذخیره تغییرات" : Model.PublishImmediately ? "ایجاد و انتشار" : "ذخیره پیش‌نویس") + } + + + + +@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? Tags { get; set; } + public bool PublishImmediately { get; set; } = false; + } +} diff --git a/src/BackOffice/Pages/PublicMessages/Components/MessageViewDialog.razor b/src/BackOffice/Pages/PublicMessages/Components/MessageViewDialog.razor new file mode 100644 index 0000000..d29edbb --- /dev/null +++ b/src/BackOffice/Pages/PublicMessages/Components/MessageViewDialog.razor @@ -0,0 +1,195 @@ +@using BackOffice.Services.PublicMessage + + + + + + @Message.Title + + + + + + + + + + + @GetTypeText(Message.Type) + + + + + @GetStatusText(Message.Status) + + + + + + + + @Message.ViewCount بازدید + + + + + + + + @if (!string.IsNullOrEmpty(Message.ImageUrl)) + { + + + + } + + + + + محتوای پیام + @Message.Content + + + + + @if (!string.IsNullOrEmpty(Message.ActionUrl)) + { + + + + @(string.IsNullOrEmpty(Message.ActionText) ? "مشاهده بیشتر" : Message.ActionText) + + + + } + + + @if (Message.Tags?.Any() == true) + { + + + تگ‌ها: + @foreach (var tag in Message.Tags) + { + @tag + } + + + } + + + + + + + تاریخ ایجاد: + @Message.CreatedAt.ToString("yyyy/MM/dd HH:mm") + + @if (Message.PublishedAt.HasValue) + { + + تاریخ انتشار: + @Message.PublishedAt.Value.ToString("yyyy/MM/dd HH:mm") + + } + @if (Message.ExpiresAt.HasValue) + { + + تاریخ انقضا: + + @Message.ExpiresAt.Value.ToString("yyyy/MM/dd") + @if (Message.ExpiresAt.Value < DateTime.Now) + { + منقضی شده + } + + + } + + + + + + + بستن + + + +@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 => "بایگانی شده", + _ => "نامشخص" + }; + } +} diff --git a/src/BackOffice/Pages/PublicMessages/PublicMessagesMainPage.razor b/src/BackOffice/Pages/PublicMessages/PublicMessagesMainPage.razor new file mode 100644 index 0000000..f098a3e --- /dev/null +++ b/src/BackOffice/Pages/PublicMessages/PublicMessagesMainPage.razor @@ -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 + +مدیریت پیام‌های عمومی + + + + + مدیریت پیام‌های عمومی + + + + + + + + + + همه + پیش‌نویس + منتشر شده + بایگانی شده + + + + + همه + اطلاعیه + خبر + هشدار + تبلیغات + + + + + پیام جدید + + + + + + + + + + + + + + @GetTypeText(context.Item.Type) + + + + + + + + + + + + + + @GetStatusText(context.Item.Status) + + + + + + + + + + + + @context.Item.ViewCount + + + + + + + + + مشاهده + + + ویرایش + + @if (context.Item.Status == MessageStatus.Draft) + { + + انتشار + + } + @if (context.Item.Status == MessageStatus.Published) + { + + بایگانی + + } + + + حذف + + + + + + + + + + + + + +@code { + private MudDataGrid? _dataGrid; + private List _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("ایجاد پیام جدید", 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("ویرایش پیام", 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("مشاهده پیام", 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 => "بایگانی شده", + _ => "نامشخص" + }; + } + +} diff --git a/src/BackOffice/Services/DiscountCategory/DiscountCategoryService.cs b/src/BackOffice/Services/DiscountCategory/DiscountCategoryService.cs new file mode 100644 index 0000000..f8e634e --- /dev/null +++ b/src/BackOffice/Services/DiscountCategory/DiscountCategoryService.cs @@ -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> 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 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 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 BuildCategoryTree(List 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 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; + } +} diff --git a/src/BackOffice/Services/DiscountCategory/IDiscountCategoryService.cs b/src/BackOffice/Services/DiscountCategory/IDiscountCategoryService.cs new file mode 100644 index 0000000..234020b --- /dev/null +++ b/src/BackOffice/Services/DiscountCategory/IDiscountCategoryService.cs @@ -0,0 +1,44 @@ +namespace BackOffice.Services.DiscountCategory; + +public interface IDiscountCategoryService +{ + Task> GetCategoriesAsync(bool? isActive = null); + Task GetByIdAsync(long id); + Task 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 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; } +} diff --git a/src/BackOffice/Services/DiscountOrder/DiscountOrderService.cs b/src/BackOffice/Services/DiscountOrder/DiscountOrderService.cs new file mode 100644 index 0000000..211638c --- /dev/null +++ b/src/BackOffice/Services/DiscountOrder/DiscountOrderService.cs @@ -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> 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 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); + } +} diff --git a/src/BackOffice/Services/DiscountOrder/IDiscountOrderService.cs b/src/BackOffice/Services/DiscountOrder/IDiscountOrderService.cs new file mode 100644 index 0000000..1a549a0 --- /dev/null +++ b/src/BackOffice/Services/DiscountOrder/IDiscountOrderService.cs @@ -0,0 +1,69 @@ +namespace BackOffice.Services.DiscountOrder; + +public interface IDiscountOrderService +{ + Task> GetOrdersAsync(OrderFilterDto? filter = null); + Task 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 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; } +} diff --git a/src/BackOffice/Services/DiscountProduct/DiscountProductService.cs b/src/BackOffice/Services/DiscountProduct/DiscountProductService.cs new file mode 100644 index 0000000..a36d304 --- /dev/null +++ b/src/BackOffice/Services/DiscountProduct/DiscountProductService.cs @@ -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> 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 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 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); + } +} diff --git a/src/BackOffice/Services/DiscountProduct/IDiscountProductService.cs b/src/BackOffice/Services/DiscountProduct/IDiscountProductService.cs new file mode 100644 index 0000000..5a6d7be --- /dev/null +++ b/src/BackOffice/Services/DiscountProduct/IDiscountProductService.cs @@ -0,0 +1,63 @@ +namespace BackOffice.Services.DiscountProduct; + +public interface IDiscountProductService +{ + Task> GetProductsAsync(ProductFilterDto? filter = null); + Task GetByIdAsync(long id); + Task 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? 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? Tags { get; set; } +} diff --git a/src/BackOffice/Services/PublicMessage/IPublicMessageService.cs b/src/BackOffice/Services/PublicMessage/IPublicMessageService.cs new file mode 100644 index 0000000..9b7570f --- /dev/null +++ b/src/BackOffice/Services/PublicMessage/IPublicMessageService.cs @@ -0,0 +1,85 @@ +namespace BackOffice.Services.PublicMessage; + +public interface IPublicMessageService +{ + Task> GetMessagesAsync(MessageFilterDto? filter = null); + Task GetByIdAsync(long id); + Task 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 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? 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? Tags { get; set; } +} diff --git a/src/BackOffice/Services/PublicMessage/PublicMessageService.cs b/src/BackOffice/Services/PublicMessage/PublicMessageService.cs new file mode 100644 index 0000000..68a5403 --- /dev/null +++ b/src/BackOffice/Services/PublicMessage/PublicMessageService.cs @@ -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> 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 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 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); + } +} diff --git a/src/BackOffice/Shared/NavMenu.razor b/src/BackOffice/Shared/NavMenu.razor index 4406c17..f0ebbf9 100644 --- a/src/BackOffice/Shared/NavMenu.razor +++ b/src/BackOffice/Shared/NavMenu.razor @@ -40,6 +40,11 @@ Icon="@Icons.Material.Filled.RequestQuote"> درخواست‌های برداشت + + گزارش برداشت‌ها + @@ -116,6 +121,39 @@ + + فروشگاه تخفیفی + + + + + + محصولات تخفیفی + + + + دسته‌بندی‌های فروشگاه + + + + سفارشات فروشگاه + + + + + پیام‌های عمومی + + + + سیستم