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) =>