Compare commits
2 Commits
f66bc9fa8c
...
8aaac51b12
| Author | SHA1 | Date | |
|---|---|---|---|
| 8aaac51b12 | |||
| b855d81817 |
@@ -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"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddTaskCommentEntity1784700000000 implements MigrationInterface {
|
||||
name = "AddTaskCommentEntity1784700000000";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
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<void> {
|
||||
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"`);
|
||||
}
|
||||
}
|
||||
@@ -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 = "نظر مورد نظر پیدا نشد!"
|
||||
}
|
||||
@@ -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<TMTaskComment[]> {
|
||||
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<TMTaskComment> {
|
||||
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<TMTaskComment> {
|
||||
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<void> {
|
||||
return this.taskCommentService.remove(id);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from "@nestjs/swagger";
|
||||
import { CreateTaskCommentDto } from "./create-task-comment.dto";
|
||||
|
||||
export class UpdateTaskCommentDto extends PartialType(CreateTaskCommentDto) {}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -5,7 +5,9 @@ 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";
|
||||
|
||||
@Entity("tm_tasks")
|
||||
export class TMTask extends BaseEntity {
|
||||
@@ -27,6 +29,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, {
|
||||
@@ -59,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",
|
||||
|
||||
@@ -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<TMTaskComment> {
|
||||
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<TMTaskComment[]> {
|
||||
await this.taskService.findOneOrFail(taskId);
|
||||
|
||||
return this.taskCommentRepository.find({
|
||||
where: { taskId },
|
||||
relations: { user: true },
|
||||
order: { createdAt: "DESC" },
|
||||
});
|
||||
}
|
||||
|
||||
async create(userId: string, dto: CreateTaskCommentDto): Promise<TMTaskComment> {
|
||||
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<TMTaskComment> {
|
||||
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<void> {
|
||||
await this.findOneOrFail(id);
|
||||
await this.taskCommentRepository.delete(id);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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<TMTaskComment> {
|
||||
constructor(
|
||||
@InjectRepository(TMTaskComment) taskCommentRepository: Repository<TMTaskComment>,
|
||||
) {
|
||||
super(taskCommentRepository.target, taskCommentRepository.manager, taskCommentRepository.queryRunner);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -9,10 +9,12 @@ 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";
|
||||
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";
|
||||
@@ -34,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]),
|
||||
TypeOrmModule.forFeature([TMWorkspace, TMProject, TMTaskPhase, TMTask, TMRemark, TMAttachment, TMCheckListItem, TMTime, TMTaskComment, User, Ticket]),
|
||||
],
|
||||
controllers: [
|
||||
WorkspaceController,
|
||||
@@ -48,6 +53,7 @@ import { TimeService } from "./providers/time.service";
|
||||
RemarkController,
|
||||
AttachmentController,
|
||||
TimeController,
|
||||
TaskCommentController,
|
||||
],
|
||||
providers: [
|
||||
WorkspaceRepository,
|
||||
@@ -66,6 +72,8 @@ import { TimeService } from "./providers/time.service";
|
||||
AttachmentService,
|
||||
TimeRepository,
|
||||
TimeService,
|
||||
TaskCommentRepository,
|
||||
TaskCommentService,
|
||||
]
|
||||
})
|
||||
export class TaskManagerModule {}
|
||||
|
||||
Reference in New Issue
Block a user