Merge branch 'rafi/taskmanager'
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
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 {
|
||||
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 { 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;
|
||||
}
|
||||
|
||||
@@ -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 { 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<TMTask> {
|
||||
}
|
||||
|
||||
async getTaskDetail(taskId: string): Promise<TMTask | null> {
|
||||
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,34 +44,45 @@ export class TaskRepository extends Repository<TMTask> {
|
||||
"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 }> {
|
||||
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 +90,7 @@ export class TaskRepository extends Repository<TMTask> {
|
||||
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 +98,47 @@ export class TaskRepository extends Repository<TMTask> {
|
||||
);
|
||||
}
|
||||
|
||||
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 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 };
|
||||
}
|
||||
|
||||
@@ -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 { 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 {}
|
||||
|
||||
Reference in New Issue
Block a user