8/7/405
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Product" ADD COLUMN "brandId" TEXT;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Product_brandId_idx" ON "Product"("brandId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Product" ADD CONSTRAINT "Product_brandId_fkey" FOREIGN KEY ("brandId") REFERENCES "Brand"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "InternalMessage" ADD COLUMN "link" TEXT;
|
||||
@@ -214,11 +214,13 @@ model Product {
|
||||
subtitle String?
|
||||
rollMeters Float @default(50)
|
||||
isNew Boolean @default(false)
|
||||
brandId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
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)
|
||||
cartItems CartItem[]
|
||||
orderItems OrderItem[]
|
||||
favorites ProductFavorite[]
|
||||
@@ -229,6 +231,7 @@ model Product {
|
||||
@@unique([companyId, code])
|
||||
@@index([catalogId])
|
||||
@@index([companyId])
|
||||
@@index([brandId])
|
||||
@@index([usageCategory])
|
||||
@@index([season])
|
||||
}
|
||||
@@ -585,6 +588,7 @@ model InternalMessage {
|
||||
senderName String
|
||||
subject String
|
||||
preview String
|
||||
link String?
|
||||
isRead Boolean @default(false)
|
||||
sentAt DateTime @default(now())
|
||||
}
|
||||
@@ -658,6 +662,7 @@ model Brand {
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
customers CustomerBrand[]
|
||||
products Product[]
|
||||
}
|
||||
|
||||
model CustomerBrand {
|
||||
|
||||
@@ -26,6 +26,7 @@ import { SuggestionsModule } from './suggestions/suggestions.module';
|
||||
import { StockNotifyModule } from './stock-notify/stock-notify.module';
|
||||
import { BannersModule } from './banners/banners.module';
|
||||
import { AddressesModule } from './addresses/addresses.module';
|
||||
import { InternalMessagesModule } from './internal-messages/internal-messages.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -54,6 +55,7 @@ import { AddressesModule } from './addresses/addresses.module';
|
||||
StockNotifyModule,
|
||||
BannersModule,
|
||||
AddressesModule,
|
||||
InternalMessagesModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@ export class BrandsService {
|
||||
const rows = await this.prisma.brand.findMany({
|
||||
where: activeOnly ? { isActive: true } : undefined,
|
||||
orderBy: [{ displayOrder: 'asc' }, { name: 'asc' }],
|
||||
include: { _count: { select: { customers: true } } },
|
||||
include: { _count: { select: { products: true } } },
|
||||
});
|
||||
return rows.map((b) => ({
|
||||
id: b.id,
|
||||
@@ -19,7 +19,7 @@ export class BrandsService {
|
||||
description: b.description ?? undefined,
|
||||
isActive: b.isActive,
|
||||
displayOrder: b.displayOrder,
|
||||
customersCount: b._count.customers,
|
||||
productsCount: b._count.products,
|
||||
createdAt: b.createdAt.toISOString(),
|
||||
updatedAt: b.updatedAt.toISOString(),
|
||||
}));
|
||||
|
||||
@@ -21,9 +21,6 @@ const customerInclude = {
|
||||
assignedAgent: {
|
||||
select: { id: true, firstName: true, lastName: true, phone: true, agentCode: true },
|
||||
},
|
||||
customerBrands: {
|
||||
include: { brand: { select: { id: true, name: true } } },
|
||||
},
|
||||
} satisfies Prisma.CustomerInclude;
|
||||
|
||||
type CustomerWithRelations = Prisma.CustomerGetPayload<{
|
||||
@@ -93,15 +90,6 @@ export class CustomersService {
|
||||
agentCode: dto.agentCode?.trim() || null,
|
||||
},
|
||||
});
|
||||
if (dto.brandIds?.length) {
|
||||
await tx.customerBrand.createMany({
|
||||
data: dto.brandIds.map((brandId) => ({
|
||||
customerId: created.id,
|
||||
brandId,
|
||||
})),
|
||||
skipDuplicates: true,
|
||||
});
|
||||
}
|
||||
return tx.customer.findUniqueOrThrow({
|
||||
where: { id: created.id },
|
||||
include: customerInclude,
|
||||
@@ -177,19 +165,6 @@ export class CustomersService {
|
||||
},
|
||||
});
|
||||
|
||||
if (dto.brandIds !== undefined) {
|
||||
await tx.customerBrand.deleteMany({ where: { customerId: id } });
|
||||
if (dto.brandIds.length) {
|
||||
await tx.customerBrand.createMany({
|
||||
data: dto.brandIds.map((brandId) => ({
|
||||
customerId: id,
|
||||
brandId,
|
||||
})),
|
||||
skipDuplicates: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return tx.customer.findUniqueOrThrow({
|
||||
where: { id },
|
||||
include: customerInclude,
|
||||
@@ -301,11 +276,6 @@ export class CustomersService {
|
||||
assignedAgentId: customer.assignedAgentId,
|
||||
assignedAgentName: agentName,
|
||||
assignedAgentPhone: customer.assignedAgent?.phone ?? '',
|
||||
brandIds: customer.customerBrands.map((cb) => cb.brandId),
|
||||
brands: customer.customerBrands.map((cb) => ({
|
||||
id: cb.brand.id,
|
||||
name: cb.brand.name,
|
||||
})),
|
||||
registeredAt: customer.createdAt.toISOString(),
|
||||
createdAt: customer.createdAt.toISOString(),
|
||||
updatedAt: customer.updatedAt.toISOString(),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
@@ -57,11 +56,6 @@ export class CreateCustomerDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
agentCode?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
brandIds?: string[];
|
||||
}
|
||||
|
||||
export class UpdateCustomerDto {
|
||||
@@ -112,11 +106,6 @@ export class UpdateCustomerDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
agentCode?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
brandIds?: string[];
|
||||
}
|
||||
|
||||
export class SetCustomerActiveDto {
|
||||
|
||||
@@ -166,6 +166,7 @@ export class DashboardService {
|
||||
senderName: m.senderName,
|
||||
subject: m.subject,
|
||||
preview: m.preview,
|
||||
link: m.link ?? '',
|
||||
isRead: m.isRead,
|
||||
sentAt: m.sentAt.toISOString(),
|
||||
})),
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { IsOptional, IsString, MinLength } from 'class-validator';
|
||||
|
||||
export class CreateInternalMessageDto {
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
senderName!: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
subject!: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
preview!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
link?: string | null;
|
||||
}
|
||||
|
||||
export class UpdateInternalMessageDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
senderName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
subject?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
preview?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
link?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { AuthType, Roles } from '../common/decorators/auth.decorators';
|
||||
import { InternalMessagesService } from './internal-messages.service';
|
||||
import {
|
||||
CreateInternalMessageDto,
|
||||
UpdateInternalMessageDto,
|
||||
} from './dto/internal-message.dto';
|
||||
|
||||
@Controller('admin/internal-messages')
|
||||
@AuthType('admin')
|
||||
@Roles('admin', 'sales', 'finance', 'warehouse')
|
||||
export class InternalMessagesController {
|
||||
constructor(private readonly messages: InternalMessagesService) {}
|
||||
|
||||
@Get()
|
||||
list(@Query('limit') limit?: string) {
|
||||
const n = limit ? Number.parseInt(limit, 10) : 100;
|
||||
return this.messages.list(Number.isFinite(n) ? n : 100);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() dto: CreateInternalMessageDto) {
|
||||
return this.messages.create(dto);
|
||||
}
|
||||
|
||||
@Patch('read-all')
|
||||
markAllRead() {
|
||||
return this.messages.markAllRead();
|
||||
}
|
||||
|
||||
@Patch(':id/read')
|
||||
markRead(@Param('id') id: string) {
|
||||
return this.messages.markRead(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(@Param('id') id: string, @Body() dto: UpdateInternalMessageDto) {
|
||||
return this.messages.update(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.messages.remove(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { InternalMessagesController } from './internal-messages.controller';
|
||||
import { InternalMessagesService } from './internal-messages.service';
|
||||
|
||||
@Module({
|
||||
controllers: [InternalMessagesController],
|
||||
providers: [InternalMessagesService],
|
||||
exports: [InternalMessagesService],
|
||||
})
|
||||
export class InternalMessagesModule {}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import {
|
||||
CreateInternalMessageDto,
|
||||
UpdateInternalMessageDto,
|
||||
} from './dto/internal-message.dto';
|
||||
|
||||
@Injectable()
|
||||
export class InternalMessagesService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async list(limit = 100) {
|
||||
const rows = await this.prisma.internalMessage.findMany({
|
||||
orderBy: { sentAt: 'desc' },
|
||||
take: Math.min(Math.max(limit, 1), 200),
|
||||
});
|
||||
return rows.map((row) => this.map(row));
|
||||
}
|
||||
|
||||
async create(dto: CreateInternalMessageDto) {
|
||||
const row = await this.prisma.internalMessage.create({
|
||||
data: {
|
||||
senderName: dto.senderName.trim(),
|
||||
subject: dto.subject.trim(),
|
||||
preview: dto.preview.trim(),
|
||||
link: dto.link?.trim() || null,
|
||||
isRead: false,
|
||||
},
|
||||
});
|
||||
return this.map(row);
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateInternalMessageDto) {
|
||||
await this.ensure(id);
|
||||
const row = await this.prisma.internalMessage.update({
|
||||
where: { id },
|
||||
data: {
|
||||
senderName: dto.senderName?.trim(),
|
||||
subject: dto.subject?.trim(),
|
||||
preview: dto.preview?.trim(),
|
||||
link:
|
||||
dto.link === undefined ? undefined : dto.link?.trim() || null,
|
||||
},
|
||||
});
|
||||
return this.map(row);
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
await this.ensure(id);
|
||||
await this.prisma.internalMessage.delete({ where: { id } });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async markRead(id: string) {
|
||||
await this.ensure(id);
|
||||
const row = await this.prisma.internalMessage.update({
|
||||
where: { id },
|
||||
data: { isRead: true },
|
||||
});
|
||||
return this.map(row);
|
||||
}
|
||||
|
||||
async markAllRead() {
|
||||
await this.prisma.internalMessage.updateMany({
|
||||
where: { isRead: false },
|
||||
data: { isRead: true },
|
||||
});
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
private async ensure(id: string) {
|
||||
const row = await this.prisma.internalMessage.findUnique({ where: { id } });
|
||||
if (!row) throw new NotFoundException('پیام یافت نشد');
|
||||
return row;
|
||||
}
|
||||
|
||||
private map(row: {
|
||||
id: string;
|
||||
senderName: string;
|
||||
subject: string;
|
||||
preview: string;
|
||||
link: string | null;
|
||||
isRead: boolean;
|
||||
sentAt: Date;
|
||||
}) {
|
||||
return {
|
||||
id: row.id,
|
||||
senderName: row.senderName,
|
||||
subject: row.subject,
|
||||
preview: row.preview,
|
||||
link: row.link ?? '',
|
||||
isRead: row.isRead,
|
||||
sentAt: row.sentAt.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,12 @@ import { NotificationTone } from '@prisma/client';
|
||||
import {
|
||||
ArrayNotEmpty,
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MinLength,
|
||||
ValidateIf,
|
||||
} from 'class-validator';
|
||||
import { NotificationsService } from './notifications.service';
|
||||
import { AuthType } from '../common/decorators/auth.decorators';
|
||||
@@ -49,8 +51,12 @@ class BroadcastDto {
|
||||
body!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
allActive?: boolean;
|
||||
|
||||
@ValidateIf((dto: BroadcastDto) => !dto.allActive)
|
||||
@IsArray()
|
||||
@ArrayNotEmpty()
|
||||
@ArrayNotEmpty({ message: 'حداقل یک مشتری انتخاب کنید یا ارسال به همه را فعال کنید.' })
|
||||
@IsString({ each: true })
|
||||
customerIds?: string[];
|
||||
|
||||
|
||||
@@ -238,12 +238,14 @@ export class NotificationsService {
|
||||
async broadcastToCustomers(input: {
|
||||
title: string;
|
||||
body: string;
|
||||
allActive?: boolean;
|
||||
customerIds?: string[];
|
||||
tone?: NotificationTone;
|
||||
}) {
|
||||
const where = input.customerIds?.length
|
||||
? { id: { in: input.customerIds }, isActive: true }
|
||||
: { isActive: true, appRole: 'customer' as const };
|
||||
const sendToAll = input.allActive === true;
|
||||
const where = sendToAll
|
||||
? { isActive: true, appRole: 'customer' as const }
|
||||
: { id: { in: input.customerIds ?? [] }, isActive: true };
|
||||
const customers = await this.prisma.customer.findMany({
|
||||
where,
|
||||
select: { id: true },
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -27,6 +27,12 @@ export class StockNotifyController {
|
||||
return this.stockNotifyService.markNotified(id);
|
||||
}
|
||||
|
||||
@AuthType('customer')
|
||||
@Get('stock-alerts')
|
||||
listMine(@CurrentUser() user: AuthPrincipal) {
|
||||
return this.stockNotifyService.listForCustomer(user.id);
|
||||
}
|
||||
|
||||
@AuthType('customer')
|
||||
@Post('stock-alerts')
|
||||
subscribe(@CurrentUser() user: AuthPrincipal, @Body() dto: SubscribeDto) {
|
||||
|
||||
@@ -5,6 +5,27 @@ import { PrismaService } from '../prisma/prisma.service';
|
||||
export class StockNotifyService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async listForCustomer(customerId: string) {
|
||||
const rows = await this.prisma.stockNotifyRequest.findMany({
|
||||
where: { customerId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
product: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
code: true,
|
||||
color: true,
|
||||
stock: true,
|
||||
imageUrlsJson: true,
|
||||
catalog: { select: { code: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return rows.map((r) => this.mapCustomer(r));
|
||||
}
|
||||
|
||||
async listAdmin(activeOnly = true) {
|
||||
const rows = await this.prisma.stockNotifyRequest.findMany({
|
||||
where: activeOnly ? { active: true } : undefined,
|
||||
@@ -68,6 +89,45 @@ export class StockNotifyService {
|
||||
return this.map(row);
|
||||
}
|
||||
|
||||
private productThumb(imageUrlsJson: unknown): string {
|
||||
if (!Array.isArray(imageUrlsJson)) return '';
|
||||
const url = imageUrlsJson.find(
|
||||
(item): item is string => typeof item === 'string' && item.trim().length > 0,
|
||||
);
|
||||
return url?.trim() ?? '';
|
||||
}
|
||||
|
||||
private mapCustomer(r: {
|
||||
id: string;
|
||||
productId: string;
|
||||
active: boolean;
|
||||
createdAt: Date;
|
||||
notifiedAt: Date | null;
|
||||
product: {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
color: string;
|
||||
stock: number | null;
|
||||
imageUrlsJson: unknown;
|
||||
catalog: { code: string };
|
||||
};
|
||||
}) {
|
||||
return {
|
||||
id: r.id,
|
||||
productId: r.productId,
|
||||
productName: r.product.name,
|
||||
productCode: r.product.code,
|
||||
catalogCode: r.product.catalog.code,
|
||||
color: r.product.color,
|
||||
stock: r.product.stock,
|
||||
imageUrl: this.productThumb(r.product.imageUrlsJson),
|
||||
active: r.active,
|
||||
createdAt: r.createdAt.toISOString(),
|
||||
notifiedAt: r.notifiedAt?.toISOString() ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
private map(r: {
|
||||
id: string;
|
||||
customerId: string;
|
||||
|
||||
Reference in New Issue
Block a user