diff --git a/src/FrontOffice.Main/Pages/Store/Categories.razor b/src/FrontOffice.Main/Pages/Store/Categories.razor index 1fac2f0..c81de5d 100644 --- a/src/FrontOffice.Main/Pages/Store/Categories.razor +++ b/src/FrontOffice.Main/Pages/Store/Categories.razor @@ -31,36 +31,48 @@ در حال بارگذاری دسته‌بندی‌ها... } - else if (_rows.Count == 0) + else if (_treeItems.Count == 0) { هنوز دسته‌بندی‌ای ثبت نشده است. } else { - - - عنوان - مسیر دسته‌بندی - اقدامات - - - - - @context.Category.Title - @if (!string.IsNullOrWhiteSpace(context.Category.ImagePath)) - { - - } - - - @context.Path - - - مشاهده محصولات + + + + + + + + + + + @(_selectedNode is null ? "یک دسته را انتخاب کنید" : $"دسته انتخابی: {_selectedNode.Text}") + + + مشاهده محصولات این دسته - - - + + + } diff --git a/src/FrontOffice.Main/Pages/Store/Categories.razor.cs b/src/FrontOffice.Main/Pages/Store/Categories.razor.cs index 85a9785..33b87ca 100644 --- a/src/FrontOffice.Main/Pages/Store/Categories.razor.cs +++ b/src/FrontOffice.Main/Pages/Store/Categories.razor.cs @@ -2,8 +2,9 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Microsoft.AspNetCore.Components; using FrontOffice.Main.Utilities; +using Microsoft.AspNetCore.Components; +using MudBlazor; namespace FrontOffice.Main.Pages.Store; @@ -13,49 +14,82 @@ public partial class Categories : ComponentBase private bool _loading; private List _items = new(); - private List _rows = new(); + private List> _treeItems = new(); + private MudTreeView? _treeView; + private string _searchPhrase = string.Empty; + private CategoryTreeItem? _selectedNode; + private Dictionary _nodeLookup = new(); + private long? _selectedCategoryId; protected override async Task OnInitializedAsync() { _loading = true; _items = await CategoryService.GetAllAsync(); - BuildRows(); + BuildTreeItems(); _loading = false; } - private void BuildRows() + private void BuildTreeItems() { - var lookup = _items.ToDictionary(item => item.Id); - var rows = new List(); - foreach (var item in _items) - { - var path = BuildPath(item, lookup); - var pathLabel = path.Count > 0 ? string.Join(" › ", path.Select(x => x.Title)) : item.Title; - rows.Add(new CategoryRow(item, pathLabel, Math.Max(0, path.Count - 1))); - } + _nodeLookup = new Dictionary(); - _rows = rows.OrderBy(row => row.Depth).ThenBy(row => row.Category.Title).ToList(); - } + var categoryIds = _items.Select(item => item.Id).ToHashSet(); - private static List BuildPath(CategoryItem category, IReadOnlyDictionary lookup) - { - var path = new List(); - var current = category; - var visited = new HashSet(); - while (current is not null && visited.Add(current.Id)) + var childrenLookup = _items + .Where(item => item.ParentId.HasValue && categoryIds.Contains(item.ParentId.Value)) + .GroupBy(item => item.ParentId!.Value) + .ToDictionary(group => group.Key, group => group.OrderBy(child => child.Title).ToList()); + + var roots = _items + .Where(item => item.ParentId is null || !categoryIds.Contains(item.ParentId.Value)) + .OrderBy(item => item.Title) + .Select(CreateNode) + .Cast>() + .ToList(); + + _treeItems = roots; + _selectedNode = null; + _selectedCategoryId = null; + + CategoryTreeItem CreateNode(CategoryItem category) { - path.Add(current); - if (current.ParentId is { } parentId && lookup.TryGetValue(parentId, out var parent)) + var node = new CategoryTreeItem(category) { - current = parent; - continue; + Icon = category.ParentId is null ? Icons.Material.Filled.Category : Icons.Material.Outlined.Category, + Expanded = category.ParentId is null + }; + + _nodeLookup[category.Id] = node; + + if (childrenLookup.TryGetValue(category.Id, out var children)) + { + node.Children = children + .Select(CreateNode) + .Cast>() + .ToList(); } - break; + return node; + } + } + + private async Task OnSearchChanged(string searchPhrase) + { + _searchPhrase = searchPhrase; + if (_treeView is not null) + { + await _treeView.FilterAsync(); + } + } + + private Task MatchesName(TreeItemData item) + { + if (string.IsNullOrWhiteSpace(_searchPhrase)) + { + return Task.FromResult(true); } - path.Reverse(); - return path; + return Task.FromResult(item.Text?.Contains(_searchPhrase, StringComparison.OrdinalIgnoreCase) ?? false); } private void NavigateToCategory(long categoryId) @@ -63,5 +97,31 @@ public partial class Categories : ComponentBase Navigation.NavigateTo($"{RouteConstants.Store.Products}?category={categoryId}"); } - private sealed record CategoryRow(CategoryItem Category, string Path, int Depth); + private Task OnSelectedValueChanged(long categoryId) + { + _selectedCategoryId = categoryId; + _selectedNode = _nodeLookup.TryGetValue(categoryId, out var node) ? node : null; + return Task.CompletedTask; + } + + private void NavigateToSelectedCategory() + { + if (_selectedNode is null) + { + return; + } + + NavigateToCategory(_selectedNode.Category.Id); + } + + private sealed class CategoryTreeItem : TreeItemData + { + public CategoryTreeItem(CategoryItem category) : base(category.Id) + { + Category = category; + Text = string.IsNullOrWhiteSpace(category.Title) ? $"دسته‌بندی {category.Id}" : category.Title; + } + + public CategoryItem Category { get; } + } } diff --git a/src/FrontOffice.Main/Pages/Store/ProductDetail.razor b/src/FrontOffice.Main/Pages/Store/ProductDetail.razor index ecaf4d4..71da6d1 100644 --- a/src/FrontOffice.Main/Pages/Store/ProductDetail.razor +++ b/src/FrontOffice.Main/Pages/Store/ProductDetail.razor @@ -23,19 +23,12 @@ else if (_product is null) else { - @if (_product is not null) - { - - دیجی کالا - @foreach (var node in _breadcrumbNodes) - { - @node.Title - } - @_product.Title - - } + @if (_product is not null) + { + + } @if (!string.IsNullOrWhiteSpace(MainImageUrl)) { @@ -69,20 +62,20 @@ else @((MarkupString)_product.FullInformation) } - @if (_categoryPaths.Count > 0) - { - - دسته‌بندی‌ها - - @foreach (var categoryPath in _categoryPaths) - { - - @GetCategoryLabel(categoryPath) - - } - - - } + @* @if (_categoryPaths.Count > 0) *@ + @* { *@ + @* *@ + @* دسته‌بندی‌ها *@ + @* *@ + @* @foreach (var categoryPath in _categoryPaths) *@ + @* { *@ + @* *@ + @* @GetCategoryLabel(categoryPath) *@ + @* *@ + @* } *@ + @* *@ + @* *@ + @* } *@ @FormatPrice(_product.Price) diff --git a/src/FrontOffice.Main/Pages/Store/ProductDetail.razor.cs b/src/FrontOffice.Main/Pages/Store/ProductDetail.razor.cs index 213ad6c..387d203 100644 --- a/src/FrontOffice.Main/Pages/Store/ProductDetail.razor.cs +++ b/src/FrontOffice.Main/Pages/Store/ProductDetail.razor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Components; using FrontOffice.Main.Utilities; +using MudBlazor; namespace FrontOffice.Main.Pages.Store; @@ -20,8 +21,8 @@ public partial class ProductDetail : ComponentBase, IDisposable private const int MaxQty = 20; private IReadOnlyList _galleryItems = Array.Empty(); private IReadOnlyList _categoryPaths = Array.Empty(); - private IReadOnlyList _breadcrumbNodes = Array.Empty(); private ProductGalleryImage? _selectedGalleryImage; + private readonly List _breadcrumbItems = new(); private long TotalPrice => (_product?.Price ?? 0) * _qty; private bool HasDiscount => _product is { Discount: > 0 and < 100 }; @@ -60,7 +61,7 @@ public partial class ProductDetail : ComponentBase, IDisposable { _galleryItems = Array.Empty(); _categoryPaths = Array.Empty(); - _breadcrumbNodes = Array.Empty(); + _breadcrumbItems.Clear(); } StateHasChanged(); @@ -177,9 +178,26 @@ public partial class ProductDetail : ComponentBase, IDisposable private void UpdateBreadcrumb() { - _breadcrumbNodes = _categoryPaths + _breadcrumbItems.Clear(); + + if (_product is null) + { + return; + } + + _breadcrumbItems.Add(new BreadcrumbItem("خانه", href: RouteConstants.Store.Products)); + + var nodes = _categoryPaths .OrderByDescending(path => path.Nodes.Count) .FirstOrDefault()?.Nodes ?? Array.Empty(); + + foreach (var node in nodes) + { + var target = $"{RouteConstants.Store.Products}?category={node.Id}"; + _breadcrumbItems.Add(new BreadcrumbItem(node.Title, href: target)); + } + + // _breadcrumbItems.Add(new BreadcrumbItem(_product.Title, href: null, disabled: true)); } private void NavigateToCategory(ProductCategoryPathInfo path) diff --git a/src/FrontOffice.Main/Utilities/CategoryService.cs b/src/FrontOffice.Main/Utilities/CategoryService.cs index f94f38b..638f3dd 100644 --- a/src/FrontOffice.Main/Utilities/CategoryService.cs +++ b/src/FrontOffice.Main/Utilities/CategoryService.cs @@ -25,7 +25,7 @@ public class CategoryService } var response = await _client.GetAllCategoriesAsync(new GetCategoriesRequest()); - _cache = response.Categories + _cache = response.Models .Select(dto => new CategoryItem( Id: dto.Id, Title: dto.Title ?? dto.Name ?? string.Empty, diff --git a/src/FrontOffice.Main/Utilities/ProductService.cs b/src/FrontOffice.Main/Utilities/ProductService.cs index 4bee96a..28451b3 100644 --- a/src/FrontOffice.Main/Utilities/ProductService.cs +++ b/src/FrontOffice.Main/Utilities/ProductService.cs @@ -98,7 +98,10 @@ public class ProductService public async Task GetByIdAsync(long id) { - if (TryGetCachedProduct(id, out var cached)) return cached; + if (TryGetCachedProduct(id, out var cached) && HasDetailedData(cached)) + { + return cached; + } try { @@ -112,6 +115,11 @@ public class ProductService } catch { + if (cached is not null) + { + return cached; + } + TryGetCachedProduct(id, out var result); return result; } @@ -134,6 +142,8 @@ public class ProductService Rate: m.Rate, RemainingCount: m.RemainingCount ); + + p = PreserveCachedDetails(p); CacheProduct(p); list.Add(p); @@ -142,6 +152,27 @@ public class ProductService return list; } + private Product PreserveCachedDetails(Product product) + { + if (_cache.TryGetValue(product.Id, out var entry)) + { + var cached = entry.Product; + var hasGallery = cached.Gallery.Count > 0; + var hasCategories = cached.Categories.Count > 0; + + if (hasGallery || hasCategories) + { + product = product with + { + Gallery = hasGallery ? cached.Gallery : product.Gallery, + Categories = hasCategories ? cached.Categories : product.Categories + }; + } + } + + return product; + } + private Product MapAndCache(GetProductsResponse model) { var gallery = model.Gallery @@ -202,6 +233,9 @@ public class ProductService .Where(entry => entry.Expiration > now) .Select(entry => entry.Product); } + + private static bool HasDetailedData(Product product) + => product.Gallery.Count > 0 || product.Categories.Count > 0; private sealed record CacheEntry(Product Product, DateTime Expiration); private static string BuildUrl(string? path) =>