task time
This commit is contained in:
@@ -0,0 +1,39 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class AddTaskTimeTable1784100000000 implements MigrationInterface {
|
||||||
|
name = "AddTaskTimeTable1784100000000";
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class MakeTaskTimeEndedAtAndDurationNullable1784200000000
|
||||||
|
implements MigrationInterface
|
||||||
|
{
|
||||||
|
name = "MakeTaskTimeEndedAtAndDurationNullable1784200000000";
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -955,4 +955,11 @@ export const enum checkListItemMessage {
|
|||||||
|
|
||||||
export const enum attachmentMessage {
|
export const enum attachmentMessage {
|
||||||
ATTACHMENT_NOT_FOUND = "ضمیمه پیدا نشد!"
|
ATTACHMENT_NOT_FOUND = "ضمیمه پیدا نشد!"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum timeMessage {
|
||||||
|
TIME_NOT_FOUND = "زمان ثبت شده پیدا نشد!",
|
||||||
|
INVALID_TIME_RANGE = "زمان پایان باید بعد از زمان شروع باشد!",
|
||||||
|
TIME_ALREADY_STARTED = "برای این تسک یک زمان در حال اجرا وجود دارد!",
|
||||||
|
NO_RUNNING_TIME = "زمان در حال اجرایی برای این تسک پیدا نشد!"
|
||||||
}
|
}
|
||||||
@@ -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<TMTime[]> {
|
||||||
|
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<TMTime> {
|
||||||
|
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<TMTime> {
|
||||||
|
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<TMTime> {
|
||||||
|
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<TMTime> {
|
||||||
|
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<void> {
|
||||||
|
return this.timeService.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { PartialType } from "@nestjs/swagger";
|
||||||
|
import { CreateTimeDto } from "./create-time.dto";
|
||||||
|
|
||||||
|
export class UpdateTimeDto extends PartialType(CreateTimeDto) {}
|
||||||
@@ -4,6 +4,7 @@ import { TMTaskPhase } from "./task-phase.entity";
|
|||||||
import { TMRemark } from "./remark.entity";
|
import { TMRemark } from "./remark.entity";
|
||||||
import { TMAttachment } from "./attachment.entity";
|
import { TMAttachment } from "./attachment.entity";
|
||||||
import { TMCheckListItem } from "./check-list-item.entity";
|
import { TMCheckListItem } from "./check-list-item.entity";
|
||||||
|
import { TMTime } from "./time.entity";
|
||||||
import { User } from "../../users/entities/user.entity";
|
import { User } from "../../users/entities/user.entity";
|
||||||
|
|
||||||
@Entity("tm_tasks")
|
@Entity("tm_tasks")
|
||||||
@@ -53,6 +54,11 @@ export class TMTask extends BaseEntity {
|
|||||||
})
|
})
|
||||||
checkListItems: TMCheckListItem[];
|
checkListItems: TMCheckListItem[];
|
||||||
|
|
||||||
|
@OneToMany(() => TMTime, (time) => time.task, {
|
||||||
|
cascade: ["insert", "update"],
|
||||||
|
})
|
||||||
|
times: TMTime[];
|
||||||
|
|
||||||
@ManyToMany(() => User, (user) => user.tasks)
|
@ManyToMany(() => User, (user) => user.tasks)
|
||||||
@JoinTable({
|
@JoinTable({
|
||||||
name: "tm_task_users",
|
name: "tm_task_users",
|
||||||
@@ -65,4 +71,5 @@ export class TMTask extends BaseEntity {
|
|||||||
attachmentCount?: number;
|
attachmentCount?: number;
|
||||||
checkListItemCount?: number;
|
checkListItemCount?: number;
|
||||||
completedCheckListItemCount?: number;
|
completedCheckListItemCount?: number;
|
||||||
|
time?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<TMTime | null> {
|
||||||
|
return this.timeRepository.findOne({
|
||||||
|
where: { adminId, taskId, endedAt: IsNull() },
|
||||||
|
order: { startedAt: "DESC" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOneOrFail(id: string): Promise<TMTime> {
|
||||||
|
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<TMTime> {
|
||||||
|
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<TMTime[]> {
|
||||||
|
return this.timeRepository.find({
|
||||||
|
where: { adminId, taskId },
|
||||||
|
order: { startedAt: "DESC" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async end(adminId: string, taskId: string): Promise<TMTime> {
|
||||||
|
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<TMTime> {
|
||||||
|
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<TMTime> {
|
||||||
|
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<void> {
|
||||||
|
await this.findOneOrFail(id);
|
||||||
|
await this.timeRepository.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { Injectable } from "@nestjs/common";
|
|||||||
import { InjectRepository } from "@nestjs/typeorm";
|
import { InjectRepository } from "@nestjs/typeorm";
|
||||||
import { Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { TMTask } from "../entities/task.entity";
|
import { TMTask } from "../entities/task.entity";
|
||||||
|
import { TMTime } from "../entities/time.entity";
|
||||||
import { CursorDirection, CursorPaginationDto } from "../../../common/DTO/cursor-pagination.dto";
|
import { CursorDirection, CursorPaginationDto } from "../../../common/DTO/cursor-pagination.dto";
|
||||||
import { decodeCursor } from "../../utils/providers/cursor-pagination.utils";
|
import { decodeCursor } from "../../utils/providers/cursor-pagination.utils";
|
||||||
|
|
||||||
@@ -12,11 +13,13 @@ export class TaskRepository extends Repository<TMTask> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getTaskDetail(taskId: string): Promise<TMTask | null> {
|
async getTaskDetail(taskId: string): Promise<TMTask | null> {
|
||||||
return this.createQueryBuilder("task")
|
const task = await this.createQueryBuilder("task")
|
||||||
.leftJoinAndSelect("task.remarks", "remark")
|
.leftJoinAndSelect("task.remarks", "remark")
|
||||||
.leftJoinAndSelect("task.attachments", "attachment")
|
.leftJoinAndSelect("task.attachments", "attachment")
|
||||||
.leftJoinAndSelect("task.checkListItems", "checkListItem")
|
.leftJoinAndSelect("task.checkListItems", "checkListItem")
|
||||||
.leftJoinAndSelect("task.users", "user")
|
.leftJoinAndSelect("task.users", "user")
|
||||||
|
.leftJoinAndSelect("task.times", "time")
|
||||||
|
.leftJoinAndSelect("time.admin", "timeAdmin")
|
||||||
|
|
||||||
.loadRelationCountAndMap("task.checkListItemCount", "task.checkListItems")
|
.loadRelationCountAndMap("task.checkListItemCount", "task.checkListItems")
|
||||||
|
|
||||||
@@ -41,12 +44,28 @@ export class TaskRepository extends Repository<TMTask> {
|
|||||||
"user.id",
|
"user.id",
|
||||||
"user.firstName",
|
"user.firstName",
|
||||||
"user.lastName",
|
"user.lastName",
|
||||||
|
|
||||||
|
"time.id",
|
||||||
|
"time.startedAt",
|
||||||
|
"time.endedAt",
|
||||||
|
"time.duration",
|
||||||
|
"time.adminId",
|
||||||
|
"timeAdmin.id",
|
||||||
|
"timeAdmin.firstName",
|
||||||
|
"timeAdmin.lastName",
|
||||||
])
|
])
|
||||||
.where("task.id = :taskId", { taskId })
|
.where("task.id = :taskId", { taskId })
|
||||||
|
|
||||||
.orderBy("checkListItem.createdAt", "ASC")
|
.orderBy("checkListItem.createdAt", "ASC")
|
||||||
|
.addOrderBy("time.createdAt", "ASC")
|
||||||
|
|
||||||
.getOne();
|
.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 }> {
|
async findTasksByTaskPhase(taskPhaseId: string, pagination: CursorPaginationDto): Promise<{ items: TMTask[]; hasMore: boolean }> {
|
||||||
@@ -103,7 +122,22 @@ export class TaskRepository extends Repository<TMTask> {
|
|||||||
.where("task.id IN (:...ids)", { ids })
|
.where("task.id IN (:...ids)", { ids })
|
||||||
.getMany();
|
.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);
|
const items = ids.map((id) => detailById.get(id)).filter((task): task is TMTask => task !== undefined);
|
||||||
|
|
||||||
return { items, hasMore };
|
return { items, hasMore };
|
||||||
|
|||||||
@@ -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<TMTime> {
|
||||||
|
constructor(@InjectRepository(TMTime) timeRepository: Repository<TMTime>) {
|
||||||
|
super(timeRepository.target, timeRepository.manager, timeRepository.queryRunner);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import { TMTask } from "./entities/task.entity";
|
|||||||
import { TMRemark } from "./entities/remark.entity";
|
import { TMRemark } from "./entities/remark.entity";
|
||||||
import { TMAttachment } from "./entities/attachment.entity";
|
import { TMAttachment } from "./entities/attachment.entity";
|
||||||
import { TMCheckListItem } from "./entities/check-list-item.entity";
|
import { TMCheckListItem } from "./entities/check-list-item.entity";
|
||||||
|
import { TMTime } from "./entities/time.entity";
|
||||||
import { WorkspaceRepository } from "./repositories/workspace.repository";
|
import { WorkspaceRepository } from "./repositories/workspace.repository";
|
||||||
import { WorkspaceController } from "./controllers/workspace.controller";
|
import { WorkspaceController } from "./controllers/workspace.controller";
|
||||||
import { WorkspaceService } from "./providers/workspace.service";
|
import { WorkspaceService } from "./providers/workspace.service";
|
||||||
@@ -30,10 +31,13 @@ import { RemarkService } from "./providers/remark.service";
|
|||||||
import { AttachmentController } from "./controllers/attachment.controller";
|
import { AttachmentController } from "./controllers/attachment.controller";
|
||||||
import { AttachmentRepository } from "./repositories/attachment.repository";
|
import { AttachmentRepository } from "./repositories/attachment.repository";
|
||||||
import { AttachmentService } from "./providers/attachment.service";
|
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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([TMWorkspace, TMProject, TMTaskPhase, TMTask, TMRemark, TMAttachment, TMCheckListItem, User]),
|
TypeOrmModule.forFeature([TMWorkspace, TMProject, TMTaskPhase, TMTask, TMRemark, TMAttachment, TMCheckListItem, TMTime, User]),
|
||||||
],
|
],
|
||||||
controllers: [
|
controllers: [
|
||||||
WorkspaceController,
|
WorkspaceController,
|
||||||
@@ -42,7 +46,8 @@ import { AttachmentService } from "./providers/attachment.service";
|
|||||||
TaskController,
|
TaskController,
|
||||||
CheckListItemController,
|
CheckListItemController,
|
||||||
RemarkController,
|
RemarkController,
|
||||||
AttachmentController
|
AttachmentController,
|
||||||
|
TimeController,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
WorkspaceRepository,
|
WorkspaceRepository,
|
||||||
@@ -58,7 +63,9 @@ import { AttachmentService } from "./providers/attachment.service";
|
|||||||
RemarkRepository,
|
RemarkRepository,
|
||||||
RemarkService,
|
RemarkService,
|
||||||
AttachmentRepository,
|
AttachmentRepository,
|
||||||
AttachmentService
|
AttachmentService,
|
||||||
|
TimeRepository,
|
||||||
|
TimeService,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class TaskManagerModule {}
|
export class TaskManagerModule {}
|
||||||
|
|||||||
Reference in New Issue
Block a user