From 4263d7118e9c8b88da496bd4985507574815bc4a Mon Sep 17 00:00:00 2001 From: realrafi Date: Tue, 14 Jul 2026 21:28:09 +0330 Subject: [PATCH 1/2] refactor: changed the findTasksByTaskPhase to include task info like remark, etc --- .../repositories/task.repository.ts | 44 ++++++++++++------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/src/modules/task-manager/repositories/task.repository.ts b/src/modules/task-manager/repositories/task.repository.ts index 0911473..78c8d90 100644 --- a/src/modules/task-manager/repositories/task.repository.ts +++ b/src/modules/task-manager/repositories/task.repository.ts @@ -49,26 +49,21 @@ export class TaskRepository extends Repository { .getOne(); } - async findTasksByTaskPhase( - taskPhaseId: string, - pagination: CursorPaginationDto, - ): Promise<{ items: TMTask[]; hasMore: boolean }> { + async findTasksByTaskPhase(taskPhaseId: string, pagination: CursorPaginationDto): Promise<{ items: TMTask[]; hasMore: boolean }> { const limit = pagination.limit || 10; const isBackward = pagination.direction === CursorDirection.PREV; - // truncated to milliseconds because a JS Date (and therefore the encoded cursor) only - // has millisecond precision, while Postgres timestamptz stores microseconds - comparing - // the raw column against the cursor would otherwise re-match the boundary row itself. const createdAtExpr = `date_trunc('milliseconds', task.createdAt)`; - const queryBuilder = this.createQueryBuilder("task") + const seekQuery = this.createQueryBuilder("task") + .select(["task.id", "task.priority", "task.createdAt"]) .where("task.taskPhaseId = :taskPhaseId", { taskPhaseId }) .limit(limit + 1); // fetch one extra to check whether there's a further page if (isBackward) { - queryBuilder.orderBy("task.priority", "ASC").addOrderBy(createdAtExpr, "DESC").addOrderBy("task.id", "DESC"); + seekQuery.orderBy("task.priority", "ASC").addOrderBy(createdAtExpr, "DESC").addOrderBy("task.id", "DESC"); } else { - queryBuilder.orderBy("task.priority", "DESC").addOrderBy(createdAtExpr, "ASC").addOrderBy("task.id", "ASC"); + seekQuery.orderBy("task.priority", "DESC").addOrderBy(createdAtExpr, "ASC").addOrderBy("task.id", "ASC"); } if (pagination.cursor) { @@ -76,7 +71,7 @@ export class TaskRepository extends Repository { const priorityOp = isBackward ? ">" : "<"; const seekOp = isBackward ? "<" : ">"; - queryBuilder.andWhere( + seekQuery.andWhere( `(task.priority ${priorityOp} :cursorPriority OR (task.priority = :cursorPriority AND ${createdAtExpr} ${seekOp} :cursorCreatedAt) OR (task.priority = :cursorPriority AND ${createdAtExpr} = :cursorCreatedAt AND task.id ${seekOp} :cursorId))`, @@ -84,13 +79,32 @@ export class TaskRepository extends Repository { ); } - const rows = await queryBuilder.getMany(); - const hasMore = rows.length > limit; - if (hasMore) rows.pop(); + const seekRows = await seekQuery.getMany(); + const hasMore = seekRows.length > limit; + if (hasMore) seekRows.pop(); // backward queries fetch in reverse order to seek from the cursor, so flip back to // the normal display order before returning - const items = isBackward ? rows.reverse() : rows; + const orderedRows = isBackward ? seekRows.reverse() : seekRows; + const ids = orderedRows.map((row) => row.id); + + if (!ids.length) return { items: [], hasMore }; + + // Detail query: hydrates exactly the ids picked above with the relations/counts the + // response needs. IN (:...ids) doesn't preserve order, so re-order to match seekQuery. + const detailRows = await this.createQueryBuilder("task") + .leftJoinAndSelect("task.remarks", "remark") + .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"]) + .where("task.id IN (:...ids)", { ids }) + .getMany(); + + const detailById = new Map(detailRows.map((task) => [task.id, task])); + const items = ids.map((id) => detailById.get(id)).filter((task): task is TMTask => task !== undefined); return { items, hasMore }; } From 30d86c9775a1618b2579c2fb2e2a8f6d7b47538b Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Wed, 15 Jul 2026 10:37:19 +0330 Subject: [PATCH 2/2] task time --- .../1784100000000-addTaskTimeTable.ts | 39 ++++++ ...-makeTaskTimeEndedAtAndDurationNullable.ts | 25 ++++ src/common/enums/message.enum.ts | 7 ++ .../controllers/time.controller.ts | 69 ++++++++++ .../task-manager/dto/time/create-time.dto.ts | 19 +++ .../task-manager/dto/time/update-time.dto.ts | 4 + .../task-manager/entities/task.entity.ts | 7 ++ .../task-manager/entities/time.entity.ts | 38 ++++++ .../task-manager/providers/time.service.ts | 119 ++++++++++++++++++ .../repositories/task.repository.ts | 38 +++++- .../repositories/time.repository.ts | 11 ++ .../task-manager/task-manager.module.ts | 13 +- 12 files changed, 384 insertions(+), 5 deletions(-) create mode 100644 database/migrations/1784100000000-addTaskTimeTable.ts create mode 100644 database/migrations/1784200000000-makeTaskTimeEndedAtAndDurationNullable.ts create mode 100644 src/modules/task-manager/controllers/time.controller.ts create mode 100644 src/modules/task-manager/dto/time/create-time.dto.ts create mode 100644 src/modules/task-manager/dto/time/update-time.dto.ts create mode 100644 src/modules/task-manager/entities/time.entity.ts create mode 100644 src/modules/task-manager/providers/time.service.ts create mode 100644 src/modules/task-manager/repositories/time.repository.ts diff --git a/database/migrations/1784100000000-addTaskTimeTable.ts b/database/migrations/1784100000000-addTaskTimeTable.ts new file mode 100644 index 0000000..614394b --- /dev/null +++ b/database/migrations/1784100000000-addTaskTimeTable.ts @@ -0,0 +1,39 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddTaskTimeTable1784100000000 implements MigrationInterface { + name = "AddTaskTimeTable1784100000000"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE "tm_times" ( + "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(), + "startedAt" TIMESTAMP WITH TIME ZONE NOT NULL, + "endedAt" TIMESTAMP WITH TIME ZONE NOT NULL, + "duration" integer NOT NULL, + "adminId" uuid NOT NULL, + "taskId" uuid NOT NULL, + CONSTRAINT "PK_tm_times" PRIMARY KEY ("id") + ) + `); + await queryRunner.query(` + ALTER TABLE "tm_times" + ADD CONSTRAINT "FK_tm_times_admin" + FOREIGN KEY ("adminId") REFERENCES "user"("id") + ON DELETE RESTRICT ON UPDATE NO ACTION + `); + await queryRunner.query(` + ALTER TABLE "tm_times" + ADD CONSTRAINT "FK_tm_times_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_times" DROP CONSTRAINT "FK_tm_times_task"`); + await queryRunner.query(`ALTER TABLE "tm_times" DROP CONSTRAINT "FK_tm_times_admin"`); + await queryRunner.query(`DROP TABLE "tm_times"`); + } +} diff --git a/database/migrations/1784200000000-makeTaskTimeEndedAtAndDurationNullable.ts b/database/migrations/1784200000000-makeTaskTimeEndedAtAndDurationNullable.ts new file mode 100644 index 0000000..b5f3c27 --- /dev/null +++ b/database/migrations/1784200000000-makeTaskTimeEndedAtAndDurationNullable.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class MakeTaskTimeEndedAtAndDurationNullable1784200000000 + implements MigrationInterface +{ + name = "MakeTaskTimeEndedAtAndDurationNullable1784200000000"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "tm_times" ALTER COLUMN "endedAt" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "tm_times" ALTER COLUMN "duration" DROP NOT NULL`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "tm_times" ALTER COLUMN "endedAt" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "tm_times" ALTER COLUMN "duration" SET NOT NULL`, + ); + } +} diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index caa1c37..1e34388 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -955,4 +955,11 @@ export const enum checkListItemMessage { export const enum attachmentMessage { ATTACHMENT_NOT_FOUND = "ضمیمه پیدا نشد!" +} + +export const enum timeMessage { + TIME_NOT_FOUND = "زمان ثبت شده پیدا نشد!", + INVALID_TIME_RANGE = "زمان پایان باید بعد از زمان شروع باشد!", + TIME_ALREADY_STARTED = "برای این تسک یک زمان در حال اجرا وجود دارد!", + NO_RUNNING_TIME = "زمان در حال اجرایی برای این تسک پیدا نشد!" } \ No newline at end of file diff --git a/src/modules/task-manager/controllers/time.controller.ts b/src/modules/task-manager/controllers/time.controller.ts new file mode 100644 index 0000000..51d05d7 --- /dev/null +++ b/src/modules/task-manager/controllers/time.controller.ts @@ -0,0 +1,69 @@ +import { Controller, Post, Patch, Delete, Param, Body, Get } from "@nestjs/common"; +import { ApiTags, ApiOperation, ApiResponse, ApiParam } from "@nestjs/swagger"; +import { TimeService } from "../providers/time.service"; +import { CreateTimeDto } from "../dto/time/create-time.dto"; +import { UpdateTimeDto } from "../dto/time/update-time.dto"; +import { TMTime } from "../entities/time.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 - Times") +@Controller("task-manager/times") +export class TimeController { + constructor(private readonly timeService: TimeService) { } + @Get("task/:taskId") + @ApiOperation({ summary: "Get all time entries for a task" }) + @ApiParam({ name: "taskId", description: "Task ID" }) + getTaskTimes(@UserDec("id") adminId: string, @Param("taskId") taskId: string): Promise { + return this.timeService.getTaskTimes(adminId, taskId); + } + + @Post() + @ApiOperation({ summary: "Create a new time entry for a task" }) + @ApiResponse({ status: 201, description: "Time entry created", type: TMTime }) + @ApiResponse({ status: 404, description: "Task not found" }) + create(@UserDec("id") adminId: string, @Body() body: CreateTimeDto): Promise { + return this.timeService.create(adminId, body); + } + + @Post("start/:taskId") + @ApiOperation({ summary: "Start tracking time for a task" }) + @ApiParam({ name: "taskId", description: "Task ID" }) + @ApiResponse({ status: 201, description: "Time tracking started", type: TMTime }) + @ApiResponse({ status: 400, description: "A running time entry already exists" }) + @ApiResponse({ status: 404, description: "Task not found" }) + start(@UserDec("id") adminId: string, @Param("taskId") taskId: string): Promise { + return this.timeService.start(adminId, taskId); + } + + @Post("end/:taskId") + @ApiOperation({ summary: "Stop the current running time entry for a task" }) + @ApiParam({ name: "taskId", description: "Task ID" }) + @ApiResponse({ status: 200, description: "Time tracking stopped", type: TMTime }) + @ApiResponse({ status: 400, description: "No running time entry found" }) + @ApiResponse({ status: 404, description: "Task not found" }) + end(@UserDec("id") adminId: string, @Param("taskId") taskId: string): Promise { + return this.timeService.end(adminId, taskId); + } + + @Patch(":id") + @ApiOperation({ summary: "Update a time entry" }) + @ApiParam({ name: "id", description: "Time entry ID" }) + @ApiResponse({ status: 200, description: "Time entry updated", type: TMTime }) + @ApiResponse({ status: 404, description: "Time entry or Task not found" }) + update(@Param("id") id: string, @Body() body: UpdateTimeDto): Promise { + return this.timeService.update(id, body); + } + + @Delete(":id") + @ApiOperation({ summary: "Delete a time entry" }) + @ApiParam({ name: "id", description: "Time entry ID" }) + @ApiResponse({ status: 200, description: "Time entry deleted" }) + @ApiResponse({ status: 404, description: "Time entry not found" }) + remove(@Param("id") id: string): Promise { + return this.timeService.remove(id); + } +} diff --git a/src/modules/task-manager/dto/time/create-time.dto.ts b/src/modules/task-manager/dto/time/create-time.dto.ts new file mode 100644 index 0000000..e7b8acd --- /dev/null +++ b/src/modules/task-manager/dto/time/create-time.dto.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsDateString, IsNotEmpty, IsUUID } from "class-validator"; + +export class CreateTimeDto { + @ApiProperty({ description: "Start time of the entry", example: "2026-07-15T08:00:00.000Z" }) + @IsDateString() + @IsNotEmpty() + startedAt: string; + + @ApiProperty({ description: "End time of the entry", example: "2026-07-15T10:30:00.000Z" }) + @IsDateString() + @IsNotEmpty() + endedAt: string; + + @ApiProperty({ description: "ID of the task this time entry belongs to", example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }) + @IsUUID() + @IsNotEmpty() + taskId: string; +} diff --git a/src/modules/task-manager/dto/time/update-time.dto.ts b/src/modules/task-manager/dto/time/update-time.dto.ts new file mode 100644 index 0000000..510be90 --- /dev/null +++ b/src/modules/task-manager/dto/time/update-time.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from "@nestjs/swagger"; +import { CreateTimeDto } from "./create-time.dto"; + +export class UpdateTimeDto extends PartialType(CreateTimeDto) {} diff --git a/src/modules/task-manager/entities/task.entity.ts b/src/modules/task-manager/entities/task.entity.ts index 2b1fecf..f06bbb3 100644 --- a/src/modules/task-manager/entities/task.entity.ts +++ b/src/modules/task-manager/entities/task.entity.ts @@ -4,6 +4,7 @@ import { TMTaskPhase } from "./task-phase.entity"; import { TMRemark } from "./remark.entity"; import { TMAttachment } from "./attachment.entity"; import { TMCheckListItem } from "./check-list-item.entity"; +import { TMTime } from "./time.entity"; import { User } from "../../users/entities/user.entity"; @Entity("tm_tasks") @@ -53,6 +54,11 @@ export class TMTask extends BaseEntity { }) checkListItems: TMCheckListItem[]; + @OneToMany(() => TMTime, (time) => time.task, { + cascade: ["insert", "update"], + }) + times: TMTime[]; + @ManyToMany(() => User, (user) => user.tasks) @JoinTable({ name: "tm_task_users", @@ -65,4 +71,5 @@ export class TMTask extends BaseEntity { attachmentCount?: number; checkListItemCount?: number; completedCheckListItemCount?: number; + time?: number; } diff --git a/src/modules/task-manager/entities/time.entity.ts b/src/modules/task-manager/entities/time.entity.ts new file mode 100644 index 0000000..49e55d9 --- /dev/null +++ b/src/modules/task-manager/entities/time.entity.ts @@ -0,0 +1,38 @@ +import { Column, Entity, JoinColumn, ManyToOne } from "typeorm"; +import { BaseEntity } from "../../../common/entities/base.entity"; +import { TMTask } from "./task.entity"; +import { User } from "../../users/entities/user.entity"; + +@Entity("tm_times") +export class TMTime extends BaseEntity { + @Column({ type: "timestamptz" }) + startedAt: Date; + + @Column({ type: "timestamptz", nullable: true }) + endedAt: Date | null; + + @Column({ type: "int", nullable: true }) + duration: number | null; + + // --- Relations --- + + @ManyToOne(() => User, { + nullable: false, + onDelete: "RESTRICT", + }) + @JoinColumn({ name: "adminId" }) + admin: User; + + @Column({ type: "uuid" }) + adminId: string; + + @ManyToOne(() => TMTask, (task) => task.times, { + nullable: false, + onDelete: "CASCADE", + }) + @JoinColumn({ name: "taskId" }) + task: TMTask; + + @Column({ type: "uuid" }) + taskId: string; +} diff --git a/src/modules/task-manager/providers/time.service.ts b/src/modules/task-manager/providers/time.service.ts new file mode 100644 index 0000000..67593db --- /dev/null +++ b/src/modules/task-manager/providers/time.service.ts @@ -0,0 +1,119 @@ +import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; +import { IsNull } from "typeorm"; +import { TimeRepository } from "../repositories/time.repository"; +import { TMTime } from "../entities/time.entity"; +import { CreateTimeDto } from "../dto/time/create-time.dto"; +import { UpdateTimeDto } from "../dto/time/update-time.dto"; +import { timeMessage } from "../../../common/enums/message.enum"; +import { TaskService } from "./task.service"; + +@Injectable() +export class TimeService { + constructor( + private readonly timeRepository: TimeRepository, + private readonly taskService: TaskService, + ) { } + + private calculateDuration(startedAt: Date, endedAt: Date): number { + if (endedAt.getTime() < startedAt.getTime()) { + throw new BadRequestException(timeMessage.INVALID_TIME_RANGE); + } + + return Math.floor((endedAt.getTime() - startedAt.getTime()) / 1000); + } + + private async findRunningTime(adminId: string, taskId: string): Promise { + return this.timeRepository.findOne({ + where: { adminId, taskId, endedAt: IsNull() }, + order: { startedAt: "DESC" }, + }); + } + + async findOneOrFail(id: string): Promise { + const time = await this.timeRepository.findOne({ + where: { id }, + relations: { admin: true, task: true }, + }); + if (!time) throw new NotFoundException(timeMessage.TIME_NOT_FOUND); + return time; + } + + async start(adminId: string, taskId: string): Promise { + await this.taskService.findOneOrFail(taskId); + + const runningTime = await this.findRunningTime(adminId, taskId); + if (runningTime) throw new BadRequestException(timeMessage.TIME_ALREADY_STARTED); + + const time = this.timeRepository.create({ + taskId, + adminId, + startedAt: new Date(), + endedAt: null, + duration: null, + }); + + return this.timeRepository.save(time); + } + + async getTaskTimes(adminId: string, taskId: string): Promise { + return this.timeRepository.find({ + where: { adminId, taskId }, + order: { startedAt: "DESC" }, + }); + } + + async end(adminId: string, taskId: string): Promise { + await this.taskService.findOneOrFail(taskId); + + const runningTime = await this.findRunningTime(adminId, taskId); + if (!runningTime) throw new BadRequestException(timeMessage.NO_RUNNING_TIME); + + const endedAt = new Date(); + runningTime.endedAt = endedAt; + runningTime.duration = this.calculateDuration(runningTime.startedAt, endedAt); + + return this.timeRepository.save(runningTime); + } + + async create(adminId: string, dto: CreateTimeDto): Promise { + await this.taskService.findOneOrFail(dto.taskId); + + const startedAt = new Date(dto.startedAt); + const endedAt = new Date(dto.endedAt); + + const time = this.timeRepository.create({ + taskId: dto.taskId, + adminId, + startedAt, + endedAt, + duration: this.calculateDuration(startedAt, endedAt), + }); + + return this.timeRepository.save(time); + } + + async update(id: string, dto: UpdateTimeDto): Promise { + const time = await this.findOneOrFail(id); + + if (dto.taskId) { + await this.taskService.findOneOrFail(dto.taskId); + } + + const startedAt = dto.startedAt ? new Date(dto.startedAt) : time.startedAt; + const endedAt = dto.endedAt ? new Date(dto.endedAt) : time.endedAt; + + Object.assign(time, { + ...dto, + startedAt, + endedAt, + duration: endedAt ? this.calculateDuration(startedAt, endedAt) : null, + }); + + return this.timeRepository.save(time); + } + + async remove(id: string): Promise { + await this.findOneOrFail(id); + await this.timeRepository.delete(id); + } +} diff --git a/src/modules/task-manager/repositories/task.repository.ts b/src/modules/task-manager/repositories/task.repository.ts index 78c8d90..7373d91 100644 --- a/src/modules/task-manager/repositories/task.repository.ts +++ b/src/modules/task-manager/repositories/task.repository.ts @@ -2,6 +2,7 @@ import { Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Repository } from "typeorm"; import { TMTask } from "../entities/task.entity"; +import { TMTime } from "../entities/time.entity"; import { CursorDirection, CursorPaginationDto } from "../../../common/DTO/cursor-pagination.dto"; import { decodeCursor } from "../../utils/providers/cursor-pagination.utils"; @@ -12,11 +13,13 @@ export class TaskRepository extends Repository { } async getTaskDetail(taskId: string): Promise { - return this.createQueryBuilder("task") + const task = await this.createQueryBuilder("task") .leftJoinAndSelect("task.remarks", "remark") .leftJoinAndSelect("task.attachments", "attachment") .leftJoinAndSelect("task.checkListItems", "checkListItem") .leftJoinAndSelect("task.users", "user") + .leftJoinAndSelect("task.times", "time") + .leftJoinAndSelect("time.admin", "timeAdmin") .loadRelationCountAndMap("task.checkListItemCount", "task.checkListItems") @@ -41,12 +44,28 @@ export class TaskRepository extends Repository { "user.id", "user.firstName", "user.lastName", + + "time.id", + "time.startedAt", + "time.endedAt", + "time.duration", + "time.adminId", + "timeAdmin.id", + "timeAdmin.firstName", + "timeAdmin.lastName", ]) .where("task.id = :taskId", { taskId }) .orderBy("checkListItem.createdAt", "ASC") + .addOrderBy("time.createdAt", "ASC") .getOne(); + + if (task) { + task.time = task.times?.reduce((sum, entry) => sum + (entry.duration ?? 0), 0) ?? 0; + } + + return task; } async findTasksByTaskPhase(taskPhaseId: string, pagination: CursorPaginationDto): Promise<{ items: TMTask[]; hasMore: boolean }> { @@ -103,7 +122,22 @@ export class TaskRepository extends Repository { .where("task.id IN (:...ids)", { ids }) .getMany(); - const detailById = new Map(detailRows.map((task) => [task.id, task])); + const timeSums = await this.manager + .createQueryBuilder(TMTime, "time") + .select("time.taskId", "taskId") + .addSelect("COALESCE(SUM(time.duration), 0)", "totalTime") + .where("time.taskId IN (:...ids)", { ids }) + .groupBy("time.taskId") + .getRawMany<{ taskId: string; totalTime: string }>(); + + const timeByTaskId = new Map(timeSums.map((row) => [row.taskId, Number(row.totalTime)])); + + const detailById = new Map( + detailRows.map((task) => { + task.time = timeByTaskId.get(task.id) ?? 0; + return [task.id, task]; + }), + ); const items = ids.map((id) => detailById.get(id)).filter((task): task is TMTask => task !== undefined); return { items, hasMore }; diff --git a/src/modules/task-manager/repositories/time.repository.ts b/src/modules/task-manager/repositories/time.repository.ts new file mode 100644 index 0000000..188db17 --- /dev/null +++ b/src/modules/task-manager/repositories/time.repository.ts @@ -0,0 +1,11 @@ +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; +import { TMTime } from "../entities/time.entity"; + +@Injectable() +export class TimeRepository extends Repository { + constructor(@InjectRepository(TMTime) timeRepository: Repository) { + super(timeRepository.target, timeRepository.manager, timeRepository.queryRunner); + } +} diff --git a/src/modules/task-manager/task-manager.module.ts b/src/modules/task-manager/task-manager.module.ts index 43bbb5b..5943d7f 100644 --- a/src/modules/task-manager/task-manager.module.ts +++ b/src/modules/task-manager/task-manager.module.ts @@ -8,6 +8,7 @@ import { TMTask } from "./entities/task.entity"; 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 { WorkspaceRepository } from "./repositories/workspace.repository"; import { WorkspaceController } from "./controllers/workspace.controller"; import { WorkspaceService } from "./providers/workspace.service"; @@ -30,10 +31,13 @@ import { RemarkService } from "./providers/remark.service"; import { AttachmentController } from "./controllers/attachment.controller"; import { AttachmentRepository } from "./repositories/attachment.repository"; import { AttachmentService } from "./providers/attachment.service"; +import { TimeController } from "./controllers/time.controller"; +import { TimeRepository } from "./repositories/time.repository"; +import { TimeService } from "./providers/time.service"; @Module({ imports: [ - TypeOrmModule.forFeature([TMWorkspace, TMProject, TMTaskPhase, TMTask, TMRemark, TMAttachment, TMCheckListItem, User]), + TypeOrmModule.forFeature([TMWorkspace, TMProject, TMTaskPhase, TMTask, TMRemark, TMAttachment, TMCheckListItem, TMTime, User]), ], controllers: [ WorkspaceController, @@ -42,7 +46,8 @@ import { AttachmentService } from "./providers/attachment.service"; TaskController, CheckListItemController, RemarkController, - AttachmentController + AttachmentController, + TimeController, ], providers: [ WorkspaceRepository, @@ -58,7 +63,9 @@ import { AttachmentService } from "./providers/attachment.service"; RemarkRepository, RemarkService, AttachmentRepository, - AttachmentService + AttachmentService, + TimeRepository, + TimeService, ] }) export class TaskManagerModule {}