add : ticket to task

This commit is contained in:
2026-07-20 11:54:39 +03:30
parent f66bc9fa8c
commit b855d81817
6 changed files with 67 additions and 5 deletions
@@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddTicketToTaskEntity1784600000000 implements MigrationInterface {
name = "AddTicketToTaskEntity1784600000000";
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(`ALTER TABLE "tm_tasks" DROP CONSTRAINT "FK_tm_tasks_ticket"`);
await queryRunner.query(`ALTER TABLE "tm_tasks" DROP COLUMN "ticketId"`);
}
}
@@ -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,
@@ -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, {
@@ -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<User>,
@InjectRepository(Ticket)
private readonly ticketRepository: Repository<Ticket>,
) {}
async findOneOrFail(id: string): Promise<TMTask> {
@@ -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<Ticket> {
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<TMTaskPhase> {
const taskPhase = await this.taskPhaseRepository.findOne({
where: { id: taskPhaseId },
@@ -50,10 +60,14 @@ export class TaskService {
}
async create(dto: CreateTaskDto): Promise<TMTask> {
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<TMTask> {
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,
});
@@ -19,6 +19,7 @@ export class TaskRepository extends Repository<TMTask> {
.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<TMTask> {
"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<TMTask> {
// 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();
@@ -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,