diff --git a/backend/src/products/dto/get-products.dto.ts b/backend/src/products/dto/get-products.dto.ts index f268e30..9c9f77d 100644 --- a/backend/src/products/dto/get-products.dto.ts +++ b/backend/src/products/dto/get-products.dto.ts @@ -25,4 +25,24 @@ export class GetProductsDto extends PaginationDto { @IsOptional() @IsString() requiresRx?: string; + + @ApiPropertyOptional({ description: 'حداقل قیمت (تومان)' }) + @IsOptional() + @IsString() + minPrice?: string; + + @ApiPropertyOptional({ description: 'حداکثر قیمت (تومان)' }) + @IsOptional() + @IsString() + maxPrice?: string; + + @ApiPropertyOptional({ description: 'فیلد مرتب‌سازی', enum: ['priceValue', 'createdAt', 'nameFa'] }) + @IsOptional() + @IsString() + sortBy?: string; + + @ApiPropertyOptional({ description: 'جهت مرتب‌سازی', enum: ['asc', 'desc'] }) + @IsOptional() + @IsEnum(['asc', 'desc']) + sortOrder?: 'asc' | 'desc'; } diff --git a/backend/src/products/products.service.ts b/backend/src/products/products.service.ts index d7c051d..be8bba7 100644 --- a/backend/src/products/products.service.ts +++ b/backend/src/products/products.service.ts @@ -13,66 +13,68 @@ export class ProductsService { search, symptom, requiresRx, + minPrice, + maxPrice, page = 1, limit = 10, sortBy = 'createdAt', sortOrder = 'desc', } = filters; - const whereClause: any = {}; + const andConditions: any[] = []; if (requiresRx !== undefined && requiresRx !== null && requiresRx !== '') { - whereClause.requiresRx = requiresRx === 'true' || requiresRx === '1'; + andConditions.push({ requiresRx: requiresRx === 'true' || requiresRx === '1' }); } if (category) { - whereClause.categorySlug = category; + andConditions.push({ categorySlug: category }); } if (petType && petType !== 'all') { - whereClause.suitableFor = { in: [petType, 'هر دو'] }; + andConditions.push({ suitableFor: { in: [petType, 'هر دو'] } }); } - // Filter by a specific symptom (from URL param ?symptom=...) if (symptom) { - whereClause.symptoms = { - some: { - symptom: { contains: symptom, mode: 'insensitive' }, + andConditions.push({ + symptoms: { + some: { + symptom: { contains: symptom, mode: 'insensitive' }, + }, }, - }; + }); + } + + if (minPrice) { + andConditions.push({ priceValue: { gte: Number(minPrice) } }); + } + + if (maxPrice) { + andConditions.push({ priceValue: { lte: Number(maxPrice) } }); } if (search) { - // 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 = [ - { artNo: { contains: search, mode: 'insensitive' } }, - { barcode: { contains: search, mode: 'insensitive' } }, - { 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' }, + andConditions.push({ + OR: [ + { artNo: { contains: search, mode: 'insensitive' } }, + { barcode: { contains: search, mode: 'insensitive' } }, + { 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 whereClause: any = andConditions.length > 0 ? { AND: andConditions } : {}; + const skip = (page - 1) * limit; const [rawProducts, total] = await Promise.all([ diff --git a/docs/backlog.md b/docs/backlog.md index 00ca1e6..7be458b 100644 --- a/docs/backlog.md +++ b/docs/backlog.md @@ -207,8 +207,8 @@ - [ ] **TASK-26.7 [SSR & HttpOnly Cookie Auth]:** ذخیره توکن‌ها درون HttpOnly Cookie جهت همگام‌سازی کامل Server & Client Components در Next.js. - [x] **TASK-26.8 [Logout State Cleanup]:** پاکسازی کامل کاتالوگ، سبد خرید و تمام کش‌های کاربر هنگام خروج (Logout Cleanup). - [x] **TASK-26.9 [Admin Token Validation Guard]:** اعتبارسنجی واقعی توکن ادمین در `ProtectedRoute.tsx` از طریق فراخوانی API پروفایل در هنگام لود پنل. -- [ ] **TASK-26.10 [Fix Pagination & Search Query Overwrite]:** اصلاح منطق `whereClause` در `products.service.ts` جهت جلوگیری از اوررایت شدن فیلترها. -- [ ] **TASK-26.11 [Price Range & Sorting Filters]:** افزودن فیلتر محدوده قیمت (`minPrice`, `maxPrice`) و مرتب‌سازی (`sort`) به DTO و اندپویینت‌های محصولات. +- [x] **TASK-26.10 [Fix Pagination & Search Query Overwrite]:** اصلاح منطق `whereClause` در `products.service.ts` جهت جلوگیری از اوررایت شدن فیلترها. +- [x] **TASK-26.11 [Price Range & Sorting Filters]:** افزودن فیلتر محدوده قیمت (`minPrice`, `maxPrice`) و مرتب‌سازی (`sort`) به DTO و اندپویینت‌های محصولات. - [ ] **TASK-26.12 [Sync Fallback IDs with Database]:** همگام‌سازی شناسه محصولات استاتیک فرانت‌اند با دیتابیس واقعی جهت جلوگیری از 404 در سفارش. - [ ] **TASK-26.13 [Admin Products Table Pagination]:** پیاده‌سازی پجینیشن سرورساید کامل در جدول مدیریت محصولات پنل ادمین. - [ ] **TASK-26.14 [Admin Wallet Adjust Endpoint]:** ایجاد اندپویینت `POST /api/users/:id/wallet-adjust` و فرم شارژ/کسر مستقیم کیف‌پول در پنل ادمین.