From 6dafe863442d3acb13f3ca085af8bfe337d50db1 Mon Sep 17 00:00:00 2001 From: parsa aghaei Date: Wed, 15 Jul 2026 15:03:09 +0330 Subject: [PATCH] =?UTF-8?q?feat:=20full=20symptoms=20filtering=20system=20?= =?UTF-8?q?=E2=80=94=20backend=20API,=20megamenu=20pills,=20sidebar=20serv?= =?UTF-8?q?er-side=20filter,=20PDP=20tags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/products/dto/get-products.dto.ts | 5 +++ backend/src/products/products.service.ts | 34 +++++++++++++- frontend/application/app/shop/page.tsx | 6 ++- .../application/components/ArchivePage.tsx | 44 ++++++++++++++----- frontend/application/components/Header.tsx | 36 ++++++++++----- .../application/components/ProductPage.tsx | 24 ++++++++++ .../lib/services/productService.ts | 2 + 7 files changed, 124 insertions(+), 27 deletions(-) diff --git a/backend/src/products/dto/get-products.dto.ts b/backend/src/products/dto/get-products.dto.ts index 7faa33d..77316fc 100644 --- a/backend/src/products/dto/get-products.dto.ts +++ b/backend/src/products/dto/get-products.dto.ts @@ -12,4 +12,9 @@ export class GetProductsDto extends PaginationDto { @IsOptional() @IsEnum(['سگ', 'گربه', 'all']) petType?: string; + + @ApiPropertyOptional({ description: 'فیلتر بر اساس یک علامت درمانی خاص' }) + @IsOptional() + @IsString() + symptom?: string; } diff --git a/backend/src/products/products.service.ts b/backend/src/products/products.service.ts index 115ffdd..e5a1821 100644 --- a/backend/src/products/products.service.ts +++ b/backend/src/products/products.service.ts @@ -7,7 +7,7 @@ export class ProductsService { constructor(private prisma: PrismaService) {} async findAll(filters: GetProductsDto) { - const { category, petType, search, page = 1, limit = 10, sortBy = 'createdAt', sortOrder = 'desc' } = filters; + const { category, petType, search, symptom, page = 1, limit = 10, sortBy = 'createdAt', sortOrder = 'desc' } = filters; const whereClause: any = {}; @@ -19,12 +19,42 @@ export class ProductsService { whereClause.suitableFor = { in: [petType, 'هر دو'] }; } + // Filter by a specific symptom (from URL param ?symptom=...) + if (symptom) { + whereClause.symptoms = { + some: { + symptom: { contains: symptom, mode: 'insensitive' } + } + }; + } + if (search) { - whereClause.OR = [ + // If symptom filter is already applied, extend via AND to also search names/desc + // If not, use OR across names, description AND symptoms + const searchConditions = [ { nameFa: { contains: search, mode: 'insensitive' } }, { nameEn: { contains: search, mode: 'insensitive' } }, { description: { contains: search, mode: 'insensitive' } }, + { shortDescription: { contains: search, mode: 'insensitive' } }, + { + symptoms: { + some: { + symptom: { contains: search, mode: 'insensitive' } + } + } + }, ]; + + if (symptom) { + // Already have a symptom filter; combine with AND + whereClause.AND = [ + { symptoms: whereClause.symptoms }, + { OR: searchConditions.filter(c => !('symptoms' in c)) }, + ]; + delete whereClause.symptoms; + } else { + whereClause.OR = searchConditions; + } } const skip = (page - 1) * limit; diff --git a/frontend/application/app/shop/page.tsx b/frontend/application/app/shop/page.tsx index 59f453c..d28e3eb 100644 --- a/frontend/application/app/shop/page.tsx +++ b/frontend/application/app/shop/page.tsx @@ -22,7 +22,11 @@ export default async function Shop({ searchParams }: { searchParams: Promise<{ [ const category = typeof resolvedParams.category === 'string' ? resolvedParams.category : "all"; const search = typeof resolvedParams.search === 'string' ? resolvedParams.search : ""; const petType = typeof resolvedParams.petType === 'string' ? resolvedParams.petType : "all"; - const symptoms = typeof resolvedParams.symptoms === 'string' ? resolvedParams.symptoms : ""; + + // Support both ?symptoms= (multi, from sidebar URL state) and ?symptom= (single, from megamenu/PDP links) + const symptomsParam = typeof resolvedParams.symptoms === 'string' ? resolvedParams.symptoms : ""; + const symptomSingle = typeof resolvedParams.symptom === 'string' ? resolvedParams.symptom : ""; + const symptoms = symptomsParam || (symptomSingle ? symptomSingle : ""); return ( { - const fetchProducts = async () => { + const fetchProducts = async (resetPage = true) => { setIsUpdating(true); try { const res = await productService.getProducts({ category: selectedCategory, petType: selectedPet, query: searchQuery, - page: 1, + // Send the first active symptom to the backend for server-side filtering + symptom: activeSymptoms.length === 1 ? activeSymptoms[0] : undefined, + page: resetPage ? 1 : page, limit: 20, }); let result = res.data; - if (activeSymptoms.length > 0) { - result = result.filter(p => p.symptoms.some(s => activeSymptoms.includes(s))); + // If multiple symptoms selected, do client-side intersection (rare case) + if (activeSymptoms.length > 1) { + result = result.filter(p => activeSymptoms.every(s => p.symptoms.includes(s))); } setFilteredProducts(result); setMeta(res.meta); - setPage(1); + if (resetPage) setPage(1); } catch (error) { console.error("Failed to fetch products:", error); toast.error("خطا در دریافت لیست محصولات از سرور. لطفاً صفحه را رفرش کنید."); @@ -306,7 +308,8 @@ export default function ArchivePage({ } }; - fetchProducts(); + useEffect(() => { + fetchProducts(true); }, [selectedCategory, selectedPet, searchQuery, activeSymptoms]); const loadMore = async () => { @@ -316,6 +319,7 @@ export default function ArchivePage({ category: selectedCategory, petType: selectedPet, query: searchQuery, + symptom: activeSymptoms.length === 1 ? activeSymptoms[0] : undefined, page: page + 1, limit: 20, }); @@ -418,15 +422,31 @@ export default function ArchivePage({ - {/* Symptoms Checklist */} + {/* Symptoms Pill Tags */}
- جستجو بر اساس علائم -
- {symptoms.map(s => ( +
+ جستجو بر اساس علائم + {activeSymptoms.length > 0 && ( + + )} +
+
+ {symptoms.length === 0 ? ( + در حال بارگذاری... + ) : symptoms.map(s => ( diff --git a/frontend/application/components/Header.tsx b/frontend/application/components/Header.tsx index d38bd16..1094ab3 100644 --- a/frontend/application/components/Header.tsx +++ b/frontend/application/components/Header.tsx @@ -154,33 +154,45 @@ export default function Header({ initial={{ opacity: 0, y: 15, scale: 0.98 }} animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, y: 15, scale: 0.98 }} - className="absolute top-[80%] right-0 w-[640px] bg-white border border-medical-gray-100 shadow-2xl rounded-[2.5rem] p-10 grid grid-cols-2 gap-10 z-50 pointer-events-auto" + className="absolute top-[80%] right-0 w-[680px] bg-white border border-medical-gray-100 shadow-2xl rounded-[2.5rem] p-8 grid grid-cols-2 gap-8 z-50 pointer-events-auto" > {menuItems.map((item, idx) => (
setIsMegaMenuOpen(false)} - className="flex items-center gap-4 mb-4 cursor-pointer block" + className="flex items-center gap-3 mb-4 cursor-pointer block" > -
+
{item.icon}
-

{item.title}

+

{item.title}

-
    - {item.solutions.map((sol, sIdx) => ( -
  • + {item.solutions.length > 0 ? ( +
    + {item.solutions.slice(0, 8).map((sol, sIdx) => ( setIsMegaMenuOpen(false)} > {sol} -
  • - ))} -
+ ))} + {item.solutions.length > 8 && ( + setIsMegaMenuOpen(false)} + > + +{item.solutions.length - 8} بیشتر + + )} +
+ ) : ( +

مشاهده همه محصولات

+ )}
))} diff --git a/frontend/application/components/ProductPage.tsx b/frontend/application/components/ProductPage.tsx index b45c832..7734c85 100644 --- a/frontend/application/components/ProductPage.tsx +++ b/frontend/application/components/ProductPage.tsx @@ -267,6 +267,30 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
+ {/* ─── Symptoms / Therapeutic Indications Section ─── */} + {fullProduct.symptoms && fullProduct.symptoms.length > 0 && ( +
+
+
+ +
+

علائم و موارد درمانی مرتبط

+
+

این محصول برای رفع علائم زیر توسط دامپزشکان توصیه می‌شود:

+
+ {fullProduct.symptoms.map((symptom: string, idx: number) => ( + + + {symptom} + + ))} +
+
+ )} {/* Key Benefits Section */} {fullProduct.keyBenefits && fullProduct.keyBenefits.length > 0 && ( diff --git a/frontend/application/lib/services/productService.ts b/frontend/application/lib/services/productService.ts index 7ddd471..8a9e02f 100644 --- a/frontend/application/lib/services/productService.ts +++ b/frontend/application/lib/services/productService.ts @@ -85,6 +85,7 @@ export class ProductService { category?: string; petType?: PetType | "all"; query?: string; + symptom?: string; page?: number; limit?: number; }): Promise<{ data: Product[]; meta: { total: number; page: number; lastPage: number; limit: number } }> { @@ -92,6 +93,7 @@ export class ProductService { if (filters?.category && filters.category !== "all") params.append('category', filters.category); if (filters?.petType && filters.petType !== "all") params.append('petType', filters.petType); if (filters?.query) params.append('search', filters.query); + if (filters?.symptom) params.append('symptom', filters.symptom); params.append('page', String(filters?.page || 1)); params.append('limit', String(filters?.limit || 12));