From b855d8181788f5f00aa6903640c986f5f31e30b6 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Mon, 20 Jul 2026 11:54:39 +0330 Subject: [PATCH] add : ticket to task --- .../1784600000000-addTicketToTaskEntity.ts | 17 ++++++++++++ .../task-manager/dto/task/create-task.dto.ts | 9 +++++++ .../task-manager/entities/task.entity.ts | 8 ++++++ .../task-manager/providers/task.service.ts | 26 ++++++++++++++++--- .../repositories/task.repository.ts | 9 ++++++- .../task-manager/task-manager.module.ts | 3 ++- 6 files changed, 67 insertions(+), 5 deletions(-) create mode 100644 database/migrations/1784600000000-addTicketToTaskEntity.ts diff --git a/database/migrations/1784600000000-addTicketToTaskEntity.ts b/database/migrations/1784600000000-addTicketToTaskEntity.ts new file mode 100644 index 0000000..90d4abf --- /dev/null +++ b/database/migrations/1784600000000-addTicketToTaskEntity.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddTicketToTaskEntity1784600000000 implements MigrationInterface { + name = "AddTicketToTaskEntity1784600000000"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tm_tasks" ADD "ticketId" uuid`); + await queryRunner.query( + `ALTER TABLE "tm_tasks" ADD CONSTRAINT "FK_tm_tasks_ticket" FOREIGN KEY ("ticketId") REFERENCES "ticket"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tm_tasks" DROP CONSTRAINT "FK_tm_tasks_ticket"`); + await queryRunner.query(`ALTER TABLE "tm_tasks" DROP COLUMN "ticketId"`); + } +} diff --git a/src/modules/task-manager/dto/task/create-task.dto.ts b/src/modules/task-manager/dto/task/create-task.dto.ts index 5d0a148..878d9a1 100644 --- a/src/modules/task-manager/dto/task/create-task.dto.ts +++ b/src/modules/task-manager/dto/task/create-task.dto.ts @@ -34,6 +34,15 @@ export class CreateTaskDto { @IsNotEmpty() taskPhaseId: string; + @ApiProperty({ + description: "ID of the related ticket", + required: false, + example: "c3d4e5f6-a7b8-9012-cdef-123456789012", + }) + @IsUUID() + @IsOptional() + ticketId?: string; + @ApiProperty({ description: "IDs of users to assign to the task", required: false, diff --git a/src/modules/task-manager/entities/task.entity.ts b/src/modules/task-manager/entities/task.entity.ts index f06bbb3..6cadc59 100644 --- a/src/modules/task-manager/entities/task.entity.ts +++ b/src/modules/task-manager/entities/task.entity.ts @@ -6,6 +6,7 @@ import { TMAttachment } from "./attachment.entity"; import { TMCheckListItem } from "./check-list-item.entity"; import { TMTime } from "./time.entity"; import { User } from "../../users/entities/user.entity"; +import { Ticket } from "../../tickets/entities/ticket.entity"; @Entity("tm_tasks") export class TMTask extends BaseEntity { @@ -27,6 +28,13 @@ export class TMTask extends BaseEntity { @Column({type: 'int', default: 1}) priority: number; + @ManyToOne(() => Ticket, { nullable: true, onDelete: "SET NULL" }) + @JoinColumn({ name: "ticketId" }) + ticket: Ticket | null; + + @Column({ type: "uuid", nullable: true }) + ticketId: string | null; + // --- Relations --- @ManyToOne(() => TMTaskPhase, (taskPhase) => taskPhase.tasks, { diff --git a/src/modules/task-manager/providers/task.service.ts b/src/modules/task-manager/providers/task.service.ts index 2a2384c..c8b78a2 100644 --- a/src/modules/task-manager/providers/task.service.ts +++ b/src/modules/task-manager/providers/task.service.ts @@ -7,7 +7,8 @@ import { CreateTaskDto } from "../dto/task/create-task.dto"; import { UpdateTaskDto } from "../dto/task/update-task.dto"; import { ChangeTaskPhaseDto } from "../dto/task/change-task-phase.dto"; import { User } from "../../users/entities/user.entity"; -import { TaskMessage, TaskPhaseMessage } from "../../../common/enums/message.enum"; +import { Ticket } from "../../tickets/entities/ticket.entity"; +import { TaskMessage, TaskPhaseMessage, TicketMessageEnum } from "../../../common/enums/message.enum"; import { TaskPhaseRepository } from "../repositories/task-phase.repository"; import { TMTaskPhase } from "../entities/task-phase.entity"; import { PermissionEnum } from "../../users/enums/permission.enum"; @@ -21,6 +22,8 @@ export class TaskService { private readonly taskPhaseRepository: TaskPhaseRepository, @InjectRepository(User) private readonly userRepository: Repository, + @InjectRepository(Ticket) + private readonly ticketRepository: Repository, ) {} async findOneOrFail(id: string): Promise { @@ -29,12 +32,19 @@ export class TaskService { relations: { taskPhase: true, users: true, + ticket: true, }, }); if (!task) throw new NotFoundException(TaskMessage.TASK_NOT_FOUND); return task; } + private async findTicketOrFail(ticketId: string): Promise { + const ticket = await this.ticketRepository.findOne({ where: { id: ticketId } }); + if (!ticket) throw new NotFoundException(TicketMessageEnum.TICKET_NOT_FOUND); + return ticket; + } + async findTaskPhaseOrFail(taskPhaseId: string): Promise { const taskPhase = await this.taskPhaseRepository.findOne({ where: { id: taskPhaseId }, @@ -50,10 +60,14 @@ export class TaskService { } async create(dto: CreateTaskDto): Promise { - const { userIds, startDate, endDate, ...rest } = dto; + const { userIds, startDate, endDate, ticketId, ...rest } = dto; const taskPhase = await this.findTaskPhaseOrFail(dto.taskPhaseId); + if (ticketId) { + await this.findTicketOrFail(ticketId); + } + if (userIds?.length) { const isUsersValid = userIds.every((userId) => taskPhase.project.users.some((user) => user.id === userId)); if (!isUsersValid) throw new BadRequestException(TaskMessage.USERS_ASSIGNED_TO_TASK_NOT_PROJECT_MEMBER); @@ -61,6 +75,7 @@ export class TaskService { const task = this.taskRepository.create({ ...rest, + ticketId, startDate: startDate ? new Date(startDate) : undefined, endDate: endDate ? new Date(endDate) : undefined, }); @@ -74,7 +89,11 @@ export class TaskService { async update(id: string, dto: UpdateTaskDto): Promise { const task = await this.findOneOrFail(id); - const { userIds, startDate, endDate, ...rest } = dto; + const { userIds, startDate, endDate, ticketId, ...rest } = dto; + + if (ticketId) { + await this.findTicketOrFail(ticketId); + } if (dto.taskPhaseId) { const taskPhase = await this.findTaskPhaseOrFail(dto.taskPhaseId); @@ -87,6 +106,7 @@ export class TaskService { Object.assign(task, { ...rest, + ...(ticketId !== undefined && { ticketId }), startDate: startDate ? new Date(startDate) : task.startDate, endDate: endDate ? new Date(endDate) : task.endDate, }); diff --git a/src/modules/task-manager/repositories/task.repository.ts b/src/modules/task-manager/repositories/task.repository.ts index 7373d91..ed058cd 100644 --- a/src/modules/task-manager/repositories/task.repository.ts +++ b/src/modules/task-manager/repositories/task.repository.ts @@ -19,6 +19,7 @@ export class TaskRepository extends Repository { .leftJoinAndSelect("task.checkListItems", "checkListItem") .leftJoinAndSelect("task.users", "user") .leftJoinAndSelect("task.times", "time") + .leftJoinAndSelect("task.ticket", "ticket") .leftJoinAndSelect("time.admin", "timeAdmin") .loadRelationCountAndMap("task.checkListItemCount", "task.checkListItems") @@ -45,6 +46,11 @@ export class TaskRepository extends Repository { "user.firstName", "user.lastName", + "ticket.id", + "ticket.subject", + "ticket.numericId", + "ticket.status", + "time.id", "time.startedAt", "time.endedAt", @@ -113,12 +119,13 @@ export class TaskRepository extends Repository { // response needs. IN (:...ids) doesn't preserve order, so re-order to match seekQuery. const detailRows = await this.createQueryBuilder("task") .leftJoinAndSelect("task.remarks", "remark") + .leftJoinAndSelect("task.ticket", "ticket") .loadRelationCountAndMap("task.attachmentCount", "task.attachments") .loadRelationCountAndMap("task.checkListItemCount", "task.checkListItems") .loadRelationCountAndMap("task.completedCheckListItemCount", "task.checkListItems", "completedCheckListItems", (qb) => qb.where("completedCheckListItems.isDone = :isDone", { isDone: true }), ) - .select(["task", "remark.id", "remark.title", "remark.color"]) + .select(["task", "remark.id", "remark.title", "remark.color", "ticket.id", "ticket.subject", "ticket.numericId", "ticket.status"]) .where("task.id IN (:...ids)", { ids }) .getMany(); diff --git a/src/modules/task-manager/task-manager.module.ts b/src/modules/task-manager/task-manager.module.ts index 5943d7f..fed1d89 100644 --- a/src/modules/task-manager/task-manager.module.ts +++ b/src/modules/task-manager/task-manager.module.ts @@ -13,6 +13,7 @@ import { WorkspaceRepository } from "./repositories/workspace.repository"; import { WorkspaceController } from "./controllers/workspace.controller"; import { WorkspaceService } from "./providers/workspace.service"; import { User } from "../users/entities/user.entity"; +import { Ticket } from "../tickets/entities/ticket.entity"; import { ProjectController } from "./controllers/project.controller"; import { ProjectRepository } from "./repositories/project.repository"; import { ProjectService } from "./providers/project.service"; @@ -37,7 +38,7 @@ import { TimeService } from "./providers/time.service"; @Module({ imports: [ - TypeOrmModule.forFeature([TMWorkspace, TMProject, TMTaskPhase, TMTask, TMRemark, TMAttachment, TMCheckListItem, TMTime, User]), + TypeOrmModule.forFeature([TMWorkspace, TMProject, TMTaskPhase, TMTask, TMRemark, TMAttachment, TMCheckListItem, TMTime, User, Ticket]), ], controllers: [ WorkspaceController,