diff --git a/database/migrations/1782807558167-taskManager.ts b/database/migrations/1782807558167-taskManager.ts new file mode 100644 index 0000000..2aed74a --- /dev/null +++ b/database/migrations/1782807558167-taskManager.ts @@ -0,0 +1,66 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class TaskManager1782807558167 implements MigrationInterface { + name = 'TaskManager1782807558167' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "tm_workspace_types" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "name" character varying(100) NOT NULL, "description" text, "order" integer NOT NULL DEFAULT '0', CONSTRAINT "UQ_f6eb300d6dfa0f7dbfdf05e2592" UNIQUE ("name"), CONSTRAINT "PK_b3437ae89cc910f1f0ad3de0fc5" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "tm_remarks" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "title" character varying(150) NOT NULL, "color" character varying(20), "taskId" uuid NOT NULL, CONSTRAINT "PK_99be294f0b23c3823d9ee784ebf" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TYPE "public"."tm_attachments_type_enum" AS ENUM('file', 'link', 'image', 'video')`); + await queryRunner.query(`CREATE TABLE "tm_attachments" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "title" character varying(150) NOT NULL, "type" "public"."tm_attachments_type_enum" NOT NULL, "file" text NOT NULL, "taskId" uuid NOT NULL, CONSTRAINT "PK_85eb42d47a75319a1ff074ff39c" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "tm_check_list_items" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "title" character varying(150) NOT NULL, "isDone" boolean NOT NULL DEFAULT false, "taskId" uuid NOT NULL, CONSTRAINT "PK_0bb2fee42ccb8a7f4c5c054ea71" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "tm_tasks" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "title" character varying(150) NOT NULL, "description" text, "color" character varying(20), "startDate" TIMESTAMP WITH TIME ZONE, "endDate" TIMESTAMP WITH TIME ZONE, "taskPhaseId" uuid NOT NULL, CONSTRAINT "PK_f975c1e9cc0cfc054482cb36028" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "tm_task_phases" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "name" character varying(100) NOT NULL, "order" integer NOT NULL DEFAULT '0', "color" character varying(20), "projectId" uuid NOT NULL, CONSTRAINT "PK_045ad1778b77407b8e4017cc9b2" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "tm_projects" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "name" character varying(150) NOT NULL, "ownerName" character varying(150), "description" text, "startDate" TIMESTAMP WITH TIME ZONE, "workspaceId" uuid NOT NULL, CONSTRAINT "PK_0ceacba52c6bb5cec6481b910e4" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "tm_workspaces" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "name" character varying(100) NOT NULL, "description" text, "color" character varying(20) NOT NULL, "isActive" boolean NOT NULL DEFAULT true, "workspaceTypeId" uuid NOT NULL, CONSTRAINT "PK_fb4be955b5b4b11aacc5169e9d5" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "tm_task_users" ("taskId" uuid NOT NULL, "userId" uuid NOT NULL, CONSTRAINT "PK_bb8f94f4ea4715ad190bc4fb23c" PRIMARY KEY ("taskId", "userId"))`); + await queryRunner.query(`CREATE INDEX "IDX_739cfd1807f78bb2ebda02e7bc" ON "tm_task_users" ("taskId") `); + await queryRunner.query(`CREATE INDEX "IDX_3fe44ce1acd8a620affb55a750" ON "tm_task_users" ("userId") `); + await queryRunner.query(`CREATE TABLE "tm_workspace_users" ("workspaceId" uuid NOT NULL, "userId" uuid NOT NULL, CONSTRAINT "PK_7f8b37c58e5267bc16cf577fcbd" PRIMARY KEY ("workspaceId", "userId"))`); + await queryRunner.query(`CREATE INDEX "IDX_ca9b0a16dfec7c4753ea4910f0" ON "tm_workspace_users" ("workspaceId") `); + await queryRunner.query(`CREATE INDEX "IDX_1c9d677417216377d55a060011" ON "tm_workspace_users" ("userId") `); + await queryRunner.query(`ALTER TABLE "reward_withdraw_request" ADD CONSTRAINT "UQ_79526c10ecb097f553e0120a36e" UNIQUE ("transactionId")`); + await queryRunner.query(`ALTER TABLE "tm_remarks" ADD CONSTRAINT "FK_bc03cabb99c42245436fcbb4e6a" FOREIGN KEY ("taskId") REFERENCES "tm_tasks"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "tm_attachments" ADD CONSTRAINT "FK_1e9ab0f9ec5b898abea95d21fb2" FOREIGN KEY ("taskId") REFERENCES "tm_tasks"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "tm_check_list_items" ADD CONSTRAINT "FK_14730b0971bcdc5547889392606" FOREIGN KEY ("taskId") REFERENCES "tm_tasks"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "tm_tasks" ADD CONSTRAINT "FK_f7462cec045e564d5fd54d17177" FOREIGN KEY ("taskPhaseId") REFERENCES "tm_task_phases"("id") ON DELETE RESTRICT ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "tm_task_phases" ADD CONSTRAINT "FK_884b8c2fe724031aa61c8181f0b" FOREIGN KEY ("projectId") REFERENCES "tm_projects"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "tm_projects" ADD CONSTRAINT "FK_c3d997618910273be84d58faa8c" FOREIGN KEY ("workspaceId") REFERENCES "tm_workspaces"("id") ON DELETE RESTRICT ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "tm_workspaces" ADD CONSTRAINT "FK_253e36f065cfc1eca58abaefb22" FOREIGN KEY ("workspaceTypeId") REFERENCES "tm_workspace_types"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "tm_task_users" ADD CONSTRAINT "FK_739cfd1807f78bb2ebda02e7bce" FOREIGN KEY ("taskId") REFERENCES "tm_tasks"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "tm_task_users" ADD CONSTRAINT "FK_3fe44ce1acd8a620affb55a7500" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "tm_workspace_users" ADD CONSTRAINT "FK_ca9b0a16dfec7c4753ea4910f00" FOREIGN KEY ("workspaceId") REFERENCES "tm_workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "tm_workspace_users" ADD CONSTRAINT "FK_1c9d677417216377d55a060011d" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tm_workspace_users" DROP CONSTRAINT "FK_1c9d677417216377d55a060011d"`); + await queryRunner.query(`ALTER TABLE "tm_workspace_users" DROP CONSTRAINT "FK_ca9b0a16dfec7c4753ea4910f00"`); + await queryRunner.query(`ALTER TABLE "tm_task_users" DROP CONSTRAINT "FK_3fe44ce1acd8a620affb55a7500"`); + await queryRunner.query(`ALTER TABLE "tm_task_users" DROP CONSTRAINT "FK_739cfd1807f78bb2ebda02e7bce"`); + await queryRunner.query(`ALTER TABLE "tm_workspaces" DROP CONSTRAINT "FK_253e36f065cfc1eca58abaefb22"`); + await queryRunner.query(`ALTER TABLE "tm_projects" DROP CONSTRAINT "FK_c3d997618910273be84d58faa8c"`); + await queryRunner.query(`ALTER TABLE "tm_task_phases" DROP CONSTRAINT "FK_884b8c2fe724031aa61c8181f0b"`); + await queryRunner.query(`ALTER TABLE "tm_tasks" DROP CONSTRAINT "FK_f7462cec045e564d5fd54d17177"`); + await queryRunner.query(`ALTER TABLE "tm_check_list_items" DROP CONSTRAINT "FK_14730b0971bcdc5547889392606"`); + await queryRunner.query(`ALTER TABLE "tm_attachments" DROP CONSTRAINT "FK_1e9ab0f9ec5b898abea95d21fb2"`); + await queryRunner.query(`ALTER TABLE "tm_remarks" DROP CONSTRAINT "FK_bc03cabb99c42245436fcbb4e6a"`); + await queryRunner.query(`ALTER TABLE "reward_withdraw_request" DROP CONSTRAINT "UQ_79526c10ecb097f553e0120a36e"`); + await queryRunner.query(`DROP INDEX "public"."IDX_1c9d677417216377d55a060011"`); + await queryRunner.query(`DROP INDEX "public"."IDX_ca9b0a16dfec7c4753ea4910f0"`); + await queryRunner.query(`DROP TABLE "tm_workspace_users"`); + await queryRunner.query(`DROP INDEX "public"."IDX_3fe44ce1acd8a620affb55a750"`); + await queryRunner.query(`DROP INDEX "public"."IDX_739cfd1807f78bb2ebda02e7bc"`); + await queryRunner.query(`DROP TABLE "tm_task_users"`); + await queryRunner.query(`DROP TABLE "tm_workspaces"`); + await queryRunner.query(`DROP TABLE "tm_projects"`); + await queryRunner.query(`DROP TABLE "tm_task_phases"`); + await queryRunner.query(`DROP TABLE "tm_tasks"`); + await queryRunner.query(`DROP TABLE "tm_check_list_items"`); + await queryRunner.query(`DROP TABLE "tm_attachments"`); + await queryRunner.query(`DROP TYPE "public"."tm_attachments_type_enum"`); + await queryRunner.query(`DROP TABLE "tm_remarks"`); + await queryRunner.query(`DROP TABLE "tm_workspace_types"`); + } + +} diff --git a/database/migrations/1782889623829-taskmanager-project.ts b/database/migrations/1782889623829-taskmanager-project.ts new file mode 100644 index 0000000..95869e2 --- /dev/null +++ b/database/migrations/1782889623829-taskmanager-project.ts @@ -0,0 +1,28 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class TaskmanagerProject1782889623829 implements MigrationInterface { + name = 'TaskmanagerProject1782889623829' + + public async up(queryRunner: QueryRunner): Promise { + 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 { + 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"`); + } + +} diff --git a/database/migrations/1783150480192-taskmanagerRemoveWorkspaceType.ts b/database/migrations/1783150480192-taskmanagerRemoveWorkspaceType.ts new file mode 100644 index 0000000..5627d12 --- /dev/null +++ b/database/migrations/1783150480192-taskmanagerRemoveWorkspaceType.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Taskmanager1783150480192 implements MigrationInterface { + name = 'Taskmanager1783150480192' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tm_workspaces" DROP CONSTRAINT "FK_253e36f065cfc1eca58abaefb22"`); + await queryRunner.query(`ALTER TABLE "tm_workspaces" DROP COLUMN "workspaceTypeId"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tm_workspaces" ADD "workspaceTypeId" uuid NOT NULL`); + await queryRunner.query(`ALTER TABLE "tm_workspaces" ADD CONSTRAINT "FK_253e36f065cfc1eca58abaefb22" FOREIGN KEY ("workspaceTypeId") REFERENCES "tm_workspace_types"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + +} diff --git a/database/migrations/1783244669371-taskmanagerMakeWorkspaceNameUnique.ts b/database/migrations/1783244669371-taskmanagerMakeWorkspaceNameUnique.ts new file mode 100644 index 0000000..6cce1f1 --- /dev/null +++ b/database/migrations/1783244669371-taskmanagerMakeWorkspaceNameUnique.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Taskmanager31783244669371 implements MigrationInterface { + name = 'Taskmanager31783244669371' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tm_workspaces" ADD CONSTRAINT "UQ_6496168de3e6c5f006d494ebac5" UNIQUE ("name")`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tm_workspaces" DROP CONSTRAINT "UQ_6496168de3e6c5f006d494ebac5"`); + } + +} diff --git a/database/migrations/1783324039378-taskmanagerAddingNewEnumValueForPermissionEnumType.ts b/database/migrations/1783324039378-taskmanagerAddingNewEnumValueForPermissionEnumType.ts new file mode 100644 index 0000000..4f0e81a --- /dev/null +++ b/database/migrations/1783324039378-taskmanagerAddingNewEnumValueForPermissionEnumType.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class TaskmanagerAddingNewEnumValueForPermissionEnumType1783324039378 implements MigrationInterface { + name = 'TaskmanagerAddingNewEnumValueForPermissionEnumType1783324039378' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TYPE "public"."permission_name_enum" RENAME TO "permission_name_enum_old"`); + await queryRunner.query(`CREATE TYPE "public"."permission_name_enum" AS ENUM('services', 'customers', 'agents', 'developers', 'invoices', 'transactions', 'discounts', 'admins', 'tickets', 'criticisms', 'contacts_us', 'advertisements', 'announcements', 'blogs', 'learnings', 'logs', 'settings', 'bank_accounts', 'payments', 'manage_sso_clients', 'support_plan', 'dmenu', 'dkala', 'dpage', 'dmail', 'reseller', 'workspace', 'project')`); + await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum" USING "name"::"text"::"public"."permission_name_enum"`); + await queryRunner.query(`DROP TYPE "public"."permission_name_enum_old"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TYPE "public"."permission_name_enum_old" AS ENUM('services', 'customers', 'agents', 'developers', 'invoices', 'transactions', 'discounts', 'admins', 'tickets', 'criticisms', 'contacts_us', 'advertisements', 'announcements', 'blogs', 'learnings', 'logs', 'settings', 'bank_accounts', 'payments', 'manage_sso_clients', 'support_plan', 'dmenu', 'dkala', 'dpage', 'dmail', 'reseller', 'workspace')`); + await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum_old" USING "name"::"text"::"public"."permission_name_enum_old"`); + await queryRunner.query(`DROP TYPE "public"."permission_name_enum"`); + await queryRunner.query(`ALTER TYPE "public"."permission_name_enum_old" RENAME TO "permission_name_enum"`); + } + +} diff --git a/database/migrations/1783335279945-TMAddingTaskPhasePermissionToPermissionEnumType.ts b/database/migrations/1783335279945-TMAddingTaskPhasePermissionToPermissionEnumType.ts new file mode 100644 index 0000000..c1191db --- /dev/null +++ b/database/migrations/1783335279945-TMAddingTaskPhasePermissionToPermissionEnumType.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class TMAddingTaskPhasePermissionToPermissionEnumType1783335279945 implements MigrationInterface { + name = 'TMAddingTaskPhasePermissionToPermissionEnumType1783335279945' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TYPE "public"."permission_name_enum" RENAME TO "permission_name_enum_old"`); + await queryRunner.query(`CREATE TYPE "public"."permission_name_enum" AS ENUM('services', 'customers', 'agents', 'developers', 'invoices', 'transactions', 'discounts', 'admins', 'tickets', 'criticisms', 'contacts_us', 'advertisements', 'announcements', 'blogs', 'learnings', 'logs', 'settings', 'bank_accounts', 'payments', 'manage_sso_clients', 'support_plan', 'dmenu', 'dkala', 'dpage', 'dmail', 'reseller', 'workspace', 'project', 'task_phase')`); + await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum" USING "name"::"text"::"public"."permission_name_enum"`); + await queryRunner.query(`DROP TYPE "public"."permission_name_enum_old"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TYPE "public"."permission_name_enum_old" AS ENUM('services', 'customers', 'agents', 'developers', 'invoices', 'transactions', 'discounts', 'admins', 'tickets', 'criticisms', 'contacts_us', 'advertisements', 'announcements', 'blogs', 'learnings', 'logs', 'settings', 'bank_accounts', 'payments', 'manage_sso_clients', 'support_plan', 'dmenu', 'dkala', 'dpage', 'dmail', 'reseller', 'workspace', 'project')`); + await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum_old" USING "name"::"text"::"public"."permission_name_enum_old"`); + await queryRunner.query(`DROP TYPE "public"."permission_name_enum"`); + await queryRunner.query(`ALTER TYPE "public"."permission_name_enum_old" RENAME TO "permission_name_enum"`); + } + +} diff --git a/database/migrations/1783428145772-TMaddingSomeNewPermissionsToTheEnum.ts b/database/migrations/1783428145772-TMaddingSomeNewPermissionsToTheEnum.ts new file mode 100644 index 0000000..f0ee3a6 --- /dev/null +++ b/database/migrations/1783428145772-TMaddingSomeNewPermissionsToTheEnum.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class TMaddingSomeNewPermissionsToTheEnum1783428145772 implements MigrationInterface { + name = 'TMaddingSomeNewPermissionsToTheEnum1783428145772' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TYPE "public"."permission_name_enum" RENAME TO "permission_name_enum_old"`); + await queryRunner.query(`CREATE TYPE "public"."permission_name_enum" AS ENUM('services', 'customers', 'agents', 'developers', 'invoices', 'transactions', 'discounts', 'admins', 'tickets', 'criticisms', 'contacts_us', 'advertisements', 'announcements', 'blogs', 'learnings', 'logs', 'settings', 'bank_accounts', 'payments', 'manage_sso_clients', 'support_plan', 'dmenu', 'dkala', 'dpage', 'dmail', 'reseller', 'workspace', 'view_all_workspaces', 'project', 'view_all_projects', 'task', 'task_phase', 'remark')`); + await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum" USING "name"::"text"::"public"."permission_name_enum"`); + await queryRunner.query(`DROP TYPE "public"."permission_name_enum_old"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TYPE "public"."permission_name_enum_old" AS ENUM('services', 'customers', 'agents', 'developers', 'invoices', 'transactions', 'discounts', 'admins', 'tickets', 'criticisms', 'contacts_us', 'advertisements', 'announcements', 'blogs', 'learnings', 'logs', 'settings', 'bank_accounts', 'payments', 'manage_sso_clients', 'support_plan', 'dmenu', 'dkala', 'dpage', 'dmail', 'reseller', 'workspace', 'project', 'task_phase')`); + await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum_old" USING "name"::"text"::"public"."permission_name_enum_old"`); + await queryRunner.query(`DROP TYPE "public"."permission_name_enum"`); + await queryRunner.query(`ALTER TYPE "public"."permission_name_enum_old" RENAME TO "permission_name_enum"`); + } + +} diff --git a/database/seeder.runner.ts b/database/seeder.runner.ts index 16446f6..20d9d81 100755 --- a/database/seeder.runner.ts +++ b/database/seeder.runner.ts @@ -5,9 +5,9 @@ import { connectionSource } from "./connection"; // import { seedCityAndProvince } from "./seeders/iran-city.seeder"; // import { seedNotifSettings } from "./seeders/notif-setting.seeder"; // import { seedPaymentGateways } from "./seeders/payment-gateway.seeder"; -// import { seedPermissionsAndRoles } from "./seeders/role.seeder"; -import { seedBlogCommentReplies } from "./seeders/blog-comment-reply.seeder"; -import { seedBlogComments } from "./seeders/blog-comment.seeder"; +import { seedPermissionsAndRoles } from "./seeders/role.seeder"; +// import { seedBlogCommentReplies } from "./seeders/blog-comment-reply.seeder"; +// import { seedBlogComments } from "./seeders/blog-comment.seeder"; const logger = new Logger("Seeder"); @@ -15,14 +15,14 @@ export const runSeeder = async () => { await connectionSource.initialize(); logger.log("start seeding database"); // - // await seedPermissionsAndRoles(connectionSource, logger); + await seedPermissionsAndRoles(connectionSource, logger); // await seedAdmin(connectionSource, logger); // await seedNotifSettings(connectionSource, logger); // await seedPaymentGateways(connectionSource, logger); // await seedCityAndProvince(connectionSource, logger); - await seedBlogComments(connectionSource, logger); - await seedBlogCommentReplies(connectionSource, logger); + // await seedBlogComments(connectionSource, logger); + // await seedBlogCommentReplies(connectionSource, logger); logger.log("seeding completed"); process.exit(0); diff --git a/database/seeders/role.seeder.ts b/database/seeders/role.seeder.ts index f731d12..747354b 100755 --- a/database/seeders/role.seeder.ts +++ b/database/seeders/role.seeder.ts @@ -1,8 +1,8 @@ import { Logger } from "@nestjs/common"; import { DataSource } from "typeorm"; -import { Permission } from "../../src/modules/users/entities/permission.entity"; -import { Role } from "../../src/modules/users/entities/role.entity"; +// import { Permission } from "../../src/modules/users/entities/permission.entity"; +// import { Role } from "../../src/modules/users/entities/role.entity"; import { PermissionEnum } from "../../src/modules/users/enums/permission.enum"; import { RoleEnum } from "../../src/modules/users/enums/role.enum"; @@ -23,8 +23,8 @@ const rolesWithPermissions: { name: RoleEnum; permissions: PermissionEnum[] }[] export const seedPermissionsAndRoles = async (dataSource: DataSource, logger: Logger) => { try { - const permissionRepo = dataSource.getRepository(Permission); - const roleRepo = dataSource.getRepository(Role); + const permissionRepo = dataSource.getRepository('Permission'); + const roleRepo = dataSource.getRepository('Role'); // Seed Permissions logger.log("Seeding Permissions..."); diff --git a/package-lock.json b/package-lock.json index b5dab37..1b1b3b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1191,7 +1191,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2695,7 +2694,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@fastify/accept-negotiator": "^2.0.0", "@fastify/send": "^4.0.0", @@ -4461,7 +4459,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.8.tgz", "integrity": "sha512-bbsOqwld/GdBfiRNc4nnjyWWENDEicq4SH+R5AuYatvf++vf1x5JIsHB1i1KtfZMD3eRte0D4K9WXuAYil6XAg==", "license": "MIT", - "peer": true, "dependencies": { "file-type": "21.0.0", "iterare": "1.2.1", @@ -4521,7 +4518,6 @@ "integrity": "sha512-7riWfmTmMhCJHZ5ZiaG+crj4t85IPCq/wLRuOUSigBYyFT2JZj0lVHtAdf4Davp9ouNI8GINBDt9h9b5Gz9nTw==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", @@ -4605,7 +4601,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-fastify/-/platform-fastify-11.1.8.tgz", "integrity": "sha512-4XiiTiTkF9UbVDAuHDyVIzgr43L2sI3vLs9A52ov2lrOJcqyKwTYL/NiCQd4dtUQm1L6M0jOrJhzpYXobX5uMw==", "license": "MIT", - "peer": true, "dependencies": { "@fastify/cors": "11.1.0", "@fastify/formbody": "8.0.2", @@ -4808,7 +4803,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-6.4.0.tgz", "integrity": "sha512-osL67i0PUuwU5nqSuJjtUJZMkxAnYB4VldgYUMGzvYRJDCqGRFMWbsbzm/CkUtPLRL30I8T74Xgt/OQxnYokiA==", "license": "MIT", - "peer": true, "peerDependencies": { "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", "@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", @@ -4916,6 +4910,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -5937,7 +5932,6 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -6107,7 +6101,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -6299,7 +6292,6 @@ "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.4", "@typescript-eslint/types": "8.46.4", @@ -7007,7 +6999,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "devOptional": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7056,7 +7047,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -7483,7 +7473,6 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "license": "MIT", - "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -7736,7 +7725,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/bl": { "version": "4.1.0", @@ -7845,7 +7835,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -7933,7 +7922,6 @@ "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.63.0.tgz", "integrity": "sha512-HT1iM3Jt4bZeg3Ru/MxrOy2iIItxcl1Pz5Ync1Vrot70jBpVguMxFEiSaDU57BwYwR4iwnObDnzct2lirKkX5A==", "license": "MIT", - "peer": true, "dependencies": { "cron-parser": "^4.9.0", "ioredis": "^5.4.1", @@ -7962,7 +7950,6 @@ "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-6.4.3.tgz", "integrity": "sha512-VV5eq/QQ5rIVix7/aICO4JyvSeEv9eIQuKL5iFwgM2BrcYoE0A/D1mNsAHJAsB0WEbNdBlKkn6Tjz6fKzh/cKQ==", "license": "MIT", - "peer": true, "dependencies": { "keyv": "^5.3.3" } @@ -8152,7 +8139,6 @@ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -8200,15 +8186,13 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/class-validator": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz", "integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==", "license": "MIT", - "peer": true, "dependencies": { "@types/validator": "^13.11.8", "libphonenumber-js": "^1.11.1", @@ -8688,7 +8672,6 @@ "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -9735,7 +9718,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9796,7 +9778,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -9898,7 +9879,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -11825,7 +11805,6 @@ "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.2.tgz", "integrity": "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==", "license": "MIT", - "peer": true, "dependencies": { "@ioredis/commands": "1.4.0", "cluster-key-slot": "^1.1.0", @@ -12587,7 +12566,6 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -14131,7 +14109,6 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.3.tgz", "integrity": "sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==", "license": "MIT", - "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -16010,7 +15987,6 @@ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", "license": "MIT-0", - "peer": true, "engines": { "node": ">=6.0.0" } @@ -16720,7 +16696,8 @@ "node_modules/pause": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", - "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==", + "peer": true }, "node_modules/peberminta": { "version": "0.9.0", @@ -16748,7 +16725,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -17066,7 +17042,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -17659,8 +17634,7 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", @@ -18030,7 +18004,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -18207,7 +18180,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -19155,6 +19127,7 @@ "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", "license": "MIT", + "peer": true, "dependencies": { "bintrees": "1.0.2" } @@ -19164,7 +19137,6 @@ "resolved": "https://registry.npmjs.org/telegraf/-/telegraf-4.16.3.tgz", "integrity": "sha512-yjEu2NwkHlXu0OARWoNhJlIjX09dRktiMQFsM678BAH/PEPVwctzL67+tvXqLCRQQvm3SDtki2saGO9hLlz68w==", "license": "MIT", - "peer": true, "dependencies": { "@telegraf/types": "^7.1.0", "abort-controller": "^3.0.0", @@ -19765,7 +19737,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -20021,7 +19992,6 @@ "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.27.tgz", "integrity": "sha512-pNV1bn+1n8qEe8tUNsNdD8ejuPcMAg47u2lUGnbsajiNUr3p2Js1XLKQjBMH0yMRMDfdX8T+fIRejFmIwy9x4A==", "license": "MIT", - "peer": true, "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^3.17.0", @@ -20247,7 +20217,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -20499,6 +20468,7 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.4.0" } @@ -20814,6 +20784,7 @@ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -20832,6 +20803,7 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -20846,6 +20818,7 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=4.0" } @@ -20856,6 +20829,7 @@ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.8.x" } @@ -20866,6 +20840,7 @@ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", diff --git a/src/common/DTO/cursor-pagination.dto.ts b/src/common/DTO/cursor-pagination.dto.ts new file mode 100644 index 0000000..2b2faf4 --- /dev/null +++ b/src/common/DTO/cursor-pagination.dto.ts @@ -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; +} diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 1b28777..a8cd15e 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -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 = "ضمیمه پیدا نشد!" +} \ No newline at end of file diff --git a/src/common/helpers/cursor-paginated.helper.ts b/src/common/helpers/cursor-paginated.helper.ts new file mode 100644 index 0000000..ec0a8b3 --- /dev/null +++ b/src/common/helpers/cursor-paginated.helper.ts @@ -0,0 +1,25 @@ +import { CursorPaginatedResult } from "../interfaces/cursor-paginated-result.interface"; + +export function buildCursorPageFormat( + data: T[], + limit: number, + previousCursor?: Date, +): CursorPaginatedResult { + 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, + }, + }; +} diff --git a/src/common/interfaces/cursor-paginated-result.interface.ts b/src/common/interfaces/cursor-paginated-result.interface.ts new file mode 100644 index 0000000..6605514 --- /dev/null +++ b/src/common/interfaces/cursor-paginated-result.interface.ts @@ -0,0 +1,9 @@ +export interface CursorPaginatedResult { + data: T[]; + meta: { + nextCursor: string | null; + prevCursor: string | null; + hasMore: boolean; + limit: number; + }; +} diff --git a/src/modules/task-manager/controllers/attachment.controller.ts b/src/modules/task-manager/controllers/attachment.controller.ts new file mode 100644 index 0000000..ec5f554 --- /dev/null +++ b/src/modules/task-manager/controllers/attachment.controller.ts @@ -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 { + 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 { + 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 { + return this.attachmentService.remove(id); + } +} \ No newline at end of file diff --git a/src/modules/task-manager/controllers/check-list-item.controller.ts b/src/modules/task-manager/controllers/check-list-item.controller.ts new file mode 100644 index 0000000..8dd0a39 --- /dev/null +++ b/src/modules/task-manager/controllers/check-list-item.controller.ts @@ -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 { + 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 { + 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 { + 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); + } +} diff --git a/src/modules/task-manager/controllers/project.controller.ts b/src/modules/task-manager/controllers/project.controller.ts new file mode 100644 index 0000000..d63207b --- /dev/null +++ b/src/modules/task-manager/controllers/project.controller.ts @@ -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 { + 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 { + 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 { + 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 { + return this.projectService.getProjectDetail(paramDto.id, user.id, user.permissions); + } +} diff --git a/src/modules/task-manager/controllers/remark.controller.ts b/src/modules/task-manager/controllers/remark.controller.ts new file mode 100644 index 0000000..66d539d --- /dev/null +++ b/src/modules/task-manager/controllers/remark.controller.ts @@ -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 { + 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 { + 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 { + return this.remarkService.remove(id); + } +} diff --git a/src/modules/task-manager/controllers/task-phase.controller.ts b/src/modules/task-manager/controllers/task-phase.controller.ts new file mode 100644 index 0000000..97ff1ac --- /dev/null +++ b/src/modules/task-manager/controllers/task-phase.controller.ts @@ -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 { + 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 { + 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 { + return this.taskPhaseService.remove(id); + } +} diff --git a/src/modules/task-manager/controllers/task.controller.ts b/src/modules/task-manager/controllers/task.controller.ts new file mode 100644 index 0000000..e4253b4 --- /dev/null +++ b/src/modules/task-manager/controllers/task.controller.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + return this.taskService.getTaskDetail(user.id, user.permissions, paramDto.id); + } +} \ No newline at end of file diff --git a/src/modules/task-manager/controllers/workspace.controller.ts b/src/modules/task-manager/controllers/workspace.controller.ts new file mode 100644 index 0000000..8f53df3 --- /dev/null +++ b/src/modules/task-manager/controllers/workspace.controller.ts @@ -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 { + 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 { + 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 { + 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); + } +} \ No newline at end of file diff --git a/src/modules/task-manager/dto/attachment/create-attachment.dto.ts b/src/modules/task-manager/dto/attachment/create-attachment.dto.ts new file mode 100644 index 0000000..dcca8d4 --- /dev/null +++ b/src/modules/task-manager/dto/attachment/create-attachment.dto.ts @@ -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; +} diff --git a/src/modules/task-manager/dto/attachment/update-attachment.dto.ts b/src/modules/task-manager/dto/attachment/update-attachment.dto.ts new file mode 100644 index 0000000..977fed9 --- /dev/null +++ b/src/modules/task-manager/dto/attachment/update-attachment.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from "@nestjs/swagger"; +import { CreateAttachmentDto } from "./create-attachment.dto"; + +export class UpdateAttachmentDto extends PartialType(CreateAttachmentDto) {} diff --git a/src/modules/task-manager/dto/check-list-item/create-check-list-item.dto.ts b/src/modules/task-manager/dto/check-list-item/create-check-list-item.dto.ts new file mode 100644 index 0000000..8966881 --- /dev/null +++ b/src/modules/task-manager/dto/check-list-item/create-check-list-item.dto.ts @@ -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; +} diff --git a/src/modules/task-manager/dto/check-list-item/update-check-list-item.dto.ts b/src/modules/task-manager/dto/check-list-item/update-check-list-item.dto.ts new file mode 100644 index 0000000..c171e29 --- /dev/null +++ b/src/modules/task-manager/dto/check-list-item/update-check-list-item.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateCheckListItemDto } from './create-check-list-item.dto'; + +export class UpdateCheckListItemDto extends PartialType(CreateCheckListItemDto) {} \ No newline at end of file diff --git a/src/modules/task-manager/dto/project/create-project.dto.ts b/src/modules/task-manager/dto/project/create-project.dto.ts new file mode 100644 index 0000000..588fcb9 --- /dev/null +++ b/src/modules/task-manager/dto/project/create-project.dto.ts @@ -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[]; +} diff --git a/src/modules/task-manager/dto/project/update-project.dto.ts b/src/modules/task-manager/dto/project/update-project.dto.ts new file mode 100644 index 0000000..2b845b1 --- /dev/null +++ b/src/modules/task-manager/dto/project/update-project.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from "@nestjs/swagger"; +import { CreateProjectDto } from "./create-project.dto"; + +export class UpdateProjectDto extends PartialType(CreateProjectDto) {} diff --git a/src/modules/task-manager/dto/remark/create-remark.dto.ts b/src/modules/task-manager/dto/remark/create-remark.dto.ts new file mode 100644 index 0000000..b735dfe --- /dev/null +++ b/src/modules/task-manager/dto/remark/create-remark.dto.ts @@ -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; +} diff --git a/src/modules/task-manager/dto/remark/update-remark.dto.ts b/src/modules/task-manager/dto/remark/update-remark.dto.ts new file mode 100644 index 0000000..06610d3 --- /dev/null +++ b/src/modules/task-manager/dto/remark/update-remark.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from "@nestjs/swagger"; +import { CreateRemarkDto } from "./create-remark.dto"; + +export class UpdateRemarkDto extends PartialType(CreateRemarkDto) {} diff --git a/src/modules/task-manager/dto/task-phase/create-task-phase.dto.ts b/src/modules/task-manager/dto/task-phase/create-task-phase.dto.ts new file mode 100644 index 0000000..69dc0f2 --- /dev/null +++ b/src/modules/task-manager/dto/task-phase/create-task-phase.dto.ts @@ -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; +} \ No newline at end of file diff --git a/src/modules/task-manager/dto/task-phase/update-task-phase.dto.ts b/src/modules/task-manager/dto/task-phase/update-task-phase.dto.ts new file mode 100644 index 0000000..45ebc93 --- /dev/null +++ b/src/modules/task-manager/dto/task-phase/update-task-phase.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from "@nestjs/swagger"; +import { CreateTaskPhaseDto } from "./create-task-phase.dto"; + +export class UpdateTaskPhaseDto extends PartialType(CreateTaskPhaseDto) {} diff --git a/src/modules/task-manager/dto/task/change-task-phase.dto.ts b/src/modules/task-manager/dto/task/change-task-phase.dto.ts new file mode 100644 index 0000000..c98872d --- /dev/null +++ b/src/modules/task-manager/dto/task/change-task-phase.dto.ts @@ -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; +} \ No newline at end of file diff --git a/src/modules/task-manager/dto/task/create-task.dto.ts b/src/modules/task-manager/dto/task/create-task.dto.ts new file mode 100644 index 0000000..5d0a148 --- /dev/null +++ b/src/modules/task-manager/dto/task/create-task.dto.ts @@ -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[]; +} diff --git a/src/modules/task-manager/dto/task/update-task.dto.ts b/src/modules/task-manager/dto/task/update-task.dto.ts new file mode 100644 index 0000000..bee1042 --- /dev/null +++ b/src/modules/task-manager/dto/task/update-task.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from "@nestjs/swagger"; +import { CreateTaskDto } from "./create-task.dto"; + +export class UpdateTaskDto extends PartialType(CreateTaskDto) {} diff --git a/src/modules/task-manager/dto/workspace/create-workspace.dto.ts b/src/modules/task-manager/dto/workspace/create-workspace.dto.ts new file mode 100644 index 0000000..99c8bf5 --- /dev/null +++ b/src/modules/task-manager/dto/workspace/create-workspace.dto.ts @@ -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[]; +} diff --git a/src/modules/task-manager/dto/workspace/update-workspace.dto.ts b/src/modules/task-manager/dto/workspace/update-workspace.dto.ts new file mode 100644 index 0000000..9dd610e --- /dev/null +++ b/src/modules/task-manager/dto/workspace/update-workspace.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from "@nestjs/swagger"; +import { CreateWorkspaceDto } from "./create-workspace.dto"; + +export class UpdateWorkspaceDto extends PartialType(CreateWorkspaceDto) {} diff --git a/src/modules/task-manager/entities/attachment.entity.ts b/src/modules/task-manager/entities/attachment.entity.ts new file mode 100644 index 0000000..ce2ef05 --- /dev/null +++ b/src/modules/task-manager/entities/attachment.entity.ts @@ -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; + } \ No newline at end of file diff --git a/src/modules/task-manager/entities/check-list-item.entity.ts b/src/modules/task-manager/entities/check-list-item.entity.ts new file mode 100644 index 0000000..870b438 --- /dev/null +++ b/src/modules/task-manager/entities/check-list-item.entity.ts @@ -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; +} diff --git a/src/modules/task-manager/entities/project.entity.ts b/src/modules/task-manager/entities/project.entity.ts new file mode 100644 index 0000000..0166a5c --- /dev/null +++ b/src/modules/task-manager/entities/project.entity.ts @@ -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[]; +} diff --git a/src/modules/task-manager/entities/remark.entity.ts b/src/modules/task-manager/entities/remark.entity.ts new file mode 100644 index 0000000..5705a20 --- /dev/null +++ b/src/modules/task-manager/entities/remark.entity.ts @@ -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; +} diff --git a/src/modules/task-manager/entities/task-phase.entity.ts b/src/modules/task-manager/entities/task-phase.entity.ts new file mode 100644 index 0000000..644166d --- /dev/null +++ b/src/modules/task-manager/entities/task-phase.entity.ts @@ -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[]; +} diff --git a/src/modules/task-manager/entities/task.entity.ts b/src/modules/task-manager/entities/task.entity.ts new file mode 100644 index 0000000..5461d83 --- /dev/null +++ b/src/modules/task-manager/entities/task.entity.ts @@ -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; +} diff --git a/src/modules/task-manager/entities/workspace.entity.ts b/src/modules/task-manager/entities/workspace.entity.ts new file mode 100644 index 0000000..c2feb0c --- /dev/null +++ b/src/modules/task-manager/entities/workspace.entity.ts @@ -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[]; +} diff --git a/src/modules/task-manager/enums/attachment-type.enum.ts b/src/modules/task-manager/enums/attachment-type.enum.ts new file mode 100644 index 0000000..e442e22 --- /dev/null +++ b/src/modules/task-manager/enums/attachment-type.enum.ts @@ -0,0 +1,6 @@ +export enum TMAttachmentType { + FILE = "file", + LINK = "link", + IMAGE = "image", + VIDEO = "video", +} diff --git a/src/modules/task-manager/providers/attachment.service.ts b/src/modules/task-manager/providers/attachment.service.ts new file mode 100644 index 0000000..72166ab --- /dev/null +++ b/src/modules/task-manager/providers/attachment.service.ts @@ -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 { + 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 { + await this.taskService.findOneOrFail(dto.taskId); + + const attachment = this.attachmentRepository.create(dto); + return this.attachmentRepository.save(attachment); + } + + async update(id: string, dto: UpdateAttachmentDto): Promise { + 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 { + await this.findOneOrFail(id); + await this.attachmentRepository.delete(id); + } +} diff --git a/src/modules/task-manager/providers/check-list-item.service.ts b/src/modules/task-manager/providers/check-list-item.service.ts new file mode 100644 index 0000000..1259e78 --- /dev/null +++ b/src/modules/task-manager/providers/check-list-item.service.ts @@ -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 { + const checkListItem = await this.checkListItemRepository.findOneById(id); + if (!checkListItem) throw new NotFoundException(checkListItemMessage.CHECKLISTITEM_NOT_FOUND); + return checkListItem; + } + + async create(dto: CreateCheckListItemDto): Promise { + await this.taskService.findOneOrFail(dto.taskId); + + const checkListItem = this.checkListItemRepository.create(dto); + return this.checkListItemRepository.save(checkListItem); + } + + async update(id: string, dto: UpdateCheckListItemDto): Promise { + 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 { + const checkListItem = await this.findOneOrFail(id); + await this.checkListItemRepository.delete(id); + return checkListItem; + } + + async toggleStatus(id: string) : Promise { + const checkListItem = await this.findOneOrFail(id); + const isDone = checkListItem.isDone; + checkListItem.isDone = !isDone; + await this.checkListItemRepository.save(checkListItem); + + return checkListItem; + } +} \ No newline at end of file diff --git a/src/modules/task-manager/providers/project.service.ts b/src/modules/task-manager/providers/project.service.ts new file mode 100644 index 0000000..384d6f6 --- /dev/null +++ b/src/modules/task-manager/providers/project.service.ts @@ -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, + ) {} + + async findOneOrFail(id: string): Promise { + const project = await this.projectRepository.findOne({ where: { id } }); + if (!project) throw new NotFoundException(ProjectMessage.PROJECT_NOT_FOUND); + return project; + } + + async create(dto: CreateProjectDto): Promise { + 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 { + 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 { + 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 { + 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; + } +} diff --git a/src/modules/task-manager/providers/remark.service.ts b/src/modules/task-manager/providers/remark.service.ts new file mode 100644 index 0000000..50fe024 --- /dev/null +++ b/src/modules/task-manager/providers/remark.service.ts @@ -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 { + const remark = await this.remarkRepository.findOne({ where: { id } }); + if (!remark) throw new NotFoundException(remarkMessage.REMARK_NOT_FOUND); + return remark; + } + + async create(dto: CreateRemarkDto): Promise { + await this.taskService.findOneOrFail(dto.taskId); + + const remark = this.remarkRepository.create(dto); + return this.remarkRepository.save(remark); + } + + async update(id: string, dto: UpdateRemarkDto): Promise { + 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 { + await this.findOneOrFail(id); + await this.remarkRepository.delete(id); + } +} diff --git a/src/modules/task-manager/providers/task-phase.service.ts b/src/modules/task-manager/providers/task-phase.service.ts new file mode 100644 index 0000000..222d2c4 --- /dev/null +++ b/src/modules/task-manager/providers/task-phase.service.ts @@ -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 { + 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 { + await this.projectService.findOneOrFail(dto.projectId); + const taskPhase = this.taskPhaseRepository.create(dto); + return this.taskPhaseRepository.save(taskPhase); + } + + async update(id: string, dto: UpdateTaskPhaseDto): Promise { + 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 { + await this.findOneOrFail(id); + await this.taskPhaseRepository.delete(id); + } +} diff --git a/src/modules/task-manager/providers/task.service.ts b/src/modules/task-manager/providers/task.service.ts new file mode 100644 index 0000000..a5d840c --- /dev/null +++ b/src/modules/task-manager/providers/task.service.ts @@ -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, + ) {} + + async findOneOrFail(id: string): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + await this.findOneOrFail(id); + await this.taskRepository.delete(id); + } + + async getTaskDetail(userId: string, permissions: PermissionEnum[], taskId: string): Promise { + 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; + } +} diff --git a/src/modules/task-manager/providers/workspace.service.ts b/src/modules/task-manager/providers/workspace.service.ts new file mode 100644 index 0000000..931ecf7 --- /dev/null +++ b/src/modules/task-manager/providers/workspace.service.ts @@ -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, + ) {} + + findAll(): Promise { + return this.workspaceRepository.find({}); + } + + async findOneOrFail(id: string): Promise { + const workspace = await this.workspaceRepository.findOneWorkSpace(id); + if (!workspace) throw new NotFoundException(WorkSpaceMessage.WORKSPACE_NOT_FOUND); + return workspace; + } + + async create(dto: CreateWorkspaceDto): Promise { + 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 { + 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 { + const workspace = await this.findOneOrFail(id); + await this.workspaceRepository.delete(id); + + return workspace; + } + + async findByUser(userId: string, permissions: PermissionEnum[]): Promise { + if(permissions.includes(PermissionEnum.VIEW_ALL_WORKSPACES)) { + return await this.workspaceRepository.find(); + } + return await this.workspaceRepository.findByUser(userId); + } +} diff --git a/src/modules/task-manager/repositories/attachment.repository.ts b/src/modules/task-manager/repositories/attachment.repository.ts new file mode 100644 index 0000000..a27d990 --- /dev/null +++ b/src/modules/task-manager/repositories/attachment.repository.ts @@ -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{ + constructor( + @InjectRepository(TMAttachment) attachmentRepository: Repository, + ) { + super(attachmentRepository.target, attachmentRepository.manager, attachmentRepository.queryRunner); + } +} \ No newline at end of file diff --git a/src/modules/task-manager/repositories/check-list-item.repository.ts b/src/modules/task-manager/repositories/check-list-item.repository.ts new file mode 100644 index 0000000..f64782e --- /dev/null +++ b/src/modules/task-manager/repositories/check-list-item.repository.ts @@ -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{ + constructor( + @InjectRepository(TMCheckListItem) checkListItemRepository: Repository, + ) { + super(checkListItemRepository.target, checkListItemRepository.manager, checkListItemRepository.queryRunner); + } +} \ No newline at end of file diff --git a/src/modules/task-manager/repositories/project.repository.ts b/src/modules/task-manager/repositories/project.repository.ts new file mode 100644 index 0000000..a7bfbf4 --- /dev/null +++ b/src/modules/task-manager/repositories/project.repository.ts @@ -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 { + constructor( + @InjectRepository(TMProject) projectRepository: Repository, + ) { + 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 { + 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(); + } +} diff --git a/src/modules/task-manager/repositories/remark.repository.ts b/src/modules/task-manager/repositories/remark.repository.ts new file mode 100644 index 0000000..8b71ea3 --- /dev/null +++ b/src/modules/task-manager/repositories/remark.repository.ts @@ -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{ + constructor( + @InjectRepository(TMRemark) remarkRepository: Repository, + ) { + super(remarkRepository.target, remarkRepository.manager, remarkRepository.queryRunner); + } +} diff --git a/src/modules/task-manager/repositories/task-phase.repository.ts b/src/modules/task-manager/repositories/task-phase.repository.ts new file mode 100644 index 0000000..b283229 --- /dev/null +++ b/src/modules/task-manager/repositories/task-phase.repository.ts @@ -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 { + constructor( + @InjectRepository(TMTaskPhase) taskPhaseRepository: Repository, + ) { + super(taskPhaseRepository.target, taskPhaseRepository.manager, taskPhaseRepository.queryRunner); + } +} diff --git a/src/modules/task-manager/repositories/task.repository.ts b/src/modules/task-manager/repositories/task.repository.ts new file mode 100644 index 0000000..68af884 --- /dev/null +++ b/src/modules/task-manager/repositories/task.repository.ts @@ -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 { + constructor(@InjectRepository(TMTask) taskRepository: Repository) { + super(taskRepository.target, taskRepository.manager, taskRepository.queryRunner); + } + + async getTaskDetail(taskId: string): Promise { + 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(); + } +} diff --git a/src/modules/task-manager/repositories/workspace.repository.ts b/src/modules/task-manager/repositories/workspace.repository.ts new file mode 100644 index 0000000..04b4697 --- /dev/null +++ b/src/modules/task-manager/repositories/workspace.repository.ts @@ -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 { + constructor(@InjectRepository(TMWorkspace) workspaceRepository: Repository) { + super(workspaceRepository.target, workspaceRepository.manager, workspaceRepository.queryRunner); + } + + async findOneWorkSpace(workspaceId: string): Promise { + 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 { + return this.createQueryBuilder("workspace") + .innerJoin("workspace.users", "users") + .where("users.id = :userId", { userId }) + .getMany(); + } +} diff --git a/src/modules/task-manager/task-manager.module.ts b/src/modules/task-manager/task-manager.module.ts new file mode 100644 index 0000000..43bbb5b --- /dev/null +++ b/src/modules/task-manager/task-manager.module.ts @@ -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 {} diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index 2237188..5b85278 100755 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -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[]; } diff --git a/src/modules/users/enums/permission.enum.ts b/src/modules/users/enums/permission.enum.ts index 5a42f37..d59cdc8 100755 --- a/src/modules/users/enums/permission.enum.ts +++ b/src/modules/users/enums/permission.enum.ts @@ -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", } diff --git a/src/modules/users/users.module.ts b/src/modules/users/users.module.ts index 1d24e7a..68afdc5 100755 --- a/src/modules/users/users.module.ts +++ b/src/modules/users/users.module.ts @@ -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,