8/7/405
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import { IsOptional, IsString, MinLength } from 'class-validator';
|
||||
|
||||
export class ProductColorJsonItemDto {
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
imageUrl?: string;
|
||||
}
|
||||
|
||||
/** نرمالسازی colorsJson (رشتهٔ legacy یا آبجکت ادمین) قبل از اعتبارسنجی */
|
||||
export function transformColorsJson(value: unknown): ProductColorJsonItemDto[] | undefined {
|
||||
if (value === undefined || value === null) return undefined;
|
||||
if (!Array.isArray(value)) return undefined;
|
||||
|
||||
const out: ProductColorJsonItemDto[] = [];
|
||||
for (const item of value) {
|
||||
if (typeof item === 'string') {
|
||||
const name = item.trim();
|
||||
if (name) out.push({ name });
|
||||
continue;
|
||||
}
|
||||
if (item && typeof item === 'object') {
|
||||
const raw = item as Record<string, unknown>;
|
||||
const name = String(raw.name ?? '').trim();
|
||||
if (!name) continue;
|
||||
const imageUrl =
|
||||
typeof raw.imageUrl === 'string' && raw.imageUrl.trim()
|
||||
? raw.imageUrl.trim()
|
||||
: undefined;
|
||||
out.push({ name, imageUrl });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -1,12 +1,18 @@
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Min,
|
||||
MinLength,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import {
|
||||
ProductColorJsonItemDto,
|
||||
transformColorsJson,
|
||||
} from './product-color-json.dto';
|
||||
|
||||
export class CreateProductDto {
|
||||
@IsString()
|
||||
@@ -15,6 +21,10 @@ export class CreateProductDto {
|
||||
@IsString()
|
||||
catalogId!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
brandId?: string | null;
|
||||
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
name!: string;
|
||||
@@ -128,9 +138,15 @@ export class CreateProductDto {
|
||||
isNew?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
colorsJson?: Array<string | { name: string; imageUrl?: string }>;
|
||||
@Transform(({ value }) => transformColorsJson(value))
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => ProductColorJsonItemDto)
|
||||
colorsJson?: ProductColorJsonItemDto[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
imageUrlsJson?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@@ -142,6 +158,10 @@ export class UpdateProductDto {
|
||||
@IsString()
|
||||
catalogId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
brandId?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@@ -253,9 +273,15 @@ export class UpdateProductDto {
|
||||
isNew?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
colorsJson?: Array<string | { name: string; imageUrl?: string }>;
|
||||
@Transform(({ value }) => transformColorsJson(value))
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => ProductColorJsonItemDto)
|
||||
colorsJson?: ProductColorJsonItemDto[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
imageUrlsJson?: string[];
|
||||
|
||||
@IsOptional()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
@@ -9,6 +10,12 @@ import { CreateProductDto, UpdateProductDto } from './dto/product.dto';
|
||||
import { loadCatalogCategories } from '../catalog/category-defs';
|
||||
import { mergeProductImages, parseColorEntries } from './color-entries';
|
||||
|
||||
const productInclude = {
|
||||
company: { select: { name: true } },
|
||||
catalog: { select: { code: true, name: true } },
|
||||
brand: { select: { id: true, name: true } },
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class ProductsService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
@@ -40,10 +47,7 @@ export class ProductsService {
|
||||
...(filters.season ? { season: filters.season as never } : {}),
|
||||
},
|
||||
orderBy: [{ isNew: 'desc' }, { name: 'asc' }, { color: 'asc' }],
|
||||
include: {
|
||||
company: { select: { name: true } },
|
||||
catalog: { select: { code: true, name: true } },
|
||||
},
|
||||
include: productInclude,
|
||||
});
|
||||
const hidePrice = await this.shouldHidePublicPrice(filters.hidePublicPrice);
|
||||
return products.map((p) => this.map(p, hidePrice));
|
||||
@@ -52,10 +56,7 @@ export class ProductsService {
|
||||
async findOne(id: string, hidePublicPrice?: boolean) {
|
||||
const product = await this.prisma.product.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
company: { select: { name: true } },
|
||||
catalog: { select: { code: true, name: true } },
|
||||
},
|
||||
include: productInclude,
|
||||
});
|
||||
if (!product) throw new NotFoundException('محصول یافت نشد');
|
||||
const hidePrice = await this.shouldHidePublicPrice(hidePublicPrice);
|
||||
@@ -84,12 +85,14 @@ export class ProductsService {
|
||||
|
||||
const code = (dto.code || dto.productCode || '').trim();
|
||||
if (!code) throw new ConflictException('کد محصول الزامی است');
|
||||
const brandId = await this.resolveBrandId(dto.brandId ?? null);
|
||||
|
||||
try {
|
||||
const product = await this.prisma.product.create({
|
||||
data: {
|
||||
companyId: dto.companyId,
|
||||
catalogId: dto.catalogId,
|
||||
brandId,
|
||||
name: dto.name.trim(),
|
||||
code,
|
||||
color: dto.color.trim(),
|
||||
@@ -108,14 +111,13 @@ export class ProductsService {
|
||||
subtitle: dto.subtitle?.trim() || null,
|
||||
rollMeters: dto.rollMeters,
|
||||
isNew: dto.isNew,
|
||||
colorsJson: (dto.colorsJson ?? undefined) as Prisma.InputJsonValue | undefined,
|
||||
colorsJson: (dto.colorsJson ?? undefined) as unknown as
|
||||
| Prisma.InputJsonValue
|
||||
| undefined,
|
||||
imageUrlsJson: (dto.imageUrlsJson ?? undefined) as Prisma.InputJsonValue | undefined,
|
||||
tagsJson: dto.tagsJson ?? undefined,
|
||||
},
|
||||
include: {
|
||||
company: { select: { name: true } },
|
||||
catalog: { select: { code: true, name: true } },
|
||||
},
|
||||
include: productInclude,
|
||||
});
|
||||
return this.map(product);
|
||||
} catch (e) {
|
||||
@@ -137,12 +139,14 @@ export class ProductsService {
|
||||
: dto.productCode !== undefined
|
||||
? dto.productCode.trim()
|
||||
: undefined;
|
||||
const brandId = await this.resolveBrandId(dto.brandId);
|
||||
|
||||
try {
|
||||
const product = await this.prisma.product.update({
|
||||
where: { id },
|
||||
data: {
|
||||
catalogId: dto.catalogId,
|
||||
...(brandId !== undefined ? { brandId } : {}),
|
||||
name: dto.name?.trim(),
|
||||
code,
|
||||
color: dto.color?.trim(),
|
||||
@@ -192,17 +196,14 @@ export class ProductsService {
|
||||
colorsJson:
|
||||
dto.colorsJson === undefined
|
||||
? undefined
|
||||
: (dto.colorsJson as Prisma.InputJsonValue),
|
||||
: (dto.colorsJson as unknown as Prisma.InputJsonValue),
|
||||
imageUrlsJson:
|
||||
dto.imageUrlsJson === undefined
|
||||
? undefined
|
||||
: (dto.imageUrlsJson as Prisma.InputJsonValue),
|
||||
tagsJson: dto.tagsJson,
|
||||
},
|
||||
include: {
|
||||
company: { select: { name: true } },
|
||||
catalog: { select: { code: true, name: true } },
|
||||
},
|
||||
include: productInclude,
|
||||
});
|
||||
return this.map(product);
|
||||
} catch (e) {
|
||||
@@ -221,10 +222,7 @@ export class ProductsService {
|
||||
const product = await this.prisma.product.update({
|
||||
where: { id },
|
||||
data: { isActive },
|
||||
include: {
|
||||
company: { select: { name: true } },
|
||||
catalog: { select: { code: true, name: true } },
|
||||
},
|
||||
include: productInclude,
|
||||
});
|
||||
return this.map(product);
|
||||
}
|
||||
@@ -277,6 +275,17 @@ export class ProductsService {
|
||||
return product;
|
||||
}
|
||||
|
||||
private async resolveBrandId(
|
||||
brandId?: string | null,
|
||||
): Promise<string | null | undefined> {
|
||||
if (brandId === undefined) return undefined;
|
||||
const id = brandId?.trim() || null;
|
||||
if (!id) return null;
|
||||
const brand = await this.prisma.brand.findUnique({ where: { id } });
|
||||
if (!brand) throw new BadRequestException('برند یافت نشد');
|
||||
return id;
|
||||
}
|
||||
|
||||
private map(
|
||||
product: {
|
||||
id: string;
|
||||
@@ -295,6 +304,7 @@ export class ProductsService {
|
||||
updatedAt: Date;
|
||||
company: { name: string };
|
||||
catalog: { code: string; name: string };
|
||||
brand?: { id: string; name: string } | null;
|
||||
usageCategory?: string;
|
||||
season?: string;
|
||||
widthCm?: number | null;
|
||||
@@ -325,6 +335,8 @@ export class ProductsService {
|
||||
catalogId: product.catalogId,
|
||||
catalogCode: product.catalog.code,
|
||||
catalogName: product.catalog.name,
|
||||
brandId: product.brand?.id ?? null,
|
||||
brandName: product.brand?.name ?? null,
|
||||
name: product.name,
|
||||
code: product.code,
|
||||
productCode: product.code,
|
||||
|
||||
Reference in New Issue
Block a user