feat: complete backend swagger annotations, admin pages pagination, and frontend application fixes

This commit is contained in:
پارسا آقایی 2026-07-10 16:13:52 +03:30
parent 24137beb32
commit 2851ef8a25
41 changed files with 478 additions and 128 deletions

View File

@ -1,13 +1,17 @@
import { Controller, Get, UseGuards, Param, Put, Post, Body, Delete, Query } from '@nestjs/common'; import { Controller, Get, UseGuards, Param, Put, Post, Body, Delete, Query } from '@nestjs/common';
import { AdminService } from './admin.service'; import { AdminService } from './admin.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery } from '@nestjs/swagger';
@ApiTags('Admin - پنل مدیریت')
@ApiBearerAuth()
@Controller('admin') @Controller('admin')
export class AdminController { export class AdminController {
constructor(private readonly adminService: AdminService) {} constructor(private readonly adminService: AdminService) {}
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Get('dashboard/stats') @Get('dashboard/stats')
@ApiOperation({ summary: 'دریافت آمار کلی داشبورد' })
async getDashboardStats() { async getDashboardStats() {
const stats = await this.adminService.getDashboardStats(); const stats = await this.adminService.getDashboardStats();
return { return {
@ -18,6 +22,11 @@ export class AdminController {
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Get('users') @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( async getUsers(
@Query('page') page?: string, @Query('page') page?: string,
@Query('limit') limit?: string, @Query('limit') limit?: string,
@ -30,6 +39,7 @@ export class AdminController {
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Put('users/:id/role') @Put('users/:id/role')
@ApiOperation({ summary: 'تغییر نقش کاربر' })
async updateUserRole(@Param('id') id: string, @Body('role') role: string) { async updateUserRole(@Param('id') id: string, @Body('role') role: string) {
const user = await this.adminService.updateUserRole(id, role); const user = await this.adminService.updateUserRole(id, role);
return { return {
@ -40,6 +50,11 @@ export class AdminController {
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Get('products') @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( async getProducts(
@Query('page') page?: string, @Query('page') page?: string,
@Query('limit') limit?: string, @Query('limit') limit?: string,
@ -52,6 +67,7 @@ export class AdminController {
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Post('products') @Post('products')
@ApiOperation({ summary: 'ایجاد محصول جدید' })
async createProduct(@Body() data: any) { async createProduct(@Body() data: any) {
const product = await this.adminService.createProduct(data); const product = await this.adminService.createProduct(data);
return { success: true, data: product }; return { success: true, data: product };
@ -59,6 +75,7 @@ export class AdminController {
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Put('products/:id') @Put('products/:id')
@ApiOperation({ summary: 'ویرایش محصول' })
async updateProduct(@Param('id') id: string, @Body() data: any) { async updateProduct(@Param('id') id: string, @Body() data: any) {
const product = await this.adminService.updateProduct(id, data); const product = await this.adminService.updateProduct(id, data);
return { return {
@ -69,6 +86,7 @@ export class AdminController {
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Delete('products/:id') @Delete('products/:id')
@ApiOperation({ summary: 'حذف محصول' })
async deleteProduct(@Param('id') id: string) { async deleteProduct(@Param('id') id: string) {
await this.adminService.deleteProduct(id); await this.adminService.deleteProduct(id);
return { return {
@ -79,6 +97,11 @@ export class AdminController {
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Get('orders') @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( async getOrders(
@Query('page') page?: string, @Query('page') page?: string,
@Query('limit') limit?: string, @Query('limit') limit?: string,
@ -91,6 +114,7 @@ export class AdminController {
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Put('orders/:id/status') @Put('orders/:id/status')
@ApiOperation({ summary: 'تغییر وضعیت سفارش' })
async updateOrderStatus(@Param('id') id: string, @Body('status') status: string) { async updateOrderStatus(@Param('id') id: string, @Body('status') status: string) {
const order = await this.adminService.updateOrderStatus(id, status); const order = await this.adminService.updateOrderStatus(id, status);
return { return {
@ -102,6 +126,10 @@ export class AdminController {
// --- Coupons --- // --- Coupons ---
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Get('coupons') @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) { async getCoupons(@Query() query: any) {
const coupons = await this.adminService.getCoupons(query); const coupons = await this.adminService.getCoupons(query);
return { success: true, ...coupons }; return { success: true, ...coupons };
@ -109,6 +137,7 @@ export class AdminController {
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Post('coupons') @Post('coupons')
@ApiOperation({ summary: 'ایجاد کد تخفیف جدید' })
async createCoupon(@Body() data: any) { async createCoupon(@Body() data: any) {
const coupon = await this.adminService.createCoupon(data); const coupon = await this.adminService.createCoupon(data);
return { success: true, data: coupon }; return { success: true, data: coupon };
@ -116,6 +145,7 @@ export class AdminController {
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Put('coupons/:id') @Put('coupons/:id')
@ApiOperation({ summary: 'ویرایش کد تخفیف' })
async updateCoupon(@Param('id') id: string, @Body() data: any) { async updateCoupon(@Param('id') id: string, @Body() data: any) {
const coupon = await this.adminService.updateCoupon(id, data); const coupon = await this.adminService.updateCoupon(id, data);
return { success: true, data: coupon }; return { success: true, data: coupon };
@ -123,6 +153,7 @@ export class AdminController {
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Put('coupons/:id/toggle') @Put('coupons/:id/toggle')
@ApiOperation({ summary: 'فعال/غیرفعال کردن کد تخفیف' })
async toggleCoupon(@Param('id') id: string, @Body('isActive') isActive: boolean) { async toggleCoupon(@Param('id') id: string, @Body('isActive') isActive: boolean) {
const coupon = await this.adminService.toggleCoupon(id, isActive); const coupon = await this.adminService.toggleCoupon(id, isActive);
return { success: true, data: coupon }; return { success: true, data: coupon };
@ -130,6 +161,7 @@ export class AdminController {
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Delete('coupons/:id') @Delete('coupons/:id')
@ApiOperation({ summary: 'حذف کد تخفیف' })
async deleteCoupon(@Param('id') id: string) { async deleteCoupon(@Param('id') id: string) {
await this.adminService.deleteCoupon(id); await this.adminService.deleteCoupon(id);
return { success: true }; return { success: true };
@ -138,6 +170,7 @@ export class AdminController {
// --- Settings --- // --- Settings ---
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Get('settings') @Get('settings')
@ApiOperation({ summary: 'دریافت تنظیمات' })
async getSettings() { async getSettings() {
const settings = await this.adminService.getSettings(); const settings = await this.adminService.getSettings();
return { success: true, data: settings }; return { success: true, data: settings };
@ -145,6 +178,7 @@ export class AdminController {
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Put('settings') @Put('settings')
@ApiOperation({ summary: 'ذخیره تنظیمات' })
async updateSettings(@Body() data: Record<string, string>) { async updateSettings(@Body() data: Record<string, string>) {
const settings = await this.adminService.updateSettings(data); const settings = await this.adminService.updateSettings(data);
return { success: true, data: settings }; return { success: true, data: settings };

View File

@ -57,7 +57,7 @@ export class AdminService {
this.prisma.user.count({ where }) 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) { async updateUserRole(id: string, role: string) {
@ -92,7 +92,7 @@ export class AdminService {
this.prisma.product.count({ where }) 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) { } catch (error) {
console.error("[AdminService] getProducts error:", error); console.error("[AdminService] getProducts error:", error);
throw new HttpException(error.message || 'Error fetching products', 500); throw new HttpException(error.message || 'Error fetching products', 500);
@ -179,7 +179,7 @@ export class AdminService {
this.prisma.order.count({ where }) 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) { async updateOrderStatus(id: string, status: string) {
@ -209,7 +209,7 @@ export class AdminService {
return { return {
data, 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)) }
}; };
} }

View File

@ -1,30 +1,40 @@
import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards, Request } from '@nestjs/common'; import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards, Request } from '@nestjs/common';
import { BlogsService } from './blogs.service'; import { BlogsService } from './blogs.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery } from '@nestjs/swagger';
@ApiTags('Admin - مدیریت مقالات (بلاگ)')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Controller('admin/blogs') @Controller('admin/blogs')
export class BlogsController { export class BlogsController {
constructor(private readonly blogsService: BlogsService) {} constructor(private readonly blogsService: BlogsService) {}
@Get() @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) { async getBlogs(@Query() query: any) {
return this.blogsService.getBlogs(query); return this.blogsService.getBlogs(query);
} }
@Post() @Post()
@ApiOperation({ summary: 'ایجاد مقاله جدید' })
async createBlog(@Body() body: any, @Request() req: any) { async createBlog(@Body() body: any, @Request() req: any) {
const data = await this.blogsService.createBlog(body, req.user.id); const data = await this.blogsService.createBlog(body, req.user.id);
return { success: true, data }; return { success: true, data };
} }
@Put(':id') @Put(':id')
@ApiOperation({ summary: 'ویرایش مقاله' })
async updateBlog(@Param('id') id: string, @Body() body: any) { async updateBlog(@Param('id') id: string, @Body() body: any) {
const data = await this.blogsService.updateBlog(id, body); const data = await this.blogsService.updateBlog(id, body);
return { success: true, data }; return { success: true, data };
} }
@Delete(':id') @Delete(':id')
@ApiOperation({ summary: 'حذف مقاله' })
async deleteBlog(@Param('id') id: string) { async deleteBlog(@Param('id') id: string) {
await this.blogsService.deleteBlog(id); await this.blogsService.deleteBlog(id);
return { success: true }; return { success: true };

View File

@ -24,7 +24,7 @@ export class BlogsService {
return { return {
data, 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)) }
}; };
} }

View File

@ -1,36 +1,47 @@
import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards } from '@nestjs/common'; import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards } from '@nestjs/common';
import { CategoriesService } from './categories.service'; import { CategoriesService } from './categories.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery } from '@nestjs/swagger';
@ApiTags('Admin - مدیریت دسته‌بندی‌ها')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Controller('admin/categories') @Controller('admin/categories')
export class CategoriesController { export class CategoriesController {
constructor(private readonly categoriesService: CategoriesService) {} constructor(private readonly categoriesService: CategoriesService) {}
@Get() @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) { async getCategories(@Query() query: any) {
return this.categoriesService.getCategories(query); return this.categoriesService.getCategories(query);
} }
@Get('all') @Get('all')
@ApiOperation({ summary: 'لیست تمام دسته‌بندی‌ها (بدون صفحه‌بندی)' })
async getAllCategoriesRaw() { async getAllCategoriesRaw() {
const data = await this.categoriesService.getAllCategoriesRaw(); const data = await this.categoriesService.getAllCategoriesRaw();
return { success: true, data }; return { success: true, data };
} }
@Post() @Post()
@ApiOperation({ summary: 'ایجاد دسته‌بندی جدید' })
async createCategory(@Body() body: any) { async createCategory(@Body() body: any) {
const data = await this.categoriesService.createCategory(body); const data = await this.categoriesService.createCategory(body);
return { success: true, data }; return { success: true, data };
} }
@Put(':id') @Put(':id')
@ApiOperation({ summary: 'ویرایش دسته‌بندی' })
async updateCategory(@Param('id') id: string, @Body() body: any) { async updateCategory(@Param('id') id: string, @Body() body: any) {
const data = await this.categoriesService.updateCategory(id, body); const data = await this.categoriesService.updateCategory(id, body);
return { success: true, data }; return { success: true, data };
} }
@Delete(':id') @Delete(':id')
@ApiOperation({ summary: 'حذف دسته‌بندی' })
async deleteCategory(@Param('id') id: string) { async deleteCategory(@Param('id') id: string) {
await this.categoriesService.deleteCategory(id); await this.categoriesService.deleteCategory(id);
return { success: true }; return { success: true };

View File

@ -23,7 +23,7 @@ export class CategoriesService {
return { return {
data, 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)) }
}; };
} }

View File

@ -2,13 +2,17 @@ import { Controller, Get, Post, Delete, Param, UseGuards, UseInterceptors, Uploa
import { FileInterceptor } from '@nestjs/platform-express'; import { FileInterceptor } from '@nestjs/platform-express';
import { MediaService } from './media.service'; import { MediaService } from './media.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
@ApiTags('Admin - مدیریت رسانه (تصاویر)')
@ApiBearerAuth()
@Controller('admin/media') @Controller('admin/media')
export class MediaController { export class MediaController {
constructor(private readonly mediaService: MediaService) {} constructor(private readonly mediaService: MediaService) {}
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Get() @Get()
@ApiOperation({ summary: 'لیست فایل‌های رسانه' })
async getAllMedia() { async getAllMedia() {
const data = await this.mediaService.getAllMedia(); const data = await this.mediaService.getAllMedia();
return { success: true, data }; return { success: true, data };
@ -16,6 +20,7 @@ export class MediaController {
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Post('upload') @Post('upload')
@ApiOperation({ summary: 'آپلود فایل جدید' })
@UseInterceptors(FileInterceptor('file')) @UseInterceptors(FileInterceptor('file'))
async uploadFile(@UploadedFile() file: Express.Multer.File) { async uploadFile(@UploadedFile() file: Express.Multer.File) {
if (!file) throw new BadRequestException('File is missing'); if (!file) throw new BadRequestException('File is missing');
@ -25,6 +30,7 @@ export class MediaController {
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Delete(':id') @Delete(':id')
@ApiOperation({ summary: 'حذف فایل' })
async deleteMedia(@Param('id') id: string) { async deleteMedia(@Param('id') id: string) {
const data = await this.mediaService.deleteMedia(id); const data = await this.mediaService.deleteMedia(id);
return { success: true, data }; return { success: true, data };

View File

@ -1,18 +1,26 @@
import { Controller, Get, Delete, Param, Query, UseGuards } from '@nestjs/common'; import { Controller, Get, Delete, Param, Query, UseGuards } from '@nestjs/common';
import { PetsService } from './pets.service'; import { PetsService } from './pets.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery } from '@nestjs/swagger';
@ApiTags('Admin - مدیریت حیوانات خانگی')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Controller('admin/pets') @Controller('admin/pets')
export class PetsController { export class PetsController {
constructor(private readonly petsService: PetsService) {} constructor(private readonly petsService: PetsService) {}
@Get() @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) { async getPets(@Query() query: any) {
return this.petsService.getPets(query); return this.petsService.getPets(query);
} }
@Delete(':id') @Delete(':id')
@ApiOperation({ summary: 'حذف حیوان خانگی' })
async deletePet(@Param('id') id: string) { async deletePet(@Param('id') id: string) {
await this.petsService.deletePet(id); await this.petsService.deletePet(id);
return { success: true }; return { success: true };

View File

@ -24,7 +24,7 @@ export class PetsService {
return { return {
data, 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)) }
}; };
} }

View File

@ -1,13 +1,17 @@
import { Controller, Get, UseGuards } from '@nestjs/common'; import { Controller, Get, UseGuards } from '@nestjs/common';
import { ReportsService } from './reports.service'; import { ReportsService } from './reports.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
@ApiTags('Admin - گزارشات')
@ApiBearerAuth()
@Controller('admin/reports') @Controller('admin/reports')
export class ReportsController { export class ReportsController {
constructor(private readonly reportsService: ReportsService) {} constructor(private readonly reportsService: ReportsService) {}
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Get() @Get()
@ApiOperation({ summary: 'دریافت گزارشات داشبورد' })
async getReports() { async getReports() {
const data = await this.reportsService.getDashboardReports(); const data = await this.reportsService.getDashboardReports();
return { success: true, data }; return { success: true, data };

View File

@ -1,30 +1,40 @@
import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards } from '@nestjs/common'; import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards } from '@nestjs/common';
import { WikiService } from './wiki.service'; import { WikiService } from './wiki.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery } from '@nestjs/swagger';
@ApiTags('Admin - مدیریت دانشنامه (اصطلاحات علمی)')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Controller('admin/wiki') @Controller('admin/wiki')
export class WikiController { export class WikiController {
constructor(private readonly wikiService: WikiService) {} constructor(private readonly wikiService: WikiService) {}
@Get() @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) { async getTerms(@Query() query: any) {
return this.wikiService.getTerms(query); return this.wikiService.getTerms(query);
} }
@Post() @Post()
@ApiOperation({ summary: 'ایجاد اصطلاح جدید' })
async createTerm(@Body() body: any) { async createTerm(@Body() body: any) {
const data = await this.wikiService.createTerm(body); const data = await this.wikiService.createTerm(body);
return { success: true, data }; return { success: true, data };
} }
@Put(':key') @Put(':key')
@ApiOperation({ summary: 'ویرایش اصطلاح' })
async updateTerm(@Param('key') key: string, @Body() body: any) { async updateTerm(@Param('key') key: string, @Body() body: any) {
const data = await this.wikiService.updateTerm(key, body); const data = await this.wikiService.updateTerm(key, body);
return { success: true, data }; return { success: true, data };
} }
@Delete(':key') @Delete(':key')
@ApiOperation({ summary: 'حذف اصطلاح' })
async deleteTerm(@Param('key') key: string) { async deleteTerm(@Param('key') key: string) {
await this.wikiService.deleteTerm(key); await this.wikiService.deleteTerm(key);
return { success: true }; return { success: true };

View File

@ -23,7 +23,7 @@ export class WikiService {
return { return {
data, 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)) }
}; };
} }

View File

@ -1,10 +1,13 @@
import { IsNotEmpty, IsString, MinLength } from 'class-validator'; import { IsNotEmpty, IsString, MinLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class LoginDto { export class LoginDto {
@ApiProperty({ description: 'شماره موبایل', example: '09123456789' })
@IsNotEmpty({ message: 'شماره موبایل الزامی است' }) @IsNotEmpty({ message: 'شماره موبایل الزامی است' })
@IsString() @IsString()
mobile: string; mobile: string;
@ApiProperty({ description: 'رمز عبور', example: 'password123' })
@IsNotEmpty({ message: 'رمز عبور الزامی است' }) @IsNotEmpty({ message: 'رمز عبور الزامی است' })
@MinLength(6, { message: 'رمز عبور باید حداقل ۶ کاراکتر باشد' }) @MinLength(6, { message: 'رمز عبور باید حداقل ۶ کاراکتر باشد' })
password: string; password: string;

View File

@ -1,22 +1,28 @@
import { IsEmail, IsNotEmpty, IsOptional, IsString, MinLength } from 'class-validator'; import { IsEmail, IsNotEmpty, IsOptional, IsString, MinLength } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class RegisterDto { export class RegisterDto {
@ApiProperty({ description: 'نام', example: 'علی' })
@IsNotEmpty({ message: 'نام الزامی است' }) @IsNotEmpty({ message: 'نام الزامی است' })
@IsString() @IsString()
firstName: string; firstName: string;
@ApiProperty({ description: 'نام خانوادگی', example: 'رضایی' })
@IsNotEmpty({ message: 'نام خانوادگی الزامی است' }) @IsNotEmpty({ message: 'نام خانوادگی الزامی است' })
@IsString() @IsString()
lastName: string; lastName: string;
@ApiPropertyOptional({ description: 'ایمیل (اختیاری)', example: 'ali@example.com' })
@IsOptional() @IsOptional()
@IsEmail({}, { message: 'ایمیل نامعتبر است' }) @IsEmail({}, { message: 'ایمیل نامعتبر است' })
email?: string; email?: string;
@ApiProperty({ description: 'شماره موبایل', example: '09123456789' })
@IsNotEmpty({ message: 'شماره موبایل الزامی است' }) @IsNotEmpty({ message: 'شماره موبایل الزامی است' })
@IsString() @IsString()
mobile: string; mobile: string;
@ApiProperty({ description: 'رمز عبور', example: 'password123' })
@IsNotEmpty({ message: 'رمز عبور الزامی است' }) @IsNotEmpty({ message: 'رمز عبور الزامی است' })
@MinLength(6, { message: 'رمز عبور باید حداقل ۶ کاراکتر باشد' }) @MinLength(6, { message: 'رمز عبور باید حداقل ۶ کاراکتر باشد' })
password: string; password: string;

View File

@ -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 { 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 - مجله سلامت') @ApiTags('Blogs - مجله سلامت')
@Controller('blogs') @Controller('blogs')
@ -13,9 +14,9 @@ export class BlogsController {
@Get() @Get()
@ApiOperation({ summary: 'دریافت لیست مقالات مجله سلامت' }) @ApiOperation({ summary: 'دریافت لیست مقالات مجله سلامت' })
@ApiOkResponse({ description: 'لیست مقالات منتشر شده' }) @ApiOkResponse({ description: 'لیست مقالات منتشر شده (صفحه‌بندی شده)' })
findAll() { findAll(@Query() query: PaginationDto) {
return this.blogsService.findAll(); return this.blogsService.findAll(query);
} }
@Get(':slug') @Get(':slug')

View File

@ -1,20 +1,48 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import { PaginationDto } from '../common/dto/pagination.dto';
@Injectable() @Injectable()
export class BlogsService { export class BlogsService {
constructor(private prisma: PrismaService) {} constructor(private prisma: PrismaService) {}
async findAll() { async findAll(filters: PaginationDto) {
return this.prisma.blog.findMany({ const { search, page = 1, limit = 10, sortBy = 'createdAt', sortOrder = 'desc' } = filters;
where: { isPublished: true },
orderBy: { createdAt: 'desc' }, const whereClause: any = { isPublished: true };
include: { if (search) {
author: { whereClause.OR = [
select: { firstName: true, lastName: true } { 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) { async findOneBySlug(slug: string) {

View File

@ -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;
}

View File

@ -9,10 +9,27 @@ export class HttpExceptionFilter implements ExceptionFilter {
const status = exception.getStatus(); const status = exception.getStatus();
const exceptionResponse: any = exception.getResponse(); 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({ response.status(status).json({
success: false, success: false,
message: typeof exceptionResponse === 'string' ? exceptionResponse : (exceptionResponse.message || 'خطای سرور'), message,
code: exceptionResponse.error || (status === 400 ? 'BAD_REQUEST' : 'ERROR'), code,
details: typeof exceptionResponse === 'object' ? exceptionResponse : {} details: typeof exceptionResponse === 'object' ? exceptionResponse : {}
}); });
} }

View File

@ -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;
}

View File

@ -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<T> {
@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',
},
},
},
],
},
};
}

View File

@ -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 { OrdersService } from './orders.service';
import { CreateOrderDto } from './dto/create-order.dto'; import { CreateOrderDto } from './dto/create-order.dto';
import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { ApiTags, ApiBearerAuth, ApiOperation, ApiResponse, ApiOkResponse, ApiCreatedResponse, ApiBadRequestResponse, ApiNotFoundResponse } from '@nestjs/swagger'; import { ApiTags, ApiBearerAuth, ApiOperation, ApiResponse, ApiOkResponse, ApiCreatedResponse, ApiBadRequestResponse, ApiNotFoundResponse } from '@nestjs/swagger';
import { PaginationDto } from '../common/dto/pagination.dto';
@ApiTags('Orders - مدیریت سفارش‌ها') @ApiTags('Orders - مدیریت سفارش‌ها')
@ApiBearerAuth() @ApiBearerAuth()
@ -58,22 +59,30 @@ export class OrdersController {
@Get() @Get()
@ApiOperation({ summary: 'لیست سفارش‌های کاربر فعلی' }) @ApiOperation({ summary: 'لیست سفارش‌های کاربر فعلی' })
@ApiOkResponse({ @ApiOkResponse({
description: 'آرایه‌ای از سفارش‌های ثبت شده کاربر', description: 'آرایه‌ای از سفارش‌های ثبت شده کاربر (صفحه‌بندی شده)',
schema: { schema: {
example: [ example: {
{ data: [
id: 'e1d2c3b4-1234-5678-abcd-ef1234567890', {
userId: 'd8c4e428-cd2f-4c55-bfa3-dfa312bb6480', id: 'e1d2c3b4-1234-5678-abcd-ef1234567890',
totalAmount: '3500000.00', userId: 'd8c4e428-cd2f-4c55-bfa3-dfa312bb6480',
charityDonation: '10000.00', totalAmount: '3500000.00',
status: 'processing', charityDonation: '10000.00',
createdAt: '2026-05-26T18:10:00.000Z' status: 'processing',
createdAt: '2026-05-26T18:10:00.000Z'
}
],
meta: {
total: 1,
page: 1,
lastPage: 1,
limit: 10
} }
] }
} }
}) })
findAll(@Req() req: any) { findAll(@Req() req: any, @Query() query: PaginationDto) {
return this.ordersService.findAllByUser(req.user.id); return this.ordersService.findAllByUser(req.user.id, query);
} }
@Get(':id') @Get(':id')

View File

@ -1,5 +1,6 @@
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import { PaginationDto } from '../common/dto/pagination.dto';
import { CreateOrderDto } from './dto/create-order.dto'; import { CreateOrderDto } from './dto/create-order.dto';
@Injectable() @Injectable()
@ -43,14 +44,32 @@ export class OrdersService {
}); });
} }
async findAllByUser(userId: string) { async findAllByUser(userId: string, filters: PaginationDto) {
return this.prisma.order.findMany({ const { page = 1, limit = 10, sortBy = 'createdAt', sortOrder = 'desc' } = filters;
where: { userId }, const skip = (page - 1) * limit;
include: {
orderItems: { include: { product: true } }, const [data, total] = await Promise.all([
}, this.prisma.order.findMany({
orderBy: { createdAt: 'desc' }, 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) { async findOne(id: string, userId: string) {

View File

@ -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 { PetsService } from './pets.service';
import { CreatePetDto } from './dto/create-pet.dto'; import { CreatePetDto } from './dto/create-pet.dto';
import { UpdatePetDto } from './dto/update-pet.dto'; import { UpdatePetDto } from './dto/update-pet.dto';
import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { ApiTags, ApiBearerAuth, ApiOperation, ApiResponse, ApiOkResponse, ApiCreatedResponse, ApiBadRequestResponse, ApiNotFoundResponse } from '@nestjs/swagger'; import { ApiTags, ApiBearerAuth, ApiOperation, ApiResponse, ApiOkResponse, ApiCreatedResponse, ApiBadRequestResponse, ApiNotFoundResponse } from '@nestjs/swagger';
import { PaginationDto } from '../common/dto/pagination.dto';
@ApiTags('Pets - مدیریت حیوانات خانگی') @ApiTags('Pets - مدیریت حیوانات خانگی')
@ApiBearerAuth() @ApiBearerAuth()
@ -61,26 +62,34 @@ export class PetsController {
@Get() @Get()
@ApiOperation({ summary: 'لیست حیوانات خانگی کاربر فعلی' }) @ApiOperation({ summary: 'لیست حیوانات خانگی کاربر فعلی' })
@ApiOkResponse({ @ApiOkResponse({
description: 'آرایه‌ای از حیوانات خانگی ثبت شده کاربر', description: 'آرایه‌ای از حیوانات خانگی ثبت شده کاربر (صفحه‌بندی شده)',
schema: { schema: {
example: [ example: {
{ data: [
id: 'c7b8d9e0-1234-5678-abcd-ef1234567890', {
userId: 'd8c4e428-cd2f-4c55-bfa3-dfa312bb6480', id: 'c7b8d9e0-1234-5678-abcd-ef1234567890',
name: 'بادی', userId: 'd8c4e428-cd2f-4c55-bfa3-dfa312bb6480',
type: 'سگ', name: 'بادی',
breed: 'ژرمن شپرد', type: 'سگ',
age: 3, breed: 'ژرمن شپرد',
weight: '25.50', age: 3,
activityLevel: 'متوسط', weight: '25.50',
imageUrl: null, activityLevel: 'متوسط',
createdAt: '2026-05-26T18:10:00.000Z' imageUrl: null,
createdAt: '2026-05-26T18:10:00.000Z'
}
],
meta: {
total: 1,
page: 1,
lastPage: 1,
limit: 10
} }
] }
} }
}) })
findAll(@Req() req: any) { findAll(@Req() req: any, @Query() query: PaginationDto) {
return this.petsService.findAllByUser(req.user.id); return this.petsService.findAllByUser(req.user.id, query);
} }
@Get(':id') @Get(':id')

View File

@ -1,5 +1,6 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import { PaginationDto } from '../common/dto/pagination.dto';
import { CreatePetDto } from './dto/create-pet.dto'; import { CreatePetDto } from './dto/create-pet.dto';
import { UpdatePetDto } from './dto/update-pet.dto'; import { UpdatePetDto } from './dto/update-pet.dto';
@ -27,11 +28,38 @@ export class PetsService {
}); });
} }
async findAllByUser(userId: string) { async findAllByUser(userId: string, filters: PaginationDto) {
return this.prisma.pet.findMany({ const { search, page = 1, limit = 10, sortBy = 'createdAt', sortOrder = 'desc' } = filters;
where: { userId },
orderBy: { createdAt: 'desc' }, 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) { async findOne(id: string, userId: string) {

View File

@ -1,19 +1,15 @@
import { ApiPropertyOptional } from '@nestjs/swagger'; import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsOptional, IsString, IsEnum } from 'class-validator'; import { IsOptional, IsString, IsEnum } from 'class-validator';
import { PaginationDto } from '../../common/dto/pagination.dto';
export class GetProductsDto { export class GetProductsDto extends PaginationDto {
@ApiPropertyOptional({ description: 'Filter by category slug' }) @ApiPropertyOptional({ description: 'فیلتر بر اساس اسلاگ دسته‌بندی' })
@IsOptional() @IsOptional()
@IsString() @IsString()
category?: string; category?: string;
@ApiPropertyOptional({ description: 'Filter by pet type', enum: ['سگ', 'گربه', 'all'] }) @ApiPropertyOptional({ description: 'فیلتر بر اساس نوع حیوان', enum: ['سگ', 'گربه', 'all'] })
@IsOptional() @IsOptional()
@IsEnum(['سگ', 'گربه', 'all']) @IsEnum(['سگ', 'گربه', 'all'])
petType?: string; petType?: string;
@ApiPropertyOptional({ description: 'Search query string' })
@IsOptional()
@IsString()
query?: string;
} }

View File

@ -25,28 +25,36 @@ export class ProductsController {
@ApiOkResponse({ @ApiOkResponse({
description: 'لیست محصولات متناسب با فیلترها (دسته، پت و جستجو)', description: 'لیست محصولات متناسب با فیلترها (دسته، پت و جستجو)',
schema: { schema: {
example: [ example: {
{ data: [
id: 'a1b2c3d4-1234-5678-abcd-ef1234567890', {
artNo: 'canhydrox-gag', id: 'a1b2c3d4-1234-5678-abcd-ef1234567890',
name: 'Canhydrox GAG (کنهیدروکس)', artNo: 'canhydrox-gag',
scientificTagline: 'برای تقویت مفاصل و استخوان‌ها', name: 'Canhydrox GAG (کنهیدروکس)',
description: 'کنهیدروکس محصولی بی‌نظیر برای مفاصل و سیستم حرکتی سگ‌ها...', scientificTagline: 'برای تقویت مفاصل و استخوان‌ها',
shortDescription: 'تقویت مفاصل و غضروف‌ها', description: 'کنهیدروکس محصولی بی‌نظیر برای مفاصل و سیستم حرکتی سگ‌ها...',
category: 'سیستم حرکتی و مفاصل', shortDescription: 'تقویت مفاصل و غضروف‌ها',
categorySlug: 'joints', category: 'سیستم حرکتی و مفاصل',
priceValue: '1750000.00', categorySlug: 'joints',
priceDisplay: '۱,۷۵۰,۰۰۰ تومان', priceValue: '1750000.00',
unit: 'عدد قرص', priceDisplay: '۱,۷۵۰,۰۰۰ تومان',
packageSize: '120.00', unit: 'عدد قرص',
dosageLogic: 'یک قرص به ازای هر ۱۰ کیلوگرم وزن بدن سگ', packageSize: '120.00',
suitableFor: 'سگ', dosageLogic: 'یک قرص به ازای هر ۱۰ کیلوگرم وزن بدن سگ',
imageUrl: 'https://example.com/canhydrox.png', suitableFor: 'سگ',
createdAt: '2026-05-26T18:10:00.000Z', imageUrl: 'https://example.com/canhydrox.png',
ingredients: [], createdAt: '2026-05-26T18:10:00.000Z',
symptoms: [] ingredients: [],
symptoms: []
}
],
meta: {
total: 1,
page: 1,
lastPage: 1,
limit: 10
} }
] }
} }
}) })
findAll(@Query() query: GetProductsDto) { findAll(@Query() query: GetProductsDto) {

View File

@ -7,7 +7,7 @@ export class ProductsService {
constructor(private prisma: PrismaService) {} constructor(private prisma: PrismaService) {}
async findAll(filters: GetProductsDto) { 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 = {}; const whereClause: any = {};
@ -19,22 +19,38 @@ export class ProductsService {
whereClause.suitableFor = { in: [petType, 'هر دو'] }; whereClause.suitableFor = { in: [petType, 'هر دو'] };
} }
if (query) { if (search) {
whereClause.OR = [ whereClause.OR = [
{ name: { contains: query, mode: 'insensitive' } }, { name: { contains: search, mode: 'insensitive' } },
{ description: { contains: query, mode: 'insensitive' } }, { description: { contains: search, mode: 'insensitive' } },
]; ];
} }
const products = await this.prisma.product.findMany({ const skip = (page - 1) * limit;
where: whereClause,
include: {
ingredients: true,
symptoms: true,
},
});
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) { async findOne(idOrSlug: string) {

View File

@ -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 { WikiService } from './wiki.service';
import { ApiTags, ApiOperation, ApiResponse, ApiOkResponse, ApiNotFoundResponse } from '@nestjs/swagger'; import { ApiTags, ApiOperation, ApiResponse, ApiOkResponse, ApiNotFoundResponse } from '@nestjs/swagger';
import { PaginationDto } from '../common/dto/pagination.dto';
@ApiTags('Wiki - دانشنامه ترکیبات') @ApiTags('Wiki - دانشنامه ترکیبات')
@Controller('wiki') @Controller('wiki')
@ -13,9 +14,9 @@ export class WikiController {
@Get() @Get()
@ApiOperation({ summary: 'دریافت تمام کلمات دانشنامه' }) @ApiOperation({ summary: 'دریافت تمام کلمات دانشنامه' })
@ApiOkResponse({ description: 'لیست تمامی ترکیبات علمی' }) @ApiOkResponse({ description: 'لیست کلمات به همراه توضیحات (صفحه‌بندی شده)' })
findAll() { findAll(@Query() query: PaginationDto) {
return this.wikiService.findAll(); return this.wikiService.findAll(query);
} }
@Get(':key') @Get(':key')

View File

@ -1,14 +1,43 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import { PaginationDto } from '../common/dto/pagination.dto';
@Injectable() @Injectable()
export class WikiService { export class WikiService {
constructor(private prisma: PrismaService) {} constructor(private prisma: PrismaService) {}
async findAll() { async findAll(filters: PaginationDto) {
return this.prisma.scientificTerm.findMany({ const { search, page = 1, limit = 10, sortBy = 'term', sortOrder = 'asc' } = filters;
orderBy: { term: 'asc' }
}); 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) { async findOneByKey(key: string) {

View File

@ -34,7 +34,7 @@ export default function Blogs() {
const res = await api.get('/admin/blogs', { params: { page, limit, search } }); const res = await api.get('/admin/blogs', { params: { page, limit, search } });
if (res.data?.data) { if (res.data?.data) {
setBlogs(res.data.data); setBlogs(res.data.data);
setTotalPages(res.data.meta?.totalPages || 1); setTotalPages(res.data.meta?.lastPage || 1);
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);

View File

@ -33,7 +33,7 @@ export default function Categories() {
const res = await api.get('/admin/categories', { params: { page, limit, search } }); const res = await api.get('/admin/categories', { params: { page, limit, search } });
if (res.data?.data) { if (res.data?.data) {
setCategories(res.data.data); setCategories(res.data.data);
setTotalPages(res.data.meta?.totalPages || 1); setTotalPages(res.data.meta?.lastPage || 1);
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);

View File

@ -25,7 +25,7 @@ export default function Coupons() {
const res = await api.get('/admin/coupons', { params: { page, limit, search } }); const res = await api.get('/admin/coupons', { params: { page, limit, search } });
if (res.data?.data) { if (res.data?.data) {
setCoupons(res.data.data); setCoupons(res.data.data);
setTotalPages(res.data.meta?.totalPages || 1); setTotalPages(res.data.meta?.lastPage || 1);
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);

View File

@ -105,7 +105,7 @@ export default function Dashboard() {
paddingAngle={5} paddingAngle={5}
dataKey="value" dataKey="value"
> >
{pieData.map((entry, index) => ( {pieData.map((_entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} /> <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))} ))}
</Pie> </Pie>

View File

@ -35,7 +35,7 @@ export default function Orders() {
const response = await api.get(`/admin/orders?${params.toString()}`); const response = await api.get(`/admin/orders?${params.toString()}`);
if (response.data?.success) { if (response.data?.success) {
setOrders(response.data.data); setOrders(response.data.data);
setTotalPages(response.data.meta?.totalPages || 1); setTotalPages(response.data.meta?.lastPage || 1);
} }
} catch (err) { } catch (err) {
console.error('Failed to fetch orders', err); console.error('Failed to fetch orders', err);

View File

@ -18,7 +18,7 @@ export default function Pets() {
const res = await api.get('/admin/pets', { params: { page, limit, search } }); const res = await api.get('/admin/pets', { params: { page, limit, search } });
if (res.data?.data) { if (res.data?.data) {
setPets(res.data.data); setPets(res.data.data);
setTotalPages(res.data.meta?.totalPages || 1); setTotalPages(res.data.meta?.lastPage || 1);
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);

View File

@ -52,7 +52,7 @@ export default function Products() {
const prodRes = await api.get('/admin/products', { params: { page, limit, search, categoryId: categoryFilter } }); const prodRes = await api.get('/admin/products', { params: { page, limit, search, categoryId: categoryFilter } });
if (prodRes.data?.data) { if (prodRes.data?.data) {
setProducts(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 // Fetch categories separately so it doesn't break products

View File

@ -31,7 +31,7 @@ export default function Users() {
const response = await api.get(`/admin/users?${params.toString()}`); const response = await api.get(`/admin/users?${params.toString()}`);
if (response.data?.success) { if (response.data?.success) {
setUsers(response.data.data); setUsers(response.data.data);
setTotalPages(response.data.meta?.totalPages || 1); setTotalPages(response.data.meta?.lastPage || 1);
} }
} catch (err) { } catch (err) {
console.error('Failed to fetch users', err); console.error('Failed to fetch users', err);

View File

@ -31,7 +31,7 @@ export default function Wiki() {
const res = await api.get('/admin/wiki', { params: { page, limit, search } }); const res = await api.get('/admin/wiki', { params: { page, limit, search } });
if (res.data?.data) { if (res.data?.data) {
setTerms(res.data.data); setTerms(res.data.data);
setTotalPages(res.data.meta?.totalPages || 1); setTotalPages(res.data.meta?.lastPage || 1);
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);

View File

@ -55,7 +55,7 @@ export class OrderService {
public async getUserOrders(): Promise<Order[]> { public async getUserOrders(): Promise<Order[]> {
try { try {
const response = await api.get('/orders'); const response = await api.get('/orders');
return response.data; return response.data.data;
} catch (error: any) { } catch (error: any) {
const message = error.response?.data?.message || 'خطا در دریافت لیست سفارش‌ها'; const message = error.response?.data?.message || 'خطا در دریافت لیست سفارش‌ها';
throw new Error(Array.isArray(message) ? message[0] : message); throw new Error(Array.isArray(message) ? message[0] : message);

View File

@ -75,7 +75,7 @@ export class ProductService {
try { try {
const response = await api.get(`/products?${params.toString()}`); 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) { } catch (error) {
console.error("[ProductService] Failed to fetch products:", error); console.error("[ProductService] Failed to fetch products:", error);
return []; return [];
@ -105,7 +105,7 @@ export class ProductService {
public async getFeaturedProducts(): Promise<Product[]> { public async getFeaturedProducts(): Promise<Product[]> {
try { try {
const response = await api.get('/products'); 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) { } catch (error) {
console.error("[ProductService] Failed to fetch featured products:", error); console.error("[ProductService] Failed to fetch featured products:", error);
return []; return [];

View File

@ -3,7 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 4000", "dev": "next dev -p 4001",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "eslint" "lint": "eslint"