feat: added project section of TM with some refactors
This commit is contained in:
@@ -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,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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty } from "@nestjs/swagger";
|
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 {
|
export class CreateProjectDto {
|
||||||
@ApiProperty({ description: "Name of the project", example: "Mobile App Redesign" })
|
@ApiProperty({ description: "Name of the project", example: "Mobile App Redesign" })
|
||||||
@@ -33,8 +33,29 @@ export class CreateProjectDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
backgroundPicture?: string;
|
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" })
|
@ApiProperty({ description: "ID of the workspace this project belongs to", example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" })
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
workspaceId: string;
|
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 { PartialType } from "@nestjs/swagger";
|
||||||
import { CreateProjectDto } from './create-project.dto';
|
import { CreateProjectDto } from "./create-project.dto";
|
||||||
|
|
||||||
export class UpdateProjectDto extends PartialType(CreateProjectDto) {}
|
export class UpdateProjectDto extends PartialType(CreateProjectDto) {}
|
||||||
|
|||||||
@@ -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 { BaseEntity } from "../../../common/entities/base.entity";
|
||||||
import { TMWorkspace } from "./workspace.entity";
|
import { TMWorkspace } from "./workspace.entity";
|
||||||
import { TMTaskPhase } from "./task-phase.entity";
|
import { TMTaskPhase } from "./task-phase.entity";
|
||||||
|
import { User } from "../../users/entities/user.entity";
|
||||||
|
|
||||||
@Entity("tm_projects")
|
@Entity("tm_projects")
|
||||||
export class TMProject extends BaseEntity {
|
export class TMProject extends BaseEntity {
|
||||||
@@ -20,6 +21,12 @@ export class TMProject extends BaseEntity {
|
|||||||
@Column({ type: "text", nullable: true })
|
@Column({ type: "text", nullable: true })
|
||||||
backgroundPicture: string;
|
backgroundPicture: string;
|
||||||
|
|
||||||
|
@Column({ type: "varchar", length: 20, nullable: true })
|
||||||
|
backgroundColor: string;
|
||||||
|
|
||||||
|
@Column({ type: "boolean", default: true })
|
||||||
|
isActive: boolean;
|
||||||
|
|
||||||
// --- Relations ---
|
// --- Relations ---
|
||||||
|
|
||||||
@ManyToOne(() => TMWorkspace, (workspace) => workspace.projects, {
|
@ManyToOne(() => TMWorkspace, (workspace) => workspace.projects, {
|
||||||
@@ -36,4 +43,12 @@ export class TMProject extends BaseEntity {
|
|||||||
cascade: ["insert", "update"],
|
cascade: ["insert", "update"],
|
||||||
})
|
})
|
||||||
taskPhases: TMTaskPhase[];
|
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,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,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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,29 +16,25 @@ import { User } from "../users/entities/user.entity";
|
|||||||
import { WorkspaceTypeService } from "./providers/workspace-type.service";
|
import { WorkspaceTypeService } from "./providers/workspace-type.service";
|
||||||
import { WorkspaceTypeRepository } from "./repositories/workspace-type.repository";
|
import { WorkspaceTypeRepository } from "./repositories/workspace-type.repository";
|
||||||
import { WorkspaceTypeController } from "./controllers/workspace-type.controller";
|
import { WorkspaceTypeController } from "./controllers/workspace-type.controller";
|
||||||
|
import { ProjectController } from "./controllers/project.controller";
|
||||||
// import { WorkspaceTypeService } from './providers/workspace-type.service';
|
import { ProjectRepository } from "./repositories/project.repository";
|
||||||
// import { WorkspaceService } from './providers/workspace.service';
|
import { ProjectService } from "./providers/project.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';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([TMWorkspaceType, TMWorkspace, TMProject, TMTaskPhase, TMTask, TMRemark, TMAttachment, TMCheckListItem, User]),
|
TypeOrmModule.forFeature([TMWorkspaceType, TMWorkspace, TMProject, TMTaskPhase, TMTask, TMRemark, TMAttachment, TMCheckListItem, User]),
|
||||||
],
|
],
|
||||||
controllers: [WorkspaceController, WorkspaceTypeController],
|
controllers: [WorkspaceController, WorkspaceTypeController, ProjectController],
|
||||||
providers: [
|
providers: [
|
||||||
WorkspaceRepository,
|
WorkspaceRepository,
|
||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
WorkspaceTypeRepository,
|
WorkspaceTypeRepository,
|
||||||
WorkspaceTypeService,
|
WorkspaceTypeService,
|
||||||
// ProjectService,
|
ProjectRepository,
|
||||||
|
ProjectService,
|
||||||
// TaskPhaseService,
|
// TaskPhaseService,
|
||||||
// TaskService,
|
// TaskService,
|
||||||
],
|
],
|
||||||
exports: [WorkspaceService, WorkspaceRepository, WorkspaceTypeService, WorkspaceTypeRepository],
|
exports: [WorkspaceService, WorkspaceRepository, WorkspaceTypeService, WorkspaceTypeRepository, ProjectService, ProjectRepository],
|
||||||
})
|
})
|
||||||
export class TaskManagerModule {}
|
export class TaskManagerModule {}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { Reseller } from "../../reseller/entities/reseller.entity";
|
|||||||
import { UserBankAccount } from "../../reseller/entities/user-bank-account.entity";
|
import { UserBankAccount } from "../../reseller/entities/user-bank-account.entity";
|
||||||
import { TMWorkspace } from "../../task-manager/entities/workspace.entity";
|
import { TMWorkspace } from "../../task-manager/entities/workspace.entity";
|
||||||
import { TMTask } from "../../task-manager/entities/task.entity";
|
import { TMTask } from "../../task-manager/entities/task.entity";
|
||||||
|
import { TMProject } from "../../task-manager/entities/project.entity";
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class User extends BaseEntity {
|
export class User extends BaseEntity {
|
||||||
@@ -185,4 +186,7 @@ export class User extends BaseEntity {
|
|||||||
|
|
||||||
@ManyToMany(() => TMTask, (task) => task.users)
|
@ManyToMany(() => TMTask, (task) => task.users)
|
||||||
tasks: TMTask[];
|
tasks: TMTask[];
|
||||||
|
|
||||||
|
@ManyToMany(() => TMProject, (project) => project.users)
|
||||||
|
projects: TMProject[];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user