feat: complete backend swagger annotations, admin pages pagination, and frontend application fixes
This commit is contained in:
parent
24137beb32
commit
2851ef8a25
@ -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<string, string>) {
|
||||
const settings = await this.adminService.updateSettings(data);
|
||||
return { success: true, data: settings };
|
||||
|
||||
@ -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)) }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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)) }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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)) }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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)) }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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)) }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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) {
|
||||
|
||||
39
backend/src/common/dto/pagination.dto.ts
Normal file
39
backend/src/common/dto/pagination.dto.ts
Normal 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;
|
||||
}
|
||||
@ -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 : {}
|
||||
});
|
||||
}
|
||||
|
||||
15
backend/src/common/schemas/error-response.schema.ts
Normal file
15
backend/src/common/schemas/error-response.schema.ts
Normal 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;
|
||||
}
|
||||
43
backend/src/common/schemas/paginated-response.schema.ts
Normal file
43
backend/src/common/schemas/paginated-response.schema.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -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')
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -105,7 +105,7 @@ export default function Dashboard() {
|
||||
paddingAngle={5}
|
||||
dataKey="value"
|
||||
>
|
||||
{pieData.map((entry, index) => (
|
||||
{pieData.map((_entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -55,7 +55,7 @@ export class OrderService {
|
||||
public async getUserOrders(): Promise<Order[]> {
|
||||
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);
|
||||
|
||||
@ -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<Product[]> {
|
||||
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 [];
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user