fix bug of edit product
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
-- Remove Catalog as independent entity; Product is the sole merchandise unit.
|
||||||
|
ALTER TABLE "Product" DROP CONSTRAINT IF EXISTS "Product_catalogId_fkey";
|
||||||
|
DROP INDEX IF EXISTS "Product_catalogId_idx";
|
||||||
|
ALTER TABLE "Product" DROP COLUMN IF EXISTS "catalogId";
|
||||||
|
DROP TABLE IF EXISTS "Catalog";
|
||||||
+1
-23
@@ -143,7 +143,6 @@ model Company {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
bankAccounts BankAccount[]
|
bankAccounts BankAccount[]
|
||||||
catalogs Catalog[]
|
|
||||||
products Product[]
|
products Product[]
|
||||||
orders Order[]
|
orders Order[]
|
||||||
customers Customer[]
|
customers Customer[]
|
||||||
@@ -169,35 +168,16 @@ model BankAccount {
|
|||||||
@@index([companyId])
|
@@index([companyId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Catalog {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
companyId String
|
|
||||||
name String
|
|
||||||
code String
|
|
||||||
description String?
|
|
||||||
imageUrl String?
|
|
||||||
displayOrder Int @default(0)
|
|
||||||
isActive Boolean @default(true)
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
|
|
||||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
|
||||||
products Product[]
|
|
||||||
|
|
||||||
@@unique([companyId, code])
|
|
||||||
@@index([companyId])
|
|
||||||
}
|
|
||||||
|
|
||||||
model Product {
|
model Product {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
companyId String
|
companyId String
|
||||||
catalogId String
|
|
||||||
name String
|
name String
|
||||||
code String
|
code String
|
||||||
color String
|
color String
|
||||||
description String?
|
description String?
|
||||||
minMeterage Float @default(30)
|
minMeterage Float @default(30)
|
||||||
orderMultiplier Float @default(10)
|
orderMultiplier Float @default(10)
|
||||||
|
/// قیمت هر متر (تومان) برای نمایش کاتالوگ / پیشفاکتور
|
||||||
basePrice Float?
|
basePrice Float?
|
||||||
stock Float?
|
stock Float?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
@@ -219,7 +199,6 @@ model Product {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||||
catalog Catalog @relation(fields: [catalogId], references: [id], onDelete: Cascade)
|
|
||||||
brand Brand? @relation(fields: [brandId], references: [id], onDelete: SetNull)
|
brand Brand? @relation(fields: [brandId], references: [id], onDelete: SetNull)
|
||||||
cartItems CartItem[]
|
cartItems CartItem[]
|
||||||
orderItems OrderItem[]
|
orderItems OrderItem[]
|
||||||
@@ -229,7 +208,6 @@ model Product {
|
|||||||
productSuggestions ProductSuggestion[]
|
productSuggestions ProductSuggestion[]
|
||||||
|
|
||||||
@@unique([companyId, code])
|
@@unique([companyId, code])
|
||||||
@@index([catalogId])
|
|
||||||
@@index([companyId])
|
@@index([companyId])
|
||||||
@@index([brandId])
|
@@index([brandId])
|
||||||
@@index([usageCategory])
|
@@index([usageCategory])
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ async function main() {
|
|||||||
await prisma.otpChallenge.deleteMany();
|
await prisma.otpChallenge.deleteMany();
|
||||||
await prisma.customerNotification.deleteMany();
|
await prisma.customerNotification.deleteMany();
|
||||||
await prisma.product.deleteMany();
|
await prisma.product.deleteMany();
|
||||||
await prisma.catalog.deleteMany();
|
|
||||||
await prisma.bankAccount.deleteMany();
|
await prisma.bankAccount.deleteMany();
|
||||||
await prisma.levelBenefit.deleteMany();
|
await prisma.levelBenefit.deleteMany();
|
||||||
await prisma.customerLevelDef.deleteMany();
|
await prisma.customerLevelDef.deleteMany();
|
||||||
@@ -130,24 +129,11 @@ async function main() {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
await prisma.catalog.createMany({
|
|
||||||
data: [
|
|
||||||
{ id: 'cat-377', companyId: 'company-par', name: 'کالیته ۳۷۷', code: '377', displayOrder: 1, isActive: true },
|
|
||||||
{ id: 'cat-378', companyId: 'company-par', name: 'کالیته ۳۷۸', code: '378', displayOrder: 2, isActive: true },
|
|
||||||
{ id: 'cat-379', companyId: 'company-par', name: 'کالیته ۳۷۹', code: '379', displayOrder: 3, isActive: true },
|
|
||||||
{ id: 'cat-380', companyId: 'company-par', name: 'کالیته ۳۸۰', code: '380', displayOrder: 4, isActive: true },
|
|
||||||
{ id: 'cat-381', companyId: 'company-par', name: 'کالیته ۳۸۱', code: '381', displayOrder: 5, isActive: true },
|
|
||||||
{ id: 'cat-fam-401', companyId: 'company-fam-baft', name: 'کالیته ۴۰۱', code: '401', displayOrder: 1, isActive: true },
|
|
||||||
{ id: 'cat-fam-402', companyId: 'company-fam-baft', name: 'کالیته ۴۰۲', code: '402', displayOrder: 2, isActive: true },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
await prisma.product.createMany({
|
await prisma.product.createMany({
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
id: 'prod-377-blue',
|
id: 'prod-377-blue',
|
||||||
companyId: 'company-par',
|
companyId: 'company-par',
|
||||||
catalogId: 'cat-377',
|
|
||||||
name: 'فاستونی ایتالیایی',
|
name: 'فاستونی ایتالیایی',
|
||||||
code: '377-BL',
|
code: '377-BL',
|
||||||
color: 'آبی',
|
color: 'آبی',
|
||||||
@@ -171,7 +157,6 @@ async function main() {
|
|||||||
{
|
{
|
||||||
id: 'prod-377-black',
|
id: 'prod-377-black',
|
||||||
companyId: 'company-par',
|
companyId: 'company-par',
|
||||||
catalogId: 'cat-377',
|
|
||||||
name: 'پارچه مشکی رسمی',
|
name: 'پارچه مشکی رسمی',
|
||||||
code: '377-BK',
|
code: '377-BK',
|
||||||
color: 'مشکی',
|
color: 'مشکی',
|
||||||
@@ -195,7 +180,6 @@ async function main() {
|
|||||||
{
|
{
|
||||||
id: 'prod-378-blue',
|
id: 'prod-378-blue',
|
||||||
companyId: 'company-par',
|
companyId: 'company-par',
|
||||||
catalogId: 'cat-378',
|
|
||||||
name: 'پیراهنی نخی آبی',
|
name: 'پیراهنی نخی آبی',
|
||||||
code: '378-BL',
|
code: '378-BL',
|
||||||
color: 'آبی',
|
color: 'آبی',
|
||||||
@@ -219,7 +203,6 @@ async function main() {
|
|||||||
{
|
{
|
||||||
id: 'prod-378-black',
|
id: 'prod-378-black',
|
||||||
companyId: 'company-par',
|
companyId: 'company-par',
|
||||||
catalogId: 'cat-378',
|
|
||||||
name: 'پیراهنی مشکی',
|
name: 'پیراهنی مشکی',
|
||||||
code: '378-BK',
|
code: '378-BK',
|
||||||
color: 'مشکی',
|
color: 'مشکی',
|
||||||
@@ -242,7 +225,6 @@ async function main() {
|
|||||||
{
|
{
|
||||||
id: 'prod-378-blue-white',
|
id: 'prod-378-blue-white',
|
||||||
companyId: 'company-par',
|
companyId: 'company-par',
|
||||||
catalogId: 'cat-378',
|
|
||||||
name: 'آبی پشت سفید',
|
name: 'آبی پشت سفید',
|
||||||
code: '378-BLW',
|
code: '378-BLW',
|
||||||
color: 'آبی پشت سفید',
|
color: 'آبی پشت سفید',
|
||||||
@@ -265,7 +247,6 @@ async function main() {
|
|||||||
{
|
{
|
||||||
id: 'prod-378-black-white',
|
id: 'prod-378-black-white',
|
||||||
companyId: 'company-par',
|
companyId: 'company-par',
|
||||||
catalogId: 'cat-378',
|
|
||||||
name: 'مشکی پشت سفید',
|
name: 'مشکی پشت سفید',
|
||||||
code: '378-BKW',
|
code: '378-BKW',
|
||||||
color: 'مشکی پشت سفید',
|
color: 'مشکی پشت سفید',
|
||||||
@@ -288,7 +269,6 @@ async function main() {
|
|||||||
{
|
{
|
||||||
id: 'prod-fam-401-1',
|
id: 'prod-fam-401-1',
|
||||||
companyId: 'company-fam-baft',
|
companyId: 'company-fam-baft',
|
||||||
catalogId: 'cat-fam-401',
|
|
||||||
name: 'پارچه مخمل',
|
name: 'پارچه مخمل',
|
||||||
code: '401-MV',
|
code: '401-MV',
|
||||||
color: 'سرمهای',
|
color: 'سرمهای',
|
||||||
@@ -311,7 +291,6 @@ async function main() {
|
|||||||
{
|
{
|
||||||
id: 'prod-fam-401-2',
|
id: 'prod-fam-401-2',
|
||||||
companyId: 'company-fam-baft',
|
companyId: 'company-fam-baft',
|
||||||
catalogId: 'cat-fam-401',
|
|
||||||
name: 'پارچه کجراه',
|
name: 'پارچه کجراه',
|
||||||
code: '401-KJ',
|
code: '401-KJ',
|
||||||
color: 'کرم',
|
color: 'کرم',
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { PrismaModule } from './prisma/prisma.module';
|
|||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
import { JwtAuthGuard } from './common/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from './common/guards/jwt-auth.guard';
|
||||||
import { CompaniesModule } from './companies/companies.module';
|
import { CompaniesModule } from './companies/companies.module';
|
||||||
import { CatalogsModule } from './catalogs/catalogs.module';
|
|
||||||
import { ProductsModule } from './products/products.module';
|
import { ProductsModule } from './products/products.module';
|
||||||
import { CustomersModule } from './customers/customers.module';
|
import { CustomersModule } from './customers/customers.module';
|
||||||
import { AdminUsersModule } from './admin-users/admin-users.module';
|
import { AdminUsersModule } from './admin-users/admin-users.module';
|
||||||
@@ -35,7 +34,6 @@ import { InternalMessagesModule } from './internal-messages/internal-messages.mo
|
|||||||
PrismaModule,
|
PrismaModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
CompaniesModule,
|
CompaniesModule,
|
||||||
CatalogsModule,
|
|
||||||
ProductsModule,
|
ProductsModule,
|
||||||
CustomersModule,
|
CustomersModule,
|
||||||
AdminUsersModule,
|
AdminUsersModule,
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export class CartsService {
|
|||||||
const product = await this.prisma.product.findUnique({
|
const product = await this.prisma.product.findUnique({
|
||||||
where: { id: dto.productId },
|
where: { id: dto.productId },
|
||||||
include: {
|
include: {
|
||||||
catalog: true,
|
|
||||||
company: true,
|
company: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -54,7 +53,7 @@ export class CartsService {
|
|||||||
data: {
|
data: {
|
||||||
cartId: cart.id,
|
cartId: cart.id,
|
||||||
productId: product.id,
|
productId: product.id,
|
||||||
catalogCode: product.catalog.code,
|
catalogCode: product.code,
|
||||||
productName: product.name,
|
productName: product.name,
|
||||||
productCode: product.code,
|
productCode: product.code,
|
||||||
color: product.color,
|
color: product.color,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export class CompaniesService {
|
|||||||
orderBy: [{ displayOrder: 'asc' }, { name: 'asc' }],
|
orderBy: [{ displayOrder: 'asc' }, { name: 'asc' }],
|
||||||
include: {
|
include: {
|
||||||
_count: {
|
_count: {
|
||||||
select: { catalogs: true, products: true, bankAccounts: true },
|
select: { products: true, bankAccounts: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -40,7 +40,7 @@ export class CompaniesService {
|
|||||||
include: {
|
include: {
|
||||||
bankAccounts: { orderBy: { displayOrder: 'asc' } },
|
bankAccounts: { orderBy: { displayOrder: 'asc' } },
|
||||||
_count: {
|
_count: {
|
||||||
select: { catalogs: true, products: true, bankAccounts: true },
|
select: { products: true, bankAccounts: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -78,7 +78,7 @@ export class CompaniesService {
|
|||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
_count: {
|
_count: {
|
||||||
select: { catalogs: true, products: true, bankAccounts: true },
|
select: { products: true, bankAccounts: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -116,7 +116,7 @@ export class CompaniesService {
|
|||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
_count: {
|
_count: {
|
||||||
select: { catalogs: true, products: true, bankAccounts: true },
|
select: { products: true, bankAccounts: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -139,7 +139,7 @@ export class CompaniesService {
|
|||||||
data: { isActive },
|
data: { isActive },
|
||||||
include: {
|
include: {
|
||||||
_count: {
|
_count: {
|
||||||
select: { catalogs: true, products: true, bankAccounts: true },
|
select: { products: true, bankAccounts: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -226,7 +226,7 @@ export class CompaniesService {
|
|||||||
cashInvoiceValidUntilHour: number | null;
|
cashInvoiceValidUntilHour: number | null;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
_count?: { catalogs: number; products: number; bankAccounts: number };
|
_count?: { products: number; bankAccounts: number };
|
||||||
},
|
},
|
||||||
fallbackMaxMeterage: number | null = null,
|
fallbackMaxMeterage: number | null = null,
|
||||||
) {
|
) {
|
||||||
@@ -247,7 +247,6 @@ export class CompaniesService {
|
|||||||
ownMaxMeteragePerInvoice: company.maxMeteragePerInvoice ?? undefined,
|
ownMaxMeteragePerInvoice: company.maxMeteragePerInvoice ?? undefined,
|
||||||
cashInvoiceValidityHours: company.cashInvoiceValidityHours,
|
cashInvoiceValidityHours: company.cashInvoiceValidityHours,
|
||||||
cashInvoiceValidUntilHour: company.cashInvoiceValidUntilHour ?? undefined,
|
cashInvoiceValidUntilHour: company.cashInvoiceValidUntilHour ?? undefined,
|
||||||
catalogCount: company._count?.catalogs ?? 0,
|
|
||||||
productCount: company._count?.products ?? 0,
|
productCount: company._count?.products ?? 0,
|
||||||
bankAccountCount: company._count?.bankAccounts ?? 0,
|
bankAccountCount: company._count?.bankAccounts ?? 0,
|
||||||
createdAt: company.createdAt.toISOString(),
|
createdAt: company.createdAt.toISOString(),
|
||||||
|
|||||||
@@ -736,7 +736,7 @@ export class OrdersService {
|
|||||||
|
|
||||||
const products = await this.prisma.product.findMany({
|
const products = await this.prisma.product.findMany({
|
||||||
where: { id: { in: uniqueProductIds } },
|
where: { id: { in: uniqueProductIds } },
|
||||||
include: { catalog: true, company: true },
|
include: { company: true },
|
||||||
});
|
});
|
||||||
const productMap = new Map(products.map((p) => [p.id, p]));
|
const productMap = new Map(products.map((p) => [p.id, p]));
|
||||||
|
|
||||||
@@ -818,7 +818,7 @@ export class OrdersService {
|
|||||||
totalMeterage += item.meterage;
|
totalMeterage += item.meterage;
|
||||||
return {
|
return {
|
||||||
productId: product.id,
|
productId: product.id,
|
||||||
catalogCode: product.catalog.code,
|
catalogCode: product.code,
|
||||||
productName: product.name,
|
productName: product.name,
|
||||||
productCode: product.code,
|
productCode: product.code,
|
||||||
color: product.color,
|
color: product.color,
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import {
|
||||||
|
colorVariantImageLists,
|
||||||
|
parseColorEntries,
|
||||||
|
parseGeneralImageUrls,
|
||||||
|
} from './color-entries';
|
||||||
|
|
||||||
|
describe('product image separation', () => {
|
||||||
|
it('keeps general images separate from color variant urls', () => {
|
||||||
|
const entries = parseColorEntries(
|
||||||
|
[
|
||||||
|
{ name: 'کرم', imageUrls: ['https://cdn/a.jpg', 'https://cdn/b.jpg'] },
|
||||||
|
{ name: 'طوسی', imageUrl: 'https://cdn/grey.jpg' },
|
||||||
|
],
|
||||||
|
'کرم',
|
||||||
|
);
|
||||||
|
const general = parseGeneralImageUrls(
|
||||||
|
['https://cdn/g1.jpg', 'https://cdn/a.jpg'],
|
||||||
|
entries,
|
||||||
|
);
|
||||||
|
expect(general).toEqual(['https://cdn/g1.jpg']);
|
||||||
|
expect(colorVariantImageLists(entries)).toEqual([
|
||||||
|
['https://cdn/a.jpg', 'https://cdn/b.jpg'],
|
||||||
|
['https://cdn/grey.jpg'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not merge color images into general list when only imageUrlsJson is set', () => {
|
||||||
|
const entries = parseColorEntries([{ name: 'Red', imageUrl: 'https://cdn/r.jpg' }]);
|
||||||
|
expect(parseGeneralImageUrls(['https://cdn/g.jpg'], entries)).toEqual([
|
||||||
|
'https://cdn/g.jpg',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,48 +1,85 @@
|
|||||||
export type ProductColorEntry = {
|
export type ProductColorEntry = {
|
||||||
name: string;
|
name: string;
|
||||||
|
/** @deprecated use imageUrls — first image for swatches */
|
||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
|
imageUrls: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function normalizeUrlList(raw: unknown): string[] {
|
||||||
|
if (!Array.isArray(raw)) return [];
|
||||||
|
const out: string[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (const item of raw) {
|
||||||
|
if (typeof item !== 'string') continue;
|
||||||
|
const url = item.trim();
|
||||||
|
if (!url || seen.has(url)) continue;
|
||||||
|
seen.add(url);
|
||||||
|
out.push(url);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
export function parseColorEntries(
|
export function parseColorEntries(
|
||||||
colorsJson: unknown,
|
colorsJson: unknown,
|
||||||
fallbackColor?: string,
|
fallbackColor?: string,
|
||||||
): ProductColorEntry[] {
|
): ProductColorEntry[] {
|
||||||
if (!Array.isArray(colorsJson) || colorsJson.length === 0) {
|
if (!Array.isArray(colorsJson) || colorsJson.length === 0) {
|
||||||
const name = fallbackColor?.trim();
|
const name = fallbackColor?.trim();
|
||||||
return name ? [{ name }] : [];
|
return name ? [{ name, imageUrls: [] }] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return colorsJson
|
return colorsJson
|
||||||
.map((item): ProductColorEntry | null => {
|
.map((item): ProductColorEntry | null => {
|
||||||
if (typeof item === 'string') {
|
if (typeof item === 'string') {
|
||||||
const name = item.trim();
|
const name = item.trim();
|
||||||
return name ? { name } : null;
|
return name ? { name, imageUrls: [] } : null;
|
||||||
}
|
}
|
||||||
if (item && typeof item === 'object') {
|
if (item && typeof item === 'object') {
|
||||||
const raw = item as { name?: unknown; imageUrl?: unknown };
|
const raw = item as {
|
||||||
|
name?: unknown;
|
||||||
|
imageUrl?: unknown;
|
||||||
|
imageUrls?: unknown;
|
||||||
|
};
|
||||||
const name = String(raw.name ?? '').trim();
|
const name = String(raw.name ?? '').trim();
|
||||||
if (!name) return null;
|
if (!name) return null;
|
||||||
const imageUrl =
|
const fromArray = normalizeUrlList(raw.imageUrls);
|
||||||
|
const legacy =
|
||||||
typeof raw.imageUrl === 'string' && raw.imageUrl.trim()
|
typeof raw.imageUrl === 'string' && raw.imageUrl.trim()
|
||||||
? raw.imageUrl
|
? raw.imageUrl.trim()
|
||||||
: undefined;
|
: undefined;
|
||||||
return { name, imageUrl };
|
const imageUrls =
|
||||||
|
fromArray.length > 0
|
||||||
|
? legacy && !fromArray.includes(legacy)
|
||||||
|
? [legacy, ...fromArray]
|
||||||
|
: fromArray
|
||||||
|
: legacy
|
||||||
|
? [legacy]
|
||||||
|
: [];
|
||||||
|
const imageUrl = imageUrls[0];
|
||||||
|
return { name, imageUrl, imageUrls };
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
.filter((entry): entry is ProductColorEntry => Boolean(entry));
|
.filter((entry): entry is ProductColorEntry => Boolean(entry));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mergeProductImages(
|
/** تصاویر عمومی محصول — فقط از imageUrlsJson، بدون merge با رنگها */
|
||||||
entries: ProductColorEntry[],
|
export function parseGeneralImageUrls(
|
||||||
imageUrlsJson: unknown,
|
imageUrlsJson: unknown,
|
||||||
|
colorEntries?: ProductColorEntry[],
|
||||||
): string[] {
|
): string[] {
|
||||||
const extras = Array.isArray(imageUrlsJson)
|
const general = normalizeUrlList(imageUrlsJson);
|
||||||
? imageUrlsJson.filter((url): url is string => typeof url === 'string' && url.trim().length > 0)
|
if (!colorEntries?.length) return general;
|
||||||
: [];
|
|
||||||
const fromColors = entries
|
const colorOnly = new Set<string>();
|
||||||
.map((entry) => entry.imageUrl)
|
for (const entry of colorEntries) {
|
||||||
.filter((url): url is string => Boolean(url));
|
for (const url of entry.imageUrls) colorOnly.add(url);
|
||||||
const seen = new Set(fromColors);
|
}
|
||||||
return [...fromColors, ...extras.filter((url) => !seen.has(url))];
|
return general.filter((url) => !colorOnly.has(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function colorVariantImageLists(
|
||||||
|
entries: ProductColorEntry[],
|
||||||
|
): string[][] {
|
||||||
|
return entries.map((entry) => [...entry.imageUrls]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IsOptional, IsString, MinLength } from 'class-validator';
|
import { IsArray, IsOptional, IsString, MinLength } from 'class-validator';
|
||||||
|
|
||||||
export class ProductColorJsonItemDto {
|
export class ProductColorJsonItemDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -8,6 +8,11 @@ export class ProductColorJsonItemDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
imageUrls?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** نرمالسازی colorsJson (رشتهٔ legacy یا آبجکت ادمین) قبل از اعتبارسنجی */
|
/** نرمالسازی colorsJson (رشتهٔ legacy یا آبجکت ادمین) قبل از اعتبارسنجی */
|
||||||
@@ -26,11 +31,20 @@ export function transformColorsJson(value: unknown): ProductColorJsonItemDto[] |
|
|||||||
const raw = item as Record<string, unknown>;
|
const raw = item as Record<string, unknown>;
|
||||||
const name = String(raw.name ?? '').trim();
|
const name = String(raw.name ?? '').trim();
|
||||||
if (!name) continue;
|
if (!name) continue;
|
||||||
|
const imageUrls = Array.isArray(raw.imageUrls)
|
||||||
|
? raw.imageUrls
|
||||||
|
.filter((u): u is string => typeof u === 'string' && u.trim().length > 0)
|
||||||
|
.map((u) => u.trim())
|
||||||
|
: undefined;
|
||||||
const imageUrl =
|
const imageUrl =
|
||||||
typeof raw.imageUrl === 'string' && raw.imageUrl.trim()
|
typeof raw.imageUrl === 'string' && raw.imageUrl.trim()
|
||||||
? raw.imageUrl.trim()
|
? raw.imageUrl.trim()
|
||||||
: undefined;
|
: undefined;
|
||||||
out.push({ name, imageUrl });
|
out.push({
|
||||||
|
name,
|
||||||
|
imageUrl,
|
||||||
|
imageUrls: imageUrls?.length ? imageUrls : imageUrl ? [imageUrl] : undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
|
|||||||
@@ -18,9 +18,6 @@ export class CreateProductDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
companyId!: string;
|
companyId!: string;
|
||||||
|
|
||||||
@IsString()
|
|
||||||
catalogId!: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
brandId?: string | null;
|
brandId?: string | null;
|
||||||
@@ -69,9 +66,10 @@ export class CreateProductDto {
|
|||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@Min(0)
|
@Min(0)
|
||||||
|
/** قیمت هر متر (تومان) */
|
||||||
basePrice?: number | null;
|
basePrice?: number | null;
|
||||||
|
|
||||||
/** Alias from admin UI */
|
/** Alias from admin UI — قیمت هر متر (تومان) */
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@@ -154,10 +152,6 @@ export class CreateProductDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateProductDto {
|
export class UpdateProductDto {
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
catalogId?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
brandId?: string | null;
|
brandId?: string | null;
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ export class ProductsController {
|
|||||||
@Get()
|
@Get()
|
||||||
findAll(
|
findAll(
|
||||||
@CurrentUser() user: AuthPrincipal | undefined,
|
@CurrentUser() user: AuthPrincipal | undefined,
|
||||||
@Query('catalogId') catalogId?: string,
|
|
||||||
@Query('companyId') companyId?: string,
|
@Query('companyId') companyId?: string,
|
||||||
@Query('color') color?: string,
|
@Query('color') color?: string,
|
||||||
@Query('activeOnly') activeOnly?: string,
|
@Query('activeOnly') activeOnly?: string,
|
||||||
@@ -42,7 +41,6 @@ export class ProductsController {
|
|||||||
) {
|
) {
|
||||||
const isAdmin = user?.type === 'admin';
|
const isAdmin = user?.type === 'admin';
|
||||||
return this.productsService.findAll({
|
return this.productsService.findAll({
|
||||||
catalogId,
|
|
||||||
companyId,
|
companyId,
|
||||||
color,
|
color,
|
||||||
activeOnly: activeOnly === 'true' || activeOnly === '1',
|
activeOnly: activeOnly === 'true' || activeOnly === '1',
|
||||||
|
|||||||
@@ -8,11 +8,14 @@ import { Prisma } from '@prisma/client';
|
|||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { CreateProductDto, UpdateProductDto } from './dto/product.dto';
|
import { CreateProductDto, UpdateProductDto } from './dto/product.dto';
|
||||||
import { loadCatalogCategories } from '../catalog/category-defs';
|
import { loadCatalogCategories } from '../catalog/category-defs';
|
||||||
import { mergeProductImages, parseColorEntries } from './color-entries';
|
import {
|
||||||
|
colorVariantImageLists,
|
||||||
|
parseColorEntries,
|
||||||
|
parseGeneralImageUrls,
|
||||||
|
} from './color-entries';
|
||||||
|
|
||||||
const productInclude = {
|
const productInclude = {
|
||||||
company: { select: { name: true } },
|
company: { select: { name: true } },
|
||||||
catalog: { select: { code: true, name: true } },
|
|
||||||
brand: { select: { id: true, name: true } },
|
brand: { select: { id: true, name: true } },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -25,7 +28,6 @@ export class ProductsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async findAll(filters: {
|
async findAll(filters: {
|
||||||
catalogId?: string;
|
|
||||||
companyId?: string;
|
companyId?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
activeOnly?: boolean;
|
activeOnly?: boolean;
|
||||||
@@ -35,7 +37,6 @@ export class ProductsService {
|
|||||||
}) {
|
}) {
|
||||||
const products = await this.prisma.product.findMany({
|
const products = await this.prisma.product.findMany({
|
||||||
where: {
|
where: {
|
||||||
...(filters.catalogId ? { catalogId: filters.catalogId } : {}),
|
|
||||||
...(filters.companyId ? { companyId: filters.companyId } : {}),
|
...(filters.companyId ? { companyId: filters.companyId } : {}),
|
||||||
...(filters.color
|
...(filters.color
|
||||||
? { color: { contains: filters.color, mode: 'insensitive' } }
|
? { color: { contains: filters.color, mode: 'insensitive' } }
|
||||||
@@ -75,14 +76,6 @@ export class ProductsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async create(dto: CreateProductDto) {
|
async create(dto: CreateProductDto) {
|
||||||
const catalog = await this.prisma.catalog.findUnique({
|
|
||||||
where: { id: dto.catalogId },
|
|
||||||
});
|
|
||||||
if (!catalog) throw new NotFoundException('کاتالوگ یافت نشد');
|
|
||||||
if (catalog.companyId !== dto.companyId) {
|
|
||||||
throw new ConflictException('کاتالوگ متعلق به این شرکت نیست');
|
|
||||||
}
|
|
||||||
|
|
||||||
const code = (dto.code || dto.productCode || '').trim();
|
const code = (dto.code || dto.productCode || '').trim();
|
||||||
if (!code) throw new ConflictException('کد محصول الزامی است');
|
if (!code) throw new ConflictException('کد محصول الزامی است');
|
||||||
const brandId = await this.resolveBrandId(dto.brandId ?? null);
|
const brandId = await this.resolveBrandId(dto.brandId ?? null);
|
||||||
@@ -91,7 +84,6 @@ export class ProductsService {
|
|||||||
const product = await this.prisma.product.create({
|
const product = await this.prisma.product.create({
|
||||||
data: {
|
data: {
|
||||||
companyId: dto.companyId,
|
companyId: dto.companyId,
|
||||||
catalogId: dto.catalogId,
|
|
||||||
brandId,
|
brandId,
|
||||||
name: dto.name.trim(),
|
name: dto.name.trim(),
|
||||||
code,
|
code,
|
||||||
@@ -145,7 +137,6 @@ export class ProductsService {
|
|||||||
const product = await this.prisma.product.update({
|
const product = await this.prisma.product.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
catalogId: dto.catalogId,
|
|
||||||
...(brandId !== undefined ? { brandId } : {}),
|
...(brandId !== undefined ? { brandId } : {}),
|
||||||
name: dto.name?.trim(),
|
name: dto.name?.trim(),
|
||||||
code,
|
code,
|
||||||
@@ -290,7 +281,6 @@ export class ProductsService {
|
|||||||
product: {
|
product: {
|
||||||
id: string;
|
id: string;
|
||||||
companyId: string;
|
companyId: string;
|
||||||
catalogId: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
code: string;
|
code: string;
|
||||||
color: string;
|
color: string;
|
||||||
@@ -303,7 +293,6 @@ export class ProductsService {
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
company: { name: string };
|
company: { name: string };
|
||||||
catalog: { code: string; name: string };
|
|
||||||
brand?: { id: string; name: string } | null;
|
brand?: { id: string; name: string } | null;
|
||||||
usageCategory?: string;
|
usageCategory?: string;
|
||||||
season?: string;
|
season?: string;
|
||||||
@@ -322,8 +311,14 @@ export class ProductsService {
|
|||||||
) {
|
) {
|
||||||
const colorEntries = parseColorEntries(product.colorsJson, product.color);
|
const colorEntries = parseColorEntries(product.colorsJson, product.color);
|
||||||
const colors = colorEntries.map((entry) => entry.name);
|
const colors = colorEntries.map((entry) => entry.name);
|
||||||
const colorImages = colorEntries.map((entry) => entry.imageUrl ?? null);
|
const colorImages = colorEntries.map(
|
||||||
const imageUrls = mergeProductImages(colorEntries, product.imageUrlsJson);
|
(entry) => entry.imageUrls[0] ?? entry.imageUrl ?? null,
|
||||||
|
);
|
||||||
|
const colorVariantImages = colorVariantImageLists(colorEntries);
|
||||||
|
const imageUrls = parseGeneralImageUrls(
|
||||||
|
product.imageUrlsJson,
|
||||||
|
colorEntries,
|
||||||
|
);
|
||||||
const tags = Array.isArray(product.tagsJson)
|
const tags = Array.isArray(product.tagsJson)
|
||||||
? (product.tagsJson as string[])
|
? (product.tagsJson as string[])
|
||||||
: [];
|
: [];
|
||||||
@@ -332,9 +327,10 @@ export class ProductsService {
|
|||||||
id: product.id,
|
id: product.id,
|
||||||
companyId: product.companyId,
|
companyId: product.companyId,
|
||||||
companyName: product.company.name,
|
companyName: product.company.name,
|
||||||
catalogId: product.catalogId,
|
/** @deprecated use `code` — kept for existing clients */
|
||||||
catalogCode: product.catalog.code,
|
catalogCode: product.code,
|
||||||
catalogName: product.catalog.name,
|
/** @deprecated use `name` */
|
||||||
|
catalogName: product.name,
|
||||||
brandId: product.brand?.id ?? null,
|
brandId: product.brand?.id ?? null,
|
||||||
brandName: product.brand?.name ?? null,
|
brandName: product.brand?.name ?? null,
|
||||||
name: product.name,
|
name: product.name,
|
||||||
@@ -359,6 +355,7 @@ export class ProductsService {
|
|||||||
weaveType: product.weaveType ?? undefined,
|
weaveType: product.weaveType ?? undefined,
|
||||||
colors,
|
colors,
|
||||||
colorImages,
|
colorImages,
|
||||||
|
colorVariantImages,
|
||||||
tags,
|
tags,
|
||||||
imageUrls,
|
imageUrls,
|
||||||
subtitle: product.subtitle ?? undefined,
|
subtitle: product.subtitle ?? undefined,
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export class StockNotifyService {
|
|||||||
color: true,
|
color: true,
|
||||||
stock: true,
|
stock: true,
|
||||||
imageUrlsJson: true,
|
imageUrlsJson: true,
|
||||||
catalog: { select: { code: true } },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -110,7 +109,6 @@ export class StockNotifyService {
|
|||||||
color: string;
|
color: string;
|
||||||
stock: number | null;
|
stock: number | null;
|
||||||
imageUrlsJson: unknown;
|
imageUrlsJson: unknown;
|
||||||
catalog: { code: string };
|
|
||||||
};
|
};
|
||||||
}) {
|
}) {
|
||||||
return {
|
return {
|
||||||
@@ -118,7 +116,7 @@ export class StockNotifyService {
|
|||||||
productId: r.productId,
|
productId: r.productId,
|
||||||
productName: r.product.name,
|
productName: r.product.name,
|
||||||
productCode: r.product.code,
|
productCode: r.product.code,
|
||||||
catalogCode: r.product.catalog.code,
|
catalogCode: r.product.code,
|
||||||
color: r.product.color,
|
color: r.product.color,
|
||||||
stock: r.product.stock,
|
stock: r.product.stock,
|
||||||
imageUrl: this.productThumb(r.product.imageUrlsJson),
|
imageUrl: this.productThumb(r.product.imageUrlsJson),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Injectable, NotFoundException } from '@nestjs/common';
|
|||||||
import { NotificationTone } from '@prisma/client';
|
import { NotificationTone } from '@prisma/client';
|
||||||
import { NotificationsService } from '../notifications/notifications.service';
|
import { NotificationsService } from '../notifications/notifications.service';
|
||||||
import {
|
import {
|
||||||
mergeProductImages,
|
parseGeneralImageUrls,
|
||||||
parseColorEntries,
|
parseColorEntries,
|
||||||
} from '../products/color-entries';
|
} from '../products/color-entries';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
@@ -19,7 +19,6 @@ const PRODUCT_INCLUDE = {
|
|||||||
imageUrlsJson: true,
|
imageUrlsJson: true,
|
||||||
widthCm: true,
|
widthCm: true,
|
||||||
composition: true,
|
composition: true,
|
||||||
catalog: { select: { name: true, code: true } },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
customer: { select: { firstName: true, lastName: true, phone: true } },
|
customer: { select: { firstName: true, lastName: true, phone: true } },
|
||||||
@@ -147,17 +146,20 @@ export class SuggestionsService {
|
|||||||
imageUrlsJson: unknown;
|
imageUrlsJson: unknown;
|
||||||
widthCm: number | null;
|
widthCm: number | null;
|
||||||
composition: string | null;
|
composition: string | null;
|
||||||
catalog: { name: string; code: string } | null;
|
|
||||||
};
|
};
|
||||||
}) {
|
}) {
|
||||||
const colorEntries = parseColorEntries(r.product.colorsJson, r.product.color);
|
const colorEntries = parseColorEntries(r.product.colorsJson, r.product.color);
|
||||||
const images = mergeProductImages(colorEntries, r.product.imageUrlsJson);
|
const imageUrls = parseGeneralImageUrls(
|
||||||
|
r.product.imageUrlsJson,
|
||||||
|
colorEntries,
|
||||||
|
);
|
||||||
const colors = colorEntries.map((entry) => entry.name);
|
const colors = colorEntries.map((entry) => entry.name);
|
||||||
const colorImages = colorEntries.map((entry) => entry.imageUrl ?? null);
|
const colorImages = colorEntries.map(
|
||||||
const catalogName = r.product.catalog?.name;
|
(entry) => entry.imageUrls[0] ?? entry.imageUrl ?? null,
|
||||||
|
);
|
||||||
const subtitle =
|
const subtitle =
|
||||||
r.product.subtitle?.trim() ||
|
r.product.subtitle?.trim() ||
|
||||||
[catalogName, r.product.color].filter(Boolean).join(' · ') ||
|
[r.product.name, r.product.color].filter(Boolean).join(' · ') ||
|
||||||
undefined;
|
undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -168,11 +170,11 @@ export class SuggestionsService {
|
|||||||
productId: r.productId,
|
productId: r.productId,
|
||||||
productName: r.product.name,
|
productName: r.product.name,
|
||||||
productCode: r.product.code,
|
productCode: r.product.code,
|
||||||
catalogName,
|
catalogName: r.product.name,
|
||||||
catalogCode: r.product.catalog?.code,
|
catalogCode: r.product.code,
|
||||||
color: r.product.color,
|
color: r.product.color,
|
||||||
subtitle,
|
subtitle,
|
||||||
imageUrl: images[0] ?? undefined,
|
imageUrl: imageUrls[0] ?? colorImages.find(Boolean) ?? undefined,
|
||||||
colors,
|
colors,
|
||||||
colorImages,
|
colorImages,
|
||||||
widthCm: r.product.widthCm ?? undefined,
|
widthCm: r.product.widthCm ?? undefined,
|
||||||
|
|||||||
Reference in New Issue
Block a user