This commit is contained in:
2026-09-30 09:53:05 +03:30
parent 5a2c4cabfd
commit 38cabb0957
19 changed files with 397 additions and 72 deletions
@@ -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;
}
+29 -3
View File
@@ -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()
+34 -22
View File
@@ -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,