Compare commits

11 Commits

22 changed files with 362 additions and 230 deletions
-11
View File
@@ -1,11 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class Migrations1783244654421 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}
@@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class TaskmanagerAddingNewEnumValueForPermissionEnumType1783324039378 implements MigrationInterface {
name = 'TaskmanagerAddingNewEnumValueForPermissionEnumType1783324039378'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TYPE "public"."permission_name_enum" RENAME TO "permission_name_enum_old"`);
await queryRunner.query(`CREATE TYPE "public"."permission_name_enum" AS ENUM('services', 'customers', 'agents', 'developers', 'invoices', 'transactions', 'discounts', 'admins', 'tickets', 'criticisms', 'contacts_us', 'advertisements', 'announcements', 'blogs', 'learnings', 'logs', 'settings', 'bank_accounts', 'payments', 'manage_sso_clients', 'support_plan', 'dmenu', 'dkala', 'dpage', 'dmail', 'reseller', 'workspace', 'project')`);
await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum" USING "name"::"text"::"public"."permission_name_enum"`);
await queryRunner.query(`DROP TYPE "public"."permission_name_enum_old"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TYPE "public"."permission_name_enum_old" AS ENUM('services', 'customers', 'agents', 'developers', 'invoices', 'transactions', 'discounts', 'admins', 'tickets', 'criticisms', 'contacts_us', 'advertisements', 'announcements', 'blogs', 'learnings', 'logs', 'settings', 'bank_accounts', 'payments', 'manage_sso_clients', 'support_plan', 'dmenu', 'dkala', 'dpage', 'dmail', 'reseller', 'workspace')`);
await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum_old" USING "name"::"text"::"public"."permission_name_enum_old"`);
await queryRunner.query(`DROP TYPE "public"."permission_name_enum"`);
await queryRunner.query(`ALTER TYPE "public"."permission_name_enum_old" RENAME TO "permission_name_enum"`);
}
}
@@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class TMAddingTaskPhasePermissionToPermissionEnumType1783335279945 implements MigrationInterface {
name = 'TMAddingTaskPhasePermissionToPermissionEnumType1783335279945'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TYPE "public"."permission_name_enum" RENAME TO "permission_name_enum_old"`);
await queryRunner.query(`CREATE TYPE "public"."permission_name_enum" AS ENUM('services', 'customers', 'agents', 'developers', 'invoices', 'transactions', 'discounts', 'admins', 'tickets', 'criticisms', 'contacts_us', 'advertisements', 'announcements', 'blogs', 'learnings', 'logs', 'settings', 'bank_accounts', 'payments', 'manage_sso_clients', 'support_plan', 'dmenu', 'dkala', 'dpage', 'dmail', 'reseller', 'workspace', 'project', 'task_phase')`);
await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum" USING "name"::"text"::"public"."permission_name_enum"`);
await queryRunner.query(`DROP TYPE "public"."permission_name_enum_old"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TYPE "public"."permission_name_enum_old" AS ENUM('services', 'customers', 'agents', 'developers', 'invoices', 'transactions', 'discounts', 'admins', 'tickets', 'criticisms', 'contacts_us', 'advertisements', 'announcements', 'blogs', 'learnings', 'logs', 'settings', 'bank_accounts', 'payments', 'manage_sso_clients', 'support_plan', 'dmenu', 'dkala', 'dpage', 'dmail', 'reseller', 'workspace', 'project')`);
await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum_old" USING "name"::"text"::"public"."permission_name_enum_old"`);
await queryRunner.query(`DROP TYPE "public"."permission_name_enum"`);
await queryRunner.query(`ALTER TYPE "public"."permission_name_enum_old" RENAME TO "permission_name_enum"`);
}
}
+20 -2
View File
@@ -920,11 +920,29 @@ export const enum WorkSpaceMessage {
WORKSPACE_EXISTS = "فضای کار با این اسم وجود دارد!", WORKSPACE_EXISTS = "فضای کار با این اسم وجود دارد!",
WORKSPACE_NOT_FOUND = "فضای کار مورد نظر شما وجود ندارد!", WORKSPACE_NOT_FOUND = "فضای کار مورد نظر شما وجود ندارد!",
USERS_DONT_BELONG_TO_THIS_WORKSPACE = "کاربران تعیین شده برای این فضای کاری نیستند!", USERS_DONT_BELONG_TO_THIS_WORKSPACE = "کاربران تعیین شده برای این فضای کاری نیستند!",
WORKSPACE_SAME_NAME_EXISTS = "فضای کاری با این اسم از قبل وجود دارد!" WORKSPACE_SAME_NAME_EXISTS = "فضای کاری با این اسم از قبل وجود دارد!",
WORKSPACE_ID_PARAM_NOT_EMPTY = "پارامتر شناسه فضای کار نباید خالی باشد!",
WORKSPACE_ID_UUID = "شناسه فضای کار باید uuid باشد"
} }
export const enum ProjectMessage { export const enum ProjectMessage {
PROJECT_EXISTS = "پروژه مورد نظر شما از قبل وجود دارد!", PROJECT_EXISTS = "پروژه مورد نظر شما از قبل وجود دارد!",
PROJECT_NOT_FOUND = "پروژه مورد نظر شما وجود ندارد!", PROJECT_NOT_FOUND = "پروژه مورد نظر شما وجود ندارد!",
PROJECT_CONTAINS_TASK_PHASES = "پروژه شامل فاز تسک است. لطفا ابتدا فاز تسک های این پروژه را حذف کنید." PROJECT_CONTAINS_TASK_PHASES = "پروژه شامل فاز تسک است. لطفا ابتدا فاز تسک های این پروژه را حذف کنید.",
NOT_MEMBER_OF_PROJECT = "شما عضو این پروژه نیستید!"
}
export const enum TaskPhaseMessage {
TASK_PHASE_NOT_FOUND = "فاز تسک مورد نظر پیدا نشد!",
TASK_PHASE_NOT_SAME_PROJECT = "فاز تسک جدید با فاز تسک قبلی در یک پروژه نیستند!"
}
export const enum TaskMessage {
TASK_NOT_FOUND = "تسک مورد نظر پیدا نشد!",
USERS_ASSIGNED_TO_TASK_NOT_PROJECT_MEMBER = "حداقل یکی از کاربران تعیین شده عضو پروژه نیستند!",
USER_NOT_TASK_MEMBER = "شما عضو این تسک نیستید!"
}
export const enum remarkMessage {
REMARK_NOT_FOUND = "برچسب مورد نظر پیدا نشد!"
} }
@@ -8,7 +8,11 @@ import { TMProject } from "../entities/project.entity";
import { AdminRoute } from "../../../common/decorators/admin.decorator"; import { AdminRoute } from "../../../common/decorators/admin.decorator";
import { PermissionsDec } from "../../../common/decorators/permission.decorator"; import { PermissionsDec } from "../../../common/decorators/permission.decorator";
import { PermissionEnum } from "../../users/enums/permission.enum"; import { PermissionEnum } from "../../users/enums/permission.enum";
import { UserDec } from "../../../common/decorators/user.decorator";
import { AuthGuards } from "../../../common/decorators/auth-guard.decorator";
import { ParamDto } from "../../../common/DTO/param.dto";
@AuthGuards()
@AdminRoute() @AdminRoute()
@ApiTags("Task Manager - Projects") @ApiTags("Task Manager - Projects")
@Controller("task-manager/projects") @Controller("task-manager/projects")
@@ -64,16 +68,27 @@ export class ProjectController {
return this.projectService.remove(id); return this.projectService.remove(id);
} }
@Get("/workspace/:wsId") @Get("user/workspace/:id")
@PermissionsDec(PermissionEnum.PROJECT) @ApiOperation({ summary: "Get user projects!" })
@ApiOperation({ summary: "Get projects of a specific workspace" }) @ApiResponse({ status: 200, description: "Got user projects successfully!" })
@ApiParam({ name: "wsId", description: "Workspace ID" }) @ApiResponse({status: 400, description: "Bad Request!"})
@ApiResponse({ status: 200, description: "received projects successfully!" }) findUserProjects(
@ApiResponse({ status: 404, description: "WorkSpace not found" }) @UserDec("id") userId: string,
findByWorkspace( @Param() paramDto : ParamDto,
@Param("wsId") wsId: string,
@Query() pagination: PaginationDto, @Query() pagination: PaginationDto,
): Promise<{ projects: TMProject[]; count: number; paginate: true }> { ): Promise<{ projects: TMProject[]; count: number; paginate: true }> {
return this.projectService.findByWorkspace(wsId, pagination); return this.projectService.findUserProjects(userId, paramDto.id, pagination);
} }
@Get("project/:id")
@ApiOperation({summary: "Get project detail"})
@ApiResponse({status: 200, description: "Got Project Detail Successfully!"})
@ApiResponse({status: 400, description: "Bad request!"})
@ApiResponse({status: 404, description: "The project was not found!"})
getProjectDetail(@Param() paramDto: ParamDto,
@UserDec('id') userId: string
): Promise<TMProject | null> {
return this.projectService.getProjectDetail(paramDto.id, userId);
}
} }
@@ -4,7 +4,14 @@ import { RemarkService } from "../providers/remark.service";
import { CreateRemarkDto } from "../dto/remark/create-remark.dto"; import { CreateRemarkDto } from "../dto/remark/create-remark.dto";
import { UpdateRemarkDto } from "../dto/remark/update-remark.dto"; import { UpdateRemarkDto } from "../dto/remark/update-remark.dto";
import { TMRemark } from "../entities/remark.entity"; import { TMRemark } from "../entities/remark.entity";
import { AuthGuards } from "../../../common/decorators/auth-guard.decorator";
import { AdminRoute } from "../../../common/decorators/admin.decorator";
import { PermissionsDec } from "../../../common/decorators/permission.decorator";
import { PermissionEnum } from "../../users/enums/permission.enum";
@AuthGuards()
@AdminRoute()
@PermissionsDec(PermissionEnum.REMARK)
@ApiTags("Task Manager - Remarks") @ApiTags("Task Manager - Remarks")
@Controller("task-manager/remarks") @Controller("task-manager/remarks")
export class RemarkController { export class RemarkController {
@@ -25,7 +32,7 @@ export class RemarkController {
@ApiResponse({ status: 200, description: "Remark found", type: TMRemark }) @ApiResponse({ status: 200, description: "Remark found", type: TMRemark })
@ApiResponse({ status: 404, description: "Remark not found" }) @ApiResponse({ status: 404, description: "Remark not found" })
findOne(@Param("id") id: string): Promise<TMRemark> { findOne(@Param("id") id: string): Promise<TMRemark> {
return this.remarkService.findOne(id); return this.remarkService.findOneOrFail(id);
} }
@Post() @Post()
@@ -4,7 +4,14 @@ import { TaskPhaseService } from "../providers/task-phase.service";
import { CreateTaskPhaseDto } from "../dto/task-phase/create-task-phase.dto"; import { CreateTaskPhaseDto } from "../dto/task-phase/create-task-phase.dto";
import { UpdateTaskPhaseDto } from "../dto/task-phase/update-task-phase.dto"; import { UpdateTaskPhaseDto } from "../dto/task-phase/update-task-phase.dto";
import { TMTaskPhase } from "../entities/task-phase.entity"; import { TMTaskPhase } from "../entities/task-phase.entity";
import { AuthGuards } from "../../../common/decorators/auth-guard.decorator";
import { AdminRoute } from "../../../common/decorators/admin.decorator";
import { PermissionsDec } from "../../../common/decorators/permission.decorator";
import { PermissionEnum } from "../../users/enums/permission.enum";
@AuthGuards()
@AdminRoute()
@PermissionsDec(PermissionEnum.TASK_PHASE)
@ApiTags("Task Manager - Task Phases") @ApiTags("Task Manager - Task Phases")
@Controller("task-manager/task-phases") @Controller("task-manager/task-phases")
export class TaskPhaseController { export class TaskPhaseController {
@@ -25,7 +32,7 @@ export class TaskPhaseController {
@ApiResponse({ status: 200, description: "TaskPhase found", type: TMTaskPhase }) @ApiResponse({ status: 200, description: "TaskPhase found", type: TMTaskPhase })
@ApiResponse({ status: 404, description: "TaskPhase not found" }) @ApiResponse({ status: 404, description: "TaskPhase not found" })
findOne(@Param("id") id: string): Promise<TMTaskPhase> { findOne(@Param("id") id: string): Promise<TMTaskPhase> {
return this.taskPhaseService.findOne(id); return this.taskPhaseService.findOneOrFail(id);
} }
@Post() @Post()
@@ -5,13 +5,22 @@ import { CreateTaskDto } from '../dto/task/create-task.dto';
import { UpdateTaskDto } from '../dto/task/update-task.dto'; import { UpdateTaskDto } from '../dto/task/update-task.dto';
import { ChangeTaskPhaseDto } from '../dto/task/change-task-phase.dto'; import { ChangeTaskPhaseDto } from '../dto/task/change-task-phase.dto';
import { TMTask } from '../entities/task.entity'; import { TMTask } from '../entities/task.entity';
import { AuthGuards } from '../../../common/decorators/auth-guard.decorator';
import { AdminRoute } from '../../../common/decorators/admin.decorator';
import { UserDec } from '../../../common/decorators/user.decorator';
import { ParamDto } from '../../../common/DTO/param.dto';
import { PermissionsDec } from '../../../common/decorators/permission.decorator';
import { PermissionEnum } from '../../users/enums/permission.enum';
@AuthGuards()
@AdminRoute()
@ApiTags('Task Manager - Tasks') @ApiTags('Task Manager - Tasks')
@Controller('task-manager/tasks') @Controller('task-manager/tasks')
export class TaskController { export class TaskController {
constructor(private readonly taskService: TaskService) {} constructor(private readonly taskService: TaskService) {}
@Get() @Get()
@PermissionsDec(PermissionEnum.TASK)
@ApiOperation({ summary: 'Get all tasks, optionally filtered by phase' }) @ApiOperation({ summary: 'Get all tasks, optionally filtered by phase' })
@ApiQuery({ name: 'taskPhaseId', required: false, description: 'Filter tasks by task phase ID' }) @ApiQuery({ name: 'taskPhaseId', required: false, description: 'Filter tasks by task phase ID' })
@ApiResponse({ status: 200, description: 'List of tasks', type: [TMTask] }) @ApiResponse({ status: 200, description: 'List of tasks', type: [TMTask] })
@@ -21,15 +30,17 @@ export class TaskController {
} }
@Get(':id') @Get(':id')
@PermissionsDec(PermissionEnum.TASK)
@ApiOperation({ summary: 'Get a task by ID' }) @ApiOperation({ summary: 'Get a task by ID' })
@ApiParam({ name: 'id', description: 'Task ID' }) @ApiParam({ name: 'id', description: 'Task ID' })
@ApiResponse({ status: 200, description: 'Task found', type: TMTask }) @ApiResponse({ status: 200, description: 'Task found', type: TMTask })
@ApiResponse({ status: 404, description: 'Task not found' }) @ApiResponse({ status: 404, description: 'Task not found' })
findOne(@Param('id') id: string): Promise<TMTask> { findOne(@Param('id') id: string): Promise<TMTask> {
return this.taskService.findOne(id); return this.taskService.findOneOrFail(id);
} }
@Post() @Post()
@PermissionsDec(PermissionEnum.TASK)
@ApiOperation({ summary: 'Create a new task' }) @ApiOperation({ summary: 'Create a new task' })
@ApiResponse({ status: 201, description: 'Task created', type: TMTask }) @ApiResponse({ status: 201, description: 'Task created', type: TMTask })
@ApiResponse({ status: 404, description: 'TaskPhase not found' }) @ApiResponse({ status: 404, description: 'TaskPhase not found' })
@@ -38,6 +49,7 @@ export class TaskController {
} }
@Patch(':id') @Patch(':id')
@PermissionsDec(PermissionEnum.TASK)
@ApiOperation({ summary: 'Update a task' }) @ApiOperation({ summary: 'Update a task' })
@ApiParam({ name: 'id', description: 'Task ID' }) @ApiParam({ name: 'id', description: 'Task ID' })
@ApiResponse({ status: 200, description: 'Task updated', type: TMTask }) @ApiResponse({ status: 200, description: 'Task updated', type: TMTask })
@@ -47,6 +59,7 @@ export class TaskController {
} }
@Patch(':id/change-phase') @Patch(':id/change-phase')
@PermissionsDec(PermissionEnum.TASK)
@ApiOperation({ summary: 'Move a task to a different phase within the same project' }) @ApiOperation({ summary: 'Move a task to a different phase within the same project' })
@ApiParam({ name: 'id', description: 'Task ID' }) @ApiParam({ name: 'id', description: 'Task ID' })
@ApiResponse({ status: 200, description: 'Task phase changed', type: TMTask }) @ApiResponse({ status: 200, description: 'Task phase changed', type: TMTask })
@@ -57,6 +70,7 @@ export class TaskController {
} }
@Delete(':id') @Delete(':id')
@PermissionsDec(PermissionEnum.TASK)
@ApiOperation({ summary: 'Delete a task' }) @ApiOperation({ summary: 'Delete a task' })
@ApiParam({ name: 'id', description: 'Task ID' }) @ApiParam({ name: 'id', description: 'Task ID' })
@ApiResponse({ status: 200, description: 'Task deleted' }) @ApiResponse({ status: 200, description: 'Task deleted' })
@@ -64,4 +78,16 @@ export class TaskController {
remove(@Param('id') id: string): Promise<void> { remove(@Param('id') id: string): Promise<void> {
return this.taskService.remove(id); return this.taskService.remove(id);
} }
@Get('detail/:id')
@ApiOperation({summary: 'Get Task Detail'})
@ApiParam({ name: 'id', description: 'Task ID' })
@ApiResponse({ status: 200, description: 'Task detail received!' })
@ApiResponse({ status: 404, description: 'Task not found' })
getTaskDetail(
@UserDec('id') userId: string,
@Param() paramDto: ParamDto
): Promise<TMTask | null> {
return this.taskService.getTaskDetail(userId, paramDto.id);
}
} }
@@ -64,7 +64,7 @@ export class WorkspaceController {
return this.workspaceService.remove(id); return this.workspaceService.remove(id);
} }
@Get('/byuser') @Get('byuser')
@ApiOperation({summary: 'Get User WorkSpaces'}) @ApiOperation({summary: 'Get User WorkSpaces'})
@ApiResponse({ status: 200, description: 'Got Workspaces successfully!' }) @ApiResponse({ status: 200, description: 'Got Workspaces successfully!' })
@ApiResponse({ status: 404, description: 'Bad Request!' }) @ApiResponse({ status: 404, description: 'Bad Request!' })
@@ -57,4 +57,9 @@ export class TMTask extends BaseEntity {
inverseJoinColumn: { name: "userId", referencedColumnName: "id" }, inverseJoinColumn: { name: "userId", referencedColumnName: "id" },
}) })
users: User[]; users: User[];
// deriven(calculated) attributes. not database columns
attachmentCount?: number;
checkListItemCount?: number;
completedCheckListItemCount?: number;
} }
@@ -1,4 +1,4 @@
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm"; import { InjectRepository } from "@nestjs/typeorm";
import { In, Repository } from "typeorm"; import { In, Repository } from "typeorm";
import { ProjectRepository } from "../repositories/project.repository"; import { ProjectRepository } from "../repositories/project.repository";
@@ -42,32 +42,6 @@ export class ProjectService {
return { projects, count, paginate: true }; return { projects, count, paginate: true };
} }
async findByWorkspace(workspaceId: string, pagination: PaginationDto): Promise<{ projects: TMProject[]; count: number; paginate: true }> {
await this.workspaceService.findOneOrFail(workspaceId);
const page = pagination.page ?? 1;
const limit = pagination.limit ?? 10;
const skip = (page - 1) * limit;
const [projects, count] = await this.projectRepository.findAndCount({
where: { workspaceId },
relations: {
users: true,
},
select: {
users: {
id: true,
firstName: true,
lastName: true,
},
},
skip,
take: limit,
});
return { projects, count, paginate: true };
}
async findOneOrFail(id: string): Promise<TMProject> { async findOneOrFail(id: string): Promise<TMProject> {
const project = await this.projectRepository.findOne({ where: { id } }); const project = await this.projectRepository.findOne({ where: { id } });
if (!project) throw new NotFoundException(ProjectMessage.PROJECT_NOT_FOUND); if (!project) throw new NotFoundException(ProjectMessage.PROJECT_NOT_FOUND);
@@ -140,8 +114,8 @@ export class ProjectService {
}, },
}); });
if(!project) throw new NotFoundException(ProjectMessage.PROJECT_NOT_FOUND); if (!project) throw new NotFoundException(ProjectMessage.PROJECT_NOT_FOUND);
const taskPhases = project.taskPhases; const taskPhases = project.taskPhases;
if (taskPhases !== undefined && taskPhases.length) { if (taskPhases !== undefined && taskPhases.length) {
throw new BadRequestException(ProjectMessage.PROJECT_CONTAINS_TASK_PHASES); throw new BadRequestException(ProjectMessage.PROJECT_CONTAINS_TASK_PHASES);
@@ -149,4 +123,35 @@ export class ProjectService {
await this.projectRepository.delete(id); await this.projectRepository.delete(id);
return project; return project;
} }
async findUserProjects(
userId: string,
wsId: string,
pagination: PaginationDto,
): Promise<{ projects: TMProject[]; count: number; paginate: true }> {
const page = pagination.page ?? 1;
const limit = pagination.limit ?? 10;
const skip = (page - 1) * limit;
const [projects, count] = await this.projectRepository.findUserProjects(userId, wsId, skip, limit);
return { projects, count, paginate: true };
}
async getProjectDetail(projectId: string, userId: string): Promise<TMProject | null> {
const project = await this.projectRepository.findOne({
where: { id: projectId },
relations: {
users: true,
},
});
if (!project) throw new NotFoundException(ProjectMessage.PROJECT_NOT_FOUND);
const isUserMember = project.users.some(user => user.id === userId);
if(!isUserMember) throw new ForbiddenException(ProjectMessage.NOT_MEMBER_OF_PROJECT);
const projectDetail = await this.projectRepository.getProjectDetail(projectId);
return projectDetail;
}
} }
@@ -1,45 +1,49 @@
import { Injectable, NotFoundException } from "@nestjs/common"; import { Injectable, NotFoundException } from "@nestjs/common";
import { RemarkRepository } from "../repositories/remark.repository"; import { RemarkRepository } from "../repositories/remark.repository";
import { TaskRepository } from "../repositories/task.repository";
import { TMRemark } from "../entities/remark.entity"; import { TMRemark } from "../entities/remark.entity";
import { CreateRemarkDto } from "../dto/remark/create-remark.dto"; import { CreateRemarkDto } from "../dto/remark/create-remark.dto";
import { UpdateRemarkDto } from "../dto/remark/update-remark.dto"; import { UpdateRemarkDto } from "../dto/remark/update-remark.dto";
import { remarkMessage } from "../../../common/enums/message.enum";
import { TaskService } from './task.service';
@Injectable() @Injectable()
export class RemarkService { export class RemarkService {
constructor( constructor(
private readonly remarkRepository: RemarkRepository, private readonly remarkRepository: RemarkRepository,
private readonly taskRepository: TaskRepository, private readonly taskService: TaskService,
) {} ) {}
findAll(): Promise<TMRemark[]> { findAll(): Promise<TMRemark[]> {
return this.remarkRepository.findAll(); return this.remarkRepository.find({ relations: { task: true } });
} }
findByTask(taskId: string): Promise<TMRemark[]> { findByTask(taskId: string): Promise<TMRemark[]> {
return this.remarkRepository.findByTask(taskId); return this.remarkRepository.find({
where: { taskId },
relations: {
task: true,
},
});
} }
async findOne(id: string): Promise<TMRemark> { async findOneOrFail(id: string): Promise<TMRemark> {
const remark = await this.remarkRepository.findOneById(id); const remark = await this.remarkRepository.findOne({ where: { id } });
if (!remark) throw new NotFoundException(`Remark #${id} not found`); if (!remark) throw new NotFoundException(remarkMessage.REMARK_NOT_FOUND);
return remark; return remark;
} }
async create(dto: CreateRemarkDto): Promise<TMRemark> { async create(dto: CreateRemarkDto): Promise<TMRemark> {
const task = await this.taskRepository.findOneById(dto.taskId); await this.taskService.findOneOrFail(dto.taskId);
if (!task) throw new NotFoundException(`Task #${dto.taskId} not found`);
const remark = this.remarkRepository.create(dto); const remark = this.remarkRepository.create(dto);
return this.remarkRepository.save(remark); return this.remarkRepository.save(remark);
} }
async update(id: string, dto: UpdateRemarkDto): Promise<TMRemark> { async update(id: string, dto: UpdateRemarkDto): Promise<TMRemark> {
const remark = await this.findOne(id); const remark = await this.findOneOrFail(id);
if (dto.taskId) { if (dto.taskId) {
const task = await this.taskRepository.findOneById(dto.taskId); await this.taskService.findOneOrFail(dto.taskId);
if (!task) throw new NotFoundException(`Task #${dto.taskId} not found`);
} }
Object.assign(remark, dto); Object.assign(remark, dto);
@@ -47,7 +51,7 @@ export class RemarkService {
} }
async remove(id: string): Promise<void> { async remove(id: string): Promise<void> {
await this.findOne(id); await this.findOneOrFail(id);
await this.remarkRepository.delete(id); await this.remarkRepository.delete(id);
} }
} }
@@ -1,45 +1,45 @@
import { Injectable, NotFoundException } from "@nestjs/common"; import { Injectable, NotFoundException } from "@nestjs/common";
import { TaskPhaseRepository } from "../repositories/task-phase.repository"; import { TaskPhaseRepository } from "../repositories/task-phase.repository";
import { ProjectRepository } from "../repositories/project.repository";
import { TMTaskPhase } from "../entities/task-phase.entity"; import { TMTaskPhase } from "../entities/task-phase.entity";
import { CreateTaskPhaseDto } from "../dto/task-phase/create-task-phase.dto"; import { CreateTaskPhaseDto } from "../dto/task-phase/create-task-phase.dto";
import { UpdateTaskPhaseDto } from "../dto/task-phase/update-task-phase.dto"; import { UpdateTaskPhaseDto } from "../dto/task-phase/update-task-phase.dto";
import { TaskPhaseMessage } from "../../../common/enums/message.enum";
import { ProjectService } from "./project.service";
@Injectable() @Injectable()
export class TaskPhaseService { export class TaskPhaseService {
constructor( constructor(
private readonly taskPhaseRepository: TaskPhaseRepository, private readonly taskPhaseRepository: TaskPhaseRepository,
private readonly projectRepository: ProjectRepository, private readonly projectService: ProjectService
) {} ) {}
findAll(): Promise<TMTaskPhase[]> { findAll(): Promise<TMTaskPhase[]> {
return this.taskPhaseRepository.findAll(); return this.taskPhaseRepository.find();
} }
findByProject(projectId: string): Promise<TMTaskPhase[]> { findByProject(projectId: string): Promise<TMTaskPhase[]> {
return this.taskPhaseRepository.findByProject(projectId); return this.taskPhaseRepository.find({
where: {projectId}
});
} }
async findOne(id: string): Promise<TMTaskPhase> { async findOneOrFail(id: string): Promise<TMTaskPhase> {
const taskPhase = await this.taskPhaseRepository.findOneById(id); const taskPhase = await this.taskPhaseRepository.findOne({where: {id}});
if (!taskPhase) throw new NotFoundException(`TaskPhase #${id} not found`); if (!taskPhase) throw new NotFoundException(TaskPhaseMessage.TASK_PHASE_NOT_FOUND);
return taskPhase; return taskPhase;
} }
async create(dto: CreateTaskPhaseDto): Promise<TMTaskPhase> { async create(dto: CreateTaskPhaseDto): Promise<TMTaskPhase> {
const project = await this.projectRepository.findOneById(dto.projectId); await this.projectService.findOneOrFail(dto.projectId);
if (!project) throw new NotFoundException(`Project #${dto.projectId} not found`);
const taskPhase = this.taskPhaseRepository.create(dto); const taskPhase = this.taskPhaseRepository.create(dto);
return this.taskPhaseRepository.save(taskPhase); return this.taskPhaseRepository.save(taskPhase);
} }
async update(id: string, dto: UpdateTaskPhaseDto): Promise<TMTaskPhase> { async update(id: string, dto: UpdateTaskPhaseDto): Promise<TMTaskPhase> {
const taskPhase = await this.findOne(id); const taskPhase = await this.findOneOrFail(id);
if (dto.projectId) { if (dto.projectId) {
const project = await this.projectRepository.findOneById(dto.projectId); await this.projectService.findOneOrFail(dto.projectId);
if (!project) throw new NotFoundException(`Project #${dto.projectId} not found`);
} }
Object.assign(taskPhase, dto); Object.assign(taskPhase, dto);
@@ -47,7 +47,7 @@ export class TaskPhaseService {
} }
async remove(id: string): Promise<void> { async remove(id: string): Promise<void> {
await this.findOne(id); await this.findOneOrFail(id);
await this.taskPhaseRepository.delete(id); await this.taskPhaseRepository.delete(id);
} }
} }
@@ -1,13 +1,15 @@
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from "@nestjs/typeorm";
import { In, Repository } from 'typeorm'; import { In, Repository } from "typeorm";
import { TaskRepository } from '../repositories/task.repository'; import { TaskRepository } from "../repositories/task.repository";
import { TaskPhaseRepository } from '../repositories/task-phase.repository'; import { TMTask } from "../entities/task.entity";
import { TMTask } from '../entities/task.entity'; import { CreateTaskDto } from "../dto/task/create-task.dto";
import { CreateTaskDto } from '../dto/task/create-task.dto'; import { UpdateTaskDto } from "../dto/task/update-task.dto";
import { UpdateTaskDto } from '../dto/task/update-task.dto'; import { ChangeTaskPhaseDto } from "../dto/task/change-task-phase.dto";
import { ChangeTaskPhaseDto } from '../dto/task/change-task-phase.dto'; import { User } from "../../users/entities/user.entity";
import { User } from '../../users/entities/user.entity'; import { TaskMessage, TaskPhaseMessage } from "../../../common/enums/message.enum";
import { TaskPhaseRepository } from "../repositories/task-phase.repository";
import { TMTaskPhase } from "../entities/task-phase.entity";
@Injectable() @Injectable()
export class TaskService { export class TaskService {
@@ -18,25 +20,51 @@ export class TaskService {
private readonly userRepository: Repository<User>, private readonly userRepository: Repository<User>,
) {} ) {}
findAll(): Promise<TMTask[]> { async findAll(): Promise<TMTask[]> {
return this.taskRepository.findAll(); return this.taskRepository.find({
relations: ["taskPhase", "remarks", "attachments", "checkListItems", "users"],
});
} }
findByPhase(taskPhaseId: string): Promise<TMTask[]> { async findByPhase(taskPhaseId: string): Promise<TMTask[]> {
return this.taskRepository.findByPhase(taskPhaseId); return this.taskRepository.find({ where: { taskPhaseId } });
} }
async findOne(id: string): Promise<TMTask> { async findOneOrFail(id: string): Promise<TMTask> {
const task = await this.taskRepository.findOneById(id); const task = await this.taskRepository.findOne({
if (!task) throw new NotFoundException(`Task #${id} not found`); where: { id },
relations: {
taskPhase: true,
users: true
},
});
if (!task) throw new NotFoundException(TaskMessage.TASK_NOT_FOUND);
return task; return task;
} }
async findTaskPhaseOrFail(taskPhaseId: string): Promise<TMTaskPhase> {
const taskPhase = await this.taskPhaseRepository.findOne({
where: { id: taskPhaseId },
relations: {
project: {
users: true,
},
},
});
if (!taskPhase) throw new NotFoundException(TaskPhaseMessage.TASK_PHASE_NOT_FOUND);
return taskPhase;
}
async create(dto: CreateTaskDto): Promise<TMTask> { async create(dto: CreateTaskDto): Promise<TMTask> {
const { userIds, startDate, endDate, ...rest } = dto; const { userIds, startDate, endDate, ...rest } = dto;
const taskPhase = await this.taskPhaseRepository.findOneById(dto.taskPhaseId); const taskPhase = await this.findTaskPhaseOrFail(dto.taskPhaseId);
if (!taskPhase) throw new NotFoundException(`TaskPhase #${dto.taskPhaseId} not found`);
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);
}
const task = this.taskRepository.create({ const task = this.taskRepository.create({
...rest, ...rest,
@@ -52,12 +80,16 @@ export class TaskService {
} }
async update(id: string, dto: UpdateTaskDto): Promise<TMTask> { async update(id: string, dto: UpdateTaskDto): Promise<TMTask> {
const task = await this.findOne(id); const task = await this.findOneOrFail(id);
const { userIds, startDate, endDate, ...rest } = dto; const { userIds, startDate, endDate, ...rest } = dto;
if (dto.taskPhaseId) { if (dto.taskPhaseId) {
const taskPhase = await this.taskPhaseRepository.findOneById(dto.taskPhaseId); const taskPhase = await this.findTaskPhaseOrFail(dto.taskPhaseId);
if (!taskPhase) throw new NotFoundException(`TaskPhase #${dto.taskPhaseId} not found`);
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);
}
} }
Object.assign(task, { Object.assign(task, {
@@ -67,24 +99,19 @@ export class TaskService {
}); });
if (userIds) { if (userIds) {
task.users = userIds.length task.users = userIds.length ? await this.userRepository.findBy({ id: In(userIds) }) : [];
? await this.userRepository.findBy({ id: In(userIds) })
: [];
} }
return this.taskRepository.save(task); return this.taskRepository.save(task);
} }
async changePhase(id: string, dto: ChangeTaskPhaseDto): Promise<TMTask> { async changePhase(id: string, dto: ChangeTaskPhaseDto): Promise<TMTask> {
const task = await this.findOne(id); const task = await this.findOneOrFail(id);
const newPhase = await this.taskPhaseRepository.findOneById(dto.taskPhaseId); const newPhase = await this.findTaskPhaseOrFail(dto.taskPhaseId);
if (!newPhase) throw new NotFoundException(`TaskPhase #${dto.taskPhaseId} not found`);
if (task.taskPhase.projectId !== newPhase.projectId) { if (task.taskPhase.projectId !== newPhase.projectId) {
throw new BadRequestException( throw new BadRequestException(TaskPhaseMessage.TASK_PHASE_NOT_SAME_PROJECT);
`TaskPhase #${dto.taskPhaseId} does not belong to the same project as the current phase`,
);
} }
task.taskPhaseId = dto.taskPhaseId; task.taskPhaseId = dto.taskPhaseId;
@@ -94,7 +121,17 @@ export class TaskService {
} }
async remove(id: string): Promise<void> { async remove(id: string): Promise<void> {
await this.findOne(id); await this.findOneOrFail(id);
await this.taskRepository.delete(id); await this.taskRepository.delete(id);
} }
}
async getTaskDetail(userId: string, taskId: string): Promise<TMTask | null> {
const task = await this.findOneOrFail(taskId);
const isTaskMember = task.users.some((user) => user.id === userId);
if(!isTaskMember) throw new ForbiddenException(TaskMessage.USER_NOT_TASK_MEMBER);
const taskDetail = await this.taskRepository.getTaskDetail(taskId);
return taskDetail;
}
}
@@ -10,4 +10,56 @@ export class ProjectRepository extends Repository<TMProject> {
) { ) {
super(projectRepository.target, projectRepository.manager, projectRepository.queryRunner); super(projectRepository.target, projectRepository.manager, projectRepository.queryRunner);
} }
async findUserProjects(userId: string, wsId: string, skip?: number, take?: number): Promise<[TMProject[], number]> {
return this.createQueryBuilder('project')
.innerJoin('project.workspace', 'workspace')
.innerJoin('project.users', 'user')
.where('workspace.id = :wsId', {wsId})
.andWhere('user.id = :userId', {userId})
.skip(skip ?? 0)
.take(take ?? 10)
.getManyAndCount();
}
async getProjectDetail(projectId: string): Promise<TMProject | null> {
return this.createQueryBuilder("project")
.leftJoinAndSelect("project.taskPhases", "taskPhase")
.leftJoinAndSelect("taskPhase.tasks", "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 }),
)
.where("project.id = :projectId", { projectId })
.select([
"project.id",
"project.name",
"taskPhase.id",
"taskPhase.name",
"task.id",
"task.title",
"task.startDate",
"task.endDate",
"remark.title",
"remark.color",
])
.getOne();
}
} }
@@ -4,44 +4,10 @@ import { Repository } from "typeorm";
import { TMRemark } from "../entities/remark.entity"; import { TMRemark } from "../entities/remark.entity";
@Injectable() @Injectable()
export class RemarkRepository { export class RemarkRepository extends Repository<TMRemark>{
constructor( constructor(
@InjectRepository(TMRemark) @InjectRepository(TMRemark) remarkRepository: Repository<TMRemark>,
private readonly repository: Repository<TMRemark>, ) {
) {} super(remarkRepository.target, remarkRepository.manager, remarkRepository.queryRunner);
findAll() {
return this.repository.find({
relations: ["task"],
});
}
findByTask(taskId: string) {
return this.repository.find({
where: { taskId },
});
}
findOneById(id: string) {
return this.repository.findOne({
where: { id },
relations: ["task"],
});
}
create(data: Partial<TMRemark>) {
return this.repository.create(data);
}
save(remark: TMRemark) {
return this.repository.save(remark);
}
update(id: string, data: Partial<TMRemark>) {
return this.repository.update(id, data);
}
delete(id: string) {
return this.repository.delete(id);
} }
} }
@@ -4,47 +4,10 @@ import { Repository } from "typeorm";
import { TMTaskPhase } from "../entities/task-phase.entity"; import { TMTaskPhase } from "../entities/task-phase.entity";
@Injectable() @Injectable()
export class TaskPhaseRepository { export class TaskPhaseRepository extends Repository<TMTaskPhase> {
constructor( constructor(
@InjectRepository(TMTaskPhase) @InjectRepository(TMTaskPhase) taskPhaseRepository: Repository<TMTaskPhase>,
private readonly repository: Repository<TMTaskPhase>, ) {
) {} super(taskPhaseRepository.target, taskPhaseRepository.manager, taskPhaseRepository.queryRunner);
findAll() {
return this.repository.find({
relations: ["project", "tasks"],
order: { order: "ASC" },
});
}
findByProject(projectId: string) {
return this.repository.find({
where: { projectId },
relations: ["tasks"],
order: { order: "ASC" },
});
}
findOneById(id: string) {
return this.repository.findOne({
where: { id },
relations: ["project", "tasks"],
});
}
create(data: Partial<TMTaskPhase>) {
return this.repository.create(data);
}
save(taskPhase: TMTaskPhase) {
return this.repository.save(taskPhase);
}
update(id: string, data: Partial<TMTaskPhase>) {
return this.repository.update(id, data);
}
delete(id: string) {
return this.repository.delete(id);
} }
} }
@@ -4,45 +4,40 @@ import { Repository } from "typeorm";
import { TMTask } from "../entities/task.entity"; import { TMTask } from "../entities/task.entity";
@Injectable() @Injectable()
export class TaskRepository { export class TaskRepository extends Repository<TMTask> {
constructor( constructor(@InjectRepository(TMTask) taskRepository: Repository<TMTask>) {
@InjectRepository(TMTask) super(taskRepository.target, taskRepository.manager, taskRepository.queryRunner);
private readonly repository: Repository<TMTask>,
) {}
findAll() {
return this.repository.find({
relations: ["taskPhase", "remarks", "attachments", "checkListItems", "users"],
});
} }
findByPhase(taskPhaseId: string) { async getTaskDetail(taskId: string): Promise<TMTask | null> {
return this.repository.find({ return this.createQueryBuilder("task")
where: { taskPhaseId }, .leftJoinAndSelect("task.remarks", "remark")
relations: ["remarks", "attachments", "checkListItems", "users"], .leftJoinAndSelect("task.attachments", "attachment")
}); .leftJoinAndSelect("task.checkListItems", "checkListItem")
} .leftJoinAndSelect("task.users", "user")
findOneById(id: string) { .loadRelationCountAndMap("task.checkListItemCount", "task.checkListItems")
return this.repository.findOne({
where: { id },
relations: ["taskPhase", "remarks", "attachments", "checkListItems", "users"],
});
}
create(data: Partial<TMTask>) { .loadRelationCountAndMap("task.completedCheckListItemCount", "task.checkListItems", "completedCheckListItems", (qb) =>
return this.repository.create(data); qb.where("completedCheckListItems.isDone = :isDone", { isDone: true }),
} )
save(task: TMTask) { .select(["remark.id",
return this.repository.save(task); "remark.title",
} "remark.color",
"checkListItem.id",
update(id: string, data: Partial<TMTask>) { "checkListItem.title",
return this.repository.update(id, data); "checkListItem.isDone",
} "attachment.id",
"attachment.title",
delete(id: string) { "attachment.type",
return this.repository.delete(id); "attachment.file",
"user.id",
"user.firstName",
"user.lastName"
])
.where("task.id = :taskId", { taskId })
.getOne();
} }
} }
+4 -1
View File
@@ -26,5 +26,8 @@ export enum PermissionEnum {
DMAIL = 'dmail', DMAIL = 'dmail',
RESELLER="reseller", RESELLER="reseller",
WORKSPACE="workspace", WORKSPACE="workspace",
PROJECT="project" PROJECT="project",
TASK_PHASE="task_phase",
TASK="task",
REMARK="remark"
} }