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,18 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsInt, IsISO8601, IsOptional, Max, Min } from "class-validator";
|
||||
|
||||
export class CursorPaginationDto {
|
||||
@IsOptional()
|
||||
@IsISO8601()
|
||||
@ApiPropertyOptional({ type: "string", description: "Cursor (createdAt of last seen item)", example: "2026-07-01T10:00:00.000Z" })
|
||||
cursor?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(50)
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ type: "number", required: false, default: 10 })
|
||||
limit?: number;
|
||||
}
|
||||
@@ -915,3 +915,43 @@ export const enum UploaderMessage {
|
||||
export const enum AccessLogMessage {
|
||||
ACCESS_LOG_NOT_FOUND = "لاگ دسترسی مورد نظر یافت نشد",
|
||||
}
|
||||
|
||||
export const enum WorkSpaceMessage {
|
||||
WORKSPACE_EXISTS = "فضای کار با این اسم وجود دارد!",
|
||||
WORKSPACE_NOT_FOUND = "فضای کار مورد نظر شما وجود ندارد!",
|
||||
USERS_DONT_BELONG_TO_THIS_WORKSPACE = "کاربران تعیین شده برای این فضای کاری نیستند!",
|
||||
WORKSPACE_SAME_NAME_EXISTS = "فضای کاری با این اسم از قبل وجود دارد!",
|
||||
WORKSPACE_ID_PARAM_NOT_EMPTY = "پارامتر شناسه فضای کار نباید خالی باشد!",
|
||||
WORKSPACE_ID_UUID = "شناسه فضای کار باید uuid باشد",
|
||||
FORBIDDEN = "شما اجازه دسترسی به این فضای کار را ندارید و یا عضو آن نیستید!"
|
||||
}
|
||||
|
||||
export const enum ProjectMessage {
|
||||
PROJECT_EXISTS = "پروژه مورد نظر شما از قبل وجود دارد!",
|
||||
PROJECT_NOT_FOUND = "پروژه مورد نظر شما وجود ندارد!",
|
||||
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 = "برچسب مورد نظر پیدا نشد!"
|
||||
}
|
||||
|
||||
export const enum checkListItemMessage {
|
||||
CHECKLISTITEM_NOT_FOUND = "آیتم چک لیست پیدا نشد!"
|
||||
}
|
||||
|
||||
export const enum attachmentMessage {
|
||||
ATTACHMENT_NOT_FOUND = "ضمیمه پیدا نشد!"
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { CursorPaginatedResult } from "../interfaces/cursor-paginated-result.interface";
|
||||
|
||||
export function buildCursorPageFormat<T extends { createdAt: Date }>(
|
||||
data: T[],
|
||||
limit: number,
|
||||
previousCursor?: Date,
|
||||
): CursorPaginatedResult<T> {
|
||||
const hasMore = data.length > limit;
|
||||
|
||||
if (hasMore) data.pop();
|
||||
|
||||
const nextCursor = hasMore ? data[data.length - 1].createdAt.toISOString() : null;
|
||||
|
||||
const prevCursor = previousCursor ? previousCursor.toISOString() : null;
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
nextCursor,
|
||||
prevCursor,
|
||||
hasMore,
|
||||
limit,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface CursorPaginatedResult<T> {
|
||||
data: T[];
|
||||
meta: {
|
||||
nextCursor: string | null;
|
||||
prevCursor: string | null;
|
||||
hasMore: boolean;
|
||||
limit: number;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Controller, Post, Patch, Delete, Param, Body } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
|
||||
import { AttachmentService } from '../providers/attachment.service';
|
||||
import { CreateAttachmentDto } from '../dto/attachment/create-attachment.dto';
|
||||
import { UpdateAttachmentDto } from '../dto/attachment/update-attachment.dto';
|
||||
import { TMAttachment } from '../entities/attachment.entity';
|
||||
import { AuthGuards } from '../../../common/decorators/auth-guard.decorator';
|
||||
import { AdminRoute } from '../../../common/decorators/admin.decorator';
|
||||
|
||||
@AuthGuards()
|
||||
@AdminRoute()
|
||||
@ApiTags('Task Manager - Attachments')
|
||||
@Controller('task-manager/attachments')
|
||||
export class AttachmentController {
|
||||
constructor(private readonly attachmentService: AttachmentService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create a new attachment' })
|
||||
@ApiResponse({ status: 201, description: 'Attachment created', type: TMAttachment })
|
||||
@ApiResponse({ status: 404, description: 'Task not found' })
|
||||
create(@Body() body: CreateAttachmentDto): Promise<TMAttachment> {
|
||||
return this.attachmentService.create(body);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@ApiOperation({ summary: 'Update an attachment' })
|
||||
@ApiParam({ name: 'id', description: 'Attachment ID' })
|
||||
@ApiResponse({ status: 200, description: 'Attachment updated', type: TMAttachment })
|
||||
@ApiResponse({ status: 404, description: 'Attachment or Task not found' })
|
||||
update(@Param('id') id: string, @Body() body: UpdateAttachmentDto): Promise<TMAttachment> {
|
||||
return this.attachmentService.update(id, body);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: 'Delete an attachment' })
|
||||
@ApiParam({ name: 'id', description: 'Attachment ID' })
|
||||
@ApiResponse({ status: 200, description: 'Attachment deleted' })
|
||||
@ApiResponse({ status: 404, description: 'Attachment not found' })
|
||||
remove(@Param('id') id: string): Promise<void> {
|
||||
return this.attachmentService.remove(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Controller, Post, Patch, Delete, Param, Body } from "@nestjs/common";
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiParam } from "@nestjs/swagger";
|
||||
import { CheckListItemService } from "../providers/check-list-item.service";
|
||||
import { CreateCheckListItemDto } from "../dto/check-list-item/create-check-list-item.dto";
|
||||
import { UpdateCheckListItemDto } from "../dto/check-list-item/update-check-list-item.dto";
|
||||
import { TMCheckListItem } from "../entities/check-list-item.entity";
|
||||
import { AuthGuards } from "../../../common/decorators/auth-guard.decorator";
|
||||
import { AdminRoute } from "../../../common/decorators/admin.decorator";
|
||||
import { ParamDto } from "../../../common/DTO/param.dto";
|
||||
|
||||
@AuthGuards()
|
||||
@AdminRoute()
|
||||
@ApiTags("Task Manager - Check List Items")
|
||||
@Controller("task-manager/check-list-items")
|
||||
export class CheckListItemController {
|
||||
constructor(private readonly checkListItemService: CheckListItemService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: "Create a new check list item" })
|
||||
@ApiResponse({ status: 201, description: "CheckListItem created", type: TMCheckListItem })
|
||||
@ApiResponse({ status: 404, description: "Task not found" })
|
||||
create(@Body() body: CreateCheckListItemDto): Promise<TMCheckListItem> {
|
||||
return this.checkListItemService.create(body);
|
||||
}
|
||||
|
||||
@Patch(":id")
|
||||
@ApiOperation({ summary: "Update a check list item" })
|
||||
@ApiParam({ name: "id", description: "CheckListItem ID" })
|
||||
@ApiResponse({ status: 200, description: "CheckListItem updated", type: TMCheckListItem })
|
||||
@ApiResponse({ status: 404, description: "CheckListItem or Task not found" })
|
||||
update(@Param("id") id: string, @Body() body: UpdateCheckListItemDto): Promise<TMCheckListItem> {
|
||||
return this.checkListItemService.update(id, body);
|
||||
}
|
||||
|
||||
@Delete(":id")
|
||||
@ApiOperation({ summary: "Delete a check list item" })
|
||||
@ApiParam({ name: "id", description: "CheckListItem ID" })
|
||||
@ApiResponse({ status: 200, description: "CheckListItem deleted", type: TMCheckListItem })
|
||||
@ApiResponse({ status: 404, description: "CheckListItem not found" })
|
||||
remove(@Param("id") id: string): Promise<TMCheckListItem> {
|
||||
return this.checkListItemService.remove(id);
|
||||
}
|
||||
|
||||
@Patch("change-status/:id")
|
||||
@ApiOperation({summary: "change status of a check list item."})
|
||||
@ApiParam({name: "id", description: "CheckListItem ID"})
|
||||
@ApiResponse({status: 200, description: "successfully changed the status!"})
|
||||
@ApiResponse({ status: 404, description: "CheckListItem not found" })
|
||||
changeStatus(@Param() paramDto: ParamDto) {
|
||||
return this.checkListItemService.toggleStatus(paramDto.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from "@nestjs/common";
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiParam } from "@nestjs/swagger";
|
||||
import { ProjectService } from "../providers/project.service";
|
||||
import { CreateProjectDto } from "../dto/project/create-project.dto";
|
||||
import { UpdateProjectDto } from "../dto/project/update-project.dto";
|
||||
import { PaginationDto } from "../../../common/DTO/pagination.dto";
|
||||
import { TMProject } from "../entities/project.entity";
|
||||
import { AdminRoute } from "../../../common/decorators/admin.decorator";
|
||||
import { PermissionsDec } from "../../../common/decorators/permission.decorator";
|
||||
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";
|
||||
import { ITokenPayload } from "../../auth/interfaces/IToken-payload";
|
||||
|
||||
@AuthGuards()
|
||||
@AdminRoute()
|
||||
@ApiTags("Task Manager - Projects")
|
||||
@Controller("task-manager/projects")
|
||||
export class ProjectController {
|
||||
constructor(private readonly projectService: ProjectService) {}
|
||||
|
||||
@Post()
|
||||
@PermissionsDec(PermissionEnum.PROJECT)
|
||||
@ApiOperation({ summary: "Create a new project" })
|
||||
@ApiResponse({ status: 201, description: "Project created", type: TMProject })
|
||||
@ApiResponse({ status: 400, description: "One or more users are not members of the workspace" })
|
||||
@ApiResponse({ status: 404, description: "Workspace not found" })
|
||||
create(@Body() body: CreateProjectDto): Promise<TMProject> {
|
||||
return this.projectService.create(body);
|
||||
}
|
||||
|
||||
@Patch(":id")
|
||||
@PermissionsDec(PermissionEnum.PROJECT)
|
||||
@ApiOperation({ summary: "Update a project" })
|
||||
@ApiParam({ name: "id", description: "Project ID" })
|
||||
@ApiResponse({ status: 200, description: "Project updated", type: TMProject })
|
||||
@ApiResponse({ status: 400, description: "One or more users are not members of the workspace" })
|
||||
@ApiResponse({ status: 404, description: "Project not found" })
|
||||
update(@Param("id") id: string, @Body() body: UpdateProjectDto): Promise<TMProject> {
|
||||
return this.projectService.update(id, body);
|
||||
}
|
||||
|
||||
@Delete(":id")
|
||||
@PermissionsDec(PermissionEnum.PROJECT)
|
||||
@ApiOperation({ summary: "Delete a project" })
|
||||
@ApiParam({ name: "id", description: "Project ID" })
|
||||
@ApiResponse({ status: 200, description: "Project deleted", type: TMProject })
|
||||
@ApiResponse({ status: 404, description: "Project not found" })
|
||||
remove(@Param("id") id: string): Promise<TMProject> {
|
||||
return this.projectService.remove(id);
|
||||
}
|
||||
|
||||
@Get("user/workspace/:id")
|
||||
@ApiOperation({ summary: "Get user projects in a specific workspace!" })
|
||||
@ApiResponse({ status: 200, description: "Got user projects successfully!" })
|
||||
@ApiResponse({ status: 400, description: "Bad Request!" })
|
||||
findUserProjectsByWorkspace(
|
||||
@UserDec() user: ITokenPayload,
|
||||
@Param() paramDto: ParamDto,
|
||||
@Query() pagination: PaginationDto,
|
||||
): Promise<{ projects: TMProject[]; count: number; paginate: true }> {
|
||||
return this.projectService.findUserProjectsByWorkspace(user.id, user.permissions, 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() user: ITokenPayload): Promise<TMProject | null> {
|
||||
return this.projectService.getProjectDetail(paramDto.id, user.id, user.permissions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Controller, Post, Patch, Delete, Param, Body } from "@nestjs/common";
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiParam } from "@nestjs/swagger";
|
||||
import { RemarkService } from "../providers/remark.service";
|
||||
import { CreateRemarkDto } from "../dto/remark/create-remark.dto";
|
||||
import { UpdateRemarkDto } from "../dto/remark/update-remark.dto";
|
||||
import { TMRemark } from "../entities/remark.entity";
|
||||
import { AuthGuards } from "../../../common/decorators/auth-guard.decorator";
|
||||
import { AdminRoute } from "../../../common/decorators/admin.decorator";
|
||||
|
||||
@AuthGuards()
|
||||
@AdminRoute()
|
||||
@ApiTags("Task Manager - Remarks")
|
||||
@Controller("task-manager/remarks")
|
||||
export class RemarkController {
|
||||
constructor(private readonly remarkService: RemarkService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: "Create a new remark" })
|
||||
@ApiResponse({ status: 201, description: "Remark created", type: TMRemark })
|
||||
@ApiResponse({ status: 404, description: "Task not found" })
|
||||
create(@Body() body: CreateRemarkDto): Promise<TMRemark> {
|
||||
return this.remarkService.create(body);
|
||||
}
|
||||
|
||||
@Patch(":id")
|
||||
@ApiOperation({ summary: "Update a remark" })
|
||||
@ApiParam({ name: "id", description: "Remark ID" })
|
||||
@ApiResponse({ status: 200, description: "Remark updated", type: TMRemark })
|
||||
@ApiResponse({ status: 404, description: "Remark or Task not found" })
|
||||
update(@Param("id") id: string, @Body() body: UpdateRemarkDto): Promise<TMRemark> {
|
||||
return this.remarkService.update(id, body);
|
||||
}
|
||||
|
||||
@Delete(":id")
|
||||
@ApiOperation({ summary: "Delete a remark" })
|
||||
@ApiParam({ name: "id", description: "Remark ID" })
|
||||
@ApiResponse({ status: 200, description: "Remark deleted" })
|
||||
@ApiResponse({ status: 404, description: "Remark not found" })
|
||||
remove(@Param("id") id: string): Promise<void> {
|
||||
return this.remarkService.remove(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Controller, Post, Patch, Delete, Param, Body } from "@nestjs/common";
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiParam } from "@nestjs/swagger";
|
||||
import { TaskPhaseService } from "../providers/task-phase.service";
|
||||
import { CreateTaskPhaseDto } from "../dto/task-phase/create-task-phase.dto";
|
||||
import { UpdateTaskPhaseDto } from "../dto/task-phase/update-task-phase.dto";
|
||||
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")
|
||||
@Controller("task-manager/task-phases")
|
||||
export class TaskPhaseController {
|
||||
constructor(private readonly taskPhaseService: TaskPhaseService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: "Create a new task phase" })
|
||||
@ApiResponse({ status: 201, description: "TaskPhase created", type: TMTaskPhase })
|
||||
@ApiResponse({ status: 404, description: "Project not found" })
|
||||
create(@Body() body: CreateTaskPhaseDto): Promise<TMTaskPhase> {
|
||||
return this.taskPhaseService.create(body);
|
||||
}
|
||||
|
||||
@Patch(":id")
|
||||
@ApiOperation({ summary: "Update a task phase" })
|
||||
@ApiParam({ name: "id", description: "TaskPhase ID" })
|
||||
@ApiResponse({ status: 200, description: "TaskPhase updated", type: TMTaskPhase })
|
||||
@ApiResponse({ status: 404, description: "TaskPhase or Project not found" })
|
||||
update(@Param("id") id: string, @Body() body: UpdateTaskPhaseDto): Promise<TMTaskPhase> {
|
||||
return this.taskPhaseService.update(id, body);
|
||||
}
|
||||
|
||||
@Delete(":id")
|
||||
@ApiOperation({ summary: "Delete a task phase" })
|
||||
@ApiParam({ name: "id", description: "TaskPhase ID" })
|
||||
@ApiResponse({ status: 200, description: "TaskPhase deleted" })
|
||||
@ApiResponse({ status: 404, description: "TaskPhase not found" })
|
||||
remove(@Param("id") id: string): Promise<void> {
|
||||
return this.taskPhaseService.remove(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Controller, Get, Post, Patch, Delete, Param, Body } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
|
||||
import { TaskService } from '../providers/task.service';
|
||||
import { CreateTaskDto } from '../dto/task/create-task.dto';
|
||||
import { UpdateTaskDto } from '../dto/task/update-task.dto';
|
||||
import { ChangeTaskPhaseDto } from '../dto/task/change-task-phase.dto';
|
||||
import { 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';
|
||||
import { ITokenPayload } from '../../auth/interfaces/IToken-payload';
|
||||
|
||||
@AuthGuards()
|
||||
@AdminRoute()
|
||||
@ApiTags('Task Manager - Tasks')
|
||||
@Controller('task-manager/tasks')
|
||||
export class TaskController {
|
||||
constructor(private readonly taskService: TaskService) {}
|
||||
|
||||
@Post()
|
||||
@PermissionsDec(PermissionEnum.TASK)
|
||||
@ApiOperation({ summary: 'Create a new task' })
|
||||
@ApiResponse({ status: 201, description: 'Task created', type: TMTask })
|
||||
@ApiResponse({ status: 404, description: 'TaskPhase not found' })
|
||||
create(@Body() body: CreateTaskDto): Promise<TMTask> {
|
||||
return this.taskService.create(body);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@PermissionsDec(PermissionEnum.TASK)
|
||||
@ApiOperation({ summary: 'Update a task' })
|
||||
@ApiParam({ name: 'id', description: 'Task ID' })
|
||||
@ApiResponse({ status: 200, description: 'Task updated', type: TMTask })
|
||||
@ApiResponse({ status: 404, description: 'Task or TaskPhase not found' })
|
||||
update(@Param('id') id: string, @Body() body: UpdateTaskDto): Promise<TMTask> {
|
||||
return this.taskService.update(id, body);
|
||||
}
|
||||
|
||||
@Patch(':id/change-phase')
|
||||
@PermissionsDec(PermissionEnum.TASK)
|
||||
@ApiOperation({ summary: 'Move a task to a different phase within the same project' })
|
||||
@ApiParam({ name: 'id', description: 'Task ID' })
|
||||
@ApiResponse({ status: 200, description: 'Task phase changed', type: TMTask })
|
||||
@ApiResponse({ status: 400, description: 'Target phase belongs to a different project' })
|
||||
@ApiResponse({ status: 404, description: 'Task or TaskPhase not found' })
|
||||
changePhase(@Param('id') id: string, @Body() body: ChangeTaskPhaseDto): Promise<TMTask> {
|
||||
return this.taskService.changePhase(id, body);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@PermissionsDec(PermissionEnum.TASK)
|
||||
@ApiOperation({ summary: 'Delete a task' })
|
||||
@ApiParam({ name: 'id', description: 'Task ID' })
|
||||
@ApiResponse({ status: 200, description: 'Task deleted' })
|
||||
@ApiResponse({ status: 404, description: 'Task not found' })
|
||||
remove(@Param('id') id: string): Promise<void> {
|
||||
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() user: ITokenPayload,
|
||||
@Param() paramDto: ParamDto
|
||||
): Promise<TMTask | null> {
|
||||
return this.taskService.getTaskDetail(user.id, user.permissions, paramDto.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Controller, Get, Post, Patch, Delete, Param, Body } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
|
||||
import { WorkspaceService } from '../providers/workspace.service';
|
||||
import { CreateWorkspaceDto } from '../dto/workspace/create-workspace.dto';
|
||||
import { UpdateWorkspaceDto } from '../dto/workspace/update-workspace.dto';
|
||||
import { TMWorkspace } from '../entities/workspace.entity';
|
||||
import { AdminRoute } from '../../../common/decorators/admin.decorator';
|
||||
import { PermissionsDec } from '../../../common/decorators/permission.decorator';
|
||||
import { PermissionEnum } from '../../users/enums/permission.enum';
|
||||
import { UserDec } from '../../../common/decorators/user.decorator';
|
||||
import { AuthGuards } from '../../../common/decorators/auth-guard.decorator';
|
||||
import { ITokenPayload } from '../../auth/interfaces/IToken-payload';
|
||||
|
||||
@AuthGuards()
|
||||
@AdminRoute()
|
||||
@ApiTags('Task Manager - Workspaces')
|
||||
@Controller('task-manager/workspaces')
|
||||
export class WorkspaceController {
|
||||
constructor(private readonly workspaceService: WorkspaceService) {}
|
||||
|
||||
@Post()
|
||||
@PermissionsDec(PermissionEnum.WORKSPACE)
|
||||
@ApiOperation({ summary: 'Create a new workspace' })
|
||||
@ApiResponse({ status: 201, description: 'Workspace created', type: TMWorkspace })
|
||||
create(@Body() body: CreateWorkspaceDto): Promise<TMWorkspace> {
|
||||
return this.workspaceService.create(body);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@PermissionsDec(PermissionEnum.WORKSPACE)
|
||||
@ApiOperation({ summary: 'Update a workspace' })
|
||||
@ApiParam({ name: 'id', description: 'Workspace ID' })
|
||||
@ApiResponse({ status: 200, description: 'Workspace updated', type: TMWorkspace })
|
||||
@ApiResponse({ status: 404, description: 'Workspace not found' })
|
||||
update(@Param('id') id: string, @Body() body: UpdateWorkspaceDto): Promise<TMWorkspace> {
|
||||
return this.workspaceService.update(id, body);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@PermissionsDec(PermissionEnum.WORKSPACE)
|
||||
@ApiOperation({ summary: 'Delete a workspace' })
|
||||
@ApiParam({ name: 'id', description: 'Workspace ID' })
|
||||
@ApiResponse({ status: 200, description: 'Workspace deleted' })
|
||||
@ApiResponse({ status: 404, description: 'Workspace not found' })
|
||||
remove(@Param('id') id: string): Promise<TMWorkspace> {
|
||||
return this.workspaceService.remove(id);
|
||||
}
|
||||
|
||||
@Get('byuser')
|
||||
@ApiOperation({summary: 'Get User WorkSpaces'})
|
||||
@ApiResponse({ status: 200, description: 'Got Workspaces successfully!' })
|
||||
@ApiResponse({ status: 404, description: 'Bad Request!' })
|
||||
findByUser(@UserDec() user: ITokenPayload) {
|
||||
return this.workspaceService.findByUser(user.id, user.permissions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsString, IsNotEmpty, IsUUID, IsEnum, MaxLength } from "class-validator";
|
||||
import { TMAttachmentType } from "../../enums/attachment-type.enum";
|
||||
|
||||
export class CreateAttachmentDto {
|
||||
@ApiProperty({ description: "Title of the attachment", example: "Project Requirements Doc" })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(150)
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ description: "Type of the attachment", enum: TMAttachmentType, example: TMAttachmentType.FILE })
|
||||
@IsEnum(TMAttachmentType)
|
||||
@IsNotEmpty()
|
||||
type: TMAttachmentType;
|
||||
|
||||
@ApiProperty({ description: "File URL or path of the attachment", example: "https://cdn.example.com/files/requirements.pdf" })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
file: string;
|
||||
|
||||
@ApiProperty({ description: "ID of the task this attachment belongs to", example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" })
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
taskId: string;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from "@nestjs/swagger";
|
||||
import { CreateAttachmentDto } from "./create-attachment.dto";
|
||||
|
||||
export class UpdateAttachmentDto extends PartialType(CreateAttachmentDto) {}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsString, IsNotEmpty, IsUUID, IsBoolean, MaxLength } from "class-validator";
|
||||
|
||||
export class CreateCheckListItemDto {
|
||||
@ApiProperty({ description: "Title of the check list item", example: "Write unit tests" })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(150)
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ description: "Whether the check list item is done", example: false })
|
||||
@IsBoolean()
|
||||
@IsNotEmpty()
|
||||
isDone: boolean;
|
||||
|
||||
@ApiProperty({ description: "ID of the task this check list item belongs to", example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" })
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
taskId: string;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateCheckListItemDto } from './create-check-list-item.dto';
|
||||
|
||||
export class UpdateCheckListItemDto extends PartialType(CreateCheckListItemDto) {}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsString, IsOptional, IsNotEmpty, IsUUID, IsDateString, IsBoolean, MaxLength } from "class-validator";
|
||||
|
||||
export class CreateProjectDto {
|
||||
@ApiProperty({ description: "Name of the project", example: "Mobile App Redesign" })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(150)
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ description: "Name of the project owner", example: "Jane Doe" })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(150)
|
||||
ownerName: string;
|
||||
|
||||
@ApiProperty({ description: "Description of the project", required: false, example: "Redesign of the mobile app UI/UX" })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({ description: "Start date of the project", example: "2026-07-01" })
|
||||
@IsDateString()
|
||||
@IsNotEmpty()
|
||||
startDate: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: "Background picture URL of the project",
|
||||
required: false,
|
||||
example: "https://cdn.example.com/projects/bg1.png",
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
backgroundPicture?: string;
|
||||
|
||||
@ApiProperty({ description: "Background color of the project (hex or named)", required: false, example: "#4F46E5" })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@MaxLength(20)
|
||||
backgroundColor?: string;
|
||||
|
||||
@ApiProperty({ description: "Whether the project is active", required: false, default: true, example: true })
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
isActive?: boolean;
|
||||
|
||||
@ApiProperty({ description: "ID of the workspace this project belongs to", example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" })
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
workspaceId: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: "IDs of users to assign to the project (must be members of the workspace)",
|
||||
required: false,
|
||||
type: [String],
|
||||
example: ["b2c3d4e5-f6a7-8901-bcde-f12345678901"],
|
||||
})
|
||||
@IsUUID("4", { each: true })
|
||||
@IsOptional()
|
||||
userIds?: string[];
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from "@nestjs/swagger";
|
||||
import { CreateProjectDto } from "./create-project.dto";
|
||||
|
||||
export class UpdateProjectDto extends PartialType(CreateProjectDto) {}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsString, IsNotEmpty, IsUUID, IsOptional, MaxLength } from "class-validator";
|
||||
|
||||
export class CreateRemarkDto {
|
||||
@ApiProperty({ description: "Title of the remark", example: "This task needs review" })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(150)
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ description: "Color code for the remark", required: false, example: "#F59E0B" })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@MaxLength(20)
|
||||
color?: string;
|
||||
|
||||
@ApiProperty({ description: "ID of the task this remark belongs to", example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" })
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
taskId: string;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from "@nestjs/swagger";
|
||||
import { CreateRemarkDto } from "./create-remark.dto";
|
||||
|
||||
export class UpdateRemarkDto extends PartialType(CreateRemarkDto) {}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsString, IsOptional, IsNotEmpty, IsUUID, IsInt, MaxLength, Min } from "class-validator";
|
||||
|
||||
export class CreateTaskPhaseDto {
|
||||
@ApiProperty({ description: "Name of the task phase", example: "In Progress" })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(100)
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ description: "Display order of the task phase", required: false, example: 1 })
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@Min(0)
|
||||
order?: number;
|
||||
|
||||
@ApiProperty({ description: "Color code for the task phase", required: false, example: "#4F46E5" })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@MaxLength(20)
|
||||
color?: string;
|
||||
|
||||
@ApiProperty({ description: "ID of the project this phase belongs to", example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" })
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
projectId: string;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from "@nestjs/swagger";
|
||||
import { CreateTaskPhaseDto } from "./create-task-phase.dto";
|
||||
|
||||
export class UpdateTaskPhaseDto extends PartialType(CreateTaskPhaseDto) {}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsUUID, IsNotEmpty } from 'class-validator';
|
||||
|
||||
export class ChangeTaskPhaseDto {
|
||||
@ApiProperty({ description: 'ID of the new task phase', example: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' })
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
taskPhaseId: string;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsString, IsOptional, IsNotEmpty, IsUUID, IsDateString, MaxLength } from "class-validator";
|
||||
|
||||
export class CreateTaskDto {
|
||||
@ApiProperty({ description: "Title of the task", example: "Implement login page" })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(150)
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ description: "Description of the task", required: false, example: "Implement the login page with JWT auth" })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({ description: "Color code for the task", required: false, example: "#4F46E5" })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@MaxLength(20)
|
||||
color?: string;
|
||||
|
||||
@ApiProperty({ description: "Start date of the task", required: false, example: "2026-07-01" })
|
||||
@IsDateString()
|
||||
@IsOptional()
|
||||
startDate?: string;
|
||||
|
||||
@ApiProperty({ description: "End date of the task", required: false, example: "2026-07-15" })
|
||||
@IsDateString()
|
||||
@IsOptional()
|
||||
endDate?: string;
|
||||
|
||||
@ApiProperty({ description: "ID of the task phase this task belongs to", example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" })
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
taskPhaseId: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: "IDs of users to assign to the task",
|
||||
required: false,
|
||||
type: [String],
|
||||
example: ["b2c3d4e5-f6a7-8901-bcde-f12345678901"],
|
||||
})
|
||||
@IsUUID("4", { each: true })
|
||||
@IsOptional()
|
||||
userIds?: string[];
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from "@nestjs/swagger";
|
||||
import { CreateTaskDto } from "./create-task.dto";
|
||||
|
||||
export class UpdateTaskDto extends PartialType(CreateTaskDto) {}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsString, IsOptional, IsBoolean, IsNotEmpty, IsUUID, MaxLength } from "class-validator";
|
||||
|
||||
export class CreateWorkspaceDto {
|
||||
@ApiProperty({ description: "Name of the workspace", example: "Engineering" })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(100)
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ description: "Description of the workspace", required: false, example: "Workspace for the engineering team" })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({ description: "Color code for the workspace (hex or named)", example: "#4F46E5" })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(20)
|
||||
color: string;
|
||||
|
||||
@ApiProperty({ description: "Whether the workspace is active", required: false, default: true, example: true })
|
||||
@IsBoolean()
|
||||
isActive: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: "IDs of users to assign to the workspace",
|
||||
required: false,
|
||||
type: [String],
|
||||
example: ["b2c3d4e5-f6a7-8901-bcde-f12345678901"],
|
||||
})
|
||||
@IsUUID("4", { each: true })
|
||||
@IsOptional()
|
||||
userIds?: string[];
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from "@nestjs/swagger";
|
||||
import { CreateWorkspaceDto } from "./create-workspace.dto";
|
||||
|
||||
export class UpdateWorkspaceDto extends PartialType(CreateWorkspaceDto) {}
|
||||
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
} from 'typeorm';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { TMTask } from './task.entity';
|
||||
import { TMAttachmentType } from '../enums/attachment-type.enum';
|
||||
|
||||
@Entity('tm_attachments')
|
||||
export class TMAttachment extends BaseEntity {
|
||||
@Column({ type: 'varchar', length: 150 })
|
||||
title: string;
|
||||
|
||||
@Column({ type: 'enum', enum: TMAttachmentType })
|
||||
type: TMAttachmentType;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
file: string;
|
||||
|
||||
// --- Relations ---
|
||||
|
||||
@ManyToOne(() => TMTask, (task) => task.attachments, {
|
||||
nullable: false,
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'taskId' })
|
||||
task: TMTask;
|
||||
|
||||
@Column({ type: 'uuid' })
|
||||
taskId: string;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Column, Entity, JoinColumn, ManyToOne } from "typeorm";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { TMTask } from "./task.entity";
|
||||
|
||||
@Entity("tm_check_list_items")
|
||||
export class TMCheckListItem extends BaseEntity {
|
||||
@Column({ type: "varchar", length: 150 })
|
||||
title: string;
|
||||
|
||||
@Column({ type: "boolean", default: false })
|
||||
isDone: boolean;
|
||||
|
||||
// --- Relations ---
|
||||
|
||||
@ManyToOne(() => TMTask, (task) => task.checkListItems, {
|
||||
nullable: false,
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
@JoinColumn({ name: "taskId" })
|
||||
task: TMTask;
|
||||
|
||||
@Column({ type: "uuid" })
|
||||
taskId: string;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Column, Entity, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToMany } from "typeorm";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { TMWorkspace } from "./workspace.entity";
|
||||
import { TMTaskPhase } from "./task-phase.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
|
||||
@Entity("tm_projects")
|
||||
export class TMProject extends BaseEntity {
|
||||
@Column({ type: "varchar", length: 150 })
|
||||
name: string;
|
||||
|
||||
@Column({ type: "varchar", length: 150, nullable: true })
|
||||
ownerName: string;
|
||||
|
||||
@Column({ type: "text", nullable: true })
|
||||
description: string;
|
||||
|
||||
@Column({ type: "timestamptz", nullable: true })
|
||||
startDate: Date;
|
||||
|
||||
@Column({ type: "text", nullable: true })
|
||||
backgroundPicture: string;
|
||||
|
||||
@Column({ type: "varchar", length: 20, nullable: true })
|
||||
backgroundColor: string;
|
||||
|
||||
@Column({ type: "boolean", default: true })
|
||||
isActive: boolean;
|
||||
|
||||
// --- Relations ---
|
||||
|
||||
@ManyToOne(() => TMWorkspace, (workspace) => workspace.projects, {
|
||||
nullable: false,
|
||||
onDelete: "RESTRICT",
|
||||
})
|
||||
@JoinColumn({ name: "workspaceId" })
|
||||
workspace: TMWorkspace;
|
||||
|
||||
@Column({ type: "uuid" })
|
||||
workspaceId: string;
|
||||
|
||||
@OneToMany(() => TMTaskPhase, (taskPhase) => taskPhase.project, {
|
||||
cascade: ["insert", "update"],
|
||||
})
|
||||
taskPhases: TMTaskPhase[];
|
||||
|
||||
@ManyToMany(() => User, (user) => user.projects)
|
||||
@JoinTable({
|
||||
name: "tm_project_users",
|
||||
joinColumn: { name: "projectId", referencedColumnName: "id" },
|
||||
inverseJoinColumn: { name: "userId", referencedColumnName: "id" },
|
||||
})
|
||||
users: User[];
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Column, Entity, JoinColumn, ManyToOne } from "typeorm";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { TMTask } from "./task.entity";
|
||||
|
||||
@Entity("tm_remarks")
|
||||
export class TMRemark extends BaseEntity {
|
||||
@Column({ type: "varchar", length: 150 })
|
||||
title: string;
|
||||
|
||||
@Column({ type: "varchar", length: 20, nullable: true })
|
||||
color: string;
|
||||
|
||||
// --- Relations ---
|
||||
|
||||
@ManyToOne(() => TMTask, (task) => task.remarks, {
|
||||
nullable: false,
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
@JoinColumn({ name: "taskId" })
|
||||
task: TMTask;
|
||||
|
||||
@Column({ type: "uuid" })
|
||||
taskId: string;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from "typeorm";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { TMProject } from "./project.entity";
|
||||
import { TMTask } from "./task.entity";
|
||||
|
||||
@Entity("tm_task_phases")
|
||||
export class TMTaskPhase extends BaseEntity {
|
||||
@Column({ type: "varchar", length: 100 })
|
||||
name: string;
|
||||
|
||||
@Column({ type: "int", default: 0 })
|
||||
order: number;
|
||||
|
||||
@Column({ type: "varchar", length: 20, nullable: true })
|
||||
color: string;
|
||||
|
||||
// --- Relations ---
|
||||
|
||||
@ManyToOne(() => TMProject, (project) => project.taskPhases, {
|
||||
nullable: false,
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
@JoinColumn({ name: "projectId" })
|
||||
project: TMProject;
|
||||
|
||||
@Column({ type: "uuid" })
|
||||
projectId: string;
|
||||
|
||||
@OneToMany(() => TMTask, (task) => task.taskPhase, {
|
||||
cascade: ["insert", "update"],
|
||||
})
|
||||
tasks: TMTask[];
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Column, Entity, JoinColumn, ManyToOne, ManyToMany, OneToMany, JoinTable } from "typeorm";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { TMTaskPhase } from "./task-phase.entity";
|
||||
import { TMRemark } from "./remark.entity";
|
||||
import { TMAttachment } from "./attachment.entity";
|
||||
import { TMCheckListItem } from "./check-list-item.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
|
||||
@Entity("tm_tasks")
|
||||
export class TMTask extends BaseEntity {
|
||||
@Column({ type: "varchar", length: 150 })
|
||||
title: string;
|
||||
|
||||
@Column({ type: "text", nullable: true })
|
||||
description: string;
|
||||
|
||||
@Column({ type: "varchar", length: 20, nullable: true })
|
||||
color: string;
|
||||
|
||||
@Column({ type: "timestamptz", nullable: true })
|
||||
startDate: Date;
|
||||
|
||||
@Column({ type: "timestamptz", nullable: true })
|
||||
endDate: Date;
|
||||
|
||||
// --- Relations ---
|
||||
|
||||
@ManyToOne(() => TMTaskPhase, (taskPhase) => taskPhase.tasks, {
|
||||
nullable: false,
|
||||
onDelete: "RESTRICT",
|
||||
})
|
||||
@JoinColumn({ name: "taskPhaseId" })
|
||||
taskPhase: TMTaskPhase;
|
||||
|
||||
@Column({ type: "uuid" })
|
||||
taskPhaseId: string;
|
||||
|
||||
@OneToMany(() => TMRemark, (remark) => remark.task, {
|
||||
cascade: ["insert", "update"],
|
||||
})
|
||||
remarks: TMRemark[];
|
||||
|
||||
@OneToMany(() => TMAttachment, (attachment) => attachment.task, {
|
||||
cascade: ["insert", "update"],
|
||||
})
|
||||
attachments: TMAttachment[];
|
||||
|
||||
@OneToMany(() => TMCheckListItem, (checkListItem) => checkListItem.task, {
|
||||
cascade: ["insert", "update"],
|
||||
})
|
||||
checkListItems: TMCheckListItem[];
|
||||
|
||||
@ManyToMany(() => User, (user) => user.tasks)
|
||||
@JoinTable({
|
||||
name: "tm_task_users",
|
||||
joinColumn: { name: "taskId", referencedColumnName: "id" },
|
||||
inverseJoinColumn: { name: "userId", referencedColumnName: "id" },
|
||||
})
|
||||
users: User[];
|
||||
|
||||
// deriven(calculated) attributes. not database columns
|
||||
attachmentCount?: number;
|
||||
checkListItemCount?: number;
|
||||
completedCheckListItemCount?: number;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Column, Entity, JoinTable, ManyToMany, OneToMany } from "typeorm";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { TMProject } from "./project.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
|
||||
@Entity("tm_workspaces")
|
||||
export class TMWorkspace extends BaseEntity {
|
||||
@Column({ type: "varchar", length: 100, unique: true })
|
||||
name: string;
|
||||
|
||||
@Column({ type: "text", nullable: true })
|
||||
description: string;
|
||||
|
||||
@Column({ type: "varchar", length: 20 })
|
||||
color: string;
|
||||
|
||||
@Column({ type: "boolean", default: true })
|
||||
isActive: boolean;
|
||||
|
||||
// --- Relations ---
|
||||
|
||||
@OneToMany(() => TMProject, (project) => project.workspace, {
|
||||
cascade: ["insert", "update"],
|
||||
})
|
||||
projects: TMProject[];
|
||||
|
||||
@ManyToMany(() => User, (user) => user.workspaces)
|
||||
@JoinTable({
|
||||
name: "tm_workspace_users",
|
||||
joinColumn: { name: "workspaceId", referencedColumnName: "id" },
|
||||
inverseJoinColumn: { name: "userId", referencedColumnName: "id" },
|
||||
})
|
||||
users: User[];
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export enum TMAttachmentType {
|
||||
FILE = "file",
|
||||
LINK = "link",
|
||||
IMAGE = "image",
|
||||
VIDEO = "video",
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { AttachmentRepository } from "../repositories/attachment.repository";
|
||||
import { TMAttachment } from "../entities/attachment.entity";
|
||||
import { CreateAttachmentDto } from "../dto/attachment/create-attachment.dto";
|
||||
import { UpdateAttachmentDto } from "../dto/attachment/update-attachment.dto";
|
||||
import { attachmentMessage } from "../../../common/enums/message.enum";
|
||||
import { TaskService } from "./task.service";
|
||||
|
||||
@Injectable()
|
||||
export class AttachmentService {
|
||||
constructor(
|
||||
private readonly attachmentRepository: AttachmentRepository,
|
||||
private readonly taskService: TaskService,
|
||||
) {}
|
||||
|
||||
async findOneOrFail(id: string): Promise<TMAttachment> {
|
||||
const attachment = await this.attachmentRepository.findOne({ where: { id }, relations: { task: true } });
|
||||
if (!attachment) throw new NotFoundException(attachmentMessage.ATTACHMENT_NOT_FOUND);
|
||||
return attachment;
|
||||
}
|
||||
|
||||
async create(dto: CreateAttachmentDto): Promise<TMAttachment> {
|
||||
await this.taskService.findOneOrFail(dto.taskId);
|
||||
|
||||
const attachment = this.attachmentRepository.create(dto);
|
||||
return this.attachmentRepository.save(attachment);
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateAttachmentDto): Promise<TMAttachment> {
|
||||
const attachment = await this.findOneOrFail(id);
|
||||
|
||||
if (dto.taskId) {
|
||||
await this.taskService.findOneOrFail(dto.taskId);
|
||||
}
|
||||
|
||||
Object.assign(attachment, dto);
|
||||
return this.attachmentRepository.save(attachment);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
await this.findOneOrFail(id);
|
||||
await this.attachmentRepository.delete(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { CheckListItemRepository } from '../repositories/check-list-item.repository';
|
||||
import { TMCheckListItem } from '../entities/check-list-item.entity';
|
||||
import { CreateCheckListItemDto } from '../dto/check-list-item/create-check-list-item.dto';
|
||||
import { UpdateCheckListItemDto } from '../dto/check-list-item/update-check-list-item.dto';
|
||||
import { checkListItemMessage } from '../../../common/enums/message.enum';
|
||||
import { TaskService } from './task.service';
|
||||
|
||||
@Injectable()
|
||||
export class CheckListItemService {
|
||||
constructor(
|
||||
private readonly checkListItemRepository: CheckListItemRepository,
|
||||
private readonly taskService: TaskService,
|
||||
) {}
|
||||
|
||||
async findOneOrFail(id: string): Promise<TMCheckListItem> {
|
||||
const checkListItem = await this.checkListItemRepository.findOneById(id);
|
||||
if (!checkListItem) throw new NotFoundException(checkListItemMessage.CHECKLISTITEM_NOT_FOUND);
|
||||
return checkListItem;
|
||||
}
|
||||
|
||||
async create(dto: CreateCheckListItemDto): Promise<TMCheckListItem> {
|
||||
await this.taskService.findOneOrFail(dto.taskId);
|
||||
|
||||
const checkListItem = this.checkListItemRepository.create(dto);
|
||||
return this.checkListItemRepository.save(checkListItem);
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateCheckListItemDto): Promise<TMCheckListItem> {
|
||||
const checkListItem = await this.findOneOrFail(id);
|
||||
|
||||
if (dto.taskId) {
|
||||
await this.taskService.findOneOrFail(dto.taskId);
|
||||
}
|
||||
|
||||
Object.assign(checkListItem, dto);
|
||||
return this.checkListItemRepository.save(checkListItem);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<TMCheckListItem> {
|
||||
const checkListItem = await this.findOneOrFail(id);
|
||||
await this.checkListItemRepository.delete(id);
|
||||
return checkListItem;
|
||||
}
|
||||
|
||||
async toggleStatus(id: string) : Promise<TMCheckListItem> {
|
||||
const checkListItem = await this.findOneOrFail(id);
|
||||
const isDone = checkListItem.isDone;
|
||||
checkListItem.isDone = !isDone;
|
||||
await this.checkListItemRepository.save(checkListItem);
|
||||
|
||||
return checkListItem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { In, Repository } from "typeorm";
|
||||
import { ProjectRepository } from "../repositories/project.repository";
|
||||
import { TMProject } from "../entities/project.entity";
|
||||
import { CreateProjectDto } from "../dto/project/create-project.dto";
|
||||
import { UpdateProjectDto } from "../dto/project/update-project.dto";
|
||||
import { PaginationDto } from "../../../common/DTO/pagination.dto";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { ProjectMessage, WorkSpaceMessage } from "../../../common/enums/message.enum";
|
||||
import { WorkspaceService } from "./workspace.service";
|
||||
import { PermissionEnum } from "../../users/enums/permission.enum";
|
||||
|
||||
@Injectable()
|
||||
export class ProjectService {
|
||||
constructor(
|
||||
private readonly projectRepository: ProjectRepository,
|
||||
private readonly workspaceService: WorkspaceService,
|
||||
@InjectRepository(User)
|
||||
private readonly userRepository: Repository<User>,
|
||||
) {}
|
||||
|
||||
async findOneOrFail(id: string): Promise<TMProject> {
|
||||
const project = await this.projectRepository.findOne({ where: { id } });
|
||||
if (!project) throw new NotFoundException(ProjectMessage.PROJECT_NOT_FOUND);
|
||||
return project;
|
||||
}
|
||||
|
||||
async create(dto: CreateProjectDto): Promise<TMProject> {
|
||||
const { userIds, startDate, ...rest } = dto;
|
||||
|
||||
const workspace = await this.workspaceService.findOneOrFail(dto.workspaceId);
|
||||
|
||||
const project = this.projectRepository.create({
|
||||
...rest,
|
||||
startDate: new Date(startDate),
|
||||
});
|
||||
|
||||
if (userIds?.length) {
|
||||
const workspaceUserIds = workspace.users.map((u) => u.id);
|
||||
const invalidUsers = userIds.filter((id) => !workspaceUserIds.includes(id));
|
||||
if (invalidUsers.length) {
|
||||
throw new BadRequestException(WorkSpaceMessage.USERS_DONT_BELONG_TO_THIS_WORKSPACE);
|
||||
}
|
||||
project.users = await this.userRepository.findBy({ id: In(userIds) });
|
||||
}
|
||||
|
||||
return this.projectRepository.save(project);
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateProjectDto): Promise<TMProject> {
|
||||
const project = await this.findOneOrFail(id);
|
||||
|
||||
const { userIds, startDate, ...rest } = dto;
|
||||
|
||||
Object.assign(project, {
|
||||
...rest,
|
||||
startDate: startDate ? new Date(startDate) : project.startDate,
|
||||
});
|
||||
|
||||
if (userIds) {
|
||||
if (userIds.length) {
|
||||
const workspaceId = dto.workspaceId ?? project.workspaceId;
|
||||
const workspace = await this.workspaceService.findOneOrFail(workspaceId);
|
||||
|
||||
const workspaceUserIds = workspace.users.map((u) => u.id);
|
||||
const invalidUsers = userIds.filter((uid) => !workspaceUserIds.includes(uid));
|
||||
if (invalidUsers.length) {
|
||||
throw new BadRequestException(WorkSpaceMessage.USERS_DONT_BELONG_TO_THIS_WORKSPACE);
|
||||
}
|
||||
project.users = await this.userRepository.findBy({ id: In(userIds) });
|
||||
} else {
|
||||
project.users = [];
|
||||
}
|
||||
}
|
||||
|
||||
return this.projectRepository.save(project);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<TMProject> {
|
||||
const project = await this.projectRepository.findOne({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
relations: {
|
||||
taskPhases: true,
|
||||
},
|
||||
select: {
|
||||
taskPhases: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!project) throw new NotFoundException(ProjectMessage.PROJECT_NOT_FOUND);
|
||||
|
||||
const taskPhases = project.taskPhases;
|
||||
if (taskPhases !== undefined && taskPhases.length) {
|
||||
throw new BadRequestException(ProjectMessage.PROJECT_CONTAINS_TASK_PHASES);
|
||||
}
|
||||
await this.projectRepository.delete(id);
|
||||
return project;
|
||||
}
|
||||
|
||||
async findUserProjectsByWorkspace(
|
||||
userId: string,
|
||||
permissions: PermissionEnum[],
|
||||
wsId: string,
|
||||
pagination: PaginationDto,
|
||||
): Promise<{ projects: TMProject[]; count: number; paginate: true }> {
|
||||
const workspace = await this.workspaceService.findOneOrFail(wsId);
|
||||
|
||||
const page = pagination.page ?? 1;
|
||||
const limit = pagination.limit ?? 10;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const isPermissionIncluded = permissions.includes(PermissionEnum.VIEW_ALL_PROJECTS);
|
||||
const isUserMemeberOfWorkspace = workspace.users.some((user) => user.id === userId);
|
||||
|
||||
var res: [TMProject[], number];
|
||||
if (isUserMemeberOfWorkspace) {
|
||||
res = await this.projectRepository.findUserProjects(userId, wsId, skip, limit);
|
||||
} else if (isPermissionIncluded) {
|
||||
res = await this.projectRepository.findAndCount({ where: { workspaceId: wsId } });
|
||||
} else {
|
||||
throw new ForbiddenException(WorkSpaceMessage.FORBIDDEN);
|
||||
}
|
||||
|
||||
const [projects, count] = res;
|
||||
|
||||
return { projects, count, paginate: true };
|
||||
}
|
||||
|
||||
async getProjectDetail(projectId: string, userId: string, permissions: PermissionEnum[]): 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 && !permissions.includes(PermissionEnum.VIEW_ALL_PROJECTS))
|
||||
throw new ForbiddenException(ProjectMessage.NOT_MEMBER_OF_PROJECT);
|
||||
|
||||
const projectDetail = await this.projectRepository.getProjectDetail(projectId);
|
||||
|
||||
return projectDetail;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { RemarkRepository } from "../repositories/remark.repository";
|
||||
import { TMRemark } from "../entities/remark.entity";
|
||||
import { CreateRemarkDto } from "../dto/remark/create-remark.dto";
|
||||
import { UpdateRemarkDto } from "../dto/remark/update-remark.dto";
|
||||
import { remarkMessage } from "../../../common/enums/message.enum";
|
||||
import { TaskService } from './task.service';
|
||||
|
||||
@Injectable()
|
||||
export class RemarkService {
|
||||
constructor(
|
||||
private readonly remarkRepository: RemarkRepository,
|
||||
private readonly taskService: TaskService,
|
||||
) {}
|
||||
|
||||
async findOneOrFail(id: string): Promise<TMRemark> {
|
||||
const remark = await this.remarkRepository.findOne({ where: { id } });
|
||||
if (!remark) throw new NotFoundException(remarkMessage.REMARK_NOT_FOUND);
|
||||
return remark;
|
||||
}
|
||||
|
||||
async create(dto: CreateRemarkDto): Promise<TMRemark> {
|
||||
await this.taskService.findOneOrFail(dto.taskId);
|
||||
|
||||
const remark = this.remarkRepository.create(dto);
|
||||
return this.remarkRepository.save(remark);
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateRemarkDto): Promise<TMRemark> {
|
||||
const remark = await this.findOneOrFail(id);
|
||||
|
||||
if (dto.taskId) {
|
||||
await this.taskService.findOneOrFail(dto.taskId);
|
||||
}
|
||||
|
||||
Object.assign(remark, dto);
|
||||
return this.remarkRepository.save(remark);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
await this.findOneOrFail(id);
|
||||
await this.remarkRepository.delete(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { TaskPhaseRepository } from "../repositories/task-phase.repository";
|
||||
import { TMTaskPhase } from "../entities/task-phase.entity";
|
||||
import { CreateTaskPhaseDto } from "../dto/task-phase/create-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()
|
||||
export class TaskPhaseService {
|
||||
constructor(
|
||||
private readonly taskPhaseRepository: TaskPhaseRepository,
|
||||
private readonly projectService: ProjectService
|
||||
) {}
|
||||
|
||||
async findOneOrFail(id: string): Promise<TMTaskPhase> {
|
||||
const taskPhase = await this.taskPhaseRepository.findOne({where: {id}});
|
||||
if (!taskPhase) throw new NotFoundException(TaskPhaseMessage.TASK_PHASE_NOT_FOUND);
|
||||
return taskPhase;
|
||||
}
|
||||
|
||||
async create(dto: CreateTaskPhaseDto): Promise<TMTaskPhase> {
|
||||
await this.projectService.findOneOrFail(dto.projectId);
|
||||
const taskPhase = this.taskPhaseRepository.create(dto);
|
||||
return this.taskPhaseRepository.save(taskPhase);
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateTaskPhaseDto): Promise<TMTaskPhase> {
|
||||
const taskPhase = await this.findOneOrFail(id);
|
||||
|
||||
if (dto.projectId) {
|
||||
await this.projectService.findOneOrFail(dto.projectId);
|
||||
}
|
||||
|
||||
Object.assign(taskPhase, dto);
|
||||
return this.taskPhaseRepository.save(taskPhase);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
await this.findOneOrFail(id);
|
||||
await this.taskPhaseRepository.delete(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { In, Repository } from "typeorm";
|
||||
import { TaskRepository } from "../repositories/task.repository";
|
||||
import { TMTask } from "../entities/task.entity";
|
||||
import { CreateTaskDto } from "../dto/task/create-task.dto";
|
||||
import { UpdateTaskDto } from "../dto/task/update-task.dto";
|
||||
import { ChangeTaskPhaseDto } from "../dto/task/change-task-phase.dto";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { TaskMessage, TaskPhaseMessage } from "../../../common/enums/message.enum";
|
||||
import { TaskPhaseRepository } from "../repositories/task-phase.repository";
|
||||
import { TMTaskPhase } from "../entities/task-phase.entity";
|
||||
import { PermissionEnum } from "../../users/enums/permission.enum";
|
||||
|
||||
@Injectable()
|
||||
export class TaskService {
|
||||
constructor(
|
||||
private readonly taskRepository: TaskRepository,
|
||||
private readonly taskPhaseRepository: TaskPhaseRepository,
|
||||
@InjectRepository(User)
|
||||
private readonly userRepository: Repository<User>,
|
||||
) {}
|
||||
|
||||
async findOneOrFail(id: string): Promise<TMTask> {
|
||||
const task = await this.taskRepository.findOne({
|
||||
where: { id },
|
||||
relations: {
|
||||
taskPhase: true,
|
||||
users: true,
|
||||
},
|
||||
});
|
||||
if (!task) throw new NotFoundException(TaskMessage.TASK_NOT_FOUND);
|
||||
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> {
|
||||
const { userIds, startDate, endDate, ...rest } = dto;
|
||||
|
||||
const taskPhase = await this.findTaskPhaseOrFail(dto.taskPhaseId);
|
||||
|
||||
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({
|
||||
...rest,
|
||||
startDate: startDate ? new Date(startDate) : undefined,
|
||||
endDate: endDate ? new Date(endDate) : undefined,
|
||||
});
|
||||
|
||||
if (userIds?.length) {
|
||||
task.users = await this.userRepository.findBy({ id: In(userIds) });
|
||||
}
|
||||
|
||||
return this.taskRepository.save(task);
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateTaskDto): Promise<TMTask> {
|
||||
const task = await this.findOneOrFail(id);
|
||||
const { userIds, startDate, endDate, ...rest } = dto;
|
||||
|
||||
if (dto.taskPhaseId) {
|
||||
const taskPhase = await this.findTaskPhaseOrFail(dto.taskPhaseId);
|
||||
|
||||
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, {
|
||||
...rest,
|
||||
startDate: startDate ? new Date(startDate) : task.startDate,
|
||||
endDate: endDate ? new Date(endDate) : task.endDate,
|
||||
});
|
||||
|
||||
if (userIds) {
|
||||
task.users = userIds.length ? await this.userRepository.findBy({ id: In(userIds) }) : [];
|
||||
}
|
||||
|
||||
return this.taskRepository.save(task);
|
||||
}
|
||||
|
||||
async changePhase(id: string, dto: ChangeTaskPhaseDto): Promise<TMTask> {
|
||||
const task = await this.findOneOrFail(id);
|
||||
|
||||
const newPhase = await this.findTaskPhaseOrFail(dto.taskPhaseId);
|
||||
|
||||
if (task.taskPhase.projectId !== newPhase.projectId) {
|
||||
throw new BadRequestException(TaskPhaseMessage.TASK_PHASE_NOT_SAME_PROJECT);
|
||||
}
|
||||
|
||||
task.taskPhaseId = dto.taskPhaseId;
|
||||
task.taskPhase = newPhase;
|
||||
|
||||
return this.taskRepository.save(task);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
await this.findOneOrFail(id);
|
||||
await this.taskRepository.delete(id);
|
||||
}
|
||||
|
||||
async getTaskDetail(userId: string, permissions: PermissionEnum[], taskId: string): Promise<TMTask | null> {
|
||||
const task = await this.findOneOrFail(taskId);
|
||||
|
||||
const isTaskMember = task.users.some((user) => user.id === userId);
|
||||
const hasPermission = permissions.includes(PermissionEnum.TASK);
|
||||
if (!isTaskMember && !hasPermission) throw new ForbiddenException(TaskMessage.USER_NOT_TASK_MEMBER);
|
||||
|
||||
const taskDetail = await this.taskRepository.getTaskDetail(taskId);
|
||||
return taskDetail;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { In, Not, Repository } from "typeorm";
|
||||
import { WorkspaceRepository } from "../repositories/workspace.repository";
|
||||
import { TMWorkspace } from "../entities/workspace.entity";
|
||||
import { CreateWorkspaceDto } from "../dto/workspace/create-workspace.dto";
|
||||
import { UpdateWorkspaceDto } from "../dto/workspace/update-workspace.dto";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { WorkSpaceMessage } from "../../../common/enums/message.enum";
|
||||
import { PermissionEnum } from "../../users/enums/permission.enum";
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceService {
|
||||
constructor(
|
||||
private readonly workspaceRepository: WorkspaceRepository,
|
||||
@InjectRepository(User)
|
||||
private readonly userRepository: Repository<User>,
|
||||
) {}
|
||||
|
||||
findAll(): Promise<TMWorkspace[]> {
|
||||
return this.workspaceRepository.find({});
|
||||
}
|
||||
|
||||
async findOneOrFail(id: string): Promise<TMWorkspace> {
|
||||
const workspace = await this.workspaceRepository.findOneWorkSpace(id);
|
||||
if (!workspace) throw new NotFoundException(WorkSpaceMessage.WORKSPACE_NOT_FOUND);
|
||||
return workspace;
|
||||
}
|
||||
|
||||
async create(dto: CreateWorkspaceDto): Promise<TMWorkspace> {
|
||||
const { userIds, ...rest } = dto;
|
||||
|
||||
const existingWorkspace = await this.workspaceRepository.findOne({ where: { name: rest.name } });
|
||||
if (existingWorkspace) {
|
||||
throw new BadRequestException(WorkSpaceMessage.WORKSPACE_EXISTS);
|
||||
}
|
||||
|
||||
const workspace = this.workspaceRepository.create(rest);
|
||||
|
||||
if (userIds?.length) {
|
||||
workspace.users = await this.userRepository.findBy({ id: In(userIds) });
|
||||
}
|
||||
|
||||
return this.workspaceRepository.save(workspace);
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateWorkspaceDto): Promise<TMWorkspace> {
|
||||
const workspace = await this.findOneOrFail(id);
|
||||
const { userIds, ...rest } = dto;
|
||||
|
||||
if (rest.name) {
|
||||
const existingWorkSpaceWithTheSameName = await this.workspaceRepository.findOne({ where: { name: rest.name, id: Not(id) } });
|
||||
if (existingWorkSpaceWithTheSameName) throw new BadRequestException(WorkSpaceMessage.WORKSPACE_SAME_NAME_EXISTS);
|
||||
}
|
||||
|
||||
Object.assign(workspace, rest);
|
||||
|
||||
if (userIds) {
|
||||
workspace.users = userIds.length ? await this.userRepository.findBy({ id: In(userIds) }) : [];
|
||||
}
|
||||
|
||||
return this.workspaceRepository.save(workspace);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<TMWorkspace> {
|
||||
const workspace = await this.findOneOrFail(id);
|
||||
await this.workspaceRepository.delete(id);
|
||||
|
||||
return workspace;
|
||||
}
|
||||
|
||||
async findByUser(userId: string, permissions: PermissionEnum[]): Promise<TMWorkspace[]> {
|
||||
if(permissions.includes(PermissionEnum.VIEW_ALL_WORKSPACES)) {
|
||||
return await this.workspaceRepository.find();
|
||||
}
|
||||
return await this.workspaceRepository.findByUser(userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { TMAttachment } from '../entities/attachment.entity';
|
||||
|
||||
@Injectable()
|
||||
export class AttachmentRepository extends Repository<TMAttachment>{
|
||||
constructor(
|
||||
@InjectRepository(TMAttachment) attachmentRepository: Repository<TMAttachment>,
|
||||
) {
|
||||
super(attachmentRepository.target, attachmentRepository.manager, attachmentRepository.queryRunner);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { TMCheckListItem } from '../entities/check-list-item.entity';
|
||||
|
||||
@Injectable()
|
||||
export class CheckListItemRepository extends Repository<TMCheckListItem>{
|
||||
constructor(
|
||||
@InjectRepository(TMCheckListItem) checkListItemRepository: Repository<TMCheckListItem>,
|
||||
) {
|
||||
super(checkListItemRepository.target, checkListItemRepository.manager, checkListItemRepository.queryRunner);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { TMProject } from "../entities/project.entity";
|
||||
|
||||
@Injectable()
|
||||
export class ProjectRepository extends Repository<TMProject> {
|
||||
constructor(
|
||||
@InjectRepository(TMProject) projectRepository: Repository<TMProject>,
|
||||
) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { TMRemark } from "../entities/remark.entity";
|
||||
|
||||
@Injectable()
|
||||
export class RemarkRepository extends Repository<TMRemark>{
|
||||
constructor(
|
||||
@InjectRepository(TMRemark) remarkRepository: Repository<TMRemark>,
|
||||
) {
|
||||
super(remarkRepository.target, remarkRepository.manager, remarkRepository.queryRunner);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { TMTaskPhase } from "../entities/task-phase.entity";
|
||||
|
||||
@Injectable()
|
||||
export class TaskPhaseRepository extends Repository<TMTaskPhase> {
|
||||
constructor(
|
||||
@InjectRepository(TMTaskPhase) taskPhaseRepository: Repository<TMTaskPhase>,
|
||||
) {
|
||||
super(taskPhaseRepository.target, taskPhaseRepository.manager, taskPhaseRepository.queryRunner);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { TMTask } from "../entities/task.entity";
|
||||
|
||||
@Injectable()
|
||||
export class TaskRepository extends Repository<TMTask> {
|
||||
constructor(@InjectRepository(TMTask) taskRepository: Repository<TMTask>) {
|
||||
super(taskRepository.target, taskRepository.manager, taskRepository.queryRunner);
|
||||
}
|
||||
|
||||
async getTaskDetail(taskId: string): Promise<TMTask | null> {
|
||||
return this.createQueryBuilder("task")
|
||||
.leftJoinAndSelect("task.remarks", "remark")
|
||||
.leftJoinAndSelect("task.attachments", "attachment")
|
||||
.leftJoinAndSelect("task.checkListItems", "checkListItem")
|
||||
.leftJoinAndSelect("task.users", "user")
|
||||
|
||||
.loadRelationCountAndMap("task.checkListItemCount", "task.checkListItems")
|
||||
|
||||
.loadRelationCountAndMap("task.completedCheckListItemCount", "task.checkListItems", "completedCheckListItems", (qb) =>
|
||||
qb.where("completedCheckListItems.isDone = :isDone", { isDone: true }),
|
||||
)
|
||||
|
||||
.select([
|
||||
"remark.id",
|
||||
"remark.title",
|
||||
"remark.color",
|
||||
"checkListItem.id",
|
||||
"checkListItem.title",
|
||||
"checkListItem.isDone",
|
||||
"attachment.id",
|
||||
"attachment.title",
|
||||
"attachment.type",
|
||||
"attachment.file",
|
||||
|
||||
"user.id",
|
||||
"user.firstName",
|
||||
"user.lastName",
|
||||
])
|
||||
.where("task.id = :taskId", { taskId })
|
||||
.getOne();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Repository } from 'typeorm';
|
||||
import { TMWorkspace } from '../entities/workspace.entity';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceRepository extends Repository<TMWorkspace> {
|
||||
constructor(@InjectRepository(TMWorkspace) workspaceRepository: Repository<TMWorkspace>) {
|
||||
super(workspaceRepository.target, workspaceRepository.manager, workspaceRepository.queryRunner);
|
||||
}
|
||||
|
||||
async findOneWorkSpace(workspaceId: string): Promise<TMWorkspace | null> {
|
||||
return this.createQueryBuilder("workspace")
|
||||
.leftJoinAndSelect('workspace.users', 'user')
|
||||
.select([
|
||||
'workspace',
|
||||
'user.id',
|
||||
'user.firstName',
|
||||
'user.lastName'
|
||||
]).where('workspace.id = :workspaceId', {workspaceId})
|
||||
.getOne();
|
||||
}
|
||||
|
||||
async findByUser(userId: string): Promise<TMWorkspace[]> {
|
||||
return this.createQueryBuilder("workspace")
|
||||
.innerJoin("workspace.users", "users")
|
||||
.where("users.id = :userId", { userId })
|
||||
.getMany();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
|
||||
import { TMWorkspace } from "./entities/workspace.entity";
|
||||
import { TMProject } from "./entities/project.entity";
|
||||
import { TMTaskPhase } from "./entities/task-phase.entity";
|
||||
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 { WorkspaceRepository } from "./repositories/workspace.repository";
|
||||
import { WorkspaceController } from "./controllers/workspace.controller";
|
||||
import { WorkspaceService } from "./providers/workspace.service";
|
||||
import { User } from "../users/entities/user.entity";
|
||||
import { ProjectController } from "./controllers/project.controller";
|
||||
import { ProjectRepository } from "./repositories/project.repository";
|
||||
import { ProjectService } from "./providers/project.service";
|
||||
import { TaskPhaseService } from "./providers/task-phase.service";
|
||||
import { TaskPhaseRepository } from "./repositories/task-phase.repository";
|
||||
import { TaskRepository } from "./repositories/task.repository";
|
||||
import { TaskService } from "./providers/task.service";
|
||||
import { CheckListItemRepository } from "./repositories/check-list-item.repository";
|
||||
import { CheckListItemService } from "./providers/check-list-item.service";
|
||||
import { TaskPhaseController } from "./controllers/task-phase.controller";
|
||||
import { TaskController } from "./controllers/task.controller";
|
||||
import { CheckListItemController } from "./controllers/check-list-item.controller";
|
||||
import { RemarkController } from "./controllers/remark.controller";
|
||||
import { RemarkRepository } from "./repositories/remark.repository";
|
||||
import { RemarkService } from "./providers/remark.service";
|
||||
import { AttachmentController } from "./controllers/attachment.controller";
|
||||
import { AttachmentRepository } from "./repositories/attachment.repository";
|
||||
import { AttachmentService } from "./providers/attachment.service";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([TMWorkspace, TMProject, TMTaskPhase, TMTask, TMRemark, TMAttachment, TMCheckListItem, User]),
|
||||
],
|
||||
controllers: [
|
||||
WorkspaceController,
|
||||
ProjectController,
|
||||
TaskPhaseController,
|
||||
TaskController,
|
||||
CheckListItemController,
|
||||
RemarkController,
|
||||
AttachmentController
|
||||
],
|
||||
providers: [
|
||||
WorkspaceRepository,
|
||||
WorkspaceService,
|
||||
ProjectRepository,
|
||||
ProjectService,
|
||||
TaskPhaseRepository,
|
||||
TaskPhaseService,
|
||||
TaskRepository,
|
||||
TaskService,
|
||||
CheckListItemRepository,
|
||||
CheckListItemService,
|
||||
RemarkRepository,
|
||||
RemarkService,
|
||||
AttachmentRepository,
|
||||
AttachmentService
|
||||
]
|
||||
})
|
||||
export class TaskManagerModule {}
|
||||
@@ -32,6 +32,9 @@ import { Wallet } from "../../wallets/entities/wallet.entity";
|
||||
import { FinancialType } from "../enums/financial-type.enum";
|
||||
import { Reseller } from "../../reseller/entities/reseller.entity";
|
||||
import { UserBankAccount } from "../../reseller/entities/user-bank-account.entity";
|
||||
import { TMWorkspace } from "../../task-manager/entities/workspace.entity";
|
||||
import { TMTask } from "../../task-manager/entities/task.entity";
|
||||
import { TMProject } from "../../task-manager/entities/project.entity";
|
||||
|
||||
@Entity()
|
||||
export class User extends BaseEntity {
|
||||
@@ -167,14 +170,23 @@ export class User extends BaseEntity {
|
||||
//---------------------------------------
|
||||
|
||||
@ManyToOne(() => Reseller, (reseller) => reseller.agents, { nullable: true })
|
||||
reseller?: Reseller
|
||||
reseller?: Reseller;
|
||||
|
||||
|
||||
@OneToOne(() => Reseller, reseller => reseller.owner, { nullable: true })
|
||||
ownedReseller?: Reseller
|
||||
@OneToOne(() => Reseller, (reseller) => reseller.owner, { nullable: true })
|
||||
ownedReseller?: Reseller;
|
||||
|
||||
//------------------------
|
||||
|
||||
@OneToOne(() => UserBankAccount, (account) => account.user, { nullable: true })
|
||||
bankAckount: UserBankAccount
|
||||
bankAckount: UserBankAccount;
|
||||
|
||||
// -----------------------
|
||||
@ManyToMany(() => TMWorkspace, (workspace) => workspace.users)
|
||||
workspaces: TMWorkspace[];
|
||||
|
||||
@ManyToMany(() => TMTask, (task) => task.users)
|
||||
tasks: TMTask[];
|
||||
|
||||
@ManyToMany(() => TMProject, (project) => project.users)
|
||||
projects: TMProject[];
|
||||
}
|
||||
|
||||
@@ -24,5 +24,11 @@ export enum PermissionEnum {
|
||||
DKALA = "dkala",
|
||||
DPAGE = "dpage",
|
||||
DMAIL = 'dmail',
|
||||
RESELLER="reseller"
|
||||
RESELLER="reseller",
|
||||
WORKSPACE="workspace", // create, update, remove
|
||||
VIEW_ALL_WORKSPACES="view_all_workspaces",
|
||||
PROJECT="project", // create, update, remove
|
||||
VIEW_ALL_PROJECTS="view_all_projects",
|
||||
TASK="task", // read, create, update, remove
|
||||
TASK_PHASE="task_phase",
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import { AdminsService } from "./providers/admins.service";
|
||||
import { CustomersService } from "./providers/customers.service";
|
||||
import { RefreshTokensRepository } from "./repositories/refresh-token.repository";
|
||||
import { ReferralsModule } from "../referrals/referrals.module";
|
||||
import { TaskManagerModule } from "../task-manager/task-manager.module";
|
||||
|
||||
|
||||
@Module({
|
||||
@@ -38,6 +39,7 @@ import { ReferralsModule } from "../referrals/referrals.module";
|
||||
AddressModule,
|
||||
ReferralsModule,
|
||||
AccessLogsModule,
|
||||
TaskManagerModule
|
||||
],
|
||||
providers: [
|
||||
UsersService,
|
||||
|
||||
Reference in New Issue
Block a user