Compare commits

11 Commits

25 changed files with 957 additions and 18 deletions
@@ -0,0 +1,28 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class TaskmanagerProject1782889623829 implements MigrationInterface {
name = 'TaskmanagerProject1782889623829'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "tm_project_users" ("projectId" uuid NOT NULL, "userId" uuid NOT NULL, CONSTRAINT "PK_5969064e19a1efa76b91de6a065" PRIMARY KEY ("projectId", "userId"))`);
await queryRunner.query(`CREATE INDEX "IDX_fcc8e135e4891925d288dbb14e" ON "tm_project_users" ("projectId") `);
await queryRunner.query(`CREATE INDEX "IDX_8e2d2c9d639d5fd806159f8698" ON "tm_project_users" ("userId") `);
await queryRunner.query(`ALTER TABLE "tm_projects" ADD "backgroundPicture" text`);
await queryRunner.query(`ALTER TABLE "tm_projects" ADD "backgroundColor" character varying(20)`);
await queryRunner.query(`ALTER TABLE "tm_projects" ADD "isActive" boolean NOT NULL DEFAULT true`);
await queryRunner.query(`ALTER TABLE "tm_project_users" ADD CONSTRAINT "FK_fcc8e135e4891925d288dbb14e5" FOREIGN KEY ("projectId") REFERENCES "tm_projects"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
await queryRunner.query(`ALTER TABLE "tm_project_users" ADD CONSTRAINT "FK_8e2d2c9d639d5fd806159f86980" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "tm_project_users" DROP CONSTRAINT "FK_8e2d2c9d639d5fd806159f86980"`);
await queryRunner.query(`ALTER TABLE "tm_project_users" DROP CONSTRAINT "FK_fcc8e135e4891925d288dbb14e5"`);
await queryRunner.query(`ALTER TABLE "tm_projects" DROP COLUMN "isActive"`);
await queryRunner.query(`ALTER TABLE "tm_projects" DROP COLUMN "backgroundColor"`);
await queryRunner.query(`ALTER TABLE "tm_projects" DROP COLUMN "backgroundPicture"`);
await queryRunner.query(`DROP INDEX "public"."IDX_8e2d2c9d639d5fd806159f8698"`);
await queryRunner.query(`DROP INDEX "public"."IDX_fcc8e135e4891925d288dbb14e"`);
await queryRunner.query(`DROP TABLE "tm_project_users"`);
}
}
@@ -0,0 +1,56 @@
import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } 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';
@ApiTags('Task Manager - Check List Items')
@Controller('task-manager/check-list-items')
export class CheckListItemController {
constructor(private readonly checkListItemService: CheckListItemService) {}
@Get()
@ApiOperation({ summary: 'Get all check list items, optionally filtered by task' })
@ApiQuery({ name: 'taskId', required: false, description: 'Filter check list items by task ID' })
@ApiResponse({ status: 200, description: 'List of check list items', type: [TMCheckListItem] })
findAll(@Query('taskId') taskId?: string): Promise<TMCheckListItem[]> {
if (taskId) return this.checkListItemService.findByTask(taskId);
return this.checkListItemService.findAll();
}
@Get(':id')
@ApiOperation({ summary: 'Get a check list item by ID' })
@ApiParam({ name: 'id', description: 'CheckListItem ID' })
@ApiResponse({ status: 200, description: 'CheckListItem found', type: TMCheckListItem })
@ApiResponse({ status: 404, description: 'CheckListItem not found' })
findOne(@Param('id') id: string): Promise<TMCheckListItem> {
return this.checkListItemService.findOne(id);
}
@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' })
@ApiResponse({ status: 404, description: 'CheckListItem not found' })
remove(@Param('id') id: string): Promise<void> {
return this.checkListItemService.remove(id);
}
}
@@ -0,0 +1,58 @@
import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } 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 { TMProject } from '../entities/project.entity';
@ApiTags('Task Manager - Projects')
@Controller('task-manager/projects')
export class ProjectController {
constructor(private readonly projectService: ProjectService) {}
@Get()
@ApiOperation({ summary: 'Get all projects, optionally filtered by workspace' })
@ApiQuery({ name: 'workspaceId', required: false, description: 'Filter projects by workspace ID' })
@ApiResponse({ status: 200, description: 'List of projects', type: [TMProject] })
findAll(@Query('workspaceId') workspaceId?: string): Promise<TMProject[]> {
if (workspaceId) return this.projectService.findByWorkspace(workspaceId);
return this.projectService.findAll();
}
@Get(':id')
@ApiOperation({ summary: 'Get a project by ID' })
@ApiParam({ name: 'id', description: 'Project ID' })
@ApiResponse({ status: 200, description: 'Project found', type: TMProject })
@ApiResponse({ status: 404, description: 'Project not found' })
findOne(@Param('id') id: string): Promise<TMProject> {
return this.projectService.findOne(id);
}
@Post()
@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')
@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')
@ApiOperation({ summary: 'Delete a project' })
@ApiParam({ name: 'id', description: 'Project ID' })
@ApiResponse({ status: 200, description: 'Project deleted' })
@ApiResponse({ status: 404, description: 'Project not found' })
remove(@Param('id') id: string): Promise<TMProject> {
return this.projectService.remove(id);
}
}
@@ -0,0 +1,56 @@
import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from "@nestjs/common";
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } 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";
@ApiTags("Task Manager - Task Phases")
@Controller("task-manager/task-phases")
export class TaskPhaseController {
constructor(private readonly taskPhaseService: TaskPhaseService) {}
@Get()
@ApiOperation({ summary: "Get all task phases, optionally filtered by project" })
@ApiQuery({ name: "projectId", required: false, description: "Filter task phases by project ID" })
@ApiResponse({ status: 200, description: "List of task phases", type: [TMTaskPhase] })
findAll(@Query("projectId") projectId?: string): Promise<TMTaskPhase[]> {
if (projectId) return this.taskPhaseService.findByProject(projectId);
return this.taskPhaseService.findAll();
}
@Get(":id")
@ApiOperation({ summary: "Get a task phase by ID" })
@ApiParam({ name: "id", description: "TaskPhase ID" })
@ApiResponse({ status: 200, description: "TaskPhase found", type: TMTaskPhase })
@ApiResponse({ status: 404, description: "TaskPhase not found" })
findOne(@Param("id") id: string): Promise<TMTaskPhase> {
return this.taskPhaseService.findOne(id);
}
@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,67 @@
import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } 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';
@ApiTags('Task Manager - Tasks')
@Controller('task-manager/tasks')
export class TaskController {
constructor(private readonly taskService: TaskService) {}
@Get()
@ApiOperation({ summary: 'Get all tasks, optionally filtered by phase' })
@ApiQuery({ name: 'taskPhaseId', required: false, description: 'Filter tasks by task phase ID' })
@ApiResponse({ status: 200, description: 'List of tasks', type: [TMTask] })
findAll(@Query('taskPhaseId') taskPhaseId?: string): Promise<TMTask[]> {
if (taskPhaseId) return this.taskService.findByPhase(taskPhaseId);
return this.taskService.findAll();
}
@Get(':id')
@ApiOperation({ summary: 'Get a task by ID' })
@ApiParam({ name: 'id', description: 'Task ID' })
@ApiResponse({ status: 200, description: 'Task found', type: TMTask })
@ApiResponse({ status: 404, description: 'Task not found' })
findOne(@Param('id') id: string): Promise<TMTask> {
return this.taskService.findOne(id);
}
@Post()
@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')
@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')
@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')
@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);
}
}
@@ -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) {}
@@ -1,5 +1,5 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsString, IsOptional, IsNotEmpty, IsUUID, IsDateString, MaxLength } from "class-validator";
import { IsString, IsOptional, IsNotEmpty, IsUUID, IsDateString, IsBoolean, MaxLength } from "class-validator";
export class CreateProjectDto {
@ApiProperty({ description: "Name of the project", example: "Mobile App Redesign" })
@@ -33,8 +33,29 @@ export class CreateProjectDto {
@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[];
}
@@ -1,4 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateProjectDto } from './create-project.dto';
import { PartialType } from "@nestjs/swagger";
import { CreateProjectDto } from "./create-project.dto";
export class UpdateProjectDto extends PartialType(CreateProjectDto) {}
export class UpdateProjectDto extends PartialType(CreateProjectDto) {}
@@ -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) {}
@@ -1,7 +1,8 @@
import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from "typeorm";
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 {
@@ -20,6 +21,12 @@ export class TMProject extends BaseEntity {
@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, {
@@ -36,4 +43,12 @@ export class TMProject extends BaseEntity {
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,53 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { CheckListItemRepository } from "../repositories/check-list-item.repository";
import { TaskRepository } from "../repositories/task.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";
@Injectable()
export class CheckListItemService {
constructor(
private readonly checkListItemRepository: CheckListItemRepository,
private readonly taskRepository: TaskRepository,
) {}
findAll(): Promise<TMCheckListItem[]> {
return this.checkListItemRepository.findAll();
}
findByTask(taskId: string): Promise<TMCheckListItem[]> {
return this.checkListItemRepository.findByTask(taskId);
}
async findOne(id: string): Promise<TMCheckListItem> {
const checkListItem = await this.checkListItemRepository.findOneById(id);
if (!checkListItem) throw new NotFoundException(`CheckListItem #${id} not found`);
return checkListItem;
}
async create(dto: CreateCheckListItemDto): Promise<TMCheckListItem> {
const task = await this.taskRepository.findOneById(dto.taskId);
if (!task) throw new NotFoundException(`Task #${dto.taskId} not found`);
const checkListItem = this.checkListItemRepository.create(dto);
return this.checkListItemRepository.save(checkListItem);
}
async update(id: string, dto: UpdateCheckListItemDto): Promise<TMCheckListItem> {
const checkListItem = await this.findOne(id);
if (dto.taskId) {
const task = await this.taskRepository.findOneById(dto.taskId);
if (!task) throw new NotFoundException(`Task #${dto.taskId} not found`);
}
Object.assign(checkListItem, dto);
return this.checkListItemRepository.save(checkListItem);
}
async remove(id: string): Promise<void> {
await this.findOne(id);
await this.checkListItemRepository.delete(id);
}
}
@@ -0,0 +1,92 @@
import { BadRequestException, 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 { User } from "../../users/entities/user.entity";
import { WorkspaceRepository } from "../repositories/workspace.repository";
@Injectable()
export class ProjectService {
constructor(
private readonly projectRepository: ProjectRepository,
private readonly workspaceRepository: WorkspaceRepository,
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
findAll(): Promise<TMProject[]> {
return this.projectRepository.findAll();
}
findByWorkspace(workspaceId: string): Promise<TMProject[]> {
return this.projectRepository.findByWorkspace(workspaceId);
}
async findOne(id: string): Promise<TMProject> {
const project = await this.projectRepository.findOneById(id);
if (!project) throw new NotFoundException(`Project #${id} not found`);
return project;
}
async create(dto: CreateProjectDto): Promise<TMProject> {
const { userIds, startDate, ...rest } = dto;
const workspace = await this.workspaceRepository.findOneById(dto.workspaceId);
if (!workspace) throw new NotFoundException(`Workspace #${dto.workspaceId} not found`);
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(`Users [${invalidUsers.join(", ")}] are not members of 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.findOne(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.workspaceRepository.findOneById(workspaceId);
if (!workspace) throw new NotFoundException(`Workspace #${workspaceId} not found`);
const workspaceUserIds = workspace.users.map((u) => u.id);
const invalidUsers = userIds.filter((uid) => !workspaceUserIds.includes(uid));
if (invalidUsers.length) {
throw new BadRequestException(`Users [${invalidUsers.join(", ")}] are not members of 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.findOne(id);
await this.projectRepository.delete(id);
return project;
}
}
@@ -0,0 +1,53 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { TaskPhaseRepository } from "../repositories/task-phase.repository";
import { ProjectRepository } from "../repositories/project.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";
@Injectable()
export class TaskPhaseService {
constructor(
private readonly taskPhaseRepository: TaskPhaseRepository,
private readonly projectRepository: ProjectRepository,
) {}
findAll(): Promise<TMTaskPhase[]> {
return this.taskPhaseRepository.findAll();
}
findByProject(projectId: string): Promise<TMTaskPhase[]> {
return this.taskPhaseRepository.findByProject(projectId);
}
async findOne(id: string): Promise<TMTaskPhase> {
const taskPhase = await this.taskPhaseRepository.findOneById(id);
if (!taskPhase) throw new NotFoundException(`TaskPhase #${id} not found`);
return taskPhase;
}
async create(dto: CreateTaskPhaseDto): Promise<TMTaskPhase> {
const project = await this.projectRepository.findOneById(dto.projectId);
if (!project) throw new NotFoundException(`Project #${dto.projectId} not found`);
const taskPhase = this.taskPhaseRepository.create(dto);
return this.taskPhaseRepository.save(taskPhase);
}
async update(id: string, dto: UpdateTaskPhaseDto): Promise<TMTaskPhase> {
const taskPhase = await this.findOne(id);
if (dto.projectId) {
const project = await this.projectRepository.findOneById(dto.projectId);
if (!project) throw new NotFoundException(`Project #${dto.projectId} not found`);
}
Object.assign(taskPhase, dto);
return this.taskPhaseRepository.save(taskPhase);
}
async remove(id: string): Promise<void> {
await this.findOne(id);
await this.taskPhaseRepository.delete(id);
}
}
@@ -0,0 +1,100 @@
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { In, Repository } from 'typeorm';
import { TaskRepository } from '../repositories/task.repository';
import { TaskPhaseRepository } from '../repositories/task-phase.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';
@Injectable()
export class TaskService {
constructor(
private readonly taskRepository: TaskRepository,
private readonly taskPhaseRepository: TaskPhaseRepository,
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
findAll(): Promise<TMTask[]> {
return this.taskRepository.findAll();
}
findByPhase(taskPhaseId: string): Promise<TMTask[]> {
return this.taskRepository.findByPhase(taskPhaseId);
}
async findOne(id: string): Promise<TMTask> {
const task = await this.taskRepository.findOneById(id);
if (!task) throw new NotFoundException(`Task #${id} not found`);
return task;
}
async create(dto: CreateTaskDto): Promise<TMTask> {
const { userIds, startDate, endDate, ...rest } = dto;
const taskPhase = await this.taskPhaseRepository.findOneById(dto.taskPhaseId);
if (!taskPhase) throw new NotFoundException(`TaskPhase #${dto.taskPhaseId} not found`);
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.findOne(id);
const { userIds, startDate, endDate, ...rest } = dto;
if (dto.taskPhaseId) {
const taskPhase = await this.taskPhaseRepository.findOneById(dto.taskPhaseId);
if (!taskPhase) throw new NotFoundException(`TaskPhase #${dto.taskPhaseId} not found`);
}
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.findOne(id);
const newPhase = await this.taskPhaseRepository.findOneById(dto.taskPhaseId);
if (!newPhase) throw new NotFoundException(`TaskPhase #${dto.taskPhaseId} not found`);
if (task.taskPhase.projectId !== newPhase.projectId) {
throw new BadRequestException(
`TaskPhase #${dto.taskPhaseId} does not belong to the same project as the current phase`,
);
}
task.taskPhaseId = dto.taskPhaseId;
task.taskPhase = newPhase;
return this.taskRepository.save(task);
}
async remove(id: string): Promise<void> {
await this.findOne(id);
await this.taskRepository.delete(id);
}
}
@@ -0,0 +1,47 @@
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 {
constructor(
@InjectRepository(TMCheckListItem)
private readonly repository: Repository<TMCheckListItem>,
) {}
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<TMCheckListItem>) {
return this.repository.create(data);
}
save(checkListItem: TMCheckListItem) {
return this.repository.save(checkListItem);
}
update(id: string, data: Partial<TMCheckListItem>) {
return this.repository.update(id, data);
}
delete(id: string) {
return this.repository.delete(id);
}
}
@@ -0,0 +1,48 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { TMProject } from "../entities/project.entity";
@Injectable()
export class ProjectRepository {
constructor(
@InjectRepository(TMProject)
private readonly repository: Repository<TMProject>,
) {}
findAll() {
return this.repository.find({
relations: ["workspace", "taskPhases", "users"],
});
}
findByWorkspace(workspaceId: string) {
return this.repository.find({
where: { workspaceId },
relations: ["taskPhases", "users"],
});
}
findOneById(id: string) {
return this.repository.findOne({
where: { id },
relations: ["workspace", "taskPhases", "users"],
});
}
create(data: Partial<TMProject>) {
return this.repository.create(data);
}
save(project: TMProject) {
return this.repository.save(project);
}
update(id: string, data: Partial<TMProject>) {
return this.repository.update(id, data);
}
delete(id: string) {
return this.repository.delete(id);
}
}
@@ -0,0 +1,50 @@
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 {
constructor(
@InjectRepository(TMTaskPhase)
private readonly repository: Repository<TMTaskPhase>,
) {}
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);
}
}
@@ -0,0 +1,48 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { TMTask } from "../entities/task.entity";
@Injectable()
export class TaskRepository {
constructor(
@InjectRepository(TMTask)
private readonly repository: Repository<TMTask>,
) {}
findAll() {
return this.repository.find({
relations: ["taskPhase", "remarks", "attachments", "checkListItems", "users"],
});
}
findByPhase(taskPhaseId: string) {
return this.repository.find({
where: { taskPhaseId },
relations: ["remarks", "attachments", "checkListItems", "users"],
});
}
findOneById(id: string) {
return this.repository.findOne({
where: { id },
relations: ["taskPhase", "remarks", "attachments", "checkListItems", "users"],
});
}
create(data: Partial<TMTask>) {
return this.repository.create(data);
}
save(task: TMTask) {
return this.repository.save(task);
}
update(id: string, data: Partial<TMTask>) {
return this.repository.update(id, data);
}
delete(id: string) {
return this.repository.delete(id);
}
}
+42 -13
View File
@@ -16,29 +16,58 @@ import { User } from "../users/entities/user.entity";
import { WorkspaceTypeService } from "./providers/workspace-type.service";
import { WorkspaceTypeRepository } from "./repositories/workspace-type.repository";
import { WorkspaceTypeController } from "./controllers/workspace-type.controller";
// import { WorkspaceTypeService } from './providers/workspace-type.service';
// import { WorkspaceService } from './providers/workspace.service';
// import { ProjectService } from './providers/project.service';
// import { TaskPhaseService } from './providers/task-phase.service';
// import { TaskService } from './providers/task.service';
// import { TaskManagerController } from './task-manager.controller';
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";
@Module({
imports: [
TypeOrmModule.forFeature([TMWorkspaceType, TMWorkspace, TMProject, TMTaskPhase, TMTask, TMRemark, TMAttachment, TMCheckListItem, User]),
],
controllers: [WorkspaceController, WorkspaceTypeController],
controllers: [
WorkspaceController,
WorkspaceTypeController,
ProjectController,
TaskPhaseController,
TaskController,
CheckListItemController,
],
providers: [
WorkspaceRepository,
WorkspaceService,
WorkspaceTypeRepository,
WorkspaceTypeService,
// ProjectService,
// TaskPhaseService,
// TaskService,
ProjectRepository,
ProjectService,
TaskPhaseRepository,
TaskPhaseService,
TaskRepository,
TaskService,
CheckListItemRepository,
CheckListItemService,
],
exports: [
WorkspaceRepository,
WorkspaceService,
WorkspaceTypeRepository,
WorkspaceTypeService,
ProjectRepository,
ProjectService,
TaskPhaseRepository,
TaskPhaseService,
TaskRepository,
TaskService,
CheckListItemRepository,
CheckListItemService,
],
exports: [WorkspaceService, WorkspaceRepository, WorkspaceTypeService, WorkspaceTypeRepository],
})
export class TaskManagerModule {}
@@ -34,6 +34,7 @@ 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 {
@@ -185,4 +186,7 @@ export class User extends BaseEntity {
@ManyToMany(() => TMTask, (task) => task.users)
tasks: TMTask[];
@ManyToMany(() => TMProject, (project) => project.users)
projects: TMProject[];
}