From bc1eafd5a1172964a6f05b06fd13acc85ceb1fbc Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Thu, 27 Nov 2025 21:38:27 +0330 Subject: [PATCH] Add category and product drag-drop management pages --- src/BackOffice/BackOffice.csproj | 13 +- .../Common/Utilities/RouteConstance.cs | 3 + .../AutoComplete/CategoryAutoComplete.razor | 15 ++ .../CategoryAutoComplete.razor.cs | 82 ++++++++ .../CategoryMultiSelectAutoComplete.razor | 15 ++ .../CategoryMultiSelectAutoComplete.razor.cs | 105 ++++++++++ .../CategoryMultiSelectCombo.razor | 17 ++ .../CategoryMultiSelectCombo.razor.cs | 77 +++++++ .../Pages/Category/CategoryMainPage.razor | 192 +++++++++++------- .../Pages/Category/CategoryMainPage.razor.cs | 119 +++++++++++ .../CategoryProductsDragDropPage.razor | 132 ++++++++++++ .../CategoryProductsDragDropPage.razor.cs | 164 +++++++++++++++ .../CreateOrUpdateCategoryDialog.razor | 10 +- .../CreateOrUpdateCategoryDialog.razor.cs | 7 +- src/BackOffice/Pages/Index.razor | 179 +++++++++++++++- .../Products/Components/CreateDialog.razor | 14 ++ .../Products/Components/CreateDialog.razor.cs | 7 +- .../Products/Components/GalleryDialog.razor | 150 ++++++++------ .../Components/GalleryDialog.razor.cs | 15 ++ .../Products/Components/UpdateDialog.razor | 14 ++ .../Products/Components/UpdateDialog.razor.cs | 26 ++- .../ProductCategoriesDragDropPage.razor | 128 ++++++++++++ .../ProductCategoriesDragDropPage.razor.cs | 162 +++++++++++++++ .../Pages/Products/ProductsMainPage.razor | 8 + .../Pages/Products/ProductsMainPage.razor.cs | 6 + src/BackOffice/Pages/User/UserMainPage.razor | 3 +- .../Components/UserOrderDetailsDialog.razor | 132 ++++++++++++ .../UserOrderDetailsDialog.razor.cs | 97 +++++++++ .../Pages/UserOrder/UserOrderMainPage.razor | 92 ++++++++- .../UserOrder/UserOrderMainPage.razor.cs | 152 +++++++++++++- src/BackOffice/Shared/NavMenu.razor | 6 + src/BackOffice/wwwroot/appsettings.json | 2 +- src/BackOffice/wwwroot/css/app.css | 16 ++ 33 files changed, 2001 insertions(+), 159 deletions(-) create mode 100644 src/BackOffice/Pages/AutoComplete/CategoryAutoComplete.razor create mode 100644 src/BackOffice/Pages/AutoComplete/CategoryAutoComplete.razor.cs create mode 100644 src/BackOffice/Pages/AutoComplete/CategoryMultiSelectAutoComplete.razor create mode 100644 src/BackOffice/Pages/AutoComplete/CategoryMultiSelectAutoComplete.razor.cs create mode 100644 src/BackOffice/Pages/AutoComplete/CategoryMultiSelectCombo.razor create mode 100644 src/BackOffice/Pages/AutoComplete/CategoryMultiSelectCombo.razor.cs create mode 100644 src/BackOffice/Pages/Category/CategoryProductsDragDropPage.razor create mode 100644 src/BackOffice/Pages/Category/CategoryProductsDragDropPage.razor.cs create mode 100644 src/BackOffice/Pages/Products/ProductCategoriesDragDropPage.razor create mode 100644 src/BackOffice/Pages/Products/ProductCategoriesDragDropPage.razor.cs create mode 100644 src/BackOffice/Pages/UserOrder/Components/UserOrderDetailsDialog.razor create mode 100644 src/BackOffice/Pages/UserOrder/Components/UserOrderDetailsDialog.razor.cs diff --git a/src/BackOffice/BackOffice.csproj b/src/BackOffice/BackOffice.csproj index d2c7337..aee54aa 100644 --- a/src/BackOffice/BackOffice.csproj +++ b/src/BackOffice/BackOffice.csproj @@ -21,22 +21,20 @@ - + - - - + - + @@ -59,4 +57,9 @@ + + + + + diff --git a/src/BackOffice/Common/Utilities/RouteConstance.cs b/src/BackOffice/Common/Utilities/RouteConstance.cs index 55b63ca..a1a4102 100644 --- a/src/BackOffice/Common/Utilities/RouteConstance.cs +++ b/src/BackOffice/Common/Utilities/RouteConstance.cs @@ -9,8 +9,11 @@ public static class RouteConstance public const string Role = "/RolePage/"; public const string UserPage = "/UserPage/"; public const string UserOrder = "/UserOrderPage/"; + public const string Orders = "/OrdersPage/"; public const string UserRole = "/UserRolePage/"; public const string UserAddress = "/UserAddressPage/"; public const string Products = "/ProductsPage/"; public const string Category = "/CategoryPage/"; + public const string ProductCategories = "/ProductCategoriesPage/"; + public const string CategoryProducts = "/CategoryProductsPage/"; } diff --git a/src/BackOffice/Pages/AutoComplete/CategoryAutoComplete.razor b/src/BackOffice/Pages/AutoComplete/CategoryAutoComplete.razor new file mode 100644 index 0000000..685824e --- /dev/null +++ b/src/BackOffice/Pages/AutoComplete/CategoryAutoComplete.razor @@ -0,0 +1,15 @@ +@using BackOffice.BFF.Category.Protobuf.Protos.Category + + + diff --git a/src/BackOffice/Pages/AutoComplete/CategoryAutoComplete.razor.cs b/src/BackOffice/Pages/AutoComplete/CategoryAutoComplete.razor.cs new file mode 100644 index 0000000..48a567c --- /dev/null +++ b/src/BackOffice/Pages/AutoComplete/CategoryAutoComplete.razor.cs @@ -0,0 +1,82 @@ +using BackOffice.BFF.Category.Protobuf.Protos.Category; +using Microsoft.AspNetCore.Components; + +namespace BackOffice.Pages.AutoComplete; + +public partial class CategoryAutoComplete +{ + [Inject] public CategoryContract.CategoryContractClient CategoryService { get; set; } = default!; + + [Parameter] public long? Value { get; set; } + [Parameter] public string? Label { get; set; } = "دسته‌بندی والد"; + [Parameter] public EventCallback ValueChanged { get; set; } + + private List _items = new(); + private GetAllCategoryByFilterResponseModel? _item; + + protected override async void OnParametersSet() + { + await base.OnParametersSetAsync(); + + if (Value.HasValue && Value.Value > 0) + { + var response = await CategoryService.GetCategoryAsync(new GetCategoryRequest + { + Id = Value.Value + }); + + if (response != null) + { + _item = new GetAllCategoryByFilterResponseModel + { + Id = response.Id, + Name = response.Name, + Title = response.Title, + Description = response.Description, + ImagePath = response.ImagePath, + ParentId = response.ParentId, + IsActive = response.IsActive, + SortOrder = response.SortOrder + }; + + StateHasChanged(); + } + } + } + + private async Task OnSelected(GetAllCategoryByFilterResponseModel? model) + { + if (model == null) + { + _item = null; + await ValueChanged.InvokeAsync(null); + } + else + { + _item = model; + await ValueChanged.InvokeAsync(model.Id); + } + } + + private async Task> Search(string value, CancellationToken token) + { + var request = new GetAllCategoryByFilterRequest + { + Filter = new GetAllCategoryByFilterFilter + { + Title = string.IsNullOrWhiteSpace(value) ? null : value + }, + PaginationState = new PaginationState + { + PageNumber = 1, + PageSize = 9 + } + }; + + var response = await CategoryService.GetAllCategoryByFilterAsync(request, cancellationToken: token); + _items = response?.Models?.ToList() ?? new List(); + + return _items; + } +} + diff --git a/src/BackOffice/Pages/AutoComplete/CategoryMultiSelectAutoComplete.razor b/src/BackOffice/Pages/AutoComplete/CategoryMultiSelectAutoComplete.razor new file mode 100644 index 0000000..46bcb56 --- /dev/null +++ b/src/BackOffice/Pages/AutoComplete/CategoryMultiSelectAutoComplete.razor @@ -0,0 +1,15 @@ +@using BackOffice.BFF.Category.Protobuf.Protos.Category + + + diff --git a/src/BackOffice/Pages/AutoComplete/CategoryMultiSelectAutoComplete.razor.cs b/src/BackOffice/Pages/AutoComplete/CategoryMultiSelectAutoComplete.razor.cs new file mode 100644 index 0000000..5effaf6 --- /dev/null +++ b/src/BackOffice/Pages/AutoComplete/CategoryMultiSelectAutoComplete.razor.cs @@ -0,0 +1,105 @@ +using BackOffice.BFF.Category.Protobuf.Protos.Category; +using Microsoft.AspNetCore.Components; + +namespace BackOffice.Pages.AutoComplete; + +public partial class CategoryMultiSelectAutoComplete +{ + [Inject] public CategoryContract.CategoryContractClient CategoryService { get; set; } = default!; + + [Parameter] public List SelectedIds { get; set; } = new(); + [Parameter] public string? Label { get; set; } = "انتخاب دسته‌بندی‌ها"; + [Parameter] public EventCallback> SelectedIdsChanged { get; set; } + + private List _items = new(); + private HashSet _selectedItems = new(); + + protected override async Task OnParametersSetAsync() + { + await base.OnParametersSetAsync(); + + if (SelectedIds == null || SelectedIds.Count == 0) + { + _selectedItems.Clear(); + return; + } + + // اگر تعداد آیدی‌های فعلی با انتخاب‌ها برابر است، از لود مجدد صرفنظر می‌کنیم + if (_selectedItems.Count == SelectedIds.Count && + _selectedItems.All(x => SelectedIds.Contains(x.Id))) + { + return; + } + + _selectedItems.Clear(); + + foreach (var id in SelectedIds.Distinct()) + { + try + { + var response = await CategoryService.GetCategoryAsync(new GetCategoryRequest + { + Id = id + }); + + if (response != null) + { + _selectedItems.Add(new GetAllCategoryByFilterResponseModel + { + Id = response.Id, + Name = response.Name, + Title = response.Title, + Description = response.Description, + ImagePath = response.ImagePath, + ParentId = response.ParentId, + IsActive = response.IsActive, + SortOrder = response.SortOrder + }); + } + } + catch + { + // در صورت خطا، از ادامه برای آن آیدی صرفنظر می‌کنیم + } + } + } + + private async Task OnSelectedValuesChanged(HashSet values) + { + _selectedItems = values ?? new HashSet(); + + var ids = _selectedItems + .Select(x => x.Id) + .Distinct() + .ToList(); + + SelectedIds = ids; + + if (SelectedIdsChanged.HasDelegate) + { + await SelectedIdsChanged.InvokeAsync(ids); + } + } + + private async Task> Search(string value, CancellationToken token) + { + var request = new GetAllCategoryByFilterRequest + { + Filter = new GetAllCategoryByFilterFilter + { + Title = string.IsNullOrWhiteSpace(value) ? null : value + }, + PaginationState = new PaginationState + { + PageNumber = 1, + PageSize = 20 + } + }; + + var response = await CategoryService.GetAllCategoryByFilterAsync(request, cancellationToken: token); + _items = response?.Models?.ToList() ?? new List(); + + return _items; + } +} + diff --git a/src/BackOffice/Pages/AutoComplete/CategoryMultiSelectCombo.razor b/src/BackOffice/Pages/AutoComplete/CategoryMultiSelectCombo.razor new file mode 100644 index 0000000..0a1592c --- /dev/null +++ b/src/BackOffice/Pages/AutoComplete/CategoryMultiSelectCombo.razor @@ -0,0 +1,17 @@ +@using BackOffice.BFF.Category.Protobuf.Protos.Category + + + @foreach (var item in _items) + { + + @item.Title + + } + diff --git a/src/BackOffice/Pages/AutoComplete/CategoryMultiSelectCombo.razor.cs b/src/BackOffice/Pages/AutoComplete/CategoryMultiSelectCombo.razor.cs new file mode 100644 index 0000000..537cbe9 --- /dev/null +++ b/src/BackOffice/Pages/AutoComplete/CategoryMultiSelectCombo.razor.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BackOffice.BFF.Category.Protobuf.Protos.Category; +using Microsoft.AspNetCore.Components; + +namespace BackOffice.Pages.AutoComplete; + +public partial class CategoryMultiSelectCombo +{ + [Inject] public CategoryContract.CategoryContractClient CategoryService { get; set; } = default!; + + [Parameter] public List SelectedIds { get; set; } = new(); + [Parameter] public EventCallback> SelectedIdsChanged { get; set; } + [Parameter] public string? Label { get; set; } = "انتخاب دسته‌بندی‌ها"; + [Parameter] public bool Disabled { get; set; } + + private List _items = new(); + private HashSet _internalSelectedIds = new(); + private MudBlazor.MudSelect? _selectRef; + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + await LoadCategoriesAsync(); + SyncInternalFromParameter(); + } + + protected override Task OnParametersSetAsync() + { + SyncInternalFromParameter(); + return base.OnParametersSetAsync(); + } + + private async Task LoadCategoriesAsync() + { + try + { + var response = await CategoryService.GetAllCategoryByFilterAsync(new GetAllCategoryByFilterRequest + { + Filter = new GetAllCategoryByFilterFilter(), + PaginationState = new PaginationState + { + PageNumber = 1, + PageSize = 1000 + } + }); + + _items = response?.Models?.ToList() ?? new List(); + } + catch + { + _items = new List(); + } + } + + private void SyncInternalFromParameter() + { + _internalSelectedIds = SelectedIds != null + ? SelectedIds.Where(id => id > 0).Distinct().ToHashSet() + : new HashSet(); + } + + private async Task OnSelectedValuesChangedAsync() + { + var current = _selectRef?.SelectedValues ?? System.Array.Empty(); + + _internalSelectedIds = current.Where(id => id > 0).Distinct().ToHashSet(); + SelectedIds = _internalSelectedIds.ToList(); + + if (SelectedIdsChanged.HasDelegate) + { + await SelectedIdsChanged.InvokeAsync(SelectedIds); + } + } +} + diff --git a/src/BackOffice/Pages/Category/CategoryMainPage.razor b/src/BackOffice/Pages/Category/CategoryMainPage.razor index 9a05073..fc9d261 100644 --- a/src/BackOffice/Pages/Category/CategoryMainPage.razor +++ b/src/BackOffice/Pages/Category/CategoryMainPage.razor @@ -2,82 +2,136 @@ @using BackOffice.BFF.Category.Protobuf.Protos.Category @using BackOffice.Common.BaseComponents +@using BackOffice.Pages.AutoComplete @using DataModel = BackOffice.BFF.Category.Protobuf.Protos.Category.GetAllCategoryByFilterResponseModel + - - - - - - - - - - - مدیریت دسته‌بندی‌ها - - - - افزودن - - - - - - - - - - - - @(context.Item.IsActive ? "فعال" : "غیرفعال") - - - - - - - - - - - - - + + + + درخت دسته‌بندی‌ها + + + همه دسته‌بندی‌ها + + @foreach (var node in _treeNodes) + { + + +
+ @node.Title +
+
+ } +
+
+
+ + + + + + + + + + + + مدیریت دسته‌بندی‌ها + + + + افزودن + -
-
-
- - - -
+ + + + + + + + @{ + var parentTitle = "-"; + if (context.Item.ParentId.HasValue && + context.Item.ParentId.Value > 0 && + _parentTitles.TryGetValue(context.Item.ParentId.Value, out var value)) + { + parentTitle = value; + } + } + @parentTitle + + + + + + @(context.Item.IsActive ? "فعال" : "غیرفعال") + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/BackOffice/Pages/Category/CategoryMainPage.razor.cs b/src/BackOffice/Pages/Category/CategoryMainPage.razor.cs index 7324188..264d980 100644 --- a/src/BackOffice/Pages/Category/CategoryMainPage.razor.cs +++ b/src/BackOffice/Pages/Category/CategoryMainPage.razor.cs @@ -1,5 +1,6 @@ using BackOffice.BFF.Category.Protobuf.Protos.Category; using BackOffice.Common.BaseComponents; +using BackOffice.Common.Utilities; using Mapster; using Microsoft.AspNetCore.Components; using MudBlazor; @@ -14,6 +15,22 @@ public partial class CategoryMainPage private BasePageComponent _basePage = default!; private MudDataGrid _gridData = default!; private GetAllCategoryByFilterRequest _request = new() { Filter = new() }; + private long? _parentFilterId; + private readonly Dictionary _parentTitles = new(); + private readonly List _treeNodes = new(); + + private sealed class CategoryTreeNode + { + public long Id { get; set; } + public string Title { get; set; } = string.Empty; + public long? ParentId { get; set; } + public int Level { get; set; } + } + + protected override async Task OnInitializedAsync() + { + await LoadTreeAsync(); + } private async Task> ServerReload(GridState state) { @@ -25,6 +42,8 @@ public partial class CategoryMainPage var result = await CategoryContract.GetAllCategoryByFilterAsync(_request); if (result != null && result.Models != null && result.Models.Any()) { + await EnsureParentTitlesAsync(result.Models); + return new GridData { Items = result.Models.ToList(), @@ -35,6 +54,83 @@ public partial class CategoryMainPage return new GridData(); } + private async Task LoadTreeAsync() + { + var request = new GetAllCategoryByFilterRequest + { + Filter = new GetAllCategoryByFilterFilter(), + PaginationState = new PaginationState + { + PageNumber = 1, + PageSize = 1000 + } + }; + + var response = await CategoryContract.GetAllCategoryByFilterAsync(request); + var models = response?.Models?.ToList() ?? new List(); + + var lookup = models + .GroupBy(x => x.ParentId ?? 0) + .ToDictionary(g => g.Key, + g => g.OrderBy(c => c.SortOrder).ThenBy(c => c.Title).ToList()); + + _treeNodes.Clear(); + + void AddChildren(long? parentId, int level) + { + var key = parentId ?? 0; + if (!lookup.TryGetValue(key, out var children)) + return; + + foreach (var child in children) + { + _treeNodes.Add(new CategoryTreeNode + { + Id = child.Id, + Title = child.Title, + ParentId = parentId, + Level = level + }); + + AddChildren(child.Id, level + 1); + } + } + + AddChildren(null, 0); + } + + private async Task EnsureParentTitlesAsync(IEnumerable items) + { + var parentIds = items + .Where(x => x.ParentId.HasValue && x.ParentId.Value > 0 && !_parentTitles.ContainsKey(x.ParentId.Value)) + .Select(x => x.ParentId.Value) + .Distinct() + .ToList(); + + if (!parentIds.Any()) + { + return; + } + + foreach (var id in parentIds) + { + try + { + var response = await CategoryContract.GetCategoryAsync(new GetCategoryRequest { Id = id }); + if (response != null) + { + _parentTitles[id] = response.Title; + } + } + catch + { + // ignore individual parent load failures + } + } + + StateHasChanged(); + } + public async Task CreateNew() { var dialog = await DialogService.ShowAsync( @@ -49,6 +145,7 @@ public partial class CategoryMainPage if (!result.Canceled) { await ReloadData(); + await LoadTreeAsync(); Snackbar.Add("دسته‌بندی با موفقیت ایجاد شد.", Severity.Success); } } @@ -71,6 +168,7 @@ public partial class CategoryMainPage if (!result.Canceled) { await ReloadData(); + await LoadTreeAsync(); Snackbar.Add("دسته‌بندی با موفقیت ویرایش شد.", Severity.Success); } } @@ -89,6 +187,7 @@ public partial class CategoryMainPage { await CategoryContract.DeleteCategoryAsync(new DeleteCategoryRequest { Id = model.Id }); await ReloadData(); + await LoadTreeAsync(); Snackbar.Add("دسته‌بندی با موفقیت حذف شد.", Severity.Success); } @@ -115,6 +214,26 @@ public partial class CategoryMainPage _basePage.IsFiltered = false; StateHasChanged(); _request = new GetAllCategoryByFilterRequest { Filter = new GetAllCategoryByFilterFilter() }; + _parentFilterId = null; await ReloadData(); } + + public Task OnParentFilterChanged(long? parentId) + { + _parentFilterId = parentId; + _request.Filter ??= new GetAllCategoryByFilterFilter(); + _request.Filter.ParentId = parentId; + return Task.CompletedTask; + } + + public async Task OnTreeNodeSelected(long? id) + { + await OnParentFilterChanged(id); + await ReloadData(); + } + + public void OpenCategoryProducts(DataModel model) + { + Navigation.NavigateTo($"{RouteConstance.CategoryProducts}{model.Id}"); + } } diff --git a/src/BackOffice/Pages/Category/CategoryProductsDragDropPage.razor b/src/BackOffice/Pages/Category/CategoryProductsDragDropPage.razor new file mode 100644 index 0000000..bdecfe4 --- /dev/null +++ b/src/BackOffice/Pages/Category/CategoryProductsDragDropPage.razor @@ -0,0 +1,132 @@ +@attribute [Route(RouteConstance.CategoryProducts + "{CategoryId:long?}")] + +@using BackOffice.BFF.Products.Protobuf.Protos.Products +@using BackOffice.Common.BaseComponents +@using BackOffice.Pages.AutoComplete + + + + + مدیریت محصولات دسته‌بندی (درگ و دراپ) + + + + + انتخاب دسته‌بندی: + + + @if (!string.IsNullOrWhiteSpace(_categoryTitle)) + { + + دسته‌بندی انتخاب‌شده: @_categoryTitle + + } + + + + @if (_isLoading) + { + + } + else if (_selectedCategoryId.HasValue) + { + + + +
+ + محصولات خارج از این دسته (@UnselectedProducts.Count) + + + @if (UnselectedProducts.Count > 0) + { + @foreach (var item in UnselectedProducts) + { +
+ + @item.Title + +
+ } + } + else + { + همه محصولات در این دسته قرار دارند. + } +
+
+
+ + + + + برای افزودن/حذف محصول، آیتم‌ها را درگ و دراپ کنید. + + + + + + +
+ + محصولات داخل این دسته (@SelectedProducts.Count) + + + @if (SelectedProducts.Count > 0) + { + @foreach (var item in SelectedProducts) + { +
+ + @item.Title + +
+ } + } + else + { + هنوز محصولی به این دسته اختصاص داده نشده است. + } +
+
+
+
+ + + + ذخیره محصولات دسته + + + } + else + { + + برای شروع، یک دسته‌بندی انتخاب کنید. + + } +
+
+
diff --git a/src/BackOffice/Pages/Category/CategoryProductsDragDropPage.razor.cs b/src/BackOffice/Pages/Category/CategoryProductsDragDropPage.razor.cs new file mode 100644 index 0000000..7f486e7 --- /dev/null +++ b/src/BackOffice/Pages/Category/CategoryProductsDragDropPage.razor.cs @@ -0,0 +1,164 @@ +using BackOffice.BFF.Products.Protobuf.Protos.Products; +using BackOffice.BFF.Category.Protobuf.Protos.Category; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using MudBlazor; + +namespace BackOffice.Pages.Category; + +public partial class CategoryProductsDragDropPage +{ + [Parameter] public long? CategoryId { get; set; } + + [Inject] public ProductsContract.ProductsContractClient ProductsContract { get; set; } = default!; + [Inject] public CategoryContract.CategoryContractClient CategoryContract { get; set; } = default!; + + private long? _selectedCategoryId; + private bool _isLoading; + private bool _isSaving; + private string? _categoryTitle; + private List _products = new(); + private long? _draggingProductId; + + private List SelectedProducts => _products.Where(p => p.Selected).ToList(); + private List UnselectedProducts => _products.Where(p => !p.Selected).ToList(); + + protected override async Task OnInitializedAsync() + { + if (CategoryId.HasValue && CategoryId.Value > 0) + { + _selectedCategoryId = CategoryId.Value; + await LoadCategoryAndProducts(CategoryId.Value); + } + } + + private async Task OnCategoryChanged(long? categoryId) + { + _selectedCategoryId = categoryId; + _products.Clear(); + + if (categoryId.HasValue) + { + await LoadCategoryAndProducts(categoryId.Value); + } + } + + private async Task LoadCategoryAndProducts(long categoryId) + { + await LoadCategoryTitle(categoryId); + await LoadProducts(categoryId); + } + + private async Task LoadCategoryTitle(long categoryId) + { + try + { + var response = await CategoryContract.GetCategoryAsync(new GetCategoryRequest + { + Id = categoryId + }); + + _categoryTitle = response?.Title ?? $"شناسه {categoryId}"; + } + catch + { + _categoryTitle = null; + } + } + + private async Task LoadProducts(long categoryId) + { + _isLoading = true; + StateHasChanged(); + + try + { + var response = await ProductsContract.GetProductsForCategoryAsync(new GetProductsForCategoryRequest + { + CategoryId = categoryId + }); + + _products = response?.Items?.ToList() ?? new List(); + } + catch + { + Snackbar.Add("در بازیابی محصولات دسته خطایی رخ داد.", Severity.Error); + } + finally + { + _isLoading = false; + StateHasChanged(); + } + } + + private void OnDragStart(long productId) + { + _draggingProductId = productId; + } + + private void OnDragOver(DragEventArgs args) + { + // allow drop + } + + private void ToggleProduct(long productId) + { + var item = _products.FirstOrDefault(p => p.Id == productId); + if (item == null) + return; + + item.Selected = !item.Selected; + } + + private void HandleDrop(bool targetSelected) + { + if (_draggingProductId == null) + return; + + var item = _products.FirstOrDefault(p => p.Id == _draggingProductId.Value); + if (item == null) + return; + + item.Selected = targetSelected; + + _draggingProductId = null; + StateHasChanged(); + } + + private void OnDropOnSelected(DragEventArgs args) => HandleDrop(true); + + private void OnDropOnUnselected(DragEventArgs args) => HandleDrop(false); + + private async Task SaveProducts() + { + if (!_selectedCategoryId.HasValue) + return; + + _isSaving = true; + StateHasChanged(); + + try + { + var request = new UpdateCategoryProductsRequest + { + CategoryId = _selectedCategoryId.Value + }; + + request.ProductIds.AddRange( + _products.Where(p => p.Selected).Select(p => p.Id)); + + await ProductsContract.UpdateCategoryProductsAsync(request); + + Snackbar.Add("محصولات دسته با موفقیت به‌روزرسانی شد.", Severity.Success); + } + catch + { + Snackbar.Add("در ذخیره‌سازی محصولات دسته خطایی رخ داد.", Severity.Error); + } + finally + { + _isSaving = false; + StateHasChanged(); + } + } +} diff --git a/src/BackOffice/Pages/Category/CreateOrUpdateCategoryDialog.razor b/src/BackOffice/Pages/Category/CreateOrUpdateCategoryDialog.razor index 7484e35..96721ce 100644 --- a/src/BackOffice/Pages/Category/CreateOrUpdateCategoryDialog.razor +++ b/src/BackOffice/Pages/Category/CreateOrUpdateCategoryDialog.razor @@ -1,5 +1,6 @@ @using BackOffice.BFF.Category.Protobuf.Protos.Category @using Microsoft.AspNetCore.Components.Forms +@using BackOffice.Pages.AutoComplete @@ -24,12 +25,9 @@ Lines="3" TextArea="true" /> - + Hello, world! + +@using BackOffice.BFF.User.Protobuf.Protos.User +@using BackOffice.BFF.UserOrder.Protobuf.Protos.UserOrder +@using BackOffice.BFF.Products.Protobuf.Protos.Products +@using BackOffice.BFF.Package.Protobuf.Protos.Package +@using BackOffice.BFF.Category.Protobuf.Protos.Category +@inject UserContract.UserContractClient UserContract +@inject UserOrderContract.UserOrderContractClient UserOrderContract +@inject ProductsContract.ProductsContractClient ProductsContract +@inject PackageContract.PackageContractClient PackageContract +@inject CategoryContract.CategoryContractClient CategoryContract + + + داشبورد مدیریت + + + + + + تعداد کاربران + @_totalUsers.ToString("N0") + + + + + + + + تعداد محصولات + @_totalProducts.ToString("N0") + + + + + + + + تعداد پکیج‌ها + @_totalPackages.ToString("N0") + + + + + + + + تعداد دسته‌بندی‌ها + @_totalCategories.ToString("N0") + + + + + + + + تعداد سفارش‌ها + @_totalOrders.ToString("N0") + + + + + + + + سفارش‌های پرداخت‌شده + @_paidOrders.ToString("N0") + + + + + + + + جمع مبلغ سفارش‌های پرداخت‌شده + @_totalPaidAmount.ToString("N0") تومان + + + + + + + + آخرین سفارش‌های پرداخت‌شده + @if (_lastOrders.Count == 0) + { + سفارشی ثبت نشده است. + } + else + { + + + شناسه سفارش + کاربر + مبلغ + تاریخ پرداخت + + + @context.Id + @context.UserFullName (@context.UserNationalCode) + @context.Price.ToString("N0") + @context.PaymentDate.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm") + + + } + + + + +@code { + private long _totalUsers; + private long _totalOrders; + private long _paidOrders; + private long _totalPaidAmount; + private long _totalProducts; + private long _totalPackages; + private long _totalCategories; + private readonly List _lastOrders = new(); + + protected override async Task OnInitializedAsync() + { + // کاربران + var userRequest = new GetAllUserByFilterRequest + { + PaginationState = new() { PageNumber = 1, PageSize = 1 }, + Filter = new() + }; + var userResult = await UserContract.GetAllUserByFilterAsync(userRequest); + _totalUsers = userResult?.MetaData?.TotalCount ?? 0; + + // سفارش‌ها + var orderRequest = new GetAllUserOrderByFilterRequest + { + PaginationState = new() { PageNumber = 1, PageSize = 10 }, + }; + var orderResult = await UserOrderContract.GetAllUserOrderByFilterAsync(orderRequest); + if (orderResult != null && orderResult.Models != null) + { + _totalOrders = orderResult.MetaData?.TotalCount ?? 0; + _paidOrders = orderResult.Models.Count(m => m.PaymentStatus); + _totalPaidAmount = orderResult.Models + .Where(m => m.PaymentStatus) + .Aggregate(0L, (sum, m) => sum + m.Price); + + foreach (var item in orderResult.Models.OrderByDescending(m => m.PaymentDate).Take(5)) + { + var full = await UserOrderContract.GetUserOrderAsync(new GetUserOrderRequest { Id = item.Id }); + _lastOrders.Add(full); + } + } + + // محصولات + var productRequest = new GetAllProductsByFilterRequest + { + PaginationState = new() { PageNumber = 1, PageSize = 1 }, + Filter = new() + }; + var productResult = await ProductsContract.GetAllProductsByFilterAsync(productRequest); + _totalProducts = productResult?.MetaData?.TotalCount ?? 0; + + // پکیج‌ها + var packageRequest = new GetAllPackageByFilterRequest + { + PaginationState = new() { PageNumber = 1, PageSize = 1 }, + Filter = new() + }; + var packageResult = await PackageContract.GetAllPackageByFilterAsync(packageRequest); + _totalPackages = packageResult?.MetaData?.TotalCount ?? 0; + + // دسته‌بندی‌ها + var categoryRequest = new GetAllCategoryByFilterRequest + { + PaginationState = new() { PageNumber = 1, PageSize = 1 }, + Filter = new() + }; + var categoryResult = await CategoryContract.GetAllCategoryByFilterAsync(categoryRequest); + _totalCategories = categoryResult?.MetaData?.TotalCount ?? 0; + } +} diff --git a/src/BackOffice/Pages/Products/Components/CreateDialog.razor b/src/BackOffice/Pages/Products/Components/CreateDialog.razor index ea53519..c0469b3 100644 --- a/src/BackOffice/Pages/Products/Components/CreateDialog.razor +++ b/src/BackOffice/Pages/Products/Components/CreateDialog.razor @@ -2,6 +2,7 @@ @using Microsoft.AspNetCore.Components.Forms @using Tizzani.MudBlazor.HtmlEditor @using BackOffice.Common.BaseComponents +@using BackOffice.Pages.AutoComplete @@ -63,6 +64,19 @@ + + دسته‌بندی‌ها + + @if (_selectedCategoryIds?.Count > 0) + { + + تعداد دسته‌بندی‌های انتخاب‌شده: @_selectedCategoryIds.Count + + } + + تصویر بندانگشتی diff --git a/src/BackOffice/Pages/Products/Components/CreateDialog.razor.cs b/src/BackOffice/Pages/Products/Components/CreateDialog.razor.cs index 3080a9e..5dcf85c 100644 --- a/src/BackOffice/Pages/Products/Components/CreateDialog.razor.cs +++ b/src/BackOffice/Pages/Products/Components/CreateDialog.razor.cs @@ -17,6 +17,7 @@ public partial class CreateDialog private readonly long _maxAllowedSize = (1024 * 1024) * 5; private bool _isLoading; private string? _mainImagePreview; + private List _selectedCategoryIds = new(); private async Task OnMainImageSelected(IBrowserFile? file) { @@ -74,7 +75,11 @@ public partial class CreateDialog try { - await ProductsContract.CreateNewProductsAsync(Model); + Model.CategoryIds.Clear(); + Model.CategoryIds.AddRange(_selectedCategoryIds); + + var createResponse = await ProductsContract.CreateNewProductsAsync(Model); + Submit(); } catch diff --git a/src/BackOffice/Pages/Products/Components/GalleryDialog.razor b/src/BackOffice/Pages/Products/Components/GalleryDialog.razor index a821e9b..3c978f8 100644 --- a/src/BackOffice/Pages/Products/Components/GalleryDialog.razor +++ b/src/BackOffice/Pages/Products/Components/GalleryDialog.razor @@ -4,75 +4,105 @@ - گالری تصاویر - @ProductTitle + + گالری تصاویر - @ProductTitle + + تعداد تصاویر: @(Items?.Count ?? 0) + + - - + + + افزودن تصویر جدید - - - - انتخاب تصویر - - - - @if (context != null) - { - @context.Name - } - else - { - فایلی انتخاب نشده - } - - + - @if (!string.IsNullOrWhiteSpace(_previewImage)) - { - - پیش‌نمایش - - } + + + + انتخاب تصویر + + + + @if (context != null) + { + @context.Name + } + else + { + فایلی انتخاب نشده + } + + - افزودن به گالری - + @if (!string.IsNullOrWhiteSpace(_previewImage)) + { + + پیش‌نمایش + + } + + @if (_isUploading) + { + + } + + + افزودن به گالری + + + - @if (Items?.Count > 0) - { - - @foreach (var item in Items) - { - - - @item.Title - @item.Title - - - - - - } - - } - else - { - هنوز تصویری برای این محصول ثبت نشده است. - } + + لیست تصاویر + @if (Items?.Count > 0) + { + + @foreach (var item in Items) + { + + +
+ @item.Title + +
+ + + @(!string.IsNullOrWhiteSpace(item.Title) ? item.Title : "بدون عنوان") + + +
+
+ } +
+ } + else + { + هنوز تصویری برای این محصول ثبت نشده است. + } +
diff --git a/src/BackOffice/Pages/Products/Components/GalleryDialog.razor.cs b/src/BackOffice/Pages/Products/Components/GalleryDialog.razor.cs index 186724e..7eb34cd 100644 --- a/src/BackOffice/Pages/Products/Components/GalleryDialog.razor.cs +++ b/src/BackOffice/Pages/Products/Components/GalleryDialog.razor.cs @@ -106,6 +106,21 @@ public partial class GalleryDialog StateHasChanged(); } + private async Task OpenPreview(GalleryItemViewModel item) + { + if (string.IsNullOrWhiteSpace(item.ImagePath)) + return; + + var parameters = new DialogParameters + { + { x => x.ImageUrl, item.ImagePath }, + { x => x.Title, item.Title } + }; + + await DialogService.ShowAsync("پیش‌نمایش تصویر", parameters, + new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true }); + } + private async Task RemoveImage(GalleryItemViewModel item) { await ProductsContract.RemoveProductImageAsync(new RemoveProductImageRequest diff --git a/src/BackOffice/Pages/Products/Components/UpdateDialog.razor b/src/BackOffice/Pages/Products/Components/UpdateDialog.razor index 307542b..7b0f058 100644 --- a/src/BackOffice/Pages/Products/Components/UpdateDialog.razor +++ b/src/BackOffice/Pages/Products/Components/UpdateDialog.razor @@ -2,6 +2,7 @@ @using Microsoft.AspNetCore.Components.Forms @using Tizzani.MudBlazor.HtmlEditor @using BackOffice.Common.BaseComponents +@using BackOffice.Pages.AutoComplete @@ -49,6 +50,19 @@ + + دسته‌بندی‌ها + + @if (_selectedCategoryIds?.Count > 0) + { + + تعداد دسته‌بندی‌های انتخاب‌شده: @_selectedCategoryIds.Count + + } + + تصویر بندانگشتی diff --git a/src/BackOffice/Pages/Products/Components/UpdateDialog.razor.cs b/src/BackOffice/Pages/Products/Components/UpdateDialog.razor.cs index 231982b..65c7279 100644 --- a/src/BackOffice/Pages/Products/Components/UpdateDialog.razor.cs +++ b/src/BackOffice/Pages/Products/Components/UpdateDialog.razor.cs @@ -17,6 +17,7 @@ public partial class UpdateDialog private readonly long _maxAllowedSize = (1024 * 1024) * 5; private bool _isLoading; private string? _mainImagePreview; + private List _selectedCategoryIds = new(); private async Task OnMainImageSelected(IBrowserFile? file) { @@ -35,10 +36,28 @@ public partial class UpdateDialog _thumbnailImageFile = file; } - protected override Task OnInitializedAsync() + protected override async Task OnInitializedAsync() { _mainImagePreview = Model.ImagePath; - return base.OnInitializedAsync(); + await base.OnInitializedAsync(); + + try + { + var response = await ProductsContract.GetCategoriesAsync(new GetCategoriesRequest + { + ProductId = Model.Id + }); + + _selectedCategoryIds = response?.Items? + .Where(c => c.Selected) + .Select(c => c.Id) + .ToList() + ?? new List(); + } + catch + { + // ignore errors for category loading + } } public async void CallUpdateMethod() @@ -80,7 +99,10 @@ public partial class UpdateDialog try { + Model.CategoryIds.Clear(); + Model.CategoryIds.AddRange(_selectedCategoryIds); await ProductsContract.UpdateProductsAsync(Model); + Submit(); } catch diff --git a/src/BackOffice/Pages/Products/ProductCategoriesDragDropPage.razor b/src/BackOffice/Pages/Products/ProductCategoriesDragDropPage.razor new file mode 100644 index 0000000..be0fb04 --- /dev/null +++ b/src/BackOffice/Pages/Products/ProductCategoriesDragDropPage.razor @@ -0,0 +1,128 @@ +@attribute [Route(RouteConstance.ProductCategories + "{ProductId:long?}")] + +@using BackOffice.BFF.Products.Protobuf.Protos.Products +@using BackOffice.Common.BaseComponents +@using BackOffice.Pages.AutoComplete + + + + + مدیریت دسته‌بندی‌های محصول (درگ و دراپ) + + + + + انتخاب محصول: + + + @if (!string.IsNullOrWhiteSpace(_productTitle)) + { + + محصول انتخاب‌شده: @_productTitle + + } + + + + @if (_isLoading) + { + + } + else if (_selectedProductId.HasValue) + { + + + +
+ دسته‌بندی‌های موجود + + @if (UnselectedCategories.Count > 0) + { + @foreach (var item in UnselectedCategories) + { +
+ + @item.Title + +
+ } + } + else + { + همه دسته‌بندی‌ها به این محصول اختصاص داده شده‌اند. + } +
+
+
+ + + + + برای افزودن/حذف دسته‌بندی، آیتم‌ها را درگ و دراپ کنید. + + + + + + +
+ دسته‌بندی‌های اختصاص داده شده + + @if (SelectedCategories.Count > 0) + { + @foreach (var item in SelectedCategories) + { +
+ + @item.Title + +
+ } + } + else + { + هنوز دسته‌بندی‌ای به این محصول اختصاص داده نشده است. + } +
+
+
+
+ + + + ذخیره دسته‌بندی‌های محصول + + + } + else + { + + برای شروع، یک محصول انتخاب کنید. + + } +
+
+
diff --git a/src/BackOffice/Pages/Products/ProductCategoriesDragDropPage.razor.cs b/src/BackOffice/Pages/Products/ProductCategoriesDragDropPage.razor.cs new file mode 100644 index 0000000..5b1f41d --- /dev/null +++ b/src/BackOffice/Pages/Products/ProductCategoriesDragDropPage.razor.cs @@ -0,0 +1,162 @@ +using BackOffice.BFF.Products.Protobuf.Protos.Products; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using MudBlazor; + +namespace BackOffice.Pages.Products; + +public partial class ProductCategoriesDragDropPage +{ + [Parameter] public long? ProductId { get; set; } + + [Inject] public ProductsContract.ProductsContractClient ProductsContract { get; set; } = default!; + + private long? _selectedProductId; + private bool _isLoading; + private bool _isSaving; + private string? _productTitle; + private List _categories = new(); + private long? _draggingCategoryId; + + private List SelectedCategories => _categories.Where(c => c.Selected).ToList(); + private List UnselectedCategories => _categories.Where(c => !c.Selected).ToList(); + + protected override async Task OnInitializedAsync() + { + if (ProductId.HasValue && ProductId.Value > 0) + { + _selectedProductId = ProductId.Value; + await LoadProductAndCategories(ProductId.Value); + } + } + + private async Task OnProductChanged(long? productId) + { + _selectedProductId = productId; + _categories.Clear(); + + if (productId.HasValue) + { + await LoadProductAndCategories(productId.Value); + } + } + + private async Task LoadProductAndCategories(long productId) + { + await LoadProductTitle(productId); + await LoadCategories(productId); + } + + private async Task LoadProductTitle(long productId) + { + try + { + var response = await ProductsContract.GetProductsAsync(new GetProductsRequest + { + Id = productId + }); + + _productTitle = response?.Title; + } + catch + { + _productTitle = null; + } + } + + private async Task LoadCategories(long productId) + { + _isLoading = true; + StateHasChanged(); + + try + { + var response = await ProductsContract.GetCategoriesAsync(new GetCategoriesRequest + { + ProductId = productId + }); + + _categories = response?.Items?.ToList() ?? new List(); + } + catch + { + Snackbar.Add("در بازیابی دسته‌بندی‌های محصول خطایی رخ داد.", Severity.Error); + } + finally + { + _isLoading = false; + StateHasChanged(); + } + } + + private void OnDragStart(long categoryId) + { + _draggingCategoryId = categoryId; + } + + private void OnDragOver(DragEventArgs args) + { + // Allow dropping by preventing default behavior + } + + private void ToggleCategory(long categoryId) + { + var item = _categories.FirstOrDefault(c => c.Id == categoryId); + if (item == null) + return; + + item.Selected = !item.Selected; + } + + private void HandleDrop(bool targetSelected) + { + if (_draggingCategoryId == null) + return; + + var item = _categories.FirstOrDefault(c => c.Id == _draggingCategoryId.Value); + if (item == null) + return; + + item.Selected = targetSelected; + + _draggingCategoryId = null; + StateHasChanged(); + } + + private void OnDropOnSelected(DragEventArgs args) => HandleDrop(true); + + private void OnDropOnUnselected(DragEventArgs args) => HandleDrop(false); + + private async Task SaveCategories() + { + if (!_selectedProductId.HasValue) + return; + + _isSaving = true; + StateHasChanged(); + + try + { + var request = new UpdateProductCategoriesRequest + { + ProductId = _selectedProductId.Value + }; + + request.CategoryIds.AddRange( + _categories.Where(c => c.Selected).Select(c => c.Id)); + + await ProductsContract.UpdateProductCategoriesAsync(request); + + Snackbar.Add("دسته‌بندی‌های محصول با موفقیت به‌روزرسانی شد.", Severity.Success); + } + catch + { + Snackbar.Add("در ذخیره‌سازی دسته‌بندی‌ها خطایی رخ داد.", Severity.Error); + } + finally + { + _isSaving = false; + StateHasChanged(); + } + } +} diff --git a/src/BackOffice/Pages/Products/ProductsMainPage.razor b/src/BackOffice/Pages/Products/ProductsMainPage.razor index acdb396..2f86167 100644 --- a/src/BackOffice/Pages/Products/ProductsMainPage.razor +++ b/src/BackOffice/Pages/Products/ProductsMainPage.razor @@ -73,6 +73,14 @@ + + + +
diff --git a/src/BackOffice/Pages/Products/ProductsMainPage.razor.cs b/src/BackOffice/Pages/Products/ProductsMainPage.razor.cs index 8d4424e..8bc903e 100644 --- a/src/BackOffice/Pages/Products/ProductsMainPage.razor.cs +++ b/src/BackOffice/Pages/Products/ProductsMainPage.razor.cs @@ -1,5 +1,6 @@ using BackOffice.BFF.Products.Protobuf.Protos.Products; using BackOffice.Common.BaseComponents; +using BackOffice.Common.Utilities; using BackOffice.Pages.Products.Components; using Mapster; using Microsoft.AspNetCore.Components; @@ -106,6 +107,11 @@ public partial class ProductsMainPage new DialogOptions { CloseButton = true, FullWidth = true, MaxWidth = MaxWidth.Medium }); } + public void OpenCategoryMapping(DataModel model) + { + Navigation.NavigateTo($"{RouteConstance.ProductCategories}{model.Id}"); + } + public async Task OpenImagePreview(string imagePath, string title) { if (string.IsNullOrWhiteSpace(imagePath)) diff --git a/src/BackOffice/Pages/User/UserMainPage.razor b/src/BackOffice/Pages/User/UserMainPage.razor index aec662d..a45be61 100644 --- a/src/BackOffice/Pages/User/UserMainPage.razor +++ b/src/BackOffice/Pages/User/UserMainPage.razor @@ -45,7 +45,7 @@ - فاکتور ها + سفارش‌ها آدرس ها مدیریت نقش آرشیو @@ -63,4 +63,3 @@ - diff --git a/src/BackOffice/Pages/UserOrder/Components/UserOrderDetailsDialog.razor b/src/BackOffice/Pages/UserOrder/Components/UserOrderDetailsDialog.razor new file mode 100644 index 0000000..cc4701b --- /dev/null +++ b/src/BackOffice/Pages/UserOrder/Components/UserOrderDetailsDialog.razor @@ -0,0 +1,132 @@ +@using BackOffice.BFF.UserOrder.Protobuf.Protos.UserOrder +@using BackOffice.Common.BaseComponents + + + + + @if (_isLoading) + { + + } + else if (_model is null) + { + + خطا در دریافت اطلاعات سفارش. + + } + else + { + + جزئیات سفارش شماره @_model.Id + + کاربر: @_model.UserFullName (@_model.UserNationalCode) + + + + + خلاصه سفارش + + مبلغ: @_model.Price.ToString("N0") تومان + شناسه پرداخت: @_model.TransactionId + + تاریخ پرداخت: + @_model.PaymentDate.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm") + + + روش پرداخت: + @GetPaymentMethodText(_model.PaymentMethod) + + + وضعیت پرداخت: + @if (_model.PaymentStatus) + { + پرداخت شده + } + else + { + پرداخت نشده + } + + + وضعیت ارسال: + + بدون ارسال / نامشخص + در انتظار ارسال + تحویل پست + تحویل به مشتری + مرجوع شده + + + + + + + + + + آدرس تحویل + + @if (!string.IsNullOrWhiteSpace(_model.UserAddressText)) + { + @_model.UserAddressText + } + else + { + آدرسی ثبت نشده است. + } + + + + + + اقلام فاکتور + + @if (_model.FactorDetails != null && _model.FactorDetails.Count > 0) + { + + + محصول + قیمت واحد + تعداد + تخفیف واحد + + + @context.ProductTitle + @context.UnitPrice?.ToString("N0") + @context.Count + @context.UnitDiscountPrice?.ToString("N0") + + + } + else + { + آیتمی برای این سفارش ثبت نشده است. + } + + + + } + + + + + بستن + + + ثبت تغییرات + + + diff --git a/src/BackOffice/Pages/UserOrder/Components/UserOrderDetailsDialog.razor.cs b/src/BackOffice/Pages/UserOrder/Components/UserOrderDetailsDialog.razor.cs new file mode 100644 index 0000000..2831f54 --- /dev/null +++ b/src/BackOffice/Pages/UserOrder/Components/UserOrderDetailsDialog.razor.cs @@ -0,0 +1,97 @@ +using BackOffice.BFF.UserOrder.Protobuf.Protos.UserOrder; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace BackOffice.Pages.UserOrder.Components; + +public partial class UserOrderDetailsDialog +{ + [CascadingParameter] IMudDialogInstance MudDialog { get; set; } = default!; + [Inject] public UserOrderContract.UserOrderContractClient UserOrderContract { get; set; } = default!; + + [Parameter] public long OrderId { get; set; } + + private GetUserOrderResponse? _model; + private bool _isLoading; + private int _deliveryStatusValue; + private string? _trackingCode; + private string? _deliveryDescription; + + protected override async Task OnInitializedAsync() + { + _isLoading = true; + try + { + _model = await UserOrderContract.GetUserOrderAsync(new GetUserOrderRequest + { + Id = OrderId + }); + + if (_model is not null) + { + _deliveryStatusValue = _model.DeliveryStatus; + _trackingCode = _model.TrackingCode; + _deliveryDescription = _model.DeliveryDescription; + } + } + catch + { + _model = null; + } + finally + { + _isLoading = false; + } + } + + private Color GetDeliveryStatusColor(int status) + { + return status switch + { + 1 => Color.Warning, // Pending + 2 => Color.Info, // InTransit + 3 => Color.Success, // Delivered + 4 => Color.Error, // Returned + _ => Color.Default // None / Unknown + }; + } + + private string GetDeliveryStatusText(int status) + { + return status switch + { + 1 => "در انتظار ارسال", + 2 => "تحویل پست", + 3 => "تحویل به مشتری", + 4 => "مرجوع شده", + _ => "بدون ارسال / نامشخص" + }; + } + + private string GetPaymentMethodText(int method) + { + return method switch + { + 1 => "کیف پول", + _ => "درگاه پرداخت" + }; + } + + private async Task SaveAsync() + { + if (_model is null) + return; + + await UserOrderContract.UpdateUserOrderAsync(new UpdateUserOrderRequest + { + Id = _model.Id, + DeliveryStatus = _deliveryStatusValue, + TrackingCode = _trackingCode ?? string.Empty, + DeliveryDescription = _deliveryDescription ?? string.Empty + }); + + MudDialog.Close(DialogResult.Ok(true)); + } + + private void Close() => MudDialog.Close(); +} diff --git a/src/BackOffice/Pages/UserOrder/UserOrderMainPage.razor b/src/BackOffice/Pages/UserOrder/UserOrderMainPage.razor index 60cd716..0220ef2 100644 --- a/src/BackOffice/Pages/UserOrder/UserOrderMainPage.razor +++ b/src/BackOffice/Pages/UserOrder/UserOrderMainPage.razor @@ -1,4 +1,5 @@ -@attribute [Route(RouteConstance.UserOrder + "{UserId:long}")] +@attribute [Route(RouteConstance.Orders)] +@attribute [Route(RouteConstance.UserOrder + "{UserId:long}")] @using BackOffice.BFF.UserOrder.Protobuf.Protos.UserOrder @using BackOffice.Pages.UserOrder.Components @@ -7,7 +8,51 @@ - + + + + + + درگاه پرداخت + کیف پول + + + + پرداخت شده + پرداخت نشده + + + + در انتظار ارسال + تحویل پست + تحویل به مشتری + مرجوع شده + - فاکتور + سفارش‌های کاربر + + + + + + @GetPaymentMethodText(context.Item.PaymentMethod) + + + @if (context.Item.PaymentStatus) @@ -40,6 +94,28 @@ + + + + + @(string.IsNullOrWhiteSpace(context.Item.UserAddressText) + ? "-" + : (context.Item.UserAddressText.Length > 30 + ? context.Item.UserAddressText.Substring(0, 30) + "..." + : context.Item.UserAddressText)) + + + + + + + + @GetDeliveryStatusText(context.Item.DeliveryStatus) + + + @@ -47,6 +123,13 @@ + + + @@ -62,6 +145,3 @@ - - - diff --git a/src/BackOffice/Pages/UserOrder/UserOrderMainPage.razor.cs b/src/BackOffice/Pages/UserOrder/UserOrderMainPage.razor.cs index dc6108a..7e020d5 100644 --- a/src/BackOffice/Pages/UserOrder/UserOrderMainPage.razor.cs +++ b/src/BackOffice/Pages/UserOrder/UserOrderMainPage.razor.cs @@ -1,19 +1,33 @@ -using BackOffice.BFF.UserOrder.Protobuf.Protos.UserOrder; +using System.Text.Json; +using BackOffice.BFF.UserOrder.Protobuf.Protos.UserOrder; using BackOffice.Common.BaseComponents; using Microsoft.AspNetCore.Components; using MudBlazor; using DataModel = BackOffice.BFF.UserOrder.Protobuf.Protos.UserOrder.GetAllUserOrderByFilterResponseModel; +using Google.Protobuf.WellKnownTypes; namespace BackOffice.Pages.UserOrder; + public partial class UserOrderMainPage { - [Parameter] public long UserId { get; set; } + [Parameter] public long? UserId { get; set; } [Inject] public UserOrderContract.UserOrderContractClient UserOrderContract { get; set; } private bool _isLoading = true; private MudDataGrid _gridData; BasePageComponent _basePage; + private int? _paymentStatusFilter; + private int? _deliveryStatusFilter; + private int? _paymentMethodFilter; + private DateTime? _paymentDateFrom; + private GetAllUserOrderByFilterRequest _request = new() { Filter = new() }; + protected override Task OnInitializedAsync() + { + _request.Filter ??= new(); + return base.OnInitializedAsync(); + } + private async Task> ServerReload(GridState state) { _request.Filter ??= new(); @@ -21,16 +35,104 @@ public partial class UserOrderMainPage _request.PaginationState.PageNumber = state.Page + 1; _request.PaginationState.PageSize = state.PageSize; - _request.Filter.UserId = UserId; + if (UserId.HasValue && UserId.Value > 0) + { + _request.Filter.UserId = UserId.Value; + } + else + { + _request.Filter.UserId = null; + } + + if (_paymentDateFrom.HasValue) + { + _request.Filter.PaymentDate = + Timestamp.FromDateTime(DateTime.SpecifyKind(_paymentDateFrom.Value.Date, DateTimeKind.Utc)); + } + else + { + _request.Filter.PaymentDate = null; + } + + if (_paymentStatusFilter.HasValue) + { + _request.Filter.PaymentStatus = _paymentStatusFilter.Value == 1; + } + else + { + _request.Filter.PaymentStatus = null; + } + + if (_deliveryStatusFilter.HasValue) + { + _request.Filter.DeliveryStatus = _deliveryStatusFilter.Value; + } + else + { + _request.Filter.DeliveryStatus = null; + } + + if (_paymentMethodFilter.HasValue) + { + _request.Filter.PaymentMethod = _paymentMethodFilter.Value; + } + else + { + _request.Filter.PaymentMethod = null; + } + + + if (IsEmptyFilter(_request.Filter)) + { + _request.Filter = null; + } + else + { + Console.WriteLine(JsonSerializer.Serialize(_request.Filter)); + } var result = await UserOrderContract.GetAllUserOrderByFilterAsync(_request); if (result != null && result.Models != null && result.Models.Any()) { - return new GridData() { Items = result.Models.ToList(), TotalItems = (int)result.MetaData.TotalCount }; + return new GridData() + { Items = result.Models.ToList(), TotalItems = (int)result.MetaData.TotalCount }; } return new GridData(); } + + private static bool IsEmptyFilter(GetAllUserOrderByFilterFilter src) + { + return src.Id == null + && src.Price == null + && src.PackageId == null + && src.TransactionId == null + && src.PaymentStatus == null + && src.PaymentDate == null + && src.UserId == null + && src.DeliveryStatus == null + && src.PaymentMethod == null; + } + + private async Task OpenDetails(DataModel model) + { + var parameters = new DialogParameters + { + { nameof(BackOffice.Pages.UserOrder.Components.UserOrderDetailsDialog.OrderId), model.Id } + }; + + var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Large, FullWidth = true }; + var dialog = + await DialogService.ShowAsync("جزئیات سفارش", + parameters, options); + var result = await dialog.Result; + + if (!result.Canceled) + { + ReLoadData(); + } + } + private async Task OnDelete(DataModel model) { var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small }; @@ -47,13 +149,16 @@ public partial class UserOrderMainPage }); ReLoadData(); } + StateHasChanged(); } + public async void ReLoadData() { if (_gridData != null) await _gridData.ReloadServerData(); } + public async Task OnFilterSubmit() { _basePage.IsFiltered = true; @@ -66,6 +171,43 @@ public partial class UserOrderMainPage _basePage.IsFiltered = false; StateHasChanged(); _request = new() { Filter = new() { } }; + _paymentStatusFilter = null; + _deliveryStatusFilter = null; + _paymentDateFrom = null; + _paymentMethodFilter = null; ReLoadData(); } -} + + private Color GetDeliveryStatusColor(int status) + { + return status switch + { + 1 => Color.Warning, // Pending + 2 => Color.Info, // InTransit + 3 => Color.Success, // Delivered + 4 => Color.Error, // Returned + _ => Color.Default // None / Unknown + }; + } + + private string GetDeliveryStatusText(int status) + { + return status switch + { + 1 => "در انتظار ارسال", + 2 => "تحویل پست", + 3 => "تحویل به مشتری", + 4 => "مرجوع شده", + _ => "بدون ارسال / نامشخص" + }; + } + + private string GetPaymentMethodText(int method) + { + return method switch + { + 1 => "کیف پول", + _ => "درگاه پرداخت" + }; + } +} \ No newline at end of file diff --git a/src/BackOffice/Shared/NavMenu.razor b/src/BackOffice/Shared/NavMenu.razor index 4409b79..6cd221d 100644 --- a/src/BackOffice/Shared/NavMenu.razor +++ b/src/BackOffice/Shared/NavMenu.razor @@ -24,6 +24,12 @@ مدیریت محصول + + سفارش‌ها + + diff --git a/src/BackOffice/wwwroot/appsettings.json b/src/BackOffice/wwwroot/appsettings.json index 6dd7f39..ade4d72 100644 --- a/src/BackOffice/wwwroot/appsettings.json +++ b/src/BackOffice/wwwroot/appsettings.json @@ -1,6 +1,6 @@ { "GwUrl": "https://bogw.kbs1.ir", - //"GwUrl": "https://localhost:6468", +// "GwUrl": "https://localhost:6468", "Authentication": { //"Authority": "https://localhost:5001", "Authority": "https://ids.afrino.co/", diff --git a/src/BackOffice/wwwroot/css/app.css b/src/BackOffice/wwwroot/css/app.css index 01a6133..4f0ba3b 100644 --- a/src/BackOffice/wwwroot/css/app.css +++ b/src/BackOffice/wwwroot/css/app.css @@ -20,6 +20,22 @@ body { font-family: "Vazir", "IRANSans", Tahoma, "Segoe UI", Arial, sans-serif; } +.drag-drop-zone { + height: 100%; + min-height: 100%; + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.drag-item { + cursor: grab; + margin-bottom: 0.5rem; +} + +.drag-item:active { + cursor: grabbing; +} + .loading-progress { position: relative; display: block;