From 915266f7b40017116dd346de5e88a0ceeda4df77 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Fri, 17 Jul 2026 19:18:37 +0330 Subject: [PATCH] designers --- database/migrations/.snapshot-negareh.json | 359 +++++++++++++----- src/modules/chat/listeners/chat.listeners.ts | 6 +- .../order/controllers/order.controller.ts | 2 +- src/modules/order/dto/assign-designer.dto.ts | 20 +- src/modules/order/dto/create-order.dto.ts | 8 +- .../order/entities/order-designer.entity.ts | 18 + src/modules/order/entities/order.entity.ts | 16 +- src/modules/order/order.module.ts | 3 +- src/modules/order/providers/order.service.ts | 79 ++-- .../order/repositories/order.repository.ts | 4 +- 10 files changed, 382 insertions(+), 133 deletions(-) create mode 100644 src/modules/order/entities/order-designer.entity.ts diff --git a/database/migrations/.snapshot-negareh.json b/database/migrations/.snapshot-negareh.json index 3318966..4e22a13 100644 --- a/database/migrations/.snapshot-negareh.json +++ b/database/migrations/.snapshot-negareh.json @@ -703,22 +703,13 @@ "nullable": true, "length": 6, "mappedType": "datetime" - }, - "fileds": { - "name": "fileds", - "type": "jsonb", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "mappedType": "json" } }, - "name": "prints", + "name": "order_prints", "schema": "public", "indexes": [ { - "keyName": "prints_pkey", + "keyName": "order_prints_pkey", "columnNames": [ "id" ], @@ -732,6 +723,122 @@ "foreignKeys": {}, "nativeEnums": {} }, + { + "columns": { + "id": { + "name": "id", + "type": "char(26)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 26, + "mappedType": "character" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + }, + "order_print_id": { + "name": "order_print_id", + "type": "char(26)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 26, + "mappedType": "character" + }, + "field_id": { + "name": "field_id", + "type": "char(26)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 26, + "mappedType": "character" + }, + "value": { + "name": "value", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + } + }, + "name": "order_print_values", + "schema": "public", + "indexes": [ + { + "keyName": "order_print_values_order_print_id_index", + "columnNames": [ + "order_print_id" + ], + "composite": false, + "constraint": false, + "primary": false, + "unique": false + }, + { + "keyName": "order_print_values_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "constraint": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "order_print_values_order_print_id_foreign": { + "constraintName": "order_print_values_order_print_id_foreign", + "columnNames": [ + "order_print_id" + ], + "localTableName": "public.order_print_values", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.order_prints", + "updateRule": "cascade" + }, + "order_print_values_field_id_foreign": { + "constraintName": "order_print_values_field_id_foreign", + "columnNames": [ + "field_id" + ], + "localTableName": "public.order_print_values", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.field", + "updateRule": "cascade" + } + }, + "nativeEnums": {} + }, { "columns": { "id": { @@ -784,6 +891,11 @@ "delete_invoice", "view_orders", "view_assigned_orders", + "change_order_status_from_any_to_any", + "change_order_status_to_cancelled", + "change_order_status_in_progress", + "change_order_status_from_in_progress_to_finished", + "change_order_status_from_finished_to_invoiced", "create_order", "update_order", "assign_designer", @@ -796,8 +908,8 @@ "manage_roles", "manage_payments", "manage_settings", - "create_print", - "view_print", + "create_print_form", + "view_print_form", "manage_learnings", "manage_criticisms", "manage_announcements", @@ -971,15 +1083,6 @@ "length": 255, "mappedType": "string" }, - "quantities": { - "name": "quantities", - "type": "jsonb", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "mappedType": "json" - }, "link_url": { "name": "link_url", "type": "text", @@ -1836,20 +1939,6 @@ "primary": false, "nullable": false, "mappedType": "integer" - }, - "status": { - "name": "status", - "type": "text", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "default": "'pending'", - "enumItems": [ - "pending", - "invoiced" - ], - "mappedType": "enum" } }, "name": "requests", @@ -2400,6 +2489,21 @@ "nullable": false, "mappedType": "integer" }, + "confirm_status": { + "name": "confirm_status", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "'pending'", + "enumItems": [ + "pending", + "partially_confirmed", + "confirmed" + ], + "mappedType": "enum" + }, "enable_tax": { "name": "enable_tax", "type": "boolean", @@ -2496,19 +2600,6 @@ ], "referencedTableName": "public.users", "updateRule": "cascade" - }, - "invoices_request_id_foreign": { - "constraintName": "invoices_request_id_foreign", - "columnNames": [ - "request_id" - ], - "localTableName": "public.invoices", - "referencedColumnNames": [ - "id" - ], - "referencedTableName": "public.requests", - "deleteRule": "set null", - "updateRule": "cascade" } }, "nativeEnums": {} @@ -3026,16 +3117,6 @@ "length": 26, "mappedType": "character" }, - "designer_id": { - "name": "designer_id", - "type": "char(26)", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "length": 26, - "mappedType": "character" - }, "creator_id": { "name": "creator_id", "type": "char(26)", @@ -3065,8 +3146,6 @@ "default": "'created'", "enumItems": [ "created", - "invoiced", - "waiting_to_confirm_invoice", "design_initiated", "design_finished", "design_revision", @@ -3078,7 +3157,8 @@ "after_print_boxing", "after_print_cartoning", "finished", - "canceled" + "canceled", + "invoiced" ], "mappedType": "enum" }, @@ -3134,16 +3214,6 @@ "primary": false, "unique": false }, - { - "keyName": "orders_designer_id_index", - "columnNames": [ - "designer_id" - ], - "composite": false, - "constraint": false, - "primary": false, - "unique": false - }, { "keyName": "orders_user_id_index", "columnNames": [ @@ -3202,7 +3272,7 @@ "referencedColumnNames": [ "id" ], - "referencedTableName": "public.prints", + "referencedTableName": "public.order_prints", "deleteRule": "set null", "updateRule": "cascade" }, @@ -3230,19 +3300,6 @@ "referencedTableName": "public.users", "updateRule": "cascade" }, - "orders_designer_id_foreign": { - "constraintName": "orders_designer_id_foreign", - "columnNames": [ - "designer_id" - ], - "localTableName": "public.orders", - "referencedColumnNames": [ - "id" - ], - "referencedTableName": "public.admins", - "deleteRule": "set null", - "updateRule": "cascade" - }, "orders_creator_id_foreign": { "constraintName": "orders_creator_id_foreign", "columnNames": [ @@ -3259,6 +3316,134 @@ }, "nativeEnums": {} }, + { + "columns": { + "id": { + "name": "id", + "type": "char(26)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 26, + "mappedType": "character" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + }, + "order_id": { + "name": "order_id", + "type": "char(26)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 26, + "mappedType": "character" + }, + "designer_id": { + "name": "designer_id", + "type": "char(26)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 26, + "mappedType": "character" + } + }, + "name": "order_designers", + "schema": "public", + "indexes": [ + { + "keyName": "order_designers_designer_id_index", + "columnNames": [ + "designer_id" + ], + "composite": false, + "constraint": false, + "primary": false, + "unique": false + }, + { + "keyName": "order_designers_order_id_index", + "columnNames": [ + "order_id" + ], + "composite": false, + "constraint": false, + "primary": false, + "unique": false + }, + { + "keyName": "order_designers_order_id_designer_id_unique", + "columnNames": [ + "order_id", + "designer_id" + ], + "composite": true, + "constraint": true, + "primary": false, + "unique": true + }, + { + "keyName": "order_designers_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "constraint": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "order_designers_order_id_foreign": { + "constraintName": "order_designers_order_id_foreign", + "columnNames": [ + "order_id" + ], + "localTableName": "public.order_designers", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.orders", + "updateRule": "cascade" + }, + "order_designers_designer_id_foreign": { + "constraintName": "order_designers_designer_id_foreign", + "columnNames": [ + "designer_id" + ], + "localTableName": "public.order_designers", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.admins", + "updateRule": "cascade" + } + }, + "nativeEnums": {} + }, { "columns": { "id": { @@ -3857,4 +4042,4 @@ } ], "nativeEnums": {} -} \ No newline at end of file +} diff --git a/src/modules/chat/listeners/chat.listeners.ts b/src/modules/chat/listeners/chat.listeners.ts index 7af6d4f..7e14f84 100644 --- a/src/modules/chat/listeners/chat.listeners.ts +++ b/src/modules/chat/listeners/chat.listeners.ts @@ -143,10 +143,10 @@ export class ChatListeners { const order = await this.em.findOne( Order, { id: refId }, - { populate: ['designer'], fields: ['id', 'designer'] }, + { populate: ['designers', 'designers.designer'] }, ); - if (order?.designer?.id) { - return [order?.designer?.id] + if (order?.designers?.length) { + return order.designers.getItems().map((od) => od.designer.id); } return []; diff --git a/src/modules/order/controllers/order.controller.ts b/src/modules/order/controllers/order.controller.ts index 2be5043..ccc9b00 100644 --- a/src/modules/order/controllers/order.controller.ts +++ b/src/modules/order/controllers/order.controller.ts @@ -99,7 +99,7 @@ export class OrderController { @Permissions(PermissionEnum.ASSIGN_DESIGNER) @ApiOperation({ summary: 'Assign Order Designer ' }) assignDesigner(@Param('orderId') orderId: string, @Body() body: AssignDesignerDto) { - return this.orderService.assignDesigner(orderId, body.designerId); + return this.orderService.assignDesigner(orderId, body); } // Permission handled inside service diff --git a/src/modules/order/dto/assign-designer.dto.ts b/src/modules/order/dto/assign-designer.dto.ts index 9894459..50d8af5 100644 --- a/src/modules/order/dto/assign-designer.dto.ts +++ b/src/modules/order/dto/assign-designer.dto.ts @@ -1,11 +1,19 @@ import { - IsString + IsArray, + IsOptional, + IsString, } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; +import { ApiPropertyOptional } from '@nestjs/swagger'; export class AssignDesignerDto { - @IsString() - @ApiProperty() - designerId: string; -} + @ApiPropertyOptional({ description: 'Single designer id (legacy)' }) + @IsOptional() + @IsString() + designerId?: string; + @ApiPropertyOptional({ type: [String], description: 'One or more designer ids' }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + designerIds?: string[]; +} diff --git a/src/modules/order/dto/create-order.dto.ts b/src/modules/order/dto/create-order.dto.ts index 83ea47e..fc03b3a 100644 --- a/src/modules/order/dto/create-order.dto.ts +++ b/src/modules/order/dto/create-order.dto.ts @@ -16,11 +16,17 @@ export class CreateOrderAsAdminDto { @IsOptional() userId?: string; - @ApiPropertyOptional({ example: '' }) + @ApiPropertyOptional({ example: '', description: 'Single designer id (legacy)' }) @IsString() @IsOptional() designerId?: string; + @ApiPropertyOptional({ type: [String], description: 'One or more designer ids' }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + designerIds?: string[]; + @ApiPropertyOptional({ example: '' }) @IsString() @IsOptional() diff --git a/src/modules/order/entities/order-designer.entity.ts b/src/modules/order/entities/order-designer.entity.ts new file mode 100644 index 0000000..9740c23 --- /dev/null +++ b/src/modules/order/entities/order-designer.entity.ts @@ -0,0 +1,18 @@ +import { Entity, Index, ManyToOne, OptionalProps, Unique } from '@mikro-orm/core'; +import { BaseEntity } from 'src/common/entities/base.entity'; +import { Admin } from 'src/modules/admin/entities/admin.entity'; +import { Order } from './order.entity'; + +@Entity({ tableName: 'order_designers' }) +@Unique({ properties: ['order', 'designer'] }) +@Index({ properties: ['order'] }) +@Index({ properties: ['designer'] }) +export class OrderDesigner extends BaseEntity { + [OptionalProps]?: 'createdAt' | 'deletedAt'; + + @ManyToOne(() => Order) + order!: Order; + + @ManyToOne(() => Admin) + designer!: Admin; +} diff --git a/src/modules/order/entities/order.entity.ts b/src/modules/order/entities/order.entity.ts index 5c26b11..47b7557 100644 --- a/src/modules/order/entities/order.entity.ts +++ b/src/modules/order/entities/order.entity.ts @@ -7,7 +7,9 @@ import { PrimaryKey, OptionalProps, OneToOne, - Opt + OneToMany, + Collection, + Opt, } from '@mikro-orm/core'; import { User } from '../../user/entities/user.entity'; import { ulid } from 'ulid'; @@ -18,10 +20,10 @@ import { InvoiceItem } from 'src/modules/invoice/entities/invoice-item.entity'; import { Product } from 'src/modules/product/entities/product.entity'; import { Category } from 'src/modules/product/entities/category.entity'; import { OrderPrint } from './order-print.entity'; +import { OrderDesigner } from './order-designer.entity'; @Entity({ tableName: 'orders' }) @Index({ properties: ['user'] }) -@Index({ properties: ['designer'] }) @Index({ properties: ['status'] }) export class Order extends BaseEntity { [OptionalProps]?: 'createdAt' | 'deletedAt' | 'orderNumber' @@ -32,10 +34,12 @@ export class Order extends BaseEntity { @ManyToOne(() => Product, { nullable: true }) product?: Product | null; - @OneToOne(() => OrderPrint, (orderPrint) => orderPrint.order, { owner: true, nullable: true }) print?: OrderPrint | null + @OneToMany(() => OrderDesigner, (orderDesigner) => orderDesigner.order, { orphanRemoval: true }) + designers = new Collection(this); + @PrimaryKey({ type: 'string', columnType: 'char(26)' }) id: string = ulid() @@ -48,9 +52,6 @@ export class Order extends BaseEntity { @ManyToOne(() => User) user!: User; - @ManyToOne(() => Admin, { nullable: true }) - designer?: Admin - @ManyToOne(() => Admin, { nullable: true }) creator?: Admin @@ -61,7 +62,6 @@ export class Order extends BaseEntity { }) orderNumber: number & Opt; - @Enum(() => OrderStatusEnum) status: OrderStatusEnum = OrderStatusEnum.CREATED; @@ -70,6 +70,4 @@ export class Order extends BaseEntity { @Property({ type: 'json' }) attachments: IAttachment[] = [] - - } diff --git a/src/modules/order/order.module.ts b/src/modules/order/order.module.ts index 1ed8ea5..1269c7e 100644 --- a/src/modules/order/order.module.ts +++ b/src/modules/order/order.module.ts @@ -5,6 +5,7 @@ import { OrderController } from './controllers/order.controller'; import { Order } from './entities/order.entity'; import { OrderPrint } from './entities/order-print.entity'; import { OrderPrintValue } from './entities/order-print-value.entity'; +import { OrderDesigner } from './entities/order-designer.entity'; import { AuthModule } from '../auth/auth.module'; import { PaymentModule } from '../payment/payments.module'; import { JwtModule } from '@nestjs/jwt'; @@ -22,7 +23,7 @@ import { OrderPrintValueRepository } from './repositories/order-print-value.repo @Module({ imports: [ - MikroOrmModule.forFeature([Order, OrderPrint, OrderPrintValue]), + MikroOrmModule.forFeature([Order, OrderPrint, OrderPrintValue, OrderDesigner]), AuthModule, forwardRef(() => PaymentModule), JwtModule, diff --git a/src/modules/order/providers/order.service.ts b/src/modules/order/providers/order.service.ts index 672f734..2a692ba 100644 --- a/src/modules/order/providers/order.service.ts +++ b/src/modules/order/providers/order.service.ts @@ -22,6 +22,7 @@ import { CreateOrderPrintDto } from '../dto/create-order-print.dto'; import { OrderPrintValueDto } from '../dto/order-print-value.dto'; import { OrderPrint } from '../entities/order-print.entity'; import { OrderPrintValue } from '../entities/order-print-value.entity'; +import { OrderDesigner } from '../entities/order-designer.entity'; import { OrderPrintRepository } from '../repositories/order-print.repository'; import { Field } from 'src/modules/form-builder/entities/field.entity'; import { Request } from 'src/modules/request/entities/request.entity'; @@ -29,6 +30,7 @@ import { Invoice } from 'src/modules/invoice/entities/invoice.entity'; import { OrderCreatedEvent } from '../events/order.events'; import { InvoiceConfirmStatusEnum } from 'src/modules/invoice/enum/invoice-confirm-status.enum'; import { ChattService } from 'src/modules/chat/providers/chat.service'; +import { Admin } from 'src/modules/admin/entities/admin.entity'; type AdminOrderTabCounts = { inProgress: number; @@ -116,15 +118,11 @@ export class OrderService { product = invoiceItem.product; } - let designer = null; - if (dto.designerId) { - designer = await this.adminService.findOrFail(dto.designerId); - } + const designerIds = this.resolveDesignerIds(dto) ?? []; const order = this.em.create(Order, { user, creator, - designer, type, product: product ?? null, title: dto.title, @@ -132,9 +130,9 @@ export class OrderService { estimatedDays: dto.estimatedDays, invoiceItem, attachments: dto.attachments, - }); + await this.setOrderDesigners(order, designerIds); await this.em.persistAndFlush(order); if (dto.keepRequestChats) { @@ -266,12 +264,9 @@ export class OrderService { order.product = product; } } - if (dto.designerId !== undefined) { - if (dto.designerId == null || dto.designerId === '') { - order.designer = undefined; - } else { - order.designer = await this.adminService.findOrFail(dto.designerId); - } + const designerIds = this.resolveDesignerIds(dto); + if (designerIds !== undefined) { + await this.setOrderDesigners(order, designerIds); } await this.em.flush(); @@ -354,11 +349,14 @@ export class OrderService { return order; } - async assignDesigner(orderId: string, designerId: string) { + async assignDesigner(orderId: string, dto: { designerId?: string; designerIds?: string[] }) { const order = await this.findOrderOrFail(orderId) - const designer = await this.adminService.findOrFail(designerId) + const designerIds = this.resolveDesignerIds(dto) + if (designerIds === undefined) { + throw new BadRequestException('designerIds or designerId is required') + } - order.designer = designer + await this.setOrderDesigners(order, designerIds) await this.em.flush() return order } @@ -433,11 +431,46 @@ export class OrderService { } } - // -----------Hrlper Methods ----------- + // -----------Helper Methods ----------- + private resolveDesignerIds(dto: { + designerId?: string | null; + designerIds?: string[]; + }): string[] | undefined { + if (dto.designerIds !== undefined) { + return [...new Set(dto.designerIds.filter(Boolean))]; + } + if (dto.designerId !== undefined) { + if (dto.designerId == null || dto.designerId === '') { + return []; + } + return [dto.designerId]; + } + return undefined; + } + + private async setOrderDesigners(order: Order, designerIds: string[]): Promise { + await this.em.populate(order, ['designers']); + + const designers: Admin[] = []; + for (const designerId of designerIds) { + designers.push(await this.adminService.findOrFail(designerId)); + } + + order.designers.removeAll(); + for (const designer of designers) { + order.designers.add( + this.em.create(OrderDesigner, { + order, + designer, + }), + ); + } + } + async findOrderOrFail(id: string): Promise { const order = await this.em.findOne(Order, { id }, { - populate: ['user', 'type', 'product', 'creator', 'designer', 'invoiceItem'], + populate: ['user', 'type', 'product', 'creator', 'designers', 'designers.designer', 'invoiceItem'], }); if (!order) { throw new NotFoundException(`Order with ID ${id} not found.`); @@ -446,12 +479,12 @@ export class OrderService { } async findOrderOrFailAssignedOrder(id: string, adminId: string): Promise { - const order = await this.em.findOne(Order, { id }, { populate: ['user', 'type', 'product', 'creator'] }); - if (!order) { - throw new NotFoundException(`Order with ID ${id} not found.`); - } - if (order?.designer?.id !== adminId) { - throw new ForbiddenException("You don't have permission to view this order") + const order = await this.findOrderOrFail(id); + const isAssigned = order.designers + .getItems() + .some((orderDesigner) => orderDesigner.designer.id === adminId); + if (!isAssigned) { + throw new ForbiddenException("You don't have permission to view this order"); } return order; } diff --git a/src/modules/order/repositories/order.repository.ts b/src/modules/order/repositories/order.repository.ts index 62b7e73..a69253c 100644 --- a/src/modules/order/repositories/order.repository.ts +++ b/src/modules/order/repositories/order.repository.ts @@ -38,7 +38,7 @@ export class OrderRepository extends EntityRepository { } if (designerId) { - where.designer = { id: designerId }; + where.designers = { designer: { id: designerId } }; } if (categoryId) { @@ -87,7 +87,7 @@ export class OrderRepository extends EntityRepository { limit, offset, orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' }, - populate: ['user', 'product', 'designer', 'invoiceItem', 'invoiceItem.invoice'], + populate: ['user', 'product', 'designers', 'designers.designer', 'invoiceItem', 'invoiceItem.invoice'], });