diff --git a/database/migrations/1784700000000-addTaskCommentEntity.ts b/database/migrations/1784700000000-addTaskCommentEntity.ts new file mode 100644 index 0000000..4a47c79 --- /dev/null +++ b/database/migrations/1784700000000-addTaskCommentEntity.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddTaskCommentEntity1784700000000 implements MigrationInterface { + name = "AddTaskCommentEntity1784700000000"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "tm_task_comments" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "content" text NOT NULL, "userId" uuid NOT NULL, "taskId" uuid NOT NULL, CONSTRAINT "PK_tm_task_comments" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `ALTER TABLE "tm_task_comments" ADD CONSTRAINT "FK_tm_task_comments_user" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "tm_task_comments" ADD CONSTRAINT "FK_tm_task_comments_task" FOREIGN KEY ("taskId") REFERENCES "tm_tasks"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tm_task_comments" DROP CONSTRAINT "FK_tm_task_comments_task"`); + await queryRunner.query(`ALTER TABLE "tm_task_comments" DROP CONSTRAINT "FK_tm_task_comments_user"`); + await queryRunner.query(`DROP TABLE "tm_task_comments"`); + } +} diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 1e34388..2b54300 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -962,4 +962,8 @@ export const enum timeMessage { INVALID_TIME_RANGE = "زمان پایان باید بعد از زمان شروع باشد!", TIME_ALREADY_STARTED = "برای این تسک یک زمان در حال اجرا وجود دارد!", NO_RUNNING_TIME = "زمان در حال اجرایی برای این تسک پیدا نشد!" +} + +export const enum taskCommentMessage { + TASK_COMMENT_NOT_FOUND = "نظر مورد نظر پیدا نشد!" } \ No newline at end of file diff --git a/src/modules/task-manager/controllers/task-comment.controller.ts b/src/modules/task-manager/controllers/task-comment.controller.ts new file mode 100644 index 0000000..f5873ce --- /dev/null +++ b/src/modules/task-manager/controllers/task-comment.controller.ts @@ -0,0 +1,50 @@ +import { Controller, Post, Patch, Delete, Param, Body, Get } from "@nestjs/common"; +import { ApiTags, ApiOperation, ApiResponse, ApiParam } from "@nestjs/swagger"; +import { TaskCommentService } from "../providers/task-comment.service"; +import { CreateTaskCommentDto } from "../dto/task-comment/create-task-comment.dto"; +import { UpdateTaskCommentDto } from "../dto/task-comment/update-task-comment.dto"; +import { TMTaskComment } from "../entities/task-comment.entity"; +import { AuthGuards } from "../../../common/decorators/auth-guard.decorator"; +import { AdminRoute } from "../../../common/decorators/admin.decorator"; +import { UserDec } from "../../../common/decorators/user.decorator"; + +@AuthGuards() +@AdminRoute() +@ApiTags("Task Manager - Comments") +@Controller("task-manager/comments") +export class TaskCommentController { + constructor(private readonly taskCommentService: TaskCommentService) {} + + @Get("task/:taskId") + @ApiOperation({ summary: "Get all comments for a task" }) + @ApiParam({ name: "taskId", description: "Task ID" }) + getTaskComments(@Param("taskId") taskId: string): Promise { + return this.taskCommentService.getTaskComments(taskId); + } + + @Post() + @ApiOperation({ summary: "Create a new comment" }) + @ApiResponse({ status: 201, description: "Comment created", type: TMTaskComment }) + @ApiResponse({ status: 404, description: "Task not found" }) + create(@UserDec("id") userId: string, @Body() body: CreateTaskCommentDto): Promise { + return this.taskCommentService.create(userId, body); + } + + @Patch(":id") + @ApiOperation({ summary: "Update a comment" }) + @ApiParam({ name: "id", description: "Comment ID" }) + @ApiResponse({ status: 200, description: "Comment updated", type: TMTaskComment }) + @ApiResponse({ status: 404, description: "Comment or Task not found" }) + update(@Param("id") id: string, @Body() body: UpdateTaskCommentDto): Promise { + return this.taskCommentService.update(id, body); + } + + @Delete(":id") + @ApiOperation({ summary: "Delete a comment" }) + @ApiParam({ name: "id", description: "Comment ID" }) + @ApiResponse({ status: 200, description: "Comment deleted" }) + @ApiResponse({ status: 404, description: "Comment not found" }) + remove(@Param("id") id: string): Promise { + return this.taskCommentService.remove(id); + } +} diff --git a/src/modules/task-manager/dto/task-comment/create-task-comment.dto.ts b/src/modules/task-manager/dto/task-comment/create-task-comment.dto.ts new file mode 100644 index 0000000..80b5905 --- /dev/null +++ b/src/modules/task-manager/dto/task-comment/create-task-comment.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsString, IsNotEmpty, IsUUID } from "class-validator"; + +export class CreateTaskCommentDto { + @ApiProperty({ description: "Comment content", example: "Please review this task before closing." }) + @IsString() + @IsNotEmpty() + content: string; + + @ApiProperty({ description: "ID of the task this comment belongs to", example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }) + @IsUUID() + @IsNotEmpty() + taskId: string; +} diff --git a/src/modules/task-manager/dto/task-comment/update-task-comment.dto.ts b/src/modules/task-manager/dto/task-comment/update-task-comment.dto.ts new file mode 100644 index 0000000..16b2493 --- /dev/null +++ b/src/modules/task-manager/dto/task-comment/update-task-comment.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from "@nestjs/swagger"; +import { CreateTaskCommentDto } from "./create-task-comment.dto"; + +export class UpdateTaskCommentDto extends PartialType(CreateTaskCommentDto) {} diff --git a/src/modules/task-manager/entities/task-comment.entity.ts b/src/modules/task-manager/entities/task-comment.entity.ts new file mode 100644 index 0000000..0385bf2 --- /dev/null +++ b/src/modules/task-manager/entities/task-comment.entity.ts @@ -0,0 +1,32 @@ +import { Column, Entity, JoinColumn, ManyToOne } from "typeorm"; +import { BaseEntity } from "../../../common/entities/base.entity"; +import { User } from "../../users/entities/user.entity"; +import { TMTask } from "./task.entity"; + +@Entity("tm_task_comments") +export class TMTaskComment extends BaseEntity { + @Column({ type: "text" }) + content: string; + + // --- Relations --- + + @ManyToOne(() => User, { + nullable: false, + onDelete: "RESTRICT", + }) + @JoinColumn({ name: "userId" }) + user: User; + + @Column({ type: "uuid" }) + userId: string; + + @ManyToOne(() => TMTask, (task) => task.comments, { + nullable: false, + onDelete: "CASCADE", + }) + @JoinColumn({ name: "taskId" }) + task: TMTask; + + @Column({ type: "uuid" }) + taskId: string; +} diff --git a/src/modules/task-manager/entities/task.entity.ts b/src/modules/task-manager/entities/task.entity.ts index 6cadc59..3c9fc1b 100644 --- a/src/modules/task-manager/entities/task.entity.ts +++ b/src/modules/task-manager/entities/task.entity.ts @@ -5,6 +5,7 @@ import { TMRemark } from "./remark.entity"; import { TMAttachment } from "./attachment.entity"; import { TMCheckListItem } from "./check-list-item.entity"; import { TMTime } from "./time.entity"; +import { TMTaskComment } from "./task-comment.entity"; import { User } from "../../users/entities/user.entity"; import { Ticket } from "../../tickets/entities/ticket.entity"; @@ -67,6 +68,11 @@ export class TMTask extends BaseEntity { }) times: TMTime[]; + @OneToMany(() => TMTaskComment, (comment) => comment.task, { + cascade: ["insert", "update"], + }) + comments: TMTaskComment[]; + @ManyToMany(() => User, (user) => user.tasks) @JoinTable({ name: "tm_task_users", diff --git a/src/modules/task-manager/providers/task-comment.service.ts b/src/modules/task-manager/providers/task-comment.service.ts new file mode 100644 index 0000000..eebac02 --- /dev/null +++ b/src/modules/task-manager/providers/task-comment.service.ts @@ -0,0 +1,60 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { TaskCommentRepository } from "../repositories/task-comment.repository"; +import { TMTaskComment } from "../entities/task-comment.entity"; +import { CreateTaskCommentDto } from "../dto/task-comment/create-task-comment.dto"; +import { UpdateTaskCommentDto } from "../dto/task-comment/update-task-comment.dto"; +import { taskCommentMessage } from "../../../common/enums/message.enum"; +import { TaskService } from "./task.service"; + +@Injectable() +export class TaskCommentService { + constructor( + private readonly taskCommentRepository: TaskCommentRepository, + private readonly taskService: TaskService, + ) {} + + async findOneOrFail(id: string): Promise { + const comment = await this.taskCommentRepository.findOne({ + where: { id }, + relations: { user: true, task: true }, + }); + if (!comment) throw new NotFoundException(taskCommentMessage.TASK_COMMENT_NOT_FOUND); + return comment; + } + + async getTaskComments(taskId: string): Promise { + await this.taskService.findOneOrFail(taskId); + + return this.taskCommentRepository.find({ + where: { taskId }, + relations: { user: true }, + order: { createdAt: "DESC" }, + }); + } + + async create(userId: string, dto: CreateTaskCommentDto): Promise { + await this.taskService.findOneOrFail(dto.taskId); + + const comment = this.taskCommentRepository.create({ + ...dto, + userId, + }); + return this.taskCommentRepository.save(comment); + } + + async update(id: string, dto: UpdateTaskCommentDto): Promise { + const comment = await this.findOneOrFail(id); + + if (dto.taskId) { + await this.taskService.findOneOrFail(dto.taskId); + } + + Object.assign(comment, dto); + return this.taskCommentRepository.save(comment); + } + + async remove(id: string): Promise { + await this.findOneOrFail(id); + await this.taskCommentRepository.delete(id); + } +} diff --git a/src/modules/task-manager/repositories/task-comment.repository.ts b/src/modules/task-manager/repositories/task-comment.repository.ts new file mode 100644 index 0000000..7c5b28f --- /dev/null +++ b/src/modules/task-manager/repositories/task-comment.repository.ts @@ -0,0 +1,13 @@ +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; +import { TMTaskComment } from "../entities/task-comment.entity"; + +@Injectable() +export class TaskCommentRepository extends Repository { + constructor( + @InjectRepository(TMTaskComment) taskCommentRepository: Repository, + ) { + super(taskCommentRepository.target, taskCommentRepository.manager, taskCommentRepository.queryRunner); + } +} diff --git a/src/modules/task-manager/task-manager.module.ts b/src/modules/task-manager/task-manager.module.ts index fed1d89..42cfa7b 100644 --- a/src/modules/task-manager/task-manager.module.ts +++ b/src/modules/task-manager/task-manager.module.ts @@ -9,6 +9,7 @@ import { TMRemark } from "./entities/remark.entity"; import { TMAttachment } from "./entities/attachment.entity"; import { TMCheckListItem } from "./entities/check-list-item.entity"; import { TMTime } from "./entities/time.entity"; +import { TMTaskComment } from "./entities/task-comment.entity"; import { WorkspaceRepository } from "./repositories/workspace.repository"; import { WorkspaceController } from "./controllers/workspace.controller"; import { WorkspaceService } from "./providers/workspace.service"; @@ -35,10 +36,13 @@ import { AttachmentService } from "./providers/attachment.service"; import { TimeController } from "./controllers/time.controller"; import { TimeRepository } from "./repositories/time.repository"; import { TimeService } from "./providers/time.service"; +import { TaskCommentController } from "./controllers/task-comment.controller"; +import { TaskCommentRepository } from "./repositories/task-comment.repository"; +import { TaskCommentService } from "./providers/task-comment.service"; @Module({ imports: [ - TypeOrmModule.forFeature([TMWorkspace, TMProject, TMTaskPhase, TMTask, TMRemark, TMAttachment, TMCheckListItem, TMTime, User, Ticket]), + TypeOrmModule.forFeature([TMWorkspace, TMProject, TMTaskPhase, TMTask, TMRemark, TMAttachment, TMCheckListItem, TMTime, TMTaskComment, User, Ticket]), ], controllers: [ WorkspaceController, @@ -49,6 +53,7 @@ import { TimeService } from "./providers/time.service"; RemarkController, AttachmentController, TimeController, + TaskCommentController, ], providers: [ WorkspaceRepository, @@ -67,6 +72,8 @@ import { TimeService } from "./providers/time.service"; AttachmentService, TimeRepository, TimeService, + TaskCommentRepository, + TaskCommentService, ] }) export class TaskManagerModule {}