From 2851ef8a25d968a32267cfdcb7ca4c1c84f3a4e2 Mon Sep 17 00:00:00 2001 From: parsaaghayi Date: Fri, 10 Jul 2026 16:13:52 +0330 Subject: [PATCH] feat: complete backend swagger annotations, admin pages pagination, and frontend application fixes --- backend/src/admin/admin.controller.ts | 34 +++++++++++++ backend/src/admin/admin.service.ts | 8 +-- backend/src/admin/blogs.controller.ts | 10 ++++ backend/src/admin/blogs.service.ts | 2 +- backend/src/admin/categories.controller.ts | 11 ++++ backend/src/admin/categories.service.ts | 2 +- backend/src/admin/media.controller.ts | 6 +++ backend/src/admin/pets.controller.ts | 8 +++ backend/src/admin/pets.service.ts | 2 +- backend/src/admin/reports.controller.ts | 4 ++ backend/src/admin/wiki.controller.ts | 10 ++++ backend/src/admin/wiki.service.ts | 2 +- backend/src/auth/dto/login.dto.ts | 3 ++ backend/src/auth/dto/register.dto.ts | 6 +++ backend/src/blogs/blogs.controller.ts | 11 ++-- backend/src/blogs/blogs.service.ts | 44 +++++++++++++--- backend/src/common/dto/pagination.dto.ts | 39 +++++++++++++++ .../common/filters/http-exception.filter.ts | 21 +++++++- .../common/schemas/error-response.schema.ts | 15 ++++++ .../schemas/paginated-response.schema.ts | 43 ++++++++++++++++ backend/src/orders/orders.controller.ts | 35 ++++++++----- backend/src/orders/orders.service.ts | 35 ++++++++++--- backend/src/pets/pets.controller.ts | 43 +++++++++------- backend/src/pets/pets.service.ts | 38 ++++++++++++-- backend/src/products/dto/get-products.dto.ts | 12 ++--- backend/src/products/products.controller.ts | 50 +++++++++++-------- backend/src/products/products.service.ts | 40 ++++++++++----- backend/src/wiki/wiki.controller.ts | 9 ++-- backend/src/wiki/wiki.service.ts | 37 ++++++++++++-- frontend/admin-panel/src/pages/Blogs.tsx | 2 +- frontend/admin-panel/src/pages/Categories.tsx | 2 +- frontend/admin-panel/src/pages/Coupons.tsx | 2 +- frontend/admin-panel/src/pages/Dashboard.tsx | 2 +- frontend/admin-panel/src/pages/Orders.tsx | 2 +- frontend/admin-panel/src/pages/Pets.tsx | 2 +- frontend/admin-panel/src/pages/Products.tsx | 2 +- frontend/admin-panel/src/pages/Users.tsx | 2 +- frontend/admin-panel/src/pages/Wiki.tsx | 2 +- .../application/lib/services/orderService.ts | 2 +- .../lib/services/productService.ts | 4 +- frontend/application/package.json | 2 +- 41 files changed, 478 insertions(+), 128 deletions(-) create mode 100644 backend/src/common/dto/pagination.dto.ts create mode 100644 backend/src/common/schemas/error-response.schema.ts create mode 100644 backend/src/common/schemas/paginated-response.schema.ts diff --git a/backend/src/admin/admin.controller.ts b/backend/src/admin/admin.controller.ts index 4c72e01..08db885 100644 --- a/backend/src/admin/admin.controller.ts +++ b/backend/src/admin/admin.controller.ts @@ -1,13 +1,17 @@ import { Controller, Get, UseGuards, Param, Put, Post, Body, Delete, Query } from '@nestjs/common'; import { AdminService } from './admin.service'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; +import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery } from '@nestjs/swagger'; +@ApiTags('Admin - پنل مدیریت') +@ApiBearerAuth() @Controller('admin') export class AdminController { constructor(private readonly adminService: AdminService) {} @UseGuards(JwtAuthGuard) @Get('dashboard/stats') + @ApiOperation({ summary: 'دریافت آمار کلی داشبورد' }) async getDashboardStats() { const stats = await this.adminService.getDashboardStats(); return { @@ -18,6 +22,11 @@ export class AdminController { @UseGuards(JwtAuthGuard) @Get('users') + @ApiOperation({ summary: 'لیست کاربران' }) + @ApiQuery({ name: 'page', required: false, description: 'شماره صفحه' }) + @ApiQuery({ name: 'limit', required: false, description: 'تعداد آیتم‌ها' }) + @ApiQuery({ name: 'search', required: false, description: 'جستجو در نام یا ایمیل' }) + @ApiQuery({ name: 'role', required: false, description: 'فیلتر بر اساس نقش کاربر' }) async getUsers( @Query('page') page?: string, @Query('limit') limit?: string, @@ -30,6 +39,7 @@ export class AdminController { @UseGuards(JwtAuthGuard) @Put('users/:id/role') + @ApiOperation({ summary: 'تغییر نقش کاربر' }) async updateUserRole(@Param('id') id: string, @Body('role') role: string) { const user = await this.adminService.updateUserRole(id, role); return { @@ -40,6 +50,11 @@ export class AdminController { @UseGuards(JwtAuthGuard) @Get('products') + @ApiOperation({ summary: 'لیست محصولات (مدیریت)' }) + @ApiQuery({ name: 'page', required: false, description: 'شماره صفحه' }) + @ApiQuery({ name: 'limit', required: false, description: 'تعداد آیتم‌ها' }) + @ApiQuery({ name: 'search', required: false, description: 'جستجو' }) + @ApiQuery({ name: 'categoryId', required: false, description: 'شناسه دسته‌بندی' }) async getProducts( @Query('page') page?: string, @Query('limit') limit?: string, @@ -52,6 +67,7 @@ export class AdminController { @UseGuards(JwtAuthGuard) @Post('products') + @ApiOperation({ summary: 'ایجاد محصول جدید' }) async createProduct(@Body() data: any) { const product = await this.adminService.createProduct(data); return { success: true, data: product }; @@ -59,6 +75,7 @@ export class AdminController { @UseGuards(JwtAuthGuard) @Put('products/:id') + @ApiOperation({ summary: 'ویرایش محصول' }) async updateProduct(@Param('id') id: string, @Body() data: any) { const product = await this.adminService.updateProduct(id, data); return { @@ -69,6 +86,7 @@ export class AdminController { @UseGuards(JwtAuthGuard) @Delete('products/:id') + @ApiOperation({ summary: 'حذف محصول' }) async deleteProduct(@Param('id') id: string) { await this.adminService.deleteProduct(id); return { @@ -79,6 +97,11 @@ export class AdminController { @UseGuards(JwtAuthGuard) @Get('orders') + @ApiOperation({ summary: 'لیست سفارش‌ها' }) + @ApiQuery({ name: 'page', required: false, description: 'شماره صفحه' }) + @ApiQuery({ name: 'limit', required: false, description: 'تعداد آیتم‌ها' }) + @ApiQuery({ name: 'search', required: false, description: 'جستجو' }) + @ApiQuery({ name: 'status', required: false, description: 'وضعیت سفارش' }) async getOrders( @Query('page') page?: string, @Query('limit') limit?: string, @@ -91,6 +114,7 @@ export class AdminController { @UseGuards(JwtAuthGuard) @Put('orders/:id/status') + @ApiOperation({ summary: 'تغییر وضعیت سفارش' }) async updateOrderStatus(@Param('id') id: string, @Body('status') status: string) { const order = await this.adminService.updateOrderStatus(id, status); return { @@ -102,6 +126,10 @@ export class AdminController { // --- Coupons --- @UseGuards(JwtAuthGuard) @Get('coupons') + @ApiOperation({ summary: 'لیست کد تخفیف‌ها' }) + @ApiQuery({ name: 'page', required: false, description: 'شماره صفحه' }) + @ApiQuery({ name: 'limit', required: false, description: 'تعداد آیتم‌ها' }) + @ApiQuery({ name: 'search', required: false, description: 'جستجو' }) async getCoupons(@Query() query: any) { const coupons = await this.adminService.getCoupons(query); return { success: true, ...coupons }; @@ -109,6 +137,7 @@ export class AdminController { @UseGuards(JwtAuthGuard) @Post('coupons') + @ApiOperation({ summary: 'ایجاد کد تخفیف جدید' }) async createCoupon(@Body() data: any) { const coupon = await this.adminService.createCoupon(data); return { success: true, data: coupon }; @@ -116,6 +145,7 @@ export class AdminController { @UseGuards(JwtAuthGuard) @Put('coupons/:id') + @ApiOperation({ summary: 'ویرایش کد تخفیف' }) async updateCoupon(@Param('id') id: string, @Body() data: any) { const coupon = await this.adminService.updateCoupon(id, data); return { success: true, data: coupon }; @@ -123,6 +153,7 @@ export class AdminController { @UseGuards(JwtAuthGuard) @Put('coupons/:id/toggle') + @ApiOperation({ summary: 'فعال/غیرفعال کردن کد تخفیف' }) async toggleCoupon(@Param('id') id: string, @Body('isActive') isActive: boolean) { const coupon = await this.adminService.toggleCoupon(id, isActive); return { success: true, data: coupon }; @@ -130,6 +161,7 @@ export class AdminController { @UseGuards(JwtAuthGuard) @Delete('coupons/:id') + @ApiOperation({ summary: 'حذف کد تخفیف' }) async deleteCoupon(@Param('id') id: string) { await this.adminService.deleteCoupon(id); return { success: true }; @@ -138,6 +170,7 @@ export class AdminController { // --- Settings --- @UseGuards(JwtAuthGuard) @Get('settings') + @ApiOperation({ summary: 'دریافت تنظیمات' }) async getSettings() { const settings = await this.adminService.getSettings(); return { success: true, data: settings }; @@ -145,6 +178,7 @@ export class AdminController { @UseGuards(JwtAuthGuard) @Put('settings') + @ApiOperation({ summary: 'ذخیره تنظیمات' }) async updateSettings(@Body() data: Record) { const settings = await this.adminService.updateSettings(data); return { success: true, data: settings }; diff --git a/backend/src/admin/admin.service.ts b/backend/src/admin/admin.service.ts index 31c17e2..bed2e5b 100644 --- a/backend/src/admin/admin.service.ts +++ b/backend/src/admin/admin.service.ts @@ -57,7 +57,7 @@ export class AdminService { this.prisma.user.count({ where }) ]); - return { data, meta: { total, page, limit, totalPages: Math.ceil(total / limit) } }; + return { data, meta: { total, page, limit, lastPage: Math.ceil(total / limit) } }; } async updateUserRole(id: string, role: string) { @@ -92,7 +92,7 @@ export class AdminService { this.prisma.product.count({ where }) ]); - return { data, meta: { total, page, limit, totalPages: Math.ceil(total / limit) } }; + return { data, meta: { total, page, limit, lastPage: Math.ceil(total / limit) } }; } catch (error) { console.error("[AdminService] getProducts error:", error); throw new HttpException(error.message || 'Error fetching products', 500); @@ -179,7 +179,7 @@ export class AdminService { this.prisma.order.count({ where }) ]); - return { data, meta: { total, page, limit, totalPages: Math.ceil(total / limit) } }; + return { data, meta: { total, page, limit, lastPage: Math.ceil(total / limit) } }; } async updateOrderStatus(id: string, status: string) { @@ -209,7 +209,7 @@ export class AdminService { return { data, - meta: { total, page: Number(page), limit: Number(limit), totalPages: Math.ceil(total / Number(limit)) } + meta: { total, page: Number(page), limit: Number(limit), lastPage: Math.ceil(total / Number(limit)) } }; } diff --git a/backend/src/admin/blogs.controller.ts b/backend/src/admin/blogs.controller.ts index b2588c8..687415c 100644 --- a/backend/src/admin/blogs.controller.ts +++ b/backend/src/admin/blogs.controller.ts @@ -1,30 +1,40 @@ import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards, Request } from '@nestjs/common'; import { BlogsService } from './blogs.service'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; +import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery } from '@nestjs/swagger'; +@ApiTags('Admin - مدیریت مقالات (بلاگ)') +@ApiBearerAuth() @UseGuards(JwtAuthGuard) @Controller('admin/blogs') export class BlogsController { constructor(private readonly blogsService: BlogsService) {} @Get() + @ApiOperation({ summary: 'لیست مقالات' }) + @ApiQuery({ name: 'page', required: false, description: 'شماره صفحه' }) + @ApiQuery({ name: 'limit', required: false, description: 'تعداد آیتم‌ها' }) + @ApiQuery({ name: 'search', required: false, description: 'جستجو' }) async getBlogs(@Query() query: any) { return this.blogsService.getBlogs(query); } @Post() + @ApiOperation({ summary: 'ایجاد مقاله جدید' }) async createBlog(@Body() body: any, @Request() req: any) { const data = await this.blogsService.createBlog(body, req.user.id); return { success: true, data }; } @Put(':id') + @ApiOperation({ summary: 'ویرایش مقاله' }) async updateBlog(@Param('id') id: string, @Body() body: any) { const data = await this.blogsService.updateBlog(id, body); return { success: true, data }; } @Delete(':id') + @ApiOperation({ summary: 'حذف مقاله' }) async deleteBlog(@Param('id') id: string) { await this.blogsService.deleteBlog(id); return { success: true }; diff --git a/backend/src/admin/blogs.service.ts b/backend/src/admin/blogs.service.ts index cffab49..e9cfd59 100644 --- a/backend/src/admin/blogs.service.ts +++ b/backend/src/admin/blogs.service.ts @@ -24,7 +24,7 @@ export class BlogsService { return { data, - meta: { total, page: Number(page), limit: Number(limit), totalPages: Math.ceil(total / Number(limit)) } + meta: { total, page: Number(page), limit: Number(limit), lastPage: Math.ceil(total / Number(limit)) } }; } diff --git a/backend/src/admin/categories.controller.ts b/backend/src/admin/categories.controller.ts index 3b63192..49baeaa 100644 --- a/backend/src/admin/categories.controller.ts +++ b/backend/src/admin/categories.controller.ts @@ -1,36 +1,47 @@ import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards } from '@nestjs/common'; import { CategoriesService } from './categories.service'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; +import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery } from '@nestjs/swagger'; +@ApiTags('Admin - مدیریت دسته‌بندی‌ها') +@ApiBearerAuth() @UseGuards(JwtAuthGuard) @Controller('admin/categories') export class CategoriesController { constructor(private readonly categoriesService: CategoriesService) {} @Get() + @ApiOperation({ summary: 'لیست دسته‌بندی‌ها (با صفحه‌بندی)' }) + @ApiQuery({ name: 'page', required: false, description: 'شماره صفحه' }) + @ApiQuery({ name: 'limit', required: false, description: 'تعداد آیتم‌ها' }) + @ApiQuery({ name: 'search', required: false, description: 'جستجو در نام' }) async getCategories(@Query() query: any) { return this.categoriesService.getCategories(query); } @Get('all') + @ApiOperation({ summary: 'لیست تمام دسته‌بندی‌ها (بدون صفحه‌بندی)' }) async getAllCategoriesRaw() { const data = await this.categoriesService.getAllCategoriesRaw(); return { success: true, data }; } @Post() + @ApiOperation({ summary: 'ایجاد دسته‌بندی جدید' }) async createCategory(@Body() body: any) { const data = await this.categoriesService.createCategory(body); return { success: true, data }; } @Put(':id') + @ApiOperation({ summary: 'ویرایش دسته‌بندی' }) async updateCategory(@Param('id') id: string, @Body() body: any) { const data = await this.categoriesService.updateCategory(id, body); return { success: true, data }; } @Delete(':id') + @ApiOperation({ summary: 'حذف دسته‌بندی' }) async deleteCategory(@Param('id') id: string) { await this.categoriesService.deleteCategory(id); return { success: true }; diff --git a/backend/src/admin/categories.service.ts b/backend/src/admin/categories.service.ts index 78d77ba..8beb9e9 100644 --- a/backend/src/admin/categories.service.ts +++ b/backend/src/admin/categories.service.ts @@ -23,7 +23,7 @@ export class CategoriesService { return { data, - meta: { total, page: Number(page), limit: Number(limit), totalPages: Math.ceil(total / Number(limit)) } + meta: { total, page: Number(page), limit: Number(limit), lastPage: Math.ceil(total / Number(limit)) } }; } diff --git a/backend/src/admin/media.controller.ts b/backend/src/admin/media.controller.ts index c02718c..d5a4d43 100644 --- a/backend/src/admin/media.controller.ts +++ b/backend/src/admin/media.controller.ts @@ -2,13 +2,17 @@ import { Controller, Get, Post, Delete, Param, UseGuards, UseInterceptors, Uploa import { FileInterceptor } from '@nestjs/platform-express'; import { MediaService } from './media.service'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; +import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; +@ApiTags('Admin - مدیریت رسانه (تصاویر)') +@ApiBearerAuth() @Controller('admin/media') export class MediaController { constructor(private readonly mediaService: MediaService) {} @UseGuards(JwtAuthGuard) @Get() + @ApiOperation({ summary: 'لیست فایل‌های رسانه' }) async getAllMedia() { const data = await this.mediaService.getAllMedia(); return { success: true, data }; @@ -16,6 +20,7 @@ export class MediaController { @UseGuards(JwtAuthGuard) @Post('upload') + @ApiOperation({ summary: 'آپلود فایل جدید' }) @UseInterceptors(FileInterceptor('file')) async uploadFile(@UploadedFile() file: Express.Multer.File) { if (!file) throw new BadRequestException('File is missing'); @@ -25,6 +30,7 @@ export class MediaController { @UseGuards(JwtAuthGuard) @Delete(':id') + @ApiOperation({ summary: 'حذف فایل' }) async deleteMedia(@Param('id') id: string) { const data = await this.mediaService.deleteMedia(id); return { success: true, data }; diff --git a/backend/src/admin/pets.controller.ts b/backend/src/admin/pets.controller.ts index 506cd28..7792771 100644 --- a/backend/src/admin/pets.controller.ts +++ b/backend/src/admin/pets.controller.ts @@ -1,18 +1,26 @@ import { Controller, Get, Delete, Param, Query, UseGuards } from '@nestjs/common'; import { PetsService } from './pets.service'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; +import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery } from '@nestjs/swagger'; +@ApiTags('Admin - مدیریت حیوانات خانگی') +@ApiBearerAuth() @UseGuards(JwtAuthGuard) @Controller('admin/pets') export class PetsController { constructor(private readonly petsService: PetsService) {} @Get() + @ApiOperation({ summary: 'لیست حیوانات خانگی' }) + @ApiQuery({ name: 'page', required: false, description: 'شماره صفحه' }) + @ApiQuery({ name: 'limit', required: false, description: 'تعداد آیتم‌ها' }) + @ApiQuery({ name: 'search', required: false, description: 'جستجو' }) async getPets(@Query() query: any) { return this.petsService.getPets(query); } @Delete(':id') + @ApiOperation({ summary: 'حذف حیوان خانگی' }) async deletePet(@Param('id') id: string) { await this.petsService.deletePet(id); return { success: true }; diff --git a/backend/src/admin/pets.service.ts b/backend/src/admin/pets.service.ts index 10c6ee2..c82f280 100644 --- a/backend/src/admin/pets.service.ts +++ b/backend/src/admin/pets.service.ts @@ -24,7 +24,7 @@ export class PetsService { return { data, - meta: { total, page: Number(page), limit: Number(limit), totalPages: Math.ceil(total / Number(limit)) } + meta: { total, page: Number(page), limit: Number(limit), lastPage: Math.ceil(total / Number(limit)) } }; } diff --git a/backend/src/admin/reports.controller.ts b/backend/src/admin/reports.controller.ts index a77188a..411e8f0 100644 --- a/backend/src/admin/reports.controller.ts +++ b/backend/src/admin/reports.controller.ts @@ -1,13 +1,17 @@ import { Controller, Get, UseGuards } from '@nestjs/common'; import { ReportsService } from './reports.service'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; +import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; +@ApiTags('Admin - گزارشات') +@ApiBearerAuth() @Controller('admin/reports') export class ReportsController { constructor(private readonly reportsService: ReportsService) {} @UseGuards(JwtAuthGuard) @Get() + @ApiOperation({ summary: 'دریافت گزارشات داشبورد' }) async getReports() { const data = await this.reportsService.getDashboardReports(); return { success: true, data }; diff --git a/backend/src/admin/wiki.controller.ts b/backend/src/admin/wiki.controller.ts index 296c865..71f7912 100644 --- a/backend/src/admin/wiki.controller.ts +++ b/backend/src/admin/wiki.controller.ts @@ -1,30 +1,40 @@ import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards } from '@nestjs/common'; import { WikiService } from './wiki.service'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; +import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery } from '@nestjs/swagger'; +@ApiTags('Admin - مدیریت دانشنامه (اصطلاحات علمی)') +@ApiBearerAuth() @UseGuards(JwtAuthGuard) @Controller('admin/wiki') export class WikiController { constructor(private readonly wikiService: WikiService) {} @Get() + @ApiOperation({ summary: 'لیست اصطلاحات دانشنامه' }) + @ApiQuery({ name: 'page', required: false, description: 'شماره صفحه' }) + @ApiQuery({ name: 'limit', required: false, description: 'تعداد آیتم‌ها' }) + @ApiQuery({ name: 'search', required: false, description: 'جستجو' }) async getTerms(@Query() query: any) { return this.wikiService.getTerms(query); } @Post() + @ApiOperation({ summary: 'ایجاد اصطلاح جدید' }) async createTerm(@Body() body: any) { const data = await this.wikiService.createTerm(body); return { success: true, data }; } @Put(':key') + @ApiOperation({ summary: 'ویرایش اصطلاح' }) async updateTerm(@Param('key') key: string, @Body() body: any) { const data = await this.wikiService.updateTerm(key, body); return { success: true, data }; } @Delete(':key') + @ApiOperation({ summary: 'حذف اصطلاح' }) async deleteTerm(@Param('key') key: string) { await this.wikiService.deleteTerm(key); return { success: true }; diff --git a/backend/src/admin/wiki.service.ts b/backend/src/admin/wiki.service.ts index 0aeb220..9a0752c 100644 --- a/backend/src/admin/wiki.service.ts +++ b/backend/src/admin/wiki.service.ts @@ -23,7 +23,7 @@ export class WikiService { return { data, - meta: { total, page: Number(page), limit: Number(limit), totalPages: Math.ceil(total / Number(limit)) } + meta: { total, page: Number(page), limit: Number(limit), lastPage: Math.ceil(total / Number(limit)) } }; } diff --git a/backend/src/auth/dto/login.dto.ts b/backend/src/auth/dto/login.dto.ts index 8a61407..023beda 100644 --- a/backend/src/auth/dto/login.dto.ts +++ b/backend/src/auth/dto/login.dto.ts @@ -1,10 +1,13 @@ import { IsNotEmpty, IsString, MinLength } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; export class LoginDto { + @ApiProperty({ description: 'شماره موبایل', example: '09123456789' }) @IsNotEmpty({ message: 'شماره موبایل الزامی است' }) @IsString() mobile: string; + @ApiProperty({ description: 'رمز عبور', example: 'password123' }) @IsNotEmpty({ message: 'رمز عبور الزامی است' }) @MinLength(6, { message: 'رمز عبور باید حداقل ۶ کاراکتر باشد' }) password: string; diff --git a/backend/src/auth/dto/register.dto.ts b/backend/src/auth/dto/register.dto.ts index 6a666b7..0a60336 100644 --- a/backend/src/auth/dto/register.dto.ts +++ b/backend/src/auth/dto/register.dto.ts @@ -1,22 +1,28 @@ import { IsEmail, IsNotEmpty, IsOptional, IsString, MinLength } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class RegisterDto { + @ApiProperty({ description: 'نام', example: 'علی' }) @IsNotEmpty({ message: 'نام الزامی است' }) @IsString() firstName: string; + @ApiProperty({ description: 'نام خانوادگی', example: 'رضایی' }) @IsNotEmpty({ message: 'نام خانوادگی الزامی است' }) @IsString() lastName: string; + @ApiPropertyOptional({ description: 'ایمیل (اختیاری)', example: 'ali@example.com' }) @IsOptional() @IsEmail({}, { message: 'ایمیل نامعتبر است' }) email?: string; + @ApiProperty({ description: 'شماره موبایل', example: '09123456789' }) @IsNotEmpty({ message: 'شماره موبایل الزامی است' }) @IsString() mobile: string; + @ApiProperty({ description: 'رمز عبور', example: 'password123' }) @IsNotEmpty({ message: 'رمز عبور الزامی است' }) @MinLength(6, { message: 'رمز عبور باید حداقل ۶ کاراکتر باشد' }) password: string; diff --git a/backend/src/blogs/blogs.controller.ts b/backend/src/blogs/blogs.controller.ts index 6f29550..66a70b0 100644 --- a/backend/src/blogs/blogs.controller.ts +++ b/backend/src/blogs/blogs.controller.ts @@ -1,6 +1,7 @@ -import { Controller, Get, Param, HttpStatus } from '@nestjs/common'; +import { Controller, Get, Param, HttpStatus, Query } from '@nestjs/common'; import { BlogsService } from './blogs.service'; -import { ApiTags, ApiOperation, ApiResponse, ApiOkResponse, ApiNotFoundResponse } from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiResponse, ApiOkResponse, ApiNotFoundResponse, ApiQuery } from '@nestjs/swagger'; +import { PaginationDto } from '../common/dto/pagination.dto'; @ApiTags('Blogs - مجله سلامت') @Controller('blogs') @@ -13,9 +14,9 @@ export class BlogsController { @Get() @ApiOperation({ summary: 'دریافت لیست مقالات مجله سلامت' }) - @ApiOkResponse({ description: 'لیست مقالات منتشر شده' }) - findAll() { - return this.blogsService.findAll(); + @ApiOkResponse({ description: 'لیست مقالات منتشر شده (صفحه‌بندی شده)' }) + findAll(@Query() query: PaginationDto) { + return this.blogsService.findAll(query); } @Get(':slug') diff --git a/backend/src/blogs/blogs.service.ts b/backend/src/blogs/blogs.service.ts index c3e8435..42f4b92 100644 --- a/backend/src/blogs/blogs.service.ts +++ b/backend/src/blogs/blogs.service.ts @@ -1,20 +1,48 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; +import { PaginationDto } from '../common/dto/pagination.dto'; @Injectable() export class BlogsService { constructor(private prisma: PrismaService) {} - async findAll() { - return this.prisma.blog.findMany({ - where: { isPublished: true }, - orderBy: { createdAt: 'desc' }, - include: { - author: { - select: { firstName: true, lastName: true } + async findAll(filters: PaginationDto) { + const { search, page = 1, limit = 10, sortBy = 'createdAt', sortOrder = 'desc' } = filters; + + const whereClause: any = { isPublished: true }; + if (search) { + whereClause.OR = [ + { title: { contains: search, mode: 'insensitive' } }, + { content: { contains: search, mode: 'insensitive' } }, + ]; + } + + const skip = (page - 1) * limit; + + const [data, total] = await Promise.all([ + this.prisma.blog.findMany({ + where: whereClause, + skip, + take: limit, + orderBy: { [sortBy]: sortOrder }, + include: { + author: { + select: { firstName: true, lastName: true } + } } + }), + this.prisma.blog.count({ where: whereClause }) + ]); + + return { + data, + meta: { + total, + page, + lastPage: Math.ceil(total / limit), + limit, } - }); + }; } async findOneBySlug(slug: string) { diff --git a/backend/src/common/dto/pagination.dto.ts b/backend/src/common/dto/pagination.dto.ts new file mode 100644 index 0000000..b8ebc23 --- /dev/null +++ b/backend/src/common/dto/pagination.dto.ts @@ -0,0 +1,39 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsOptional, IsInt, Min, IsString, IsEnum } from 'class-validator'; +import { Type } from 'class-transformer'; + +export enum SortOrder { + ASC = 'asc', + DESC = 'desc', +} + +export class PaginationDto { + @ApiPropertyOptional({ description: 'شماره صفحه (شروع از ۱)', minimum: 1, default: 1 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page?: number = 1; + + @ApiPropertyOptional({ description: 'تعداد آیتم‌ها در هر صفحه', minimum: 1, default: 10 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + limit?: number = 10; + + @ApiPropertyOptional({ description: 'فیلد برای مرتب‌سازی', default: 'createdAt' }) + @IsOptional() + @IsString() + sortBy?: string = 'createdAt'; + + @ApiPropertyOptional({ description: 'جهت مرتب‌سازی (asc/desc)', enum: SortOrder, default: SortOrder.DESC }) + @IsOptional() + @IsEnum(SortOrder) + sortOrder?: SortOrder = SortOrder.DESC; + + @ApiPropertyOptional({ description: 'عبارت جستجو' }) + @IsOptional() + @IsString() + search?: string; +} diff --git a/backend/src/common/filters/http-exception.filter.ts b/backend/src/common/filters/http-exception.filter.ts index 23dcc94..5472ad8 100644 --- a/backend/src/common/filters/http-exception.filter.ts +++ b/backend/src/common/filters/http-exception.filter.ts @@ -9,10 +9,27 @@ export class HttpExceptionFilter implements ExceptionFilter { const status = exception.getStatus(); const exceptionResponse: any = exception.getResponse(); + let message = typeof exceptionResponse === 'string' ? exceptionResponse : (exceptionResponse.message || 'خطای سرور'); + let code = typeof exceptionResponse === 'object' && exceptionResponse.error ? exceptionResponse.error : (status === 400 ? 'BAD_REQUEST' : 'ERROR'); + + // Convert array of class-validator errors to a generic Farsi message if it's a 400 + if (Array.isArray(message) && status === 400) { + message = 'اطلاعات وارد شده نامعتبر است. لطفاً فرم را بررسی کنید.'; + } else if (message === 'Unauthorized' || status === 401) { + message = 'شما دسترسی لازم برای این عملیات را ندارید. لطفاً وارد شوید.'; + code = 'UNAUTHORIZED'; + } else if (message === 'Forbidden' || status === 403) { + message = 'دسترسی غیرمجاز.'; + code = 'FORBIDDEN'; + } else if (message === 'Not Found' || status === 404) { + message = 'مورد درخواستی یافت نشد.'; + code = 'NOT_FOUND'; + } + response.status(status).json({ success: false, - message: typeof exceptionResponse === 'string' ? exceptionResponse : (exceptionResponse.message || 'خطای سرور'), - code: exceptionResponse.error || (status === 400 ? 'BAD_REQUEST' : 'ERROR'), + message, + code, details: typeof exceptionResponse === 'object' ? exceptionResponse : {} }); } diff --git a/backend/src/common/schemas/error-response.schema.ts b/backend/src/common/schemas/error-response.schema.ts new file mode 100644 index 0000000..ae61a95 --- /dev/null +++ b/backend/src/common/schemas/error-response.schema.ts @@ -0,0 +1,15 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class ApiErrorResponse { + @ApiProperty({ description: 'موفقیت‌آمیز بودن درخواست', example: false }) + success: boolean; + + @ApiProperty({ description: 'پیام خطا', example: 'اطلاعات وارد شده نامعتبر است. لطفاً فرم را بررسی کنید.' }) + message: string; + + @ApiProperty({ description: 'کد خطا', example: 'BAD_REQUEST' }) + code: string; + + @ApiProperty({ description: 'جزئیات خطا (در صورت وجود)', required: false, example: {} }) + details?: any; +} diff --git a/backend/src/common/schemas/paginated-response.schema.ts b/backend/src/common/schemas/paginated-response.schema.ts new file mode 100644 index 0000000..61ee725 --- /dev/null +++ b/backend/src/common/schemas/paginated-response.schema.ts @@ -0,0 +1,43 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class PaginationMeta { + @ApiProperty({ description: 'تعداد کل آیتم‌ها', example: 100 }) + total: number; + + @ApiProperty({ description: 'شماره صفحه فعلی', example: 1 }) + page: number; + + @ApiProperty({ description: 'تعداد کل صفحات', example: 10 }) + lastPage: number; + + @ApiProperty({ description: 'تعداد آیتم‌ها در هر صفحه', example: 10 }) + limit: number; +} + +export class PaginatedResponse { + @ApiProperty({ description: 'آرایه آیتم‌ها' }) + data: T[]; + + @ApiProperty({ description: 'اطلاعات صفحه‌بندی' }) + meta: PaginationMeta; +} + +export function createPaginatedSchema(dtoModel: any) { + return { + schema: { + allOf: [ + { + properties: { + data: { + type: 'array', + items: { $ref: `#/components/schemas/${dtoModel.name}` }, + }, + meta: { + $ref: '#/components/schemas/PaginationMeta', + }, + }, + }, + ], + }, + }; +} diff --git a/backend/src/orders/orders.controller.ts b/backend/src/orders/orders.controller.ts index 95b8236..ca59489 100644 --- a/backend/src/orders/orders.controller.ts +++ b/backend/src/orders/orders.controller.ts @@ -1,8 +1,9 @@ -import { Controller, Get, Post, Body, Param, UseGuards, Req, HttpStatus } from '@nestjs/common'; +import { Controller, Get, Post, Body, Param, UseGuards, Req, HttpStatus, Query } from '@nestjs/common'; import { OrdersService } from './orders.service'; import { CreateOrderDto } from './dto/create-order.dto'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { ApiTags, ApiBearerAuth, ApiOperation, ApiResponse, ApiOkResponse, ApiCreatedResponse, ApiBadRequestResponse, ApiNotFoundResponse } from '@nestjs/swagger'; +import { PaginationDto } from '../common/dto/pagination.dto'; @ApiTags('Orders - مدیریت سفارش‌ها') @ApiBearerAuth() @@ -58,22 +59,30 @@ export class OrdersController { @Get() @ApiOperation({ summary: 'لیست سفارش‌های کاربر فعلی' }) @ApiOkResponse({ - description: 'آرایه‌ای از سفارش‌های ثبت شده کاربر', + description: 'آرایه‌ای از سفارش‌های ثبت شده کاربر (صفحه‌بندی شده)', schema: { - example: [ - { - id: 'e1d2c3b4-1234-5678-abcd-ef1234567890', - userId: 'd8c4e428-cd2f-4c55-bfa3-dfa312bb6480', - totalAmount: '3500000.00', - charityDonation: '10000.00', - status: 'processing', - createdAt: '2026-05-26T18:10:00.000Z' + example: { + data: [ + { + id: 'e1d2c3b4-1234-5678-abcd-ef1234567890', + userId: 'd8c4e428-cd2f-4c55-bfa3-dfa312bb6480', + totalAmount: '3500000.00', + charityDonation: '10000.00', + status: 'processing', + createdAt: '2026-05-26T18:10:00.000Z' + } + ], + meta: { + total: 1, + page: 1, + lastPage: 1, + limit: 10 } - ] + } } }) - findAll(@Req() req: any) { - return this.ordersService.findAllByUser(req.user.id); + findAll(@Req() req: any, @Query() query: PaginationDto) { + return this.ordersService.findAllByUser(req.user.id, query); } @Get(':id') diff --git a/backend/src/orders/orders.service.ts b/backend/src/orders/orders.service.ts index 18d5d2b..efe80dc 100644 --- a/backend/src/orders/orders.service.ts +++ b/backend/src/orders/orders.service.ts @@ -1,5 +1,6 @@ import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; +import { PaginationDto } from '../common/dto/pagination.dto'; import { CreateOrderDto } from './dto/create-order.dto'; @Injectable() @@ -43,14 +44,32 @@ export class OrdersService { }); } - async findAllByUser(userId: string) { - return this.prisma.order.findMany({ - where: { userId }, - include: { - orderItems: { include: { product: true } }, - }, - orderBy: { createdAt: 'desc' }, - }); + async findAllByUser(userId: string, filters: PaginationDto) { + const { page = 1, limit = 10, sortBy = 'createdAt', sortOrder = 'desc' } = filters; + const skip = (page - 1) * limit; + + const [data, total] = await Promise.all([ + this.prisma.order.findMany({ + where: { userId }, + skip, + take: limit, + orderBy: { [sortBy]: sortOrder }, + include: { + orderItems: { include: { product: true } }, + }, + }), + this.prisma.order.count({ where: { userId } }) + ]); + + return { + data, + meta: { + total, + page, + lastPage: Math.ceil(total / limit), + limit, + } + }; } async findOne(id: string, userId: string) { diff --git a/backend/src/pets/pets.controller.ts b/backend/src/pets/pets.controller.ts index e47b535..cbaf5b7 100644 --- a/backend/src/pets/pets.controller.ts +++ b/backend/src/pets/pets.controller.ts @@ -1,9 +1,10 @@ -import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, Req, HttpStatus } from '@nestjs/common'; +import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, Req, HttpStatus, Query } from '@nestjs/common'; import { PetsService } from './pets.service'; import { CreatePetDto } from './dto/create-pet.dto'; import { UpdatePetDto } from './dto/update-pet.dto'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { ApiTags, ApiBearerAuth, ApiOperation, ApiResponse, ApiOkResponse, ApiCreatedResponse, ApiBadRequestResponse, ApiNotFoundResponse } from '@nestjs/swagger'; +import { PaginationDto } from '../common/dto/pagination.dto'; @ApiTags('Pets - مدیریت حیوانات خانگی') @ApiBearerAuth() @@ -61,26 +62,34 @@ export class PetsController { @Get() @ApiOperation({ summary: 'لیست حیوانات خانگی کاربر فعلی' }) @ApiOkResponse({ - description: 'آرایه‌ای از حیوانات خانگی ثبت شده کاربر', + description: 'آرایه‌ای از حیوانات خانگی ثبت شده کاربر (صفحه‌بندی شده)', schema: { - example: [ - { - id: 'c7b8d9e0-1234-5678-abcd-ef1234567890', - userId: 'd8c4e428-cd2f-4c55-bfa3-dfa312bb6480', - name: 'بادی', - type: 'سگ', - breed: 'ژرمن شپرد', - age: 3, - weight: '25.50', - activityLevel: 'متوسط', - imageUrl: null, - createdAt: '2026-05-26T18:10:00.000Z' + example: { + data: [ + { + id: 'c7b8d9e0-1234-5678-abcd-ef1234567890', + userId: 'd8c4e428-cd2f-4c55-bfa3-dfa312bb6480', + name: 'بادی', + type: 'سگ', + breed: 'ژرمن شپرد', + age: 3, + weight: '25.50', + activityLevel: 'متوسط', + imageUrl: null, + createdAt: '2026-05-26T18:10:00.000Z' + } + ], + meta: { + total: 1, + page: 1, + lastPage: 1, + limit: 10 } - ] + } } }) - findAll(@Req() req: any) { - return this.petsService.findAllByUser(req.user.id); + findAll(@Req() req: any, @Query() query: PaginationDto) { + return this.petsService.findAllByUser(req.user.id, query); } @Get(':id') diff --git a/backend/src/pets/pets.service.ts b/backend/src/pets/pets.service.ts index f0d9b32..1a22645 100644 --- a/backend/src/pets/pets.service.ts +++ b/backend/src/pets/pets.service.ts @@ -1,5 +1,6 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; +import { PaginationDto } from '../common/dto/pagination.dto'; import { CreatePetDto } from './dto/create-pet.dto'; import { UpdatePetDto } from './dto/update-pet.dto'; @@ -27,11 +28,38 @@ export class PetsService { }); } - async findAllByUser(userId: string) { - return this.prisma.pet.findMany({ - where: { userId }, - orderBy: { createdAt: 'desc' }, - }); + async findAllByUser(userId: string, filters: PaginationDto) { + const { search, page = 1, limit = 10, sortBy = 'createdAt', sortOrder = 'desc' } = filters; + + const whereClause: any = { userId }; + if (search) { + whereClause.OR = [ + { name: { contains: search, mode: 'insensitive' } }, + { breed: { contains: search, mode: 'insensitive' } }, + ]; + } + + const skip = (page - 1) * limit; + + const [data, total] = await Promise.all([ + this.prisma.pet.findMany({ + where: whereClause, + skip, + take: limit, + orderBy: { [sortBy]: sortOrder }, + }), + this.prisma.pet.count({ where: whereClause }) + ]); + + return { + data, + meta: { + total, + page, + lastPage: Math.ceil(total / limit), + limit, + } + }; } async findOne(id: string, userId: string) { diff --git a/backend/src/products/dto/get-products.dto.ts b/backend/src/products/dto/get-products.dto.ts index 5ca05c7..7faa33d 100644 --- a/backend/src/products/dto/get-products.dto.ts +++ b/backend/src/products/dto/get-products.dto.ts @@ -1,19 +1,15 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; import { IsOptional, IsString, IsEnum } from 'class-validator'; +import { PaginationDto } from '../../common/dto/pagination.dto'; -export class GetProductsDto { - @ApiPropertyOptional({ description: 'Filter by category slug' }) +export class GetProductsDto extends PaginationDto { + @ApiPropertyOptional({ description: 'فیلتر بر اساس اسلاگ دسته‌بندی' }) @IsOptional() @IsString() category?: string; - @ApiPropertyOptional({ description: 'Filter by pet type', enum: ['سگ', 'گربه', 'all'] }) + @ApiPropertyOptional({ description: 'فیلتر بر اساس نوع حیوان', enum: ['سگ', 'گربه', 'all'] }) @IsOptional() @IsEnum(['سگ', 'گربه', 'all']) petType?: string; - - @ApiPropertyOptional({ description: 'Search query string' }) - @IsOptional() - @IsString() - query?: string; } diff --git a/backend/src/products/products.controller.ts b/backend/src/products/products.controller.ts index 0a53c91..6850eb0 100644 --- a/backend/src/products/products.controller.ts +++ b/backend/src/products/products.controller.ts @@ -25,28 +25,36 @@ export class ProductsController { @ApiOkResponse({ description: 'لیست محصولات متناسب با فیلترها (دسته، پت و جستجو)', schema: { - example: [ - { - id: 'a1b2c3d4-1234-5678-abcd-ef1234567890', - artNo: 'canhydrox-gag', - name: 'Canhydrox GAG (کنهیدروکس)', - scientificTagline: 'برای تقویت مفاصل و استخوان‌ها', - description: 'کنهیدروکس محصولی بی‌نظیر برای مفاصل و سیستم حرکتی سگ‌ها...', - shortDescription: 'تقویت مفاصل و غضروف‌ها', - category: 'سیستم حرکتی و مفاصل', - categorySlug: 'joints', - priceValue: '1750000.00', - priceDisplay: '۱,۷۵۰,۰۰۰ تومان', - unit: 'عدد قرص', - packageSize: '120.00', - dosageLogic: 'یک قرص به ازای هر ۱۰ کیلوگرم وزن بدن سگ', - suitableFor: 'سگ', - imageUrl: 'https://example.com/canhydrox.png', - createdAt: '2026-05-26T18:10:00.000Z', - ingredients: [], - symptoms: [] + example: { + data: [ + { + id: 'a1b2c3d4-1234-5678-abcd-ef1234567890', + artNo: 'canhydrox-gag', + name: 'Canhydrox GAG (کنهیدروکس)', + scientificTagline: 'برای تقویت مفاصل و استخوان‌ها', + description: 'کنهیدروکس محصولی بی‌نظیر برای مفاصل و سیستم حرکتی سگ‌ها...', + shortDescription: 'تقویت مفاصل و غضروف‌ها', + category: 'سیستم حرکتی و مفاصل', + categorySlug: 'joints', + priceValue: '1750000.00', + priceDisplay: '۱,۷۵۰,۰۰۰ تومان', + unit: 'عدد قرص', + packageSize: '120.00', + dosageLogic: 'یک قرص به ازای هر ۱۰ کیلوگرم وزن بدن سگ', + suitableFor: 'سگ', + imageUrl: 'https://example.com/canhydrox.png', + createdAt: '2026-05-26T18:10:00.000Z', + ingredients: [], + symptoms: [] + } + ], + meta: { + total: 1, + page: 1, + lastPage: 1, + limit: 10 } - ] + } } }) findAll(@Query() query: GetProductsDto) { diff --git a/backend/src/products/products.service.ts b/backend/src/products/products.service.ts index 1b27bb3..34accf7 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, query } = filters; + const { category, petType, search, page = 1, limit = 10, sortBy = 'createdAt', sortOrder = 'desc' } = filters; const whereClause: any = {}; @@ -19,22 +19,38 @@ export class ProductsService { whereClause.suitableFor = { in: [petType, 'هر دو'] }; } - if (query) { + if (search) { whereClause.OR = [ - { name: { contains: query, mode: 'insensitive' } }, - { description: { contains: query, mode: 'insensitive' } }, + { name: { contains: search, mode: 'insensitive' } }, + { description: { contains: search, mode: 'insensitive' } }, ]; } - const products = await this.prisma.product.findMany({ - where: whereClause, - include: { - ingredients: true, - symptoms: true, - }, - }); + const skip = (page - 1) * limit; - return products; + const [data, total] = await Promise.all([ + this.prisma.product.findMany({ + where: whereClause, + skip, + take: limit, + orderBy: { [sortBy]: sortOrder }, + include: { + ingredients: true, + symptoms: true, + }, + }), + this.prisma.product.count({ where: whereClause }), + ]); + + return { + data, + meta: { + total, + page, + lastPage: Math.ceil(total / limit), + limit, + }, + }; } async findOne(idOrSlug: string) { diff --git a/backend/src/wiki/wiki.controller.ts b/backend/src/wiki/wiki.controller.ts index 863ac32..a66ede4 100644 --- a/backend/src/wiki/wiki.controller.ts +++ b/backend/src/wiki/wiki.controller.ts @@ -1,6 +1,7 @@ -import { Controller, Get, Param, HttpStatus } from '@nestjs/common'; +import { Controller, Get, Param, HttpStatus, Query } from '@nestjs/common'; import { WikiService } from './wiki.service'; import { ApiTags, ApiOperation, ApiResponse, ApiOkResponse, ApiNotFoundResponse } from '@nestjs/swagger'; +import { PaginationDto } from '../common/dto/pagination.dto'; @ApiTags('Wiki - دانشنامه ترکیبات') @Controller('wiki') @@ -13,9 +14,9 @@ export class WikiController { @Get() @ApiOperation({ summary: 'دریافت تمام کلمات دانشنامه' }) - @ApiOkResponse({ description: 'لیست تمامی ترکیبات علمی' }) - findAll() { - return this.wikiService.findAll(); + @ApiOkResponse({ description: 'لیست کلمات به همراه توضیحات (صفحه‌بندی شده)' }) + findAll(@Query() query: PaginationDto) { + return this.wikiService.findAll(query); } @Get(':key') diff --git a/backend/src/wiki/wiki.service.ts b/backend/src/wiki/wiki.service.ts index 3da4602..85c0d75 100644 --- a/backend/src/wiki/wiki.service.ts +++ b/backend/src/wiki/wiki.service.ts @@ -1,14 +1,43 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; +import { PaginationDto } from '../common/dto/pagination.dto'; @Injectable() export class WikiService { constructor(private prisma: PrismaService) {} - async findAll() { - return this.prisma.scientificTerm.findMany({ - orderBy: { term: 'asc' } - }); + async findAll(filters: PaginationDto) { + const { search, page = 1, limit = 10, sortBy = 'term', sortOrder = 'asc' } = filters; + + const whereClause: any = {}; + if (search) { + whereClause.OR = [ + { term: { contains: search, mode: 'insensitive' } }, + { definition: { contains: search, mode: 'insensitive' } }, + ]; + } + + const skip = (page - 1) * limit; + + const [data, total] = await Promise.all([ + this.prisma.scientificTerm.findMany({ + where: whereClause, + skip, + take: limit, + orderBy: { [sortBy]: sortOrder }, + }), + this.prisma.scientificTerm.count({ where: whereClause }) + ]); + + return { + data, + meta: { + total, + page, + lastPage: Math.ceil(total / limit), + limit, + } + }; } async findOneByKey(key: string) { diff --git a/frontend/admin-panel/src/pages/Blogs.tsx b/frontend/admin-panel/src/pages/Blogs.tsx index 22e5253..82969fa 100644 --- a/frontend/admin-panel/src/pages/Blogs.tsx +++ b/frontend/admin-panel/src/pages/Blogs.tsx @@ -34,7 +34,7 @@ export default function Blogs() { const res = await api.get('/admin/blogs', { params: { page, limit, search } }); if (res.data?.data) { setBlogs(res.data.data); - setTotalPages(res.data.meta?.totalPages || 1); + setTotalPages(res.data.meta?.lastPage || 1); } } catch (err) { console.error(err); diff --git a/frontend/admin-panel/src/pages/Categories.tsx b/frontend/admin-panel/src/pages/Categories.tsx index 2be6f60..da9d1f6 100644 --- a/frontend/admin-panel/src/pages/Categories.tsx +++ b/frontend/admin-panel/src/pages/Categories.tsx @@ -33,7 +33,7 @@ export default function Categories() { const res = await api.get('/admin/categories', { params: { page, limit, search } }); if (res.data?.data) { setCategories(res.data.data); - setTotalPages(res.data.meta?.totalPages || 1); + setTotalPages(res.data.meta?.lastPage || 1); } } catch (err) { console.error(err); diff --git a/frontend/admin-panel/src/pages/Coupons.tsx b/frontend/admin-panel/src/pages/Coupons.tsx index 3f66fb3..9baa62a 100644 --- a/frontend/admin-panel/src/pages/Coupons.tsx +++ b/frontend/admin-panel/src/pages/Coupons.tsx @@ -25,7 +25,7 @@ export default function Coupons() { const res = await api.get('/admin/coupons', { params: { page, limit, search } }); if (res.data?.data) { setCoupons(res.data.data); - setTotalPages(res.data.meta?.totalPages || 1); + setTotalPages(res.data.meta?.lastPage || 1); } } catch (err) { console.error(err); diff --git a/frontend/admin-panel/src/pages/Dashboard.tsx b/frontend/admin-panel/src/pages/Dashboard.tsx index b285743..7fb9704 100644 --- a/frontend/admin-panel/src/pages/Dashboard.tsx +++ b/frontend/admin-panel/src/pages/Dashboard.tsx @@ -105,7 +105,7 @@ export default function Dashboard() { paddingAngle={5} dataKey="value" > - {pieData.map((entry, index) => ( + {pieData.map((_entry, index) => ( ))} diff --git a/frontend/admin-panel/src/pages/Orders.tsx b/frontend/admin-panel/src/pages/Orders.tsx index fb00e61..98c0f9c 100644 --- a/frontend/admin-panel/src/pages/Orders.tsx +++ b/frontend/admin-panel/src/pages/Orders.tsx @@ -35,7 +35,7 @@ export default function Orders() { const response = await api.get(`/admin/orders?${params.toString()}`); if (response.data?.success) { setOrders(response.data.data); - setTotalPages(response.data.meta?.totalPages || 1); + setTotalPages(response.data.meta?.lastPage || 1); } } catch (err) { console.error('Failed to fetch orders', err); diff --git a/frontend/admin-panel/src/pages/Pets.tsx b/frontend/admin-panel/src/pages/Pets.tsx index 4f3a65c..2fe3017 100644 --- a/frontend/admin-panel/src/pages/Pets.tsx +++ b/frontend/admin-panel/src/pages/Pets.tsx @@ -18,7 +18,7 @@ export default function Pets() { const res = await api.get('/admin/pets', { params: { page, limit, search } }); if (res.data?.data) { setPets(res.data.data); - setTotalPages(res.data.meta?.totalPages || 1); + setTotalPages(res.data.meta?.lastPage || 1); } } catch (err) { console.error(err); diff --git a/frontend/admin-panel/src/pages/Products.tsx b/frontend/admin-panel/src/pages/Products.tsx index b48d859..75f4791 100644 --- a/frontend/admin-panel/src/pages/Products.tsx +++ b/frontend/admin-panel/src/pages/Products.tsx @@ -52,7 +52,7 @@ export default function Products() { const prodRes = await api.get('/admin/products', { params: { page, limit, search, categoryId: categoryFilter } }); if (prodRes.data?.data) { setProducts(prodRes.data.data); - setTotalPages(prodRes.data.meta?.totalPages || 1); + setTotalPages(prodRes.data.meta?.lastPage || 1); } // Fetch categories separately so it doesn't break products diff --git a/frontend/admin-panel/src/pages/Users.tsx b/frontend/admin-panel/src/pages/Users.tsx index 0efc08b..36d06a9 100644 --- a/frontend/admin-panel/src/pages/Users.tsx +++ b/frontend/admin-panel/src/pages/Users.tsx @@ -31,7 +31,7 @@ export default function Users() { const response = await api.get(`/admin/users?${params.toString()}`); if (response.data?.success) { setUsers(response.data.data); - setTotalPages(response.data.meta?.totalPages || 1); + setTotalPages(response.data.meta?.lastPage || 1); } } catch (err) { console.error('Failed to fetch users', err); diff --git a/frontend/admin-panel/src/pages/Wiki.tsx b/frontend/admin-panel/src/pages/Wiki.tsx index 94a173c..5b5c40f 100644 --- a/frontend/admin-panel/src/pages/Wiki.tsx +++ b/frontend/admin-panel/src/pages/Wiki.tsx @@ -31,7 +31,7 @@ export default function Wiki() { const res = await api.get('/admin/wiki', { params: { page, limit, search } }); if (res.data?.data) { setTerms(res.data.data); - setTotalPages(res.data.meta?.totalPages || 1); + setTotalPages(res.data.meta?.lastPage || 1); } } catch (err) { console.error(err); diff --git a/frontend/application/lib/services/orderService.ts b/frontend/application/lib/services/orderService.ts index c8d9240..f976f57 100644 --- a/frontend/application/lib/services/orderService.ts +++ b/frontend/application/lib/services/orderService.ts @@ -55,7 +55,7 @@ export class OrderService { public async getUserOrders(): Promise { try { const response = await api.get('/orders'); - return response.data; + return response.data.data; } catch (error: any) { const message = error.response?.data?.message || 'خطا در دریافت لیست سفارش‌ها'; throw new Error(Array.isArray(message) ? message[0] : message); diff --git a/frontend/application/lib/services/productService.ts b/frontend/application/lib/services/productService.ts index 092253d..14fb761 100644 --- a/frontend/application/lib/services/productService.ts +++ b/frontend/application/lib/services/productService.ts @@ -75,7 +75,7 @@ export class ProductService { try { const response = await api.get(`/products?${params.toString()}`); - return response.data.map((item: any) => this.mapBackendToFrontend(item)); + return response.data.data.map((item: any) => this.mapBackendToFrontend(item)); } catch (error) { console.error("[ProductService] Failed to fetch products:", error); return []; @@ -105,7 +105,7 @@ export class ProductService { public async getFeaturedProducts(): Promise { try { const response = await api.get('/products'); - return response.data.slice(0, 4).map((item: any) => this.mapBackendToFrontend(item)); + return response.data.data.slice(0, 4).map((item: any) => this.mapBackendToFrontend(item)); } catch (error) { console.error("[ProductService] Failed to fetch featured products:", error); return []; diff --git a/frontend/application/package.json b/frontend/application/package.json index 6a99b2a..56e0aa4 100644 --- a/frontend/application/package.json +++ b/frontend/application/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev -p 4000", + "dev": "next dev -p 4001", "build": "next build", "start": "next start", "lint": "eslint"