diff --git a/backend/src/admin/admin.controller.ts b/backend/src/admin/admin.controller.ts index 2de1352..994b041 100644 --- a/backend/src/admin/admin.controller.ts +++ b/backend/src/admin/admin.controller.ts @@ -9,7 +9,8 @@ import { Query, UseGuards, } from '@nestjs/common'; -import { AdminService, ProductInput, CouponInput } from './admin.service'; +import { AdminService, CouponInput } from './admin.service'; +import { ProductDto } from './dto/product.dto'; import { AdminQueryDto } from './dto/admin-query.dto'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { @@ -127,14 +128,14 @@ export class AdminController { @Post('products') @ApiOperation({ summary: 'ایجاد محصول جدید' }) - async createProduct(@Body() data: ProductInput) { + async createProduct(@Body() data: ProductDto) { const product = await this.adminService.createProduct(data); return { success: true, data: product }; } @Put('products/:id') @ApiOperation({ summary: 'ویرایش محصول' }) - async updateProduct(@Param('id') id: string, @Body() data: ProductInput) { + async updateProduct(@Param('id') id: string, @Body() data: ProductDto) { const product = await this.adminService.updateProduct(id, data); return { success: true, diff --git a/backend/src/admin/admin.service.ts b/backend/src/admin/admin.service.ts index d0bdbbb..6d2793f 100644 --- a/backend/src/admin/admin.service.ts +++ b/backend/src/admin/admin.service.ts @@ -9,6 +9,7 @@ import { PrismaService } from '../prisma/prisma.service'; import { RedisService } from '../redis/redis.service'; import * as bcrypt from 'bcryptjs'; import { CreateUserDto, UpdateUserDto } from './dto/user.dto'; +import { ProductDto } from './dto/product.dto'; import { normalizeMobile } from '../common/utils/phone.utils'; import { CANONICAL_ALIASES } from '../settings/settings.service'; @@ -21,34 +22,6 @@ export class PaginationQuery { status?: string; } -export class ProductInput { - artNo?: string; - nameFa?: string; - nameEn?: string; - scientificTagline?: string; - description?: string; - shortDescription?: string; - categoryId?: string; - categorySlug?: string; - priceValue?: number; - priceDisplay?: string; - unit?: string; - packageSize?: number; - dosageLogic?: string; - suitableFor?: string; - imageUrl?: string; - images?: string | string[]; - podcastUrl?: string | null; - videoUrl?: string | null; - pdfUrl?: string | null; - metaTitle?: string; - metaDescription?: string; - keywords?: string; - canonicalUrl?: string; - slug?: string; - symptoms?: string[]; -} - export class CouponTargetInput { targetType!: string; targetId!: string; @@ -333,7 +306,7 @@ export class AdminService { } } - async createProduct(data: ProductInput) { + async createProduct(data: ProductDto) { const product = await this.prisma.product.create({ data: { artNo: data.artNo || `ART-${Date.now()}`, @@ -344,7 +317,9 @@ export class AdminService { shortDescription: data.shortDescription || '', categoryId: data.categoryId || '', categorySlug: data.categorySlug || 'general', + buyPrice: data.buyPrice !== undefined ? data.buyPrice : 0, priceValue: data.priceValue || 0, + wholesalePrice: data.wholesalePrice !== undefined ? data.wholesalePrice : null, priceDisplay: data.priceDisplay || '', unit: data.unit || 'عدد', packageSize: data.packageSize || 100, @@ -381,7 +356,7 @@ export class AdminService { return product; } - async updateProduct(id: string, data: ProductInput) { + async updateProduct(id: string, data: ProductDto) { const existing = await this.prisma.product.findUnique({ where: { id } }); if (!existing) { throw new NotFoundException(`محصولی با شناسه ${id} یافت نشد.`); @@ -398,7 +373,9 @@ export class AdminService { shortDescription: data.shortDescription, categoryId: data.categoryId, categorySlug: data.categorySlug, + buyPrice: data.buyPrice !== undefined ? data.buyPrice : undefined, priceValue: data.priceValue, + wholesalePrice: data.wholesalePrice !== undefined ? data.wholesalePrice : undefined, priceDisplay: data.priceDisplay, unit: data.unit, packageSize: data.packageSize, diff --git a/backend/src/admin/dto/product.dto.ts b/backend/src/admin/dto/product.dto.ts new file mode 100644 index 0000000..03799b6 --- /dev/null +++ b/backend/src/admin/dto/product.dto.ts @@ -0,0 +1,165 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsString, + IsOptional, + IsNumber, + IsArray, + IsBoolean, +} from 'class-validator'; + +export class ProductDto { + @ApiPropertyOptional({ description: 'شماره آرتیکل محصول' }) + @IsOptional() + @IsString() + artNo?: string; + + @ApiPropertyOptional({ description: 'نام فارسی محصول' }) + @IsOptional() + @IsString() + nameFa?: string; + + @ApiPropertyOptional({ description: 'نام انگلیسی محصول' }) + @IsOptional() + @IsString() + nameEn?: string; + + @ApiPropertyOptional({ description: 'شعار علمی محصول' }) + @IsOptional() + @IsString() + scientificTagline?: string; + + @ApiPropertyOptional({ description: 'توضیحات کامل محصول' }) + @IsOptional() + @IsString() + description?: string; + + @ApiPropertyOptional({ description: 'توضیحات کوتاه محصول' }) + @IsOptional() + @IsString() + shortDescription?: string; + + @ApiPropertyOptional({ description: 'شناسه دسته‌بندی' }) + @IsOptional() + @IsString() + categoryId?: string; + + @ApiPropertyOptional({ description: 'اسلاگ دسته‌بندی' }) + @IsOptional() + @IsString() + categorySlug?: string; + + @ApiPropertyOptional({ description: 'قیمت خرید محصول' }) + @IsOptional() + @IsNumber() + buyPrice?: number; + + @ApiPropertyOptional({ description: 'قیمت فروش محصول' }) + @IsOptional() + @IsNumber() + priceValue?: number; + + @ApiPropertyOptional({ description: 'قیمت عمده‌فروشی محصول' }) + @IsOptional() + @IsNumber() + wholesalePrice?: number; + + @ApiPropertyOptional({ description: 'درصد سود قیمت مصرف‌کننده' }) + @IsOptional() + @IsNumber() + priceValueMarginPercent?: number; + + @ApiPropertyOptional({ description: 'درصد سود قیمت عمده‌فروشی' }) + @IsOptional() + @IsNumber() + wholesaleMarginPercent?: number; + + @ApiPropertyOptional({ description: 'نمایش متنی قیمت' }) + @IsOptional() + @IsString() + priceDisplay?: string; + + @ApiPropertyOptional({ description: 'واحد شمارش محصول' }) + @IsOptional() + @IsString() + unit?: string; + + @ApiPropertyOptional({ description: 'اندازه و وزن بسته' }) + @IsOptional() + @IsNumber() + packageSize?: number; + + @ApiPropertyOptional({ description: 'منطق دوزبندی و نحوه مصرف' }) + @IsOptional() + @IsString() + dosageLogic?: string; + + @ApiPropertyOptional({ description: 'مناسب برای گونه حیوان' }) + @IsOptional() + @IsString() + suitableFor?: string; + + @ApiPropertyOptional({ description: 'لینک تصویر اصلی محصول' }) + @IsOptional() + @IsString() + imageUrl?: string; + + @ApiPropertyOptional({ description: 'گالری تصاویر محصول' }) + @IsOptional() + @IsArray() + images?: string[]; + + @ApiPropertyOptional({ description: 'لینک پادکست' }) + @IsOptional() + @IsString() + podcastUrl?: string | null; + + @ApiPropertyOptional({ description: 'لینک ویدیو' }) + @IsOptional() + @IsString() + videoUrl?: string | null; + + @ApiPropertyOptional({ description: 'لینک فایل PDF' }) + @IsOptional() + @IsString() + pdfUrl?: string | null; + + @ApiPropertyOptional({ description: 'عنوان سئو' }) + @IsOptional() + @IsString() + metaTitle?: string; + + @ApiPropertyOptional({ description: 'توضیحات متای سئو' }) + @IsOptional() + @IsString() + metaDescription?: string; + + @ApiPropertyOptional({ description: 'کلمات کلیدی سئو' }) + @IsOptional() + @IsString() + keywords?: string; + + @ApiPropertyOptional({ description: 'آدرس کانونیکال سئو' }) + @IsOptional() + @IsString() + canonicalUrl?: string; + + @ApiPropertyOptional({ description: 'اسلاگ محصول' }) + @IsOptional() + @IsString() + slug?: string; + + @ApiPropertyOptional({ description: 'علائم درمانی مرتبط' }) + @IsOptional() + @IsArray() + symptoms?: string[]; + + @ApiPropertyOptional({ description: 'آیا پیش‌خرید فعال است؟' }) + @IsOptional() + @IsBoolean() + isPreorder?: boolean; + + @ApiPropertyOptional({ description: 'مبلغ بیعانه پیش‌خرید' }) + @IsOptional() + @IsString() + preorderDeposit?: string; +}