51 Commits

Author SHA1 Message Date
morteza 30d86c9775 task time 2026-07-15 10:37:19 +03:30
realrafi 4263d7118e refactor: changed the findTasksByTaskPhase to include task info like remark, etc 2026-07-14 21:39:37 +03:30
morteza f35df6492c remove unused joins 2026-07-14 15:07:53 +03:30
realrafi 008bddb97a fix&feature: fixxing some logical bogs and adding cursor pagination 2026-07-14 13:51:16 +03:30
realrafi 8e23a06793 refactor: order the taskPhases by order attr in projectDetail API 2026-07-12 14:52:24 +03:30
realrafi 1440b62373 fix: fixing getTaskDetail returning null 2026-07-11 12:38:00 +03:30
realrafi d93e4be6ef refactor: made the findOne project by id to return users of each project as well 2026-07-11 11:36:17 +03:30
realrafi 9ded147354 feat: added fineOne API for workspace & project for update purposes 2026-07-09 20:39:08 +03:30
realrafi 4cb20f6c0d refactor: removed unnecessary methods from checkListItem repo 2026-07-08 11:16:32 +03:30
realrafi 95c947a587 refactor: removed unnecessary functions from attachment 2026-07-08 11:15:20 +03:30
realrafi 8348692481 refactor: updated permissions 2026-07-08 11:09:43 +03:30
realrafi 677fc5b384 refactor: removed some functions and added toggleStatus to checklistitem 2026-07-08 11:08:28 +03:30
realrafi 8481a0e86f refactor: removed unnecessary functions from remark 2026-07-08 10:50:40 +03:30
realrafi b32d613a1b refactor: removed unnecessary functions from task 2026-07-08 10:48:03 +03:30
realrafi b0668c7b2c refactor: removed unnecessary functions from taskphase 2026-07-08 10:32:45 +03:30
realrafi ff08da1330 refactor: changed project functions logic of permissions 2026-07-08 10:27:01 +03:30
realrafi 29d647556c refactor: changed the get workspaces by user logic 2026-07-07 15:53:18 +03:30
realrafi a22d6ceb81 refactor: making the checklistitem service to use enum messages 2026-07-07 12:03:42 +03:30
realrafi eb6fbc3ce5 refactor: refactored attachment entirely 2026-07-07 11:59:05 +03:30
realrafi 9565eda0fd refactor: refactored the checkListItem 2026-07-07 11:51:42 +03:30
realrafi 8533682303 refactor: refactored remark entirely 2026-07-06 17:00:44 +03:30
realrafi e5c14bf5cc refactor: refactored taskDetailAPI by adding some values 2026-07-06 16:44:19 +03:30
realrafi e310411a4d refactor: refactored the getProjectDetail function to include more values 2026-07-06 16:17:02 +03:30
realrafi 0cd5a6a0cc feat: added getTaskDetail API 2026-07-06 16:01:36 +03:30
realrafi da71c20d6d refactor: refactored task APIs 2026-07-06 15:21:34 +03:30
realrafi 263ede68b2 refactor: removed typo forward slash from API paths 2026-07-06 14:30:41 +03:30
realrafi d9283e3a90 refactor: refactored the taskphase 2026-07-06 14:26:03 +03:30
realrafi 5964a9b20d feat: added project detail API 2026-07-06 14:05:38 +03:30
realrafi 5715cbe9b1 refactor: changed the getUserProjects to get projects in a specific workspace 2026-07-06 13:24:36 +03:30
realrafi 2590a222b4 refactor: removing useless get projects by workspace function 2026-07-06 12:16:23 +03:30
realrafi 16a0c14149 feat: added get user projects function in project 2026-07-06 12:06:33 +03:30
realrafi 9f61152d94 refactor: changed functions to use pagination logic correctly 2026-07-05 19:20:08 +03:30
realrafi a14bd490be fix: fixed the update func in workspace service to include same name checking 2026-07-05 17:12:30 +03:30
realrafi 47a5a2abb4 refactor: refactored project 2026-07-05 16:22:36 +03:30
realrafi 5763cb7d6c refactor: refactoring some project service functions 2026-07-05 14:43:05 +03:30
realrafi 35d0d9f7cd refactor: refactoring the project repository 2026-07-05 14:23:09 +03:30
realrafi d4bd593f5b fix: fixing some errors in workspace and seeding 2026-07-05 14:17:24 +03:30
realrafi c5b2cb93f0 feat: added get workspaces by user and some refactors 2026-07-04 17:32:02 +03:30
realrafi e57dd84ebc refactor: removing the workspace type entirely 2026-07-04 11:47:36 +03:30
realrafi 8d962ea6ee fix: fixing some errors in project service and controller 2026-07-04 10:52:44 +03:30
realrafi 06ec73be92 feat: added cursor-based pagination to checklistitems 2026-07-04 10:14:33 +03:30
realrafi c22a41d29b refactor: made project service to use pagination 2026-07-02 16:51:11 +03:30
realrafi 4b95d35d87 feat: added attachment controller 2026-07-02 12:59:33 +03:30
realrafi fc23a94fb6 feat: added attachment service 2026-07-02 12:35:59 +03:30
realrafi 1b70e49d93 feat: added attachment repo 2026-07-02 12:17:22 +03:30
realrafi 3ad73a5271 feat: added attachment DTOs 2026-07-02 12:11:44 +03:30
realrafi a51b19f85c refactor: refactoring task manger module 2026-07-02 12:02:54 +03:30
realrafi a199623eb4 feat: added remark controller 2026-07-02 11:58:01 +03:30
realrafi 4f76af87a6 feat: added remark service 2026-07-02 11:34:35 +03:30
realrafi 27e9ad76a5 feat: added remark repository 2026-07-02 11:21:46 +03:30
realrafi c99a8a435c feat: added remark DTOs 2026-07-02 10:56:34 +03:30
60 changed files with 1701 additions and 711 deletions
+75
View File
@@ -0,0 +1,75 @@
NODE_ENV=production
PORT=3500
DB_NAME=dscDev
DB_USER=postgres
DB_PASS=1RWVdlUcZSf6PoFBQO8cprZk2DTkC89ttqeJN7aU2WQRDUEusK4z2FfNqNtV4I10
DB_HOST=78.157.34.13
DB_PORT=5432
SMS_API_URL=https://api.sms.ir/v1
SMS_API_KEY=QkkNxhyXZ6GtEf1soOTtikomO3mA4LaNQDH8mol8huDIwh00
SMS_PATTERN_OTP=842088
SMS_PATTERN_INVOICE=683883
SMS_PATTERN_LOGIN=149141
SMS_PATTERN_INVOICE_CREATED=829067
SMS_PATTERN_ANNOUNCEMENT=752330
SMS_PATTERN_WALLET_CHARGE=727726
SMS_PATTERN_WALLET_DEDUCTION=360785
SMS_PATTERN_TICKET_CREATED=853506
SMS_PATTERN_TICKET_ANSWERED=179464
SMS_PATTERN_TICKET_ASSIGNED_ADMIN=321927
SMS_PATTERN_INVOICE_APPROVED=303807
SMS_PATTERN_INVOICE_PAID=533817
SMS_PATTERN_RECURRING_INVOICE_DRAFT=219287
SMS_PATTERN_SUBSCRIPTION_CANCELLED=166142
SMS_PATTERN_INVOICE_REMINDER=370266
SMS_PATTERN_INVOICE_OVERDUE=160735
SMS_PATTERN_PAYMENT_REMINDER=629772
SMS_PATTERN_PAYMENT_CANCELLATION=197945
SMS_PATTERN_BLOG_NEW_COMMENT=378881
SMS_PATTERN_NEW_SERVICE_REVIEW=593446
SMS_PATTERN_NEW_CUSTOMER=966214
SMS_PATTERN_NEW_SUBSCRIPTION=757095
SMS_PATTERN_NEW_TICKET_GLOBAL=536596
SMS_PATTERN_NEW_CRITICISM=983724
SMS_PATTERN_USER_PASSWORD=486237
SMTP_HOST=smtps.danakcorp.com
SMTP_PORT=465
SMTP_USER=no-reply-danakcorp.com
SMTP_PASS=Mahyar.SR4@
MAIL_FROM=no-reply <no-reply@danakcorp.com>
EMAIL_SECRET=xDkdptbC2N7LoR2Aa4Q9U/aAGOi4DxG2748
REDIS_URI=redis://localhost:6379/1
CACHE_TTL=180000
THROTTLE_TTL=300000
THROTTLE_LIMIT=10
JWT_SECRET_KEY=2ygxIB1PJhK/L2PToqR3+52gRmUIdPfwTguYlB7ZRCdRyvA/N36jjkjbYJLoMxEL\n6xGmk5CQmCmn9c0FsBlN1g
JWT_ISSUER=console-danak
ACCESS_TOKEN_EXPIRE=80000
REFRESH_TOKEN_EXPIRE=6
BUCKET_NAME=danak-project
BUCKET_REGION=default
BUCKET_URL=https://s3.ir-thr-at1.arvanstorage.ir
BUCKET_ACCESS_KEY=4bd15218-60f1-4ce7-a7aa-3fd319b13372
BUCKET_SECRET_KEY=de772e5bd62fb04fb3669e67e09ddb101db60c65d77eea66b215ce58275e6a76
BUCKET_UPLOAD_URL=https://storage.danakcorp.com
ZARINPAL_MERCHANT_ID=fc0b7503-bba5-40d1-9f9d-dae52ac02eef
PARSIAN_LOGIN_ACCOUNT=uqXnB2q8rjkL6vN0xOh0
SITE_URL=https://console.danakcorp.com
API_URL=https://api.danakcorp.com
CALLBACK_URL=https://api.danakcorp.com/payments/verify
TELEGRAM_BOT_TOKEN=7839690719:AAGsyfwympR4rDLm2JCx43CQh52s1o865uY
TELEGRAM_BOT_NAME=danak_logs_bot
TELEGRAM_CHAT_ID=7801476800
EXTERNAL_API_KEYS=uqXssnB2q8rs2soa9almm1jkL6vN0xOh0
DMENU_BACKEND_URL=https://dmenu-api.danakcorp.com
DMENU_USERNAME=danak@dsc.com
DMENU_PASSWORD=DsCdAnAk?@ABC
DKALA_BACKEND_URL=https://dkala-api.danakcorp.com
DKALA_USERNAME=danak@dsc.com
DKALA_PASSWORD=DsCdAnAk?@ABC
DPAGE_BACKEND_URL=https://dpage-api.danakcorp.com
DPAGE_USERNAME=danak@dsc.com
DPAGE_PASSWORD=DsCdAnAk?@ABC
DMAIL_BACKEND_URL=https://dmail-api.danakcorp.com
DMAIL_USERNAME=danak@dsc.com
DMAIL_PASSWORD=DsCdAnAk?@ABC
@@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class Taskmanager1783150480192 implements MigrationInterface {
name = 'Taskmanager1783150480192'
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
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`);
}
}
@@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class Taskmanager31783244669371 implements MigrationInterface {
name = 'Taskmanager31783244669371'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "tm_workspaces" ADD CONSTRAINT "UQ_6496168de3e6c5f006d494ebac5" UNIQUE ("name")`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "tm_workspaces" DROP CONSTRAINT "UQ_6496168de3e6c5f006d494ebac5"`);
}
}
@@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class TaskmanagerAddingNewEnumValueForPermissionEnumType1783324039378 implements MigrationInterface {
name = 'TaskmanagerAddingNewEnumValueForPermissionEnumType1783324039378'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TYPE "public"."permission_name_enum" RENAME TO "permission_name_enum_old"`);
await queryRunner.query(`CREATE TYPE "public"."permission_name_enum" AS ENUM('services', 'customers', 'agents', 'developers', 'invoices', 'transactions', 'discounts', 'admins', 'tickets', 'criticisms', 'contacts_us', 'advertisements', 'announcements', 'blogs', 'learnings', 'logs', 'settings', 'bank_accounts', 'payments', 'manage_sso_clients', 'support_plan', 'dmenu', 'dkala', 'dpage', 'dmail', 'reseller', 'workspace', 'project')`);
await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum" USING "name"::"text"::"public"."permission_name_enum"`);
await queryRunner.query(`DROP TYPE "public"."permission_name_enum_old"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TYPE "public"."permission_name_enum_old" AS ENUM('services', 'customers', 'agents', 'developers', 'invoices', 'transactions', 'discounts', 'admins', 'tickets', 'criticisms', 'contacts_us', 'advertisements', 'announcements', 'blogs', 'learnings', 'logs', 'settings', 'bank_accounts', 'payments', 'manage_sso_clients', 'support_plan', 'dmenu', 'dkala', 'dpage', 'dmail', 'reseller', 'workspace')`);
await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum_old" USING "name"::"text"::"public"."permission_name_enum_old"`);
await queryRunner.query(`DROP TYPE "public"."permission_name_enum"`);
await queryRunner.query(`ALTER TYPE "public"."permission_name_enum_old" RENAME TO "permission_name_enum"`);
}
}
@@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class TMAddingTaskPhasePermissionToPermissionEnumType1783335279945 implements MigrationInterface {
name = 'TMAddingTaskPhasePermissionToPermissionEnumType1783335279945'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TYPE "public"."permission_name_enum" RENAME TO "permission_name_enum_old"`);
await queryRunner.query(`CREATE TYPE "public"."permission_name_enum" AS ENUM('services', 'customers', 'agents', 'developers', 'invoices', 'transactions', 'discounts', 'admins', 'tickets', 'criticisms', 'contacts_us', 'advertisements', 'announcements', 'blogs', 'learnings', 'logs', 'settings', 'bank_accounts', 'payments', 'manage_sso_clients', 'support_plan', 'dmenu', 'dkala', 'dpage', 'dmail', 'reseller', 'workspace', 'project', 'task_phase')`);
await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum" USING "name"::"text"::"public"."permission_name_enum"`);
await queryRunner.query(`DROP TYPE "public"."permission_name_enum_old"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TYPE "public"."permission_name_enum_old" AS ENUM('services', 'customers', 'agents', 'developers', 'invoices', 'transactions', 'discounts', 'admins', 'tickets', 'criticisms', 'contacts_us', 'advertisements', 'announcements', 'blogs', 'learnings', 'logs', 'settings', 'bank_accounts', 'payments', 'manage_sso_clients', 'support_plan', 'dmenu', 'dkala', 'dpage', 'dmail', 'reseller', 'workspace', 'project')`);
await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum_old" USING "name"::"text"::"public"."permission_name_enum_old"`);
await queryRunner.query(`DROP TYPE "public"."permission_name_enum"`);
await queryRunner.query(`ALTER TYPE "public"."permission_name_enum_old" RENAME TO "permission_name_enum"`);
}
}
@@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class TMaddingSomeNewPermissionsToTheEnum1783428145772 implements MigrationInterface {
name = 'TMaddingSomeNewPermissionsToTheEnum1783428145772'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TYPE "public"."permission_name_enum" RENAME TO "permission_name_enum_old"`);
await queryRunner.query(`CREATE TYPE "public"."permission_name_enum" AS ENUM('services', 'customers', 'agents', 'developers', 'invoices', 'transactions', 'discounts', 'admins', 'tickets', 'criticisms', 'contacts_us', 'advertisements', 'announcements', 'blogs', 'learnings', 'logs', 'settings', 'bank_accounts', 'payments', 'manage_sso_clients', 'support_plan', 'dmenu', 'dkala', 'dpage', 'dmail', 'reseller', 'workspace', '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<void> {
await queryRunner.query(`CREATE TYPE "public"."permission_name_enum_old" AS ENUM('services', 'customers', 'agents', 'developers', 'invoices', 'transactions', 'discounts', 'admins', 'tickets', 'criticisms', 'contacts_us', 'advertisements', 'announcements', 'blogs', 'learnings', 'logs', 'settings', 'bank_accounts', 'payments', 'manage_sso_clients', 'support_plan', 'dmenu', 'dkala', 'dpage', 'dmail', 'reseller', 'workspace', 'project', '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"`);
}
}
@@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class TMChangingFewLastPermissions1783759715100 implements MigrationInterface {
name = 'TMChangingFewLastPermissions1783759715100'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TYPE "public"."permission_name_enum" RENAME TO "permission_name_enum_old"`);
await queryRunner.query(`CREATE TYPE "public"."permission_name_enum" AS ENUM('services', 'customers', 'agents', 'developers', 'invoices', 'transactions', 'discounts', 'admins', 'tickets', 'criticisms', 'contacts_us', 'advertisements', 'announcements', 'blogs', 'learnings', 'logs', 'settings', 'bank_accounts', 'payments', 'manage_sso_clients', 'support_plan', 'dmenu', 'dkala', 'dpage', 'dmail', 'reseller', 'workspace', 'view_all_workspaces', 'project', 'view_all_projects', 'task', 'task_phase')`);
await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum" USING "name"::"text"::"public"."permission_name_enum"`);
await queryRunner.query(`DROP TYPE "public"."permission_name_enum_old"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TYPE "public"."permission_name_enum_old" AS ENUM('admins', 'advertisements', 'agents', 'announcements', 'bank_accounts', 'blogs', 'contacts_us', 'criticisms', 'customers', 'developers', 'discounts', 'dkala', 'dmail', 'dmenu', 'dpage', 'invoices', 'learnings', 'logs', 'manage_sso_clients', 'payments', 'project', 'remark', 'reseller', 'services', 'settings', 'support_plan', 'task', 'task_phase', 'tickets', 'transactions', 'view_all_projects', 'view_all_workspaces', 'workspace')`);
await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum_old" USING "name"::"text"::"public"."permission_name_enum_old"`);
await queryRunner.query(`DROP TYPE "public"."permission_name_enum"`);
await queryRunner.query(`ALTER TYPE "public"."permission_name_enum_old" RENAME TO "permission_name_enum"`);
}
}
@@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddedPriorityToTaskEntity1784014433653 implements MigrationInterface {
name = 'AddedPriorityToTaskEntity1784014433653'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "tm_tasks" ADD "priority" integer NOT NULL DEFAULT '1'`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "tm_tasks" DROP COLUMN "priority"`);
}
}
@@ -0,0 +1,39 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddTaskTimeTable1784100000000 implements MigrationInterface {
name = "AddTaskTimeTable1784100000000";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE "tm_times" (
"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(),
"startedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
"endedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
"duration" integer NOT NULL,
"adminId" uuid NOT NULL,
"taskId" uuid NOT NULL,
CONSTRAINT "PK_tm_times" PRIMARY KEY ("id")
)
`);
await queryRunner.query(`
ALTER TABLE "tm_times"
ADD CONSTRAINT "FK_tm_times_admin"
FOREIGN KEY ("adminId") REFERENCES "user"("id")
ON DELETE RESTRICT ON UPDATE NO ACTION
`);
await queryRunner.query(`
ALTER TABLE "tm_times"
ADD CONSTRAINT "FK_tm_times_task"
FOREIGN KEY ("taskId") REFERENCES "tm_tasks"("id")
ON DELETE CASCADE ON UPDATE NO ACTION
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "tm_times" DROP CONSTRAINT "FK_tm_times_task"`);
await queryRunner.query(`ALTER TABLE "tm_times" DROP CONSTRAINT "FK_tm_times_admin"`);
await queryRunner.query(`DROP TABLE "tm_times"`);
}
}
@@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MakeTaskTimeEndedAtAndDurationNullable1784200000000
implements MigrationInterface
{
name = "MakeTaskTimeEndedAtAndDurationNullable1784200000000";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "tm_times" ALTER COLUMN "endedAt" DROP NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "tm_times" ALTER COLUMN "duration" DROP NOT NULL`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "tm_times" ALTER COLUMN "endedAt" SET NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "tm_times" ALTER COLUMN "duration" SET NOT NULL`,
);
}
}
+6 -6
View File
@@ -5,9 +5,9 @@ import { connectionSource } from "./connection";
// import { seedCityAndProvince } from "./seeders/iran-city.seeder"; // import { seedCityAndProvince } from "./seeders/iran-city.seeder";
// import { seedNotifSettings } from "./seeders/notif-setting.seeder"; // import { seedNotifSettings } from "./seeders/notif-setting.seeder";
// import { seedPaymentGateways } from "./seeders/payment-gateway.seeder"; // import { seedPaymentGateways } from "./seeders/payment-gateway.seeder";
// import { seedPermissionsAndRoles } from "./seeders/role.seeder"; import { seedPermissionsAndRoles } from "./seeders/role.seeder";
import { seedBlogCommentReplies } from "./seeders/blog-comment-reply.seeder"; // import { seedBlogCommentReplies } from "./seeders/blog-comment-reply.seeder";
import { seedBlogComments } from "./seeders/blog-comment.seeder"; // import { seedBlogComments } from "./seeders/blog-comment.seeder";
const logger = new Logger("Seeder"); const logger = new Logger("Seeder");
@@ -15,14 +15,14 @@ export const runSeeder = async () => {
await connectionSource.initialize(); await connectionSource.initialize();
logger.log("start seeding database"); logger.log("start seeding database");
// //
// await seedPermissionsAndRoles(connectionSource, logger); await seedPermissionsAndRoles(connectionSource, logger);
// await seedAdmin(connectionSource, logger); // await seedAdmin(connectionSource, logger);
// await seedNotifSettings(connectionSource, logger); // await seedNotifSettings(connectionSource, logger);
// await seedPaymentGateways(connectionSource, logger); // await seedPaymentGateways(connectionSource, logger);
// await seedCityAndProvince(connectionSource, logger); // await seedCityAndProvince(connectionSource, logger);
await seedBlogComments(connectionSource, logger); // await seedBlogComments(connectionSource, logger);
await seedBlogCommentReplies(connectionSource, logger); // await seedBlogCommentReplies(connectionSource, logger);
logger.log("seeding completed"); logger.log("seeding completed");
process.exit(0); process.exit(0);
+4 -4
View File
@@ -1,8 +1,8 @@
import { Logger } from "@nestjs/common"; import { Logger } from "@nestjs/common";
import { DataSource } from "typeorm"; import { DataSource } from "typeorm";
import { Permission } from "../../src/modules/users/entities/permission.entity"; // import { Permission } from "../../src/modules/users/entities/permission.entity";
import { Role } from "../../src/modules/users/entities/role.entity"; // import { Role } from "../../src/modules/users/entities/role.entity";
import { PermissionEnum } from "../../src/modules/users/enums/permission.enum"; import { PermissionEnum } from "../../src/modules/users/enums/permission.enum";
import { RoleEnum } from "../../src/modules/users/enums/role.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) => { export const seedPermissionsAndRoles = async (dataSource: DataSource, logger: Logger) => {
try { try {
const permissionRepo = dataSource.getRepository(Permission); const permissionRepo = dataSource.getRepository('Permission');
const roleRepo = dataSource.getRepository(Role); const roleRepo = dataSource.getRepository('Role');
// Seed Permissions // Seed Permissions
logger.log("Seeding Permissions..."); logger.log("Seeding Permissions...");
+33
View File
@@ -0,0 +1,33 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsEnum, IsInt, IsOptional, IsString, Max, Min } from "class-validator";
export enum CursorDirection {
NEXT = "next",
PREV = "prev",
}
export class CursorPaginationDto {
@IsOptional()
@IsString()
@ApiPropertyOptional({
type: "string",
description:
"Base64encoded cursor containing the priority, createdAt (ISO) and id of the last seen item. Leave empty for the first page.",
example: "MjAyNi0wNy0wMVQxMDowMDowMC4wMDBafDU=",
})
cursor?: string;
@IsOptional()
@IsInt()
@Min(1)
@Max(50)
@Type(() => Number)
@ApiPropertyOptional({ type: "number", required: false, default: 10 })
limit?: number;
@IsOptional()
@IsEnum(CursorDirection)
@ApiPropertyOptional({ enum: CursorDirection, default: CursorDirection.NEXT, description: "Direction to page in relative to 'cursor'." })
direction?: CursorDirection;
}
@@ -0,0 +1,29 @@
import { applyDecorators } from "@nestjs/common";
import { ApiQuery } from "@nestjs/swagger";
import { CursorDirection } from "../DTO/cursor-pagination.dto";
export function CursorPagination() {
return applyDecorators(
ApiQuery({
name: "cursor",
required: false,
type: "string",
description: "Base64encoded cursor of the last item (contains priority, createdAt and id). Leave empty for the first page.",
example: "MjAyNi0wNy0wMVQxMDowMDowMC4wMDBafDU=",
}),
ApiQuery({
name: "limit",
required: false,
type: "number",
description: "Number of items per page (max 50)",
example: 10,
}),
ApiQuery({
name: "direction",
required: false,
enum: CursorDirection,
description: "Direction to page in relative to 'cursor': 'next' (default) or 'prev'.",
example: CursorDirection.NEXT,
}),
);
}
+48
View File
@@ -915,3 +915,51 @@ export const enum UploaderMessage {
export const enum AccessLogMessage { export const enum AccessLogMessage {
ACCESS_LOG_NOT_FOUND = "لاگ دسترسی مورد نظر یافت نشد", 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 = "شما عضو این تسک نیستید!",
TASKS_NOT_IN_THE_SAME_TASK_PHASE = "تسک های مورد نظر داخل یک فاز تسک یکسان نیستند!"
}
export const enum remarkMessage {
REMARK_NOT_FOUND = "برچسب مورد نظر پیدا نشد!"
}
export const enum checkListItemMessage {
CHECKLISTITEM_NOT_FOUND = "آیتم چک لیست پیدا نشد!"
}
export const enum attachmentMessage {
ATTACHMENT_NOT_FOUND = "ضمیمه پیدا نشد!"
}
export const enum timeMessage {
TIME_NOT_FOUND = "زمان ثبت شده پیدا نشد!",
INVALID_TIME_RANGE = "زمان پایان باید بعد از زمان شروع باشد!",
TIME_ALREADY_STARTED = "برای این تسک یک زمان در حال اجرا وجود دارد!",
NO_RUNNING_TIME = "زمان در حال اجرایی برای این تسک پیدا نشد!"
}
@@ -0,0 +1,7 @@
export interface ICursorPageFormat {
limit: number;
nextCursor: string | null;
previousCursor: string | null;
hasNextPage: boolean;
hasPreviousPage: boolean;
}
@@ -3,6 +3,7 @@ import { FastifyRequest } from "fastify";
import { Observable, map } from "rxjs"; import { Observable, map } from "rxjs";
import { IPageFormat } from "../../common/interfaces/IPagination"; import { IPageFormat } from "../../common/interfaces/IPagination";
import { ICursorPageFormat } from "../../common/interfaces/ICursor-pagination";
@Injectable() @Injectable()
export class PaginationInterceptor implements NestInterceptor { export class PaginationInterceptor implements NestInterceptor {
@@ -10,7 +11,11 @@ export class PaginationInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> { intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const request = context.switchToHttp().getRequest<FastifyRequest>(); const request = context.switchToHttp().getRequest<FastifyRequest>();
const query = request.query as { page: string; limit: string }; const query = request.query as {
page: string;
limit: string;
cursor?: string; // for cursor pagination
};
const page = parseInt(query.page, 10) || 1; const page = parseInt(query.page, 10) || 1;
const limit = parseInt(query.limit, 10) || 10; const limit = parseInt(query.limit, 10) || 10;
@@ -20,17 +25,40 @@ export class PaginationInterceptor implements NestInterceptor {
return next.handle().pipe( return next.handle().pipe(
map((data) => { map((data) => {
// ---------- Offset (pagebased) pagination ----------
if (data && (data.paginate || data.count)) { if (data && (data.paginate || data.count)) {
const { count, paginate, ...response } = data; const { count, paginate, ...response } = data;
this.logger.log(`paginate response from ${className}.${handlerName}`); this.logger.log(`offset paginate response from ${className}.${handlerName}`);
const pager = this.formatPage(page, limit, count, request); const pager = this.formatPage(page, limit, count, request);
return { pager, ...response };
return {
pager,
...response,
};
} }
// ---------- Cursorbased pagination ----------
if (data && data.cursorPaginate === true) {
const {
data: items,
nextCursor,
previousCursor,
hasNextPage,
hasPreviousPage,
cursorPaginate,
...rest
} = data;
this.logger.log(`cursor paginate response from ${className}.${handlerName}`);
const pager: ICursorPageFormat = {
limit,
nextCursor,
previousCursor,
hasNextPage,
hasPreviousPage,
};
return { items, pager, ...rest };
}
// ---------- No pagination ----------
return data; return data;
}), }),
); );
@@ -0,0 +1,42 @@
import { Controller, Post, Patch, Delete, Param, Body } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
import { AttachmentService } from '../providers/attachment.service';
import { CreateAttachmentDto } from '../dto/attachment/create-attachment.dto';
import { UpdateAttachmentDto } from '../dto/attachment/update-attachment.dto';
import { TMAttachment } from '../entities/attachment.entity';
import { AuthGuards } from '../../../common/decorators/auth-guard.decorator';
import { AdminRoute } from '../../../common/decorators/admin.decorator';
@AuthGuards()
@AdminRoute()
@ApiTags('Task Manager - Attachments')
@Controller('task-manager/attachments')
export class AttachmentController {
constructor(private readonly attachmentService: AttachmentService) {}
@Post()
@ApiOperation({ summary: 'Create a new attachment' })
@ApiResponse({ status: 201, description: 'Attachment created', type: TMAttachment })
@ApiResponse({ status: 404, description: 'Task not found' })
create(@Body() body: CreateAttachmentDto): Promise<TMAttachment> {
return this.attachmentService.create(body);
}
@Patch(':id')
@ApiOperation({ summary: 'Update an attachment' })
@ApiParam({ name: 'id', description: 'Attachment ID' })
@ApiResponse({ status: 200, description: 'Attachment updated', type: TMAttachment })
@ApiResponse({ status: 404, description: 'Attachment or Task not found' })
update(@Param('id') id: string, @Body() body: UpdateAttachmentDto): Promise<TMAttachment> {
return this.attachmentService.update(id, body);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete an attachment' })
@ApiParam({ name: 'id', description: 'Attachment ID' })
@ApiResponse({ status: 200, description: 'Attachment deleted' })
@ApiResponse({ status: 404, description: 'Attachment not found' })
remove(@Param('id') id: string): Promise<void> {
return this.attachmentService.remove(id);
}
}
@@ -1,56 +1,52 @@
import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from '@nestjs/common'; import { Controller, Post, Patch, Delete, Param, Body } from "@nestjs/common";
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger'; import { ApiTags, ApiOperation, ApiResponse, ApiParam } from "@nestjs/swagger";
import { CheckListItemService } from '../providers/check-list-item.service'; import { CheckListItemService } from "../providers/check-list-item.service";
import { CreateCheckListItemDto } from '../dto/check-list-item/create-check-list-item.dto'; 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 { UpdateCheckListItemDto } from "../dto/check-list-item/update-check-list-item.dto";
import { TMCheckListItem } from '../entities/check-list-item.entity'; 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";
@ApiTags('Task Manager - Check List Items') @AuthGuards()
@Controller('task-manager/check-list-items') @AdminRoute()
@ApiTags("Task Manager - Check List Items")
@Controller("task-manager/check-list-items")
export class CheckListItemController { export class CheckListItemController {
constructor(private readonly checkListItemService: CheckListItemService) {} constructor(private readonly checkListItemService: CheckListItemService) {}
@Get()
@ApiOperation({ summary: 'Get all check list items, optionally filtered by task' })
@ApiQuery({ name: 'taskId', required: false, description: 'Filter check list items by task ID' })
@ApiResponse({ status: 200, description: 'List of check list items', type: [TMCheckListItem] })
findAll(@Query('taskId') taskId?: string): Promise<TMCheckListItem[]> {
if (taskId) return this.checkListItemService.findByTask(taskId);
return this.checkListItemService.findAll();
}
@Get(':id')
@ApiOperation({ summary: 'Get a check list item by ID' })
@ApiParam({ name: 'id', description: 'CheckListItem ID' })
@ApiResponse({ status: 200, description: 'CheckListItem found', type: TMCheckListItem })
@ApiResponse({ status: 404, description: 'CheckListItem not found' })
findOne(@Param('id') id: string): Promise<TMCheckListItem> {
return this.checkListItemService.findOne(id);
}
@Post() @Post()
@ApiOperation({ summary: 'Create a new check list item' }) @ApiOperation({ summary: "Create a new check list item" })
@ApiResponse({ status: 201, description: 'CheckListItem created', type: TMCheckListItem }) @ApiResponse({ status: 201, description: "CheckListItem created", type: TMCheckListItem })
@ApiResponse({ status: 404, description: 'Task not found' }) @ApiResponse({ status: 404, description: "Task not found" })
create(@Body() body: CreateCheckListItemDto): Promise<TMCheckListItem> { create(@Body() body: CreateCheckListItemDto): Promise<TMCheckListItem> {
return this.checkListItemService.create(body); return this.checkListItemService.create(body);
} }
@Patch(':id') @Patch(":id")
@ApiOperation({ summary: 'Update a check list item' }) @ApiOperation({ summary: "Update a check list item" })
@ApiParam({ name: 'id', description: 'CheckListItem ID' }) @ApiParam({ name: "id", description: "CheckListItem ID" })
@ApiResponse({ status: 200, description: 'CheckListItem updated', type: TMCheckListItem }) @ApiResponse({ status: 200, description: "CheckListItem updated", type: TMCheckListItem })
@ApiResponse({ status: 404, description: 'CheckListItem or Task not found' }) @ApiResponse({ status: 404, description: "CheckListItem or Task not found" })
update(@Param('id') id: string, @Body() body: UpdateCheckListItemDto): Promise<TMCheckListItem> { update(@Param("id") id: string, @Body() body: UpdateCheckListItemDto): Promise<TMCheckListItem> {
return this.checkListItemService.update(id, body); return this.checkListItemService.update(id, body);
} }
@Delete(':id') @Delete(":id")
@ApiOperation({ summary: 'Delete a check list item' }) @ApiOperation({ summary: "Delete a check list item" })
@ApiParam({ name: 'id', description: 'CheckListItem ID' }) @ApiParam({ name: "id", description: "CheckListItem ID" })
@ApiResponse({ status: 200, description: 'CheckListItem deleted' }) @ApiResponse({ status: 200, description: "CheckListItem deleted", type: TMCheckListItem })
@ApiResponse({ status: 404, description: 'CheckListItem not found' }) @ApiResponse({ status: 404, description: "CheckListItem not found" })
remove(@Param('id') id: string): Promise<void> { remove(@Param("id") id: string): Promise<TMCheckListItem> {
return this.checkListItemService.remove(id); 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);
}
} }
@@ -1,58 +1,83 @@
import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from '@nestjs/common'; import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from "@nestjs/common";
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger'; import { ApiTags, ApiOperation, ApiResponse, ApiParam } from "@nestjs/swagger";
import { ProjectService } from '../providers/project.service'; import { ProjectService } from "../providers/project.service";
import { CreateProjectDto } from '../dto/project/create-project.dto'; import { CreateProjectDto } from "../dto/project/create-project.dto";
import { UpdateProjectDto } from '../dto/project/update-project.dto'; import { UpdateProjectDto } from "../dto/project/update-project.dto";
import { TMProject } from '../entities/project.entity'; 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";
@ApiTags('Task Manager - Projects') @AuthGuards()
@Controller('task-manager/projects') @AdminRoute()
@ApiTags("Task Manager - Projects")
@Controller("task-manager/projects")
export class ProjectController { export class ProjectController {
constructor(private readonly projectService: ProjectService) {} constructor(private readonly projectService: ProjectService) {}
@Get()
@ApiOperation({ summary: 'Get all projects, optionally filtered by workspace' })
@ApiQuery({ name: 'workspaceId', required: false, description: 'Filter projects by workspace ID' })
@ApiResponse({ status: 200, description: 'List of projects', type: [TMProject] })
findAll(@Query('workspaceId') workspaceId?: string): Promise<TMProject[]> {
if (workspaceId) return this.projectService.findByWorkspace(workspaceId);
return this.projectService.findAll();
}
@Get(':id') @Get(':id')
@ApiOperation({ summary: 'Get a project by ID' }) @ApiOperation({summary: 'Get one project by ID'})
@ApiParam({ name: 'id', description: 'Project ID' }) @ApiResponse({status: 200, description: 'received the project successfully!'})
@ApiResponse({ status: 200, description: 'Project found', type: TMProject }) @ApiResponse({status: 404, description: 'Project Not Found!'})
@ApiResponse({ status: 404, description: 'Project not found' }) @ApiResponse({status: 400, description: 'Bad Request!'})
findOne(@Param('id') id: string): Promise<TMProject> { findOne(@Param('id') id: string) {
return this.projectService.findOne(id); return this.projectService.findOneOrFail(id);
} }
@Post() @Post()
@ApiOperation({ summary: 'Create a new project' }) @PermissionsDec(PermissionEnum.PROJECT)
@ApiResponse({ status: 201, description: 'Project created', type: TMProject }) @ApiOperation({ summary: "Create a new project" })
@ApiResponse({ status: 400, description: 'One or more users are not members of the workspace' }) @ApiResponse({ status: 201, description: "Project created", type: TMProject })
@ApiResponse({ status: 404, description: 'Workspace not found' }) @ApiResponse({ status: 400, description: "One or more users are not members of the workspace" })
@ApiResponse({ status: 404, description: "Workspace not found" })
create(@Body() body: CreateProjectDto): Promise<TMProject> { create(@Body() body: CreateProjectDto): Promise<TMProject> {
return this.projectService.create(body); return this.projectService.create(body);
} }
@Patch(':id') @Patch(":id")
@ApiOperation({ summary: 'Update a project' }) @PermissionsDec(PermissionEnum.PROJECT)
@ApiParam({ name: 'id', description: 'Project ID' }) @ApiOperation({ summary: "Update a project" })
@ApiResponse({ status: 200, description: 'Project updated', type: TMProject }) @ApiParam({ name: "id", description: "Project ID" })
@ApiResponse({ status: 400, description: 'One or more users are not members of the workspace' }) @ApiResponse({ status: 200, description: "Project updated", type: TMProject })
@ApiResponse({ status: 404, description: 'Project not found' }) @ApiResponse({ status: 400, description: "One or more users are not members of the workspace" })
update(@Param('id') id: string, @Body() body: UpdateProjectDto): Promise<TMProject> { @ApiResponse({ status: 404, description: "Project not found" })
update(@Param("id") id: string, @Body() body: UpdateProjectDto): Promise<TMProject> {
return this.projectService.update(id, body); return this.projectService.update(id, body);
} }
@Delete(':id') @Delete(":id")
@ApiOperation({ summary: 'Delete a project' }) @PermissionsDec(PermissionEnum.PROJECT)
@ApiParam({ name: 'id', description: 'Project ID' }) @ApiOperation({ summary: "Delete a project" })
@ApiResponse({ status: 200, description: 'Project deleted' }) @ApiParam({ name: "id", description: "Project ID" })
@ApiResponse({ status: 404, description: 'Project not found' }) @ApiResponse({ status: 200, description: "Project deleted", type: TMProject })
remove(@Param('id') id: string): Promise<TMProject> { @ApiResponse({ status: 404, description: "Project not found" })
remove(@Param("id") id: string): Promise<TMProject> {
return this.projectService.remove(id); 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("detail/:id")
@ApiOperation({ summary: "Get project detail" })
@ApiResponse({ status: 200, description: "Got Project Detail Successfully!" })
@ApiResponse({ status: 400, description: "Bad request!" })
@ApiResponse({ status: 404, description: "The project was not found!" })
getProjectDetail(@Param() paramDto: ParamDto, @UserDec() user: ITokenPayload): Promise<TMProject | null> {
return this.projectService.getProjectDetail(paramDto.id, user.id, user.permissions);
}
} }
@@ -0,0 +1,42 @@
import { Controller, Post, Patch, Delete, Param, Body } from "@nestjs/common";
import { ApiTags, ApiOperation, ApiResponse, ApiParam } from "@nestjs/swagger";
import { RemarkService } from "../providers/remark.service";
import { CreateRemarkDto } from "../dto/remark/create-remark.dto";
import { UpdateRemarkDto } from "../dto/remark/update-remark.dto";
import { TMRemark } from "../entities/remark.entity";
import { AuthGuards } from "../../../common/decorators/auth-guard.decorator";
import { AdminRoute } from "../../../common/decorators/admin.decorator";
@AuthGuards()
@AdminRoute()
@ApiTags("Task Manager - Remarks")
@Controller("task-manager/remarks")
export class RemarkController {
constructor(private readonly remarkService: RemarkService) {}
@Post()
@ApiOperation({ summary: "Create a new remark" })
@ApiResponse({ status: 201, description: "Remark created", type: TMRemark })
@ApiResponse({ status: 404, description: "Task not found" })
create(@Body() body: CreateRemarkDto): Promise<TMRemark> {
return this.remarkService.create(body);
}
@Patch(":id")
@ApiOperation({ summary: "Update a remark" })
@ApiParam({ name: "id", description: "Remark ID" })
@ApiResponse({ status: 200, description: "Remark updated", type: TMRemark })
@ApiResponse({ status: 404, description: "Remark or Task not found" })
update(@Param("id") id: string, @Body() body: UpdateRemarkDto): Promise<TMRemark> {
return this.remarkService.update(id, body);
}
@Delete(":id")
@ApiOperation({ summary: "Delete a remark" })
@ApiParam({ name: "id", description: "Remark ID" })
@ApiResponse({ status: 200, description: "Remark deleted" })
@ApiResponse({ status: 404, description: "Remark not found" })
remove(@Param("id") id: string): Promise<void> {
return this.remarkService.remove(id);
}
}
@@ -1,33 +1,22 @@
import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from "@nestjs/common"; import { Controller, Post, Patch, Delete, Param, Body } from "@nestjs/common";
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from "@nestjs/swagger"; import { ApiTags, ApiOperation, ApiResponse, ApiParam } from "@nestjs/swagger";
import { TaskPhaseService } from "../providers/task-phase.service"; import { TaskPhaseService } from "../providers/task-phase.service";
import { CreateTaskPhaseDto } from "../dto/task-phase/create-task-phase.dto"; import { CreateTaskPhaseDto } from "../dto/task-phase/create-task-phase.dto";
import { UpdateTaskPhaseDto } from "../dto/task-phase/update-task-phase.dto"; import { UpdateTaskPhaseDto } from "../dto/task-phase/update-task-phase.dto";
import { TMTaskPhase } from "../entities/task-phase.entity"; import { TMTaskPhase } from "../entities/task-phase.entity";
import { AuthGuards } from "../../../common/decorators/auth-guard.decorator";
import { AdminRoute } from "../../../common/decorators/admin.decorator";
import { PermissionsDec } from "../../../common/decorators/permission.decorator";
import { PermissionEnum } from "../../users/enums/permission.enum";
@AuthGuards()
@AdminRoute()
@PermissionsDec(PermissionEnum.TASK_PHASE)
@ApiTags("Task Manager - Task Phases") @ApiTags("Task Manager - Task Phases")
@Controller("task-manager/task-phases") @Controller("task-manager/task-phases")
export class TaskPhaseController { export class TaskPhaseController {
constructor(private readonly taskPhaseService: TaskPhaseService) {} constructor(private readonly taskPhaseService: TaskPhaseService) {}
@Get()
@ApiOperation({ summary: "Get all task phases, optionally filtered by project" })
@ApiQuery({ name: "projectId", required: false, description: "Filter task phases by project ID" })
@ApiResponse({ status: 200, description: "List of task phases", type: [TMTaskPhase] })
findAll(@Query("projectId") projectId?: string): Promise<TMTaskPhase[]> {
if (projectId) return this.taskPhaseService.findByProject(projectId);
return this.taskPhaseService.findAll();
}
@Get(":id")
@ApiOperation({ summary: "Get a task phase by ID" })
@ApiParam({ name: "id", description: "TaskPhase ID" })
@ApiResponse({ status: 200, description: "TaskPhase found", type: TMTaskPhase })
@ApiResponse({ status: 404, description: "TaskPhase not found" })
findOne(@Param("id") id: string): Promise<TMTaskPhase> {
return this.taskPhaseService.findOne(id);
}
@Post() @Post()
@ApiOperation({ summary: "Create a new task phase" }) @ApiOperation({ summary: "Create a new task phase" })
@ApiResponse({ status: 201, description: "TaskPhase created", type: TMTaskPhase }) @ApiResponse({ status: 201, description: "TaskPhase created", type: TMTaskPhase })
@@ -1,67 +1,104 @@
import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from '@nestjs/common'; import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from "@nestjs/common";
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger'; import { ApiTags, ApiOperation, ApiResponse, ApiParam } from "@nestjs/swagger";
import { TaskService } from '../providers/task.service'; import { TaskService } from "../providers/task.service";
import { CreateTaskDto } from '../dto/task/create-task.dto'; import { CreateTaskDto } from "../dto/task/create-task.dto";
import { UpdateTaskDto } from '../dto/task/update-task.dto'; import { UpdateTaskDto } from "../dto/task/update-task.dto";
import { ChangeTaskPhaseDto } from '../dto/task/change-task-phase.dto'; import { ChangeTaskPhaseDto } from "../dto/task/change-task-phase.dto";
import { TMTask } from '../entities/task.entity'; import { TMTask } from "../entities/task.entity";
import { AuthGuards } from "../../../common/decorators/auth-guard.decorator";
import { AdminRoute } from "../../../common/decorators/admin.decorator";
import { UserDec } from "../../../common/decorators/user.decorator";
import { ParamDto } from "../../../common/DTO/param.dto";
import { PermissionsDec } from "../../../common/decorators/permission.decorator";
import { PermissionEnum } from "../../users/enums/permission.enum";
import { ITokenPayload } from "../../auth/interfaces/IToken-payload";
import { CursorPagination } from "../../../common/decorators/cursor-pagination.decorator";
import { CursorPaginationDto } from "../../../common/DTO/cursor-pagination.dto";
@ApiTags('Task Manager - Tasks') @AuthGuards()
@Controller('task-manager/tasks') @AdminRoute()
@ApiTags("Task Manager - Tasks")
@Controller("task-manager/tasks")
export class TaskController { export class TaskController {
constructor(private readonly taskService: TaskService) {} constructor(private readonly taskService: TaskService) {}
@Get()
@ApiOperation({ summary: 'Get all tasks, optionally filtered by phase' })
@ApiQuery({ name: 'taskPhaseId', required: false, description: 'Filter tasks by task phase ID' })
@ApiResponse({ status: 200, description: 'List of tasks', type: [TMTask] })
findAll(@Query('taskPhaseId') taskPhaseId?: string): Promise<TMTask[]> {
if (taskPhaseId) return this.taskService.findByPhase(taskPhaseId);
return this.taskService.findAll();
}
@Get(':id')
@ApiOperation({ summary: 'Get a task by ID' })
@ApiParam({ name: 'id', description: 'Task ID' })
@ApiResponse({ status: 200, description: 'Task found', type: TMTask })
@ApiResponse({ status: 404, description: 'Task not found' })
findOne(@Param('id') id: string): Promise<TMTask> {
return this.taskService.findOne(id);
}
@Post() @Post()
@ApiOperation({ summary: 'Create a new task' }) @PermissionsDec(PermissionEnum.TASK)
@ApiResponse({ status: 201, description: 'Task created', type: TMTask }) @ApiOperation({ summary: "Create a new task" })
@ApiResponse({ status: 404, description: 'TaskPhase not found' }) @ApiResponse({ status: 201, description: "Task created", type: TMTask })
@ApiResponse({ status: 404, description: "TaskPhase not found" })
create(@Body() body: CreateTaskDto): Promise<TMTask> { create(@Body() body: CreateTaskDto): Promise<TMTask> {
return this.taskService.create(body); return this.taskService.create(body);
} }
@Patch(':id') @Patch(":id")
@ApiOperation({ summary: 'Update a task' }) @PermissionsDec(PermissionEnum.TASK)
@ApiParam({ name: 'id', description: 'Task ID' }) @ApiOperation({ summary: "Update a task" })
@ApiResponse({ status: 200, description: 'Task updated', type: TMTask }) @ApiParam({ name: "id", description: "Task ID" })
@ApiResponse({ status: 404, description: 'Task or TaskPhase not found' }) @ApiResponse({ status: 200, description: "Task updated", type: TMTask })
update(@Param('id') id: string, @Body() body: UpdateTaskDto): Promise<TMTask> { @ApiResponse({ status: 404, description: "Task or TaskPhase not found" })
update(@Param("id") id: string, @Body() body: UpdateTaskDto): Promise<TMTask> {
return this.taskService.update(id, body); return this.taskService.update(id, body);
} }
@Patch(':id/change-phase') @Patch(":id/change-phase")
@ApiOperation({ summary: 'Move a task to a different phase within the same project' }) @PermissionsDec(PermissionEnum.TASK)
@ApiParam({ name: 'id', description: 'Task ID' }) @ApiOperation({ summary: "Move a task to a different phase within the same project" })
@ApiResponse({ status: 200, description: 'Task phase changed', type: TMTask }) @ApiParam({ name: "id", description: "Task ID" })
@ApiResponse({ status: 400, description: 'Target phase belongs to a different project' }) @ApiResponse({ status: 200, description: "Task phase changed", type: TMTask })
@ApiResponse({ status: 404, description: 'Task or TaskPhase not found' }) @ApiResponse({ status: 400, description: "Target phase belongs to a different project" })
changePhase(@Param('id') id: string, @Body() body: ChangeTaskPhaseDto): Promise<TMTask> { @ApiResponse({ status: 404, description: "Task or TaskPhase not found" })
changePhase(@Param("id") id: string, @Body() body: ChangeTaskPhaseDto): Promise<TMTask> {
return this.taskService.changePhase(id, body); return this.taskService.changePhase(id, body);
} }
@Delete(':id') @Delete(":id")
@ApiOperation({ summary: 'Delete a task' }) @PermissionsDec(PermissionEnum.TASK)
@ApiParam({ name: 'id', description: 'Task ID' }) @ApiOperation({ summary: "Delete a task" })
@ApiResponse({ status: 200, description: 'Task deleted' }) @ApiParam({ name: "id", description: "Task ID" })
@ApiResponse({ status: 404, description: 'Task not found' }) @ApiResponse({ status: 200, description: "Task deleted" })
remove(@Param('id') id: string): Promise<void> { @ApiResponse({ status: 404, description: "Task not found" })
remove(@Param("id") id: string): Promise<void> {
return this.taskService.remove(id); return this.taskService.remove(id);
} }
@Get("detail/:id")
@ApiOperation({ summary: "Get Task Detail" })
@ApiParam({ name: "id", description: "Task ID" })
@ApiResponse({ status: 200, description: "Task detail received!" })
@ApiResponse({ status: 404, description: "Task not found" })
getTaskDetail(@UserDec() user: ITokenPayload, @Param() paramDto: ParamDto): Promise<TMTask | null> {
return this.taskService.getTaskDetail(user.id, user.permissions, paramDto.id);
}
@Get("task-phase/:id")
@ApiOperation({ summary: "Get Tasks by task phase" })
@ApiParam({ name: "id", description: "Task phase id" })
@ApiResponse({ status: 200, description: "Got Tasks by task phase successfully!" })
@ApiResponse({ status: 400, description: "Bad request from user." })
@CursorPagination()
findByTaskPhase(
@Param("id") taskPhaseId: string,
@Query() pagination: CursorPaginationDto,
): Promise<{
data: TMTask[];
nextCursor: string | null;
previousCursor: string | null;
hasNextPage: boolean;
hasPreviousPage: boolean;
cursorPaginate: true;
}> {
return this.taskService.findTasksByTaskPhase(taskPhaseId, pagination);
}
@Patch("change-priority/:srcid/:desid")
@ApiOperation({ summary: "changing the task priority" })
@ApiParam({ name: "srcid", description: "ID of the source task" })
@ApiParam({ name: "desid", description: "ID of the destination task" })
@ApiResponse({ status: 200, description: "Changed the task priority successfully!" })
@ApiResponse({ status: 400, description: "Bad Request from user." })
@ApiResponse({ status: 404, description: "Task not found!" })
changePriority(@Param("srcid") srcId: string, @Param("desid") desId: string): Promise<TMTask> {
return this.taskService.changePriority(srcId, desId);
}
} }
@@ -0,0 +1,69 @@
import { Controller, Post, Patch, Delete, Param, Body, Get } from "@nestjs/common";
import { ApiTags, ApiOperation, ApiResponse, ApiParam } from "@nestjs/swagger";
import { TimeService } from "../providers/time.service";
import { CreateTimeDto } from "../dto/time/create-time.dto";
import { UpdateTimeDto } from "../dto/time/update-time.dto";
import { TMTime } from "../entities/time.entity";
import { AuthGuards } from "../../../common/decorators/auth-guard.decorator";
import { AdminRoute } from "../../../common/decorators/admin.decorator";
import { UserDec } from "../../../common/decorators/user.decorator";
@AuthGuards()
@AdminRoute()
@ApiTags("Task Manager - Times")
@Controller("task-manager/times")
export class TimeController {
constructor(private readonly timeService: TimeService) { }
@Get("task/:taskId")
@ApiOperation({ summary: "Get all time entries for a task" })
@ApiParam({ name: "taskId", description: "Task ID" })
getTaskTimes(@UserDec("id") adminId: string, @Param("taskId") taskId: string): Promise<TMTime[]> {
return this.timeService.getTaskTimes(adminId, taskId);
}
@Post()
@ApiOperation({ summary: "Create a new time entry for a task" })
@ApiResponse({ status: 201, description: "Time entry created", type: TMTime })
@ApiResponse({ status: 404, description: "Task not found" })
create(@UserDec("id") adminId: string, @Body() body: CreateTimeDto): Promise<TMTime> {
return this.timeService.create(adminId, body);
}
@Post("start/:taskId")
@ApiOperation({ summary: "Start tracking time for a task" })
@ApiParam({ name: "taskId", description: "Task ID" })
@ApiResponse({ status: 201, description: "Time tracking started", type: TMTime })
@ApiResponse({ status: 400, description: "A running time entry already exists" })
@ApiResponse({ status: 404, description: "Task not found" })
start(@UserDec("id") adminId: string, @Param("taskId") taskId: string): Promise<TMTime> {
return this.timeService.start(adminId, taskId);
}
@Post("end/:taskId")
@ApiOperation({ summary: "Stop the current running time entry for a task" })
@ApiParam({ name: "taskId", description: "Task ID" })
@ApiResponse({ status: 200, description: "Time tracking stopped", type: TMTime })
@ApiResponse({ status: 400, description: "No running time entry found" })
@ApiResponse({ status: 404, description: "Task not found" })
end(@UserDec("id") adminId: string, @Param("taskId") taskId: string): Promise<TMTime> {
return this.timeService.end(adminId, taskId);
}
@Patch(":id")
@ApiOperation({ summary: "Update a time entry" })
@ApiParam({ name: "id", description: "Time entry ID" })
@ApiResponse({ status: 200, description: "Time entry updated", type: TMTime })
@ApiResponse({ status: 404, description: "Time entry or Task not found" })
update(@Param("id") id: string, @Body() body: UpdateTimeDto): Promise<TMTime> {
return this.timeService.update(id, body);
}
@Delete(":id")
@ApiOperation({ summary: "Delete a time entry" })
@ApiParam({ name: "id", description: "Time entry ID" })
@ApiResponse({ status: 200, description: "Time entry deleted" })
@ApiResponse({ status: 404, description: "Time entry not found" })
remove(@Param("id") id: string): Promise<void> {
return this.timeService.remove(id);
}
}
@@ -1,55 +0,0 @@
import { Controller, Get, Post, Patch, Delete, Param, Body } from "@nestjs/common";
import { ApiTags, ApiOperation, ApiResponse, ApiParam } from "@nestjs/swagger";
import { WorkspaceTypeService } from "../providers/workspace-type.service";
import { CreateWorkspaceTypeDto } from "../dto/workspace-type/create-workspace-type.dto";
import { UpdateWorkspaceTypeDto } from "../dto/workspace-type/update-workspace-type.dto";
import { TMWorkspaceType } from "../entities/workspace-type.entity";
@ApiTags("Task Manager")
@Controller("task-manager/workspace-types")
export class WorkspaceTypeController {
constructor(private readonly workspaceTypeService: WorkspaceTypeService) {}
@Get()
@ApiOperation({ summary: "Get all workspace types" })
@ApiResponse({ status: 200, description: "List of workspace types", type: [TMWorkspaceType] })
findAll(): Promise<TMWorkspaceType[]> {
return this.workspaceTypeService.findAll();
}
@Get(":id")
@ApiOperation({ summary: "Get a workspace type by ID" })
@ApiParam({ name: "id", description: "WorkspaceType ID" })
@ApiResponse({ status: 200, description: "WorkspaceType found", type: TMWorkspaceType })
@ApiResponse({ status: 404, description: "WorkspaceType not found" })
findOne(@Param("id") id: string): Promise<TMWorkspaceType> {
return this.workspaceTypeService.findOne(id);
}
@Post()
@ApiOperation({ summary: "Create a new workspace type" })
@ApiResponse({ status: 201, description: "WorkspaceType created", type: TMWorkspaceType })
@ApiResponse({ status: 409, description: "WorkspaceType with this name already exists" })
create(@Body() body: CreateWorkspaceTypeDto): Promise<TMWorkspaceType> {
return this.workspaceTypeService.create(body);
}
@Patch(":id")
@ApiOperation({ summary: "Update a workspace type" })
@ApiParam({ name: "id", description: "WorkspaceType ID" })
@ApiResponse({ status: 200, description: "WorkspaceType updated", type: TMWorkspaceType })
@ApiResponse({ status: 404, description: "WorkspaceType not found" })
@ApiResponse({ status: 409, description: "WorkspaceType with this name already exists" })
update(@Param("id") id: string, @Body() body: UpdateWorkspaceTypeDto): Promise<TMWorkspaceType> {
return this.workspaceTypeService.update(id, body);
}
@Delete(":id")
@ApiOperation({ summary: "Delete a workspace type" })
@ApiParam({ name: "id", description: "WorkspaceType ID" })
@ApiResponse({ status: 200, description: "WorkspaceType deleted" })
@ApiResponse({ status: 404, description: "WorkspaceType not found" })
remove(@Param("id") id: string): Promise<TMWorkspaceType> {
return this.workspaceTypeService.remove(id);
}
}
@@ -4,29 +4,32 @@ import { WorkspaceService } from '../providers/workspace.service';
import { CreateWorkspaceDto } from '../dto/workspace/create-workspace.dto'; import { CreateWorkspaceDto } from '../dto/workspace/create-workspace.dto';
import { UpdateWorkspaceDto } from '../dto/workspace/update-workspace.dto'; import { UpdateWorkspaceDto } from '../dto/workspace/update-workspace.dto';
import { TMWorkspace } from '../entities/workspace.entity'; 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';
@ApiTags('Task Manager') @AuthGuards()
@AdminRoute()
@ApiTags('Task Manager - Workspaces')
@Controller('task-manager/workspaces') @Controller('task-manager/workspaces')
export class WorkspaceController { export class WorkspaceController {
constructor(private readonly workspaceService: WorkspaceService) {} constructor(private readonly workspaceService: WorkspaceService) {}
@Get()
@ApiOperation({ summary: 'Get all workspaces' })
@ApiResponse({ status: 200, description: 'List of workspaces', type: [TMWorkspace] })
findAll(): Promise<TMWorkspace[]> {
return this.workspaceService.findAll();
}
@Get(':id') @Get(':id')
@ApiOperation({ summary: 'Get a workspace by ID' }) @PermissionsDec(PermissionEnum.WORKSPACE)
@ApiParam({ name: 'id', description: 'Workspace ID' }) @ApiOperation({summary: "Get one workspace by ID"})
@ApiResponse({ status: 200, description: 'Workspace found', type: TMWorkspace }) @ApiResponse({status: 200, description: 'Received workspace', type: TMWorkspace})
@ApiResponse({ status: 404, description: 'Workspace not found' }) @ApiResponse({status: 404, description: 'Workspace Not Found!'})
@ApiResponse({status: 400, description: 'Bad Request!'})
findOne(@Param('id') id: string): Promise<TMWorkspace> { findOne(@Param('id') id: string): Promise<TMWorkspace> {
return this.workspaceService.findOneOrFail(id); return this.workspaceService.findOneOrFail(id);
} }
@Post() @Post()
@PermissionsDec(PermissionEnum.WORKSPACE)
@ApiOperation({ summary: 'Create a new workspace' }) @ApiOperation({ summary: 'Create a new workspace' })
@ApiResponse({ status: 201, description: 'Workspace created', type: TMWorkspace }) @ApiResponse({ status: 201, description: 'Workspace created', type: TMWorkspace })
create(@Body() body: CreateWorkspaceDto): Promise<TMWorkspace> { create(@Body() body: CreateWorkspaceDto): Promise<TMWorkspace> {
@@ -34,6 +37,7 @@ export class WorkspaceController {
} }
@Patch(':id') @Patch(':id')
@PermissionsDec(PermissionEnum.WORKSPACE)
@ApiOperation({ summary: 'Update a workspace' }) @ApiOperation({ summary: 'Update a workspace' })
@ApiParam({ name: 'id', description: 'Workspace ID' }) @ApiParam({ name: 'id', description: 'Workspace ID' })
@ApiResponse({ status: 200, description: 'Workspace updated', type: TMWorkspace }) @ApiResponse({ status: 200, description: 'Workspace updated', type: TMWorkspace })
@@ -43,6 +47,7 @@ export class WorkspaceController {
} }
@Delete(':id') @Delete(':id')
@PermissionsDec(PermissionEnum.WORKSPACE)
@ApiOperation({ summary: 'Delete a workspace' }) @ApiOperation({ summary: 'Delete a workspace' })
@ApiParam({ name: 'id', description: 'Workspace ID' }) @ApiParam({ name: 'id', description: 'Workspace ID' })
@ApiResponse({ status: 200, description: 'Workspace deleted' }) @ApiResponse({ status: 200, description: 'Workspace deleted' })
@@ -50,4 +55,12 @@ export class WorkspaceController {
remove(@Param('id') id: string): Promise<TMWorkspace> { remove(@Param('id') id: string): Promise<TMWorkspace> {
return this.workspaceService.remove(id); return this.workspaceService.remove(id);
} }
@Get('byuser')
@ApiOperation({summary: 'Get User WorkSpaces'})
@ApiResponse({ status: 200, description: 'Got Workspaces successfully!' })
@ApiResponse({ status: 404, description: 'Bad Request!' })
findByUser(@UserDec() user: ITokenPayload) {
return this.workspaceService.findByUser(user.id, user.permissions);
}
} }
@@ -0,0 +1,26 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsString, IsNotEmpty, IsUUID, IsEnum, MaxLength } from "class-validator";
import { TMAttachmentType } from "../../enums/attachment-type.enum";
export class CreateAttachmentDto {
@ApiProperty({ description: "Title of the attachment", example: "Project Requirements Doc" })
@IsString()
@IsNotEmpty()
@MaxLength(150)
title: string;
@ApiProperty({ description: "Type of the attachment", enum: TMAttachmentType, example: TMAttachmentType.FILE })
@IsEnum(TMAttachmentType)
@IsNotEmpty()
type: TMAttachmentType;
@ApiProperty({ description: "File URL or path of the attachment", example: "https://cdn.example.com/files/requirements.pdf" })
@IsString()
@IsNotEmpty()
file: string;
@ApiProperty({ description: "ID of the task this attachment belongs to", example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" })
@IsUUID()
@IsNotEmpty()
taskId: string;
}
@@ -0,0 +1,4 @@
import { PartialType } from "@nestjs/swagger";
import { CreateAttachmentDto } from "./create-attachment.dto";
export class UpdateAttachmentDto extends PartialType(CreateAttachmentDto) {}
@@ -0,0 +1,21 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsString, IsNotEmpty, IsUUID, IsOptional, MaxLength } from "class-validator";
export class CreateRemarkDto {
@ApiProperty({ description: "Title of the remark", example: "This task needs review" })
@IsString()
@IsNotEmpty()
@MaxLength(150)
title: string;
@ApiProperty({ description: "Color code for the remark", required: false, example: "#F59E0B" })
@IsString()
@IsOptional()
@MaxLength(20)
color?: string;
@ApiProperty({ description: "ID of the task this remark belongs to", example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" })
@IsUUID()
@IsNotEmpty()
taskId: string;
}
@@ -0,0 +1,4 @@
import { PartialType } from "@nestjs/swagger";
import { CreateRemarkDto } from "./create-remark.dto";
export class UpdateRemarkDto extends PartialType(CreateRemarkDto) {}
@@ -0,0 +1,19 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsDateString, IsNotEmpty, IsUUID } from "class-validator";
export class CreateTimeDto {
@ApiProperty({ description: "Start time of the entry", example: "2026-07-15T08:00:00.000Z" })
@IsDateString()
@IsNotEmpty()
startedAt: string;
@ApiProperty({ description: "End time of the entry", example: "2026-07-15T10:30:00.000Z" })
@IsDateString()
@IsNotEmpty()
endedAt: string;
@ApiProperty({ description: "ID of the task this time entry belongs to", example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" })
@IsUUID()
@IsNotEmpty()
taskId: string;
}
@@ -0,0 +1,4 @@
import { PartialType } from "@nestjs/swagger";
import { CreateTimeDto } from "./create-time.dto";
export class UpdateTimeDto extends PartialType(CreateTimeDto) {}
@@ -1,25 +0,0 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsString, IsOptional, IsInt, IsNotEmpty, MaxLength, Min } from "class-validator";
export class CreateWorkspaceTypeDto {
@ApiProperty({ description: "Name of the workspace type", example: "Engineering" })
@IsString()
@IsNotEmpty()
@MaxLength(100)
name: string;
@ApiProperty({
description: "Description of the workspace type",
required: false,
example: "Workspace type for engineering related work",
})
@IsString()
@IsOptional()
description?: string;
@ApiProperty({ description: "Display order of the workspace type", example: 1 })
@IsInt()
@IsNotEmpty()
@Min(0)
order: number;
}
@@ -1,4 +0,0 @@
import { PartialType } from '@nestjs/swagger';
import { CreateWorkspaceTypeDto } from './create-workspace-type.dto';
export class UpdateWorkspaceTypeDto extends PartialType(CreateWorkspaceTypeDto) {}
@@ -8,11 +8,6 @@ export class CreateWorkspaceDto {
@MaxLength(100) @MaxLength(100)
name: string; name: string;
@ApiProperty({ description: "ID of the workspace type", example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" })
@IsUUID()
@IsNotEmpty()
workspaceTypeId: string;
@ApiProperty({ description: "Description of the workspace", required: false, example: "Workspace for the engineering team" }) @ApiProperty({ description: "Description of the workspace", required: false, example: "Workspace for the engineering team" })
@IsString() @IsString()
@IsOptional() @IsOptional()
@@ -4,6 +4,7 @@ import { TMTaskPhase } from "./task-phase.entity";
import { TMRemark } from "./remark.entity"; import { TMRemark } from "./remark.entity";
import { TMAttachment } from "./attachment.entity"; import { TMAttachment } from "./attachment.entity";
import { TMCheckListItem } from "./check-list-item.entity"; import { TMCheckListItem } from "./check-list-item.entity";
import { TMTime } from "./time.entity";
import { User } from "../../users/entities/user.entity"; import { User } from "../../users/entities/user.entity";
@Entity("tm_tasks") @Entity("tm_tasks")
@@ -23,6 +24,9 @@ export class TMTask extends BaseEntity {
@Column({ type: "timestamptz", nullable: true }) @Column({ type: "timestamptz", nullable: true })
endDate: Date; endDate: Date;
@Column({type: 'int', default: 1})
priority: number;
// --- Relations --- // --- Relations ---
@ManyToOne(() => TMTaskPhase, (taskPhase) => taskPhase.tasks, { @ManyToOne(() => TMTaskPhase, (taskPhase) => taskPhase.tasks, {
@@ -50,6 +54,11 @@ export class TMTask extends BaseEntity {
}) })
checkListItems: TMCheckListItem[]; checkListItems: TMCheckListItem[];
@OneToMany(() => TMTime, (time) => time.task, {
cascade: ["insert", "update"],
})
times: TMTime[];
@ManyToMany(() => User, (user) => user.tasks) @ManyToMany(() => User, (user) => user.tasks)
@JoinTable({ @JoinTable({
name: "tm_task_users", name: "tm_task_users",
@@ -57,4 +66,10 @@ export class TMTask extends BaseEntity {
inverseJoinColumn: { name: "userId", referencedColumnName: "id" }, inverseJoinColumn: { name: "userId", referencedColumnName: "id" },
}) })
users: User[]; users: User[];
// deriven(calculated) attributes. not database columns
attachmentCount?: number;
checkListItemCount?: number;
completedCheckListItemCount?: number;
time?: number;
} }
@@ -0,0 +1,38 @@
import { Column, Entity, JoinColumn, ManyToOne } from "typeorm";
import { BaseEntity } from "../../../common/entities/base.entity";
import { TMTask } from "./task.entity";
import { User } from "../../users/entities/user.entity";
@Entity("tm_times")
export class TMTime extends BaseEntity {
@Column({ type: "timestamptz" })
startedAt: Date;
@Column({ type: "timestamptz", nullable: true })
endedAt: Date | null;
@Column({ type: "int", nullable: true })
duration: number | null;
// --- Relations ---
@ManyToOne(() => User, {
nullable: false,
onDelete: "RESTRICT",
})
@JoinColumn({ name: "adminId" })
admin: User;
@Column({ type: "uuid" })
adminId: string;
@ManyToOne(() => TMTask, (task) => task.times, {
nullable: false,
onDelete: "CASCADE",
})
@JoinColumn({ name: "taskId" })
task: TMTask;
@Column({ type: "uuid" })
taskId: string;
}
@@ -1,20 +0,0 @@
import { Column, Entity, OneToMany } from "typeorm";
import { BaseEntity } from "../../../common/entities/base.entity";
import { TMWorkspace } from "./workspace.entity";
@Entity("tm_workspace_types")
export class TMWorkspaceType extends BaseEntity {
@Column({ type: "varchar", length: 100, unique: true })
name: string;
@Column({ type: "text", nullable: true })
description: string;
@Column({ type: "int", default: 0 })
order: number;
// --- Relations ---
@OneToMany(() => TMWorkspace, (workspace) => workspace.workspaceType)
workspaces: TMWorkspace[];
}
@@ -1,12 +1,11 @@
import { Column, Entity, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToMany } from "typeorm"; import { Column, Entity, JoinTable, ManyToMany, OneToMany } from "typeorm";
import { BaseEntity } from "../../../common/entities/base.entity"; import { BaseEntity } from "../../../common/entities/base.entity";
import { TMWorkspaceType } from "./workspace-type.entity";
import { TMProject } from "./project.entity"; import { TMProject } from "./project.entity";
import { User } from "../../users/entities/user.entity"; import { User } from "../../users/entities/user.entity";
@Entity("tm_workspaces") @Entity("tm_workspaces")
export class TMWorkspace extends BaseEntity { export class TMWorkspace extends BaseEntity {
@Column({ type: "varchar", length: 100 }) @Column({ type: "varchar", length: 100, unique: true })
name: string; name: string;
@Column({ type: "text", nullable: true }) @Column({ type: "text", nullable: true })
@@ -20,13 +19,6 @@ export class TMWorkspace extends BaseEntity {
// --- Relations --- // --- Relations ---
@ManyToOne(() => TMWorkspaceType, { nullable: false, eager: true })
@JoinColumn({ name: "workspaceTypeId" })
workspaceType: TMWorkspaceType;
@Column({ type: "uuid" })
workspaceTypeId: string;
@OneToMany(() => TMProject, (project) => project.workspace, { @OneToMany(() => TMProject, (project) => project.workspace, {
cascade: ["insert", "update"], cascade: ["insert", "update"],
}) })
@@ -0,0 +1,44 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { AttachmentRepository } from "../repositories/attachment.repository";
import { TMAttachment } from "../entities/attachment.entity";
import { CreateAttachmentDto } from "../dto/attachment/create-attachment.dto";
import { UpdateAttachmentDto } from "../dto/attachment/update-attachment.dto";
import { attachmentMessage } from "../../../common/enums/message.enum";
import { TaskService } from "./task.service";
@Injectable()
export class AttachmentService {
constructor(
private readonly attachmentRepository: AttachmentRepository,
private readonly taskService: TaskService,
) {}
async findOneOrFail(id: string): Promise<TMAttachment> {
const attachment = await this.attachmentRepository.findOne({ where: { id }, relations: { task: true } });
if (!attachment) throw new NotFoundException(attachmentMessage.ATTACHMENT_NOT_FOUND);
return attachment;
}
async create(dto: CreateAttachmentDto): Promise<TMAttachment> {
await this.taskService.findOneOrFail(dto.taskId);
const attachment = this.attachmentRepository.create(dto);
return this.attachmentRepository.save(attachment);
}
async update(id: string, dto: UpdateAttachmentDto): Promise<TMAttachment> {
const attachment = await this.findOneOrFail(id);
if (dto.taskId) {
await this.taskService.findOneOrFail(dto.taskId);
}
Object.assign(attachment, dto);
return this.attachmentRepository.save(attachment);
}
async remove(id: string): Promise<void> {
await this.findOneOrFail(id);
await this.attachmentRepository.delete(id);
}
}
@@ -1,53 +1,54 @@
import { Injectable, NotFoundException } from "@nestjs/common"; import { Injectable, NotFoundException } from '@nestjs/common';
import { CheckListItemRepository } from "../repositories/check-list-item.repository"; import { CheckListItemRepository } from '../repositories/check-list-item.repository';
import { TaskRepository } from "../repositories/task.repository"; import { TMCheckListItem } from '../entities/check-list-item.entity';
import { TMCheckListItem } from "../entities/check-list-item.entity"; import { CreateCheckListItemDto } from '../dto/check-list-item/create-check-list-item.dto';
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 { UpdateCheckListItemDto } from "../dto/check-list-item/update-check-list-item.dto"; import { checkListItemMessage } from '../../../common/enums/message.enum';
import { TaskService } from './task.service';
@Injectable() @Injectable()
export class CheckListItemService { export class CheckListItemService {
constructor( constructor(
private readonly checkListItemRepository: CheckListItemRepository, private readonly checkListItemRepository: CheckListItemRepository,
private readonly taskRepository: TaskRepository, private readonly taskService: TaskService,
) {} ) {}
findAll(): Promise<TMCheckListItem[]> { async findOneOrFail(id: string): Promise<TMCheckListItem> {
return this.checkListItemRepository.findAll();
}
findByTask(taskId: string): Promise<TMCheckListItem[]> {
return this.checkListItemRepository.findByTask(taskId);
}
async findOne(id: string): Promise<TMCheckListItem> {
const checkListItem = await this.checkListItemRepository.findOneById(id); const checkListItem = await this.checkListItemRepository.findOneById(id);
if (!checkListItem) throw new NotFoundException(`CheckListItem #${id} not found`); if (!checkListItem) throw new NotFoundException(checkListItemMessage.CHECKLISTITEM_NOT_FOUND);
return checkListItem; return checkListItem;
} }
async create(dto: CreateCheckListItemDto): Promise<TMCheckListItem> { async create(dto: CreateCheckListItemDto): Promise<TMCheckListItem> {
const task = await this.taskRepository.findOneById(dto.taskId); await this.taskService.findOneOrFail(dto.taskId);
if (!task) throw new NotFoundException(`Task #${dto.taskId} not found`);
const checkListItem = this.checkListItemRepository.create(dto); const checkListItem = this.checkListItemRepository.create(dto);
return this.checkListItemRepository.save(checkListItem); return this.checkListItemRepository.save(checkListItem);
} }
async update(id: string, dto: UpdateCheckListItemDto): Promise<TMCheckListItem> { async update(id: string, dto: UpdateCheckListItemDto): Promise<TMCheckListItem> {
const checkListItem = await this.findOne(id); const checkListItem = await this.findOneOrFail(id);
if (dto.taskId) { if (dto.taskId) {
const task = await this.taskRepository.findOneById(dto.taskId); await this.taskService.findOneOrFail(dto.taskId);
if (!task) throw new NotFoundException(`Task #${dto.taskId} not found`);
} }
Object.assign(checkListItem, dto); Object.assign(checkListItem, dto);
return this.checkListItemRepository.save(checkListItem); return this.checkListItemRepository.save(checkListItem);
} }
async remove(id: string): Promise<void> { async remove(id: string): Promise<TMCheckListItem> {
await this.findOne(id); const checkListItem = await this.findOneOrFail(id);
await this.checkListItemRepository.delete(id); await this.checkListItemRepository.delete(id);
return checkListItem;
}
async toggleStatus(id: string) : Promise<TMCheckListItem> {
const checkListItem = await this.findOneOrFail(id);
const isDone = checkListItem.isDone;
checkListItem.isDone = !isDone;
await this.checkListItemRepository.save(checkListItem);
return checkListItem;
} }
} }
@@ -1,41 +1,52 @@
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm"; import { InjectRepository } from "@nestjs/typeorm";
import { In, Repository } from "typeorm"; import { In, Repository } from "typeorm";
import { ProjectRepository } from "../repositories/project.repository"; import { ProjectRepository } from "../repositories/project.repository";
import { TMProject } from "../entities/project.entity"; import { TMProject } from "../entities/project.entity";
import { CreateProjectDto } from "../dto/project/create-project.dto"; import { CreateProjectDto } from "../dto/project/create-project.dto";
import { UpdateProjectDto } from "../dto/project/update-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 { User } from "../../users/entities/user.entity";
import { WorkspaceRepository } from "../repositories/workspace.repository"; import { ProjectMessage, WorkSpaceMessage } from "../../../common/enums/message.enum";
import { WorkspaceService } from "./workspace.service";
import { PermissionEnum } from "../../users/enums/permission.enum";
@Injectable() @Injectable()
export class ProjectService { export class ProjectService {
constructor( constructor(
private readonly projectRepository: ProjectRepository, private readonly projectRepository: ProjectRepository,
private readonly workspaceRepository: WorkspaceRepository, private readonly workspaceService: WorkspaceService,
@InjectRepository(User) @InjectRepository(User)
private readonly userRepository: Repository<User>, private readonly userRepository: Repository<User>,
) {} ) {}
findAll(): Promise<TMProject[]> { async findOneOrFail(id: string): Promise<TMProject> {
return this.projectRepository.findAll(); const project = await this.projectRepository.findOne({
where: { id },
relations: {
users: true,
taskPhases: true
},
select: {
users: {
id: true,
firstName: true,
lastName: true
},
taskPhases: {
id: true,
name: true,
} }
findByWorkspace(workspaceId: string): Promise<TMProject[]> {
return this.projectRepository.findByWorkspace(workspaceId);
} }
});
async findOne(id: string): Promise<TMProject> { if (!project) throw new NotFoundException(ProjectMessage.PROJECT_NOT_FOUND);
const project = await this.projectRepository.findOneById(id);
if (!project) throw new NotFoundException(`Project #${id} not found`);
return project; return project;
} }
async create(dto: CreateProjectDto): Promise<TMProject> { async create(dto: CreateProjectDto): Promise<TMProject> {
const { userIds, startDate, ...rest } = dto; const { userIds, startDate, ...rest } = dto;
const workspace = await this.workspaceRepository.findOneById(dto.workspaceId); const workspace = await this.workspaceService.findOneOrFail(dto.workspaceId);
if (!workspace) throw new NotFoundException(`Workspace #${dto.workspaceId} not found`);
const project = this.projectRepository.create({ const project = this.projectRepository.create({
...rest, ...rest,
@@ -46,7 +57,7 @@ export class ProjectService {
const workspaceUserIds = workspace.users.map((u) => u.id); const workspaceUserIds = workspace.users.map((u) => u.id);
const invalidUsers = userIds.filter((id) => !workspaceUserIds.includes(id)); const invalidUsers = userIds.filter((id) => !workspaceUserIds.includes(id));
if (invalidUsers.length) { if (invalidUsers.length) {
throw new BadRequestException(`Users [${invalidUsers.join(", ")}] are not members of this workspace`); throw new BadRequestException(WorkSpaceMessage.USERS_DONT_BELONG_TO_THIS_WORKSPACE);
} }
project.users = await this.userRepository.findBy({ id: In(userIds) }); project.users = await this.userRepository.findBy({ id: In(userIds) });
} }
@@ -55,7 +66,8 @@ export class ProjectService {
} }
async update(id: string, dto: UpdateProjectDto): Promise<TMProject> { async update(id: string, dto: UpdateProjectDto): Promise<TMProject> {
const project = await this.findOne(id); const project = await this.findOneOrFail(id);
const { userIds, startDate, ...rest } = dto; const { userIds, startDate, ...rest } = dto;
Object.assign(project, { Object.assign(project, {
@@ -66,13 +78,12 @@ export class ProjectService {
if (userIds) { if (userIds) {
if (userIds.length) { if (userIds.length) {
const workspaceId = dto.workspaceId ?? project.workspaceId; const workspaceId = dto.workspaceId ?? project.workspaceId;
const workspace = await this.workspaceRepository.findOneById(workspaceId); const workspace = await this.workspaceService.findOneOrFail(workspaceId);
if (!workspace) throw new NotFoundException(`Workspace #${workspaceId} not found`);
const workspaceUserIds = workspace.users.map((u) => u.id); const workspaceUserIds = workspace.users.map((u) => u.id);
const invalidUsers = userIds.filter((uid) => !workspaceUserIds.includes(uid)); const invalidUsers = userIds.filter((uid) => !workspaceUserIds.includes(uid));
if (invalidUsers.length) { if (invalidUsers.length) {
throw new BadRequestException(`Users [${invalidUsers.join(", ")}] are not members of this workspace`); throw new BadRequestException(WorkSpaceMessage.USERS_DONT_BELONG_TO_THIS_WORKSPACE);
} }
project.users = await this.userRepository.findBy({ id: In(userIds) }); project.users = await this.userRepository.findBy({ id: In(userIds) });
} else { } else {
@@ -84,9 +95,74 @@ export class ProjectService {
} }
async remove(id: string): Promise<TMProject> { async remove(id: string): Promise<TMProject> {
const project = await this.findOne(id); const project = await this.projectRepository.findOne({
await this.projectRepository.delete(id); 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; 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 && !isPermissionIncluded) {
res = await this.projectRepository.findUserProjects(userId, wsId, skip, limit);
} else if (isPermissionIncluded) {
res = await this.projectRepository.findAndCount({ where: { workspaceId: wsId } });
} else {
throw new ForbiddenException(WorkSpaceMessage.FORBIDDEN);
}
const [projects, count] = res;
return { projects, count, paginate: true };
}
async getProjectDetail(projectId: string, userId: string, permissions: PermissionEnum[]): Promise<TMProject | null> {
const project = await this.projectRepository.findOne({
where: { id: projectId },
relations: {
users: true,
},
});
if (!project) throw new NotFoundException(ProjectMessage.PROJECT_NOT_FOUND);
const isUserMember = project.users.some((user) => user.id === userId);
if (!isUserMember && !permissions.includes(PermissionEnum.VIEW_ALL_PROJECTS))
throw new ForbiddenException(ProjectMessage.NOT_MEMBER_OF_PROJECT);
const projectDetail = await this.projectRepository.getProjectDetail(projectId);
return projectDetail;
}
} }
@@ -0,0 +1,44 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { RemarkRepository } from "../repositories/remark.repository";
import { TMRemark } from "../entities/remark.entity";
import { CreateRemarkDto } from "../dto/remark/create-remark.dto";
import { UpdateRemarkDto } from "../dto/remark/update-remark.dto";
import { remarkMessage } from "../../../common/enums/message.enum";
import { TaskService } from './task.service';
@Injectable()
export class RemarkService {
constructor(
private readonly remarkRepository: RemarkRepository,
private readonly taskService: TaskService,
) {}
async findOneOrFail(id: string): Promise<TMRemark> {
const remark = await this.remarkRepository.findOne({ where: { id } });
if (!remark) throw new NotFoundException(remarkMessage.REMARK_NOT_FOUND);
return remark;
}
async create(dto: CreateRemarkDto): Promise<TMRemark> {
await this.taskService.findOneOrFail(dto.taskId);
const remark = this.remarkRepository.create(dto);
return this.remarkRepository.save(remark);
}
async update(id: string, dto: UpdateRemarkDto): Promise<TMRemark> {
const remark = await this.findOneOrFail(id);
if (dto.taskId) {
await this.taskService.findOneOrFail(dto.taskId);
}
Object.assign(remark, dto);
return this.remarkRepository.save(remark);
}
async remove(id: string): Promise<void> {
await this.findOneOrFail(id);
await this.remarkRepository.delete(id);
}
}
@@ -1,45 +1,35 @@
import { Injectable, NotFoundException } from "@nestjs/common"; import { Injectable, NotFoundException } from "@nestjs/common";
import { TaskPhaseRepository } from "../repositories/task-phase.repository"; import { TaskPhaseRepository } from "../repositories/task-phase.repository";
import { ProjectRepository } from "../repositories/project.repository";
import { TMTaskPhase } from "../entities/task-phase.entity"; import { TMTaskPhase } from "../entities/task-phase.entity";
import { CreateTaskPhaseDto } from "../dto/task-phase/create-task-phase.dto"; import { CreateTaskPhaseDto } from "../dto/task-phase/create-task-phase.dto";
import { UpdateTaskPhaseDto } from "../dto/task-phase/update-task-phase.dto"; import { UpdateTaskPhaseDto } from "../dto/task-phase/update-task-phase.dto";
import { TaskPhaseMessage } from "../../../common/enums/message.enum";
import { ProjectService } from "./project.service";
@Injectable() @Injectable()
export class TaskPhaseService { export class TaskPhaseService {
constructor( constructor(
private readonly taskPhaseRepository: TaskPhaseRepository, private readonly taskPhaseRepository: TaskPhaseRepository,
private readonly projectRepository: ProjectRepository, private readonly projectService: ProjectService
) {} ) {}
findAll(): Promise<TMTaskPhase[]> { async findOneOrFail(id: string): Promise<TMTaskPhase> {
return this.taskPhaseRepository.findAll(); const taskPhase = await this.taskPhaseRepository.findOne({where: {id}});
} if (!taskPhase) throw new NotFoundException(TaskPhaseMessage.TASK_PHASE_NOT_FOUND);
findByProject(projectId: string): Promise<TMTaskPhase[]> {
return this.taskPhaseRepository.findByProject(projectId);
}
async findOne(id: string): Promise<TMTaskPhase> {
const taskPhase = await this.taskPhaseRepository.findOneById(id);
if (!taskPhase) throw new NotFoundException(`TaskPhase #${id} not found`);
return taskPhase; return taskPhase;
} }
async create(dto: CreateTaskPhaseDto): Promise<TMTaskPhase> { async create(dto: CreateTaskPhaseDto): Promise<TMTaskPhase> {
const project = await this.projectRepository.findOneById(dto.projectId); await this.projectService.findOneOrFail(dto.projectId);
if (!project) throw new NotFoundException(`Project #${dto.projectId} not found`);
const taskPhase = this.taskPhaseRepository.create(dto); const taskPhase = this.taskPhaseRepository.create(dto);
return this.taskPhaseRepository.save(taskPhase); return this.taskPhaseRepository.save(taskPhase);
} }
async update(id: string, dto: UpdateTaskPhaseDto): Promise<TMTaskPhase> { async update(id: string, dto: UpdateTaskPhaseDto): Promise<TMTaskPhase> {
const taskPhase = await this.findOne(id); const taskPhase = await this.findOneOrFail(id);
if (dto.projectId) { if (dto.projectId) {
const project = await this.projectRepository.findOneById(dto.projectId); await this.projectService.findOneOrFail(dto.projectId);
if (!project) throw new NotFoundException(`Project #${dto.projectId} not found`);
} }
Object.assign(taskPhase, dto); Object.assign(taskPhase, dto);
@@ -47,7 +37,7 @@ export class TaskPhaseService {
} }
async remove(id: string): Promise<void> { async remove(id: string): Promise<void> {
await this.findOne(id); await this.findOneOrFail(id);
await this.taskPhaseRepository.delete(id); await this.taskPhaseRepository.delete(id);
} }
} }
@@ -1,13 +1,18 @@
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from "@nestjs/typeorm";
import { In, Repository } from 'typeorm'; import { In, Repository } from "typeorm";
import { TaskRepository } from '../repositories/task.repository'; import { TaskRepository } from "../repositories/task.repository";
import { TaskPhaseRepository } from '../repositories/task-phase.repository'; import { TMTask } from "../entities/task.entity";
import { TMTask } from '../entities/task.entity'; import { CreateTaskDto } from "../dto/task/create-task.dto";
import { CreateTaskDto } from '../dto/task/create-task.dto'; import { UpdateTaskDto } from "../dto/task/update-task.dto";
import { UpdateTaskDto } from '../dto/task/update-task.dto'; import { ChangeTaskPhaseDto } from "../dto/task/change-task-phase.dto";
import { ChangeTaskPhaseDto } from '../dto/task/change-task-phase.dto'; import { User } from "../../users/entities/user.entity";
import { User } from '../../users/entities/user.entity'; import { TaskMessage, TaskPhaseMessage } from "../../../common/enums/message.enum";
import { TaskPhaseRepository } from "../repositories/task-phase.repository";
import { TMTaskPhase } from "../entities/task-phase.entity";
import { PermissionEnum } from "../../users/enums/permission.enum";
import { encodeCursor } from "../../utils/providers/cursor-pagination.utils";
import { CursorDirection, CursorPaginationDto } from "../../../common/DTO/cursor-pagination.dto";
@Injectable() @Injectable()
export class TaskService { export class TaskService {
@@ -18,25 +23,41 @@ export class TaskService {
private readonly userRepository: Repository<User>, private readonly userRepository: Repository<User>,
) {} ) {}
findAll(): Promise<TMTask[]> { async findOneOrFail(id: string): Promise<TMTask> {
return this.taskRepository.findAll(); const task = await this.taskRepository.findOne({
} where: { id },
relations: {
findByPhase(taskPhaseId: string): Promise<TMTask[]> { taskPhase: true,
return this.taskRepository.findByPhase(taskPhaseId); users: true,
} },
});
async findOne(id: string): Promise<TMTask> { if (!task) throw new NotFoundException(TaskMessage.TASK_NOT_FOUND);
const task = await this.taskRepository.findOneById(id);
if (!task) throw new NotFoundException(`Task #${id} not found`);
return task; return task;
} }
async findTaskPhaseOrFail(taskPhaseId: string): Promise<TMTaskPhase> {
const taskPhase = await this.taskPhaseRepository.findOne({
where: { id: taskPhaseId },
relations: {
project: {
users: true,
},
},
});
if (!taskPhase) throw new NotFoundException(TaskPhaseMessage.TASK_PHASE_NOT_FOUND);
return taskPhase;
}
async create(dto: CreateTaskDto): Promise<TMTask> { async create(dto: CreateTaskDto): Promise<TMTask> {
const { userIds, startDate, endDate, ...rest } = dto; const { userIds, startDate, endDate, ...rest } = dto;
const taskPhase = await this.taskPhaseRepository.findOneById(dto.taskPhaseId); const taskPhase = await this.findTaskPhaseOrFail(dto.taskPhaseId);
if (!taskPhase) throw new NotFoundException(`TaskPhase #${dto.taskPhaseId} not found`);
if (userIds?.length) {
const isUsersValid = userIds.every((userId) => taskPhase.project.users.some((user) => user.id === userId));
if (!isUsersValid) throw new BadRequestException(TaskMessage.USERS_ASSIGNED_TO_TASK_NOT_PROJECT_MEMBER);
}
const task = this.taskRepository.create({ const task = this.taskRepository.create({
...rest, ...rest,
@@ -52,12 +73,16 @@ export class TaskService {
} }
async update(id: string, dto: UpdateTaskDto): Promise<TMTask> { async update(id: string, dto: UpdateTaskDto): Promise<TMTask> {
const task = await this.findOne(id); const task = await this.findOneOrFail(id);
const { userIds, startDate, endDate, ...rest } = dto; const { userIds, startDate, endDate, ...rest } = dto;
if (dto.taskPhaseId) { if (dto.taskPhaseId) {
const taskPhase = await this.taskPhaseRepository.findOneById(dto.taskPhaseId); const taskPhase = await this.findTaskPhaseOrFail(dto.taskPhaseId);
if (!taskPhase) throw new NotFoundException(`TaskPhase #${dto.taskPhaseId} not found`);
if (userIds?.length) {
const isUsersValid = userIds.every((userId) => taskPhase.project.users.some((user) => user.id === userId));
if (!isUsersValid) throw new BadRequestException(TaskMessage.USERS_ASSIGNED_TO_TASK_NOT_PROJECT_MEMBER);
}
} }
Object.assign(task, { Object.assign(task, {
@@ -67,24 +92,19 @@ export class TaskService {
}); });
if (userIds) { if (userIds) {
task.users = userIds.length task.users = userIds.length ? await this.userRepository.findBy({ id: In(userIds) }) : [];
? await this.userRepository.findBy({ id: In(userIds) })
: [];
} }
return this.taskRepository.save(task); return this.taskRepository.save(task);
} }
async changePhase(id: string, dto: ChangeTaskPhaseDto): Promise<TMTask> { async changePhase(id: string, dto: ChangeTaskPhaseDto): Promise<TMTask> {
const task = await this.findOne(id); const task = await this.findOneOrFail(id);
const newPhase = await this.taskPhaseRepository.findOneById(dto.taskPhaseId); const newPhase = await this.findTaskPhaseOrFail(dto.taskPhaseId);
if (!newPhase) throw new NotFoundException(`TaskPhase #${dto.taskPhaseId} not found`);
if (task.taskPhase.projectId !== newPhase.projectId) { if (task.taskPhase.projectId !== newPhase.projectId) {
throw new BadRequestException( throw new BadRequestException(TaskPhaseMessage.TASK_PHASE_NOT_SAME_PROJECT);
`TaskPhase #${dto.taskPhaseId} does not belong to the same project as the current phase`,
);
} }
task.taskPhaseId = dto.taskPhaseId; task.taskPhaseId = dto.taskPhaseId;
@@ -94,7 +114,75 @@ export class TaskService {
} }
async remove(id: string): Promise<void> { async remove(id: string): Promise<void> {
await this.findOne(id); await this.findOneOrFail(id);
await this.taskRepository.delete(id); await this.taskRepository.delete(id);
} }
async getTaskDetail(userId: string, permissions: PermissionEnum[], taskId: string): Promise<TMTask | null> {
const task = await this.findOneOrFail(taskId);
const isTaskMember = task.users.some((user) => user.id === userId);
const hasPermission = permissions.includes(PermissionEnum.TASK);
if (!isTaskMember && !hasPermission) throw new ForbiddenException(TaskMessage.USER_NOT_TASK_MEMBER);
const taskDetail = await this.taskRepository.getTaskDetail(taskId);
return taskDetail;
}
async findTasksByTaskPhase(
taskPhaseId: string,
pagination: CursorPaginationDto,
): Promise<{
data: TMTask[];
nextCursor: string | null;
previousCursor: string | null;
hasNextPage: boolean;
hasPreviousPage: boolean;
cursorPaginate: true;
}> {
await this.findTaskPhaseOrFail(taskPhaseId);
const isBackward = pagination.direction === CursorDirection.PREV;
const { items, hasMore } = await this.taskRepository.findTasksByTaskPhase(taskPhaseId, pagination);
// paging backward always has more ahead (we came from there); paging forward relies on
// whether a cursor was even supplied to know whether anything precedes this page
const hasNextPage = items.length > 0 && (isBackward ? true : hasMore);
const hasPreviousPage = items.length > 0 && (isBackward ? hasMore : !!pagination.cursor);
const nextCursor = hasNextPage ? encodeCursor(items[items.length - 1]) : null;
const previousCursor = hasPreviousPage ? encodeCursor(items[0]) : null;
return { data: items, nextCursor, previousCursor, hasNextPage, hasPreviousPage, cursorPaginate: true };
}
async changePriority(srcTaskId: string, destTaskId: string): Promise<TMTask> {
const srcTask = await this.findOneOrFail(srcTaskId);
const desTask = await this.findOneOrFail(destTaskId);
if (srcTask.taskPhaseId !== desTask.taskPhaseId)
throw new BadRequestException(TaskMessage.TASKS_NOT_IN_THE_SAME_TASK_PHASE);
let movingUp: boolean = false;
if(srcTask.priority < desTask.priority) {
movingUp = true;
} else if(srcTask.priority < desTask.priority) {
movingUp = false;
} else {
if(srcTask.createdAt > desTask.createdAt) {
movingUp = true;
}else {
movingUp = false;
}
}
const priorityChange: number = movingUp ? 1 : -1;
srcTask.priority = desTask.priority + priorityChange;
await this.taskRepository.save(srcTask);
return srcTask;
}
} }
@@ -0,0 +1,119 @@
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { IsNull } from "typeorm";
import { TimeRepository } from "../repositories/time.repository";
import { TMTime } from "../entities/time.entity";
import { CreateTimeDto } from "../dto/time/create-time.dto";
import { UpdateTimeDto } from "../dto/time/update-time.dto";
import { timeMessage } from "../../../common/enums/message.enum";
import { TaskService } from "./task.service";
@Injectable()
export class TimeService {
constructor(
private readonly timeRepository: TimeRepository,
private readonly taskService: TaskService,
) { }
private calculateDuration(startedAt: Date, endedAt: Date): number {
if (endedAt.getTime() < startedAt.getTime()) {
throw new BadRequestException(timeMessage.INVALID_TIME_RANGE);
}
return Math.floor((endedAt.getTime() - startedAt.getTime()) / 1000);
}
private async findRunningTime(adminId: string, taskId: string): Promise<TMTime | null> {
return this.timeRepository.findOne({
where: { adminId, taskId, endedAt: IsNull() },
order: { startedAt: "DESC" },
});
}
async findOneOrFail(id: string): Promise<TMTime> {
const time = await this.timeRepository.findOne({
where: { id },
relations: { admin: true, task: true },
});
if (!time) throw new NotFoundException(timeMessage.TIME_NOT_FOUND);
return time;
}
async start(adminId: string, taskId: string): Promise<TMTime> {
await this.taskService.findOneOrFail(taskId);
const runningTime = await this.findRunningTime(adminId, taskId);
if (runningTime) throw new BadRequestException(timeMessage.TIME_ALREADY_STARTED);
const time = this.timeRepository.create({
taskId,
adminId,
startedAt: new Date(),
endedAt: null,
duration: null,
});
return this.timeRepository.save(time);
}
async getTaskTimes(adminId: string, taskId: string): Promise<TMTime[]> {
return this.timeRepository.find({
where: { adminId, taskId },
order: { startedAt: "DESC" },
});
}
async end(adminId: string, taskId: string): Promise<TMTime> {
await this.taskService.findOneOrFail(taskId);
const runningTime = await this.findRunningTime(adminId, taskId);
if (!runningTime) throw new BadRequestException(timeMessage.NO_RUNNING_TIME);
const endedAt = new Date();
runningTime.endedAt = endedAt;
runningTime.duration = this.calculateDuration(runningTime.startedAt, endedAt);
return this.timeRepository.save(runningTime);
}
async create(adminId: string, dto: CreateTimeDto): Promise<TMTime> {
await this.taskService.findOneOrFail(dto.taskId);
const startedAt = new Date(dto.startedAt);
const endedAt = new Date(dto.endedAt);
const time = this.timeRepository.create({
taskId: dto.taskId,
adminId,
startedAt,
endedAt,
duration: this.calculateDuration(startedAt, endedAt),
});
return this.timeRepository.save(time);
}
async update(id: string, dto: UpdateTimeDto): Promise<TMTime> {
const time = await this.findOneOrFail(id);
if (dto.taskId) {
await this.taskService.findOneOrFail(dto.taskId);
}
const startedAt = dto.startedAt ? new Date(dto.startedAt) : time.startedAt;
const endedAt = dto.endedAt ? new Date(dto.endedAt) : time.endedAt;
Object.assign(time, {
...dto,
startedAt,
endedAt,
duration: endedAt ? this.calculateDuration(startedAt, endedAt) : null,
});
return this.timeRepository.save(time);
}
async remove(id: string): Promise<void> {
await this.findOneOrFail(id);
await this.timeRepository.delete(id);
}
}
@@ -1,47 +0,0 @@
import { Injectable, NotFoundException, ConflictException } from "@nestjs/common";
import { WorkspaceTypeRepository } from "../repositories/workspace-type.repository";
import { TMWorkspaceType } from "../entities/workspace-type.entity";
import { CreateWorkspaceTypeDto } from "../dto/workspace-type/create-workspace-type.dto";
import { UpdateWorkspaceTypeDto } from "../dto/workspace-type/update-workspace-type.dto";
@Injectable()
export class WorkspaceTypeService {
constructor(private readonly workspaceTypeRepository: WorkspaceTypeRepository) {}
findAll(): Promise<TMWorkspaceType[]> {
return this.workspaceTypeRepository.findAll();
}
async findOne(id: string): Promise<TMWorkspaceType> {
const workspaceType = await this.workspaceTypeRepository.findOneById(id);
if (!workspaceType) throw new NotFoundException(`WorkspaceType #${id} not found`);
return workspaceType;
}
async create(dto: CreateWorkspaceTypeDto): Promise<TMWorkspaceType> {
const existing = await this.workspaceTypeRepository.findOneByName(dto.name);
if (existing) throw new ConflictException(`WorkspaceType with name "${dto.name}" already exists`);
const workspaceType = this.workspaceTypeRepository.create(dto);
return this.workspaceTypeRepository.save(workspaceType);
}
async update(id: string, dto: UpdateWorkspaceTypeDto): Promise<TMWorkspaceType> {
await this.findOne(id);
if (dto.name) {
const existing = await this.workspaceTypeRepository.findOneByName(dto.name);
if (existing && existing.id !== id) throw new ConflictException(`WorkspaceType with name "${dto.name}" already exists`);
}
await this.workspaceTypeRepository.update(id, dto);
return this.findOne(id);
}
async remove(id: string): Promise<TMWorkspaceType> {
const workspaceType = await this.findOne(id);
await this.workspaceTypeRepository.delete(id);
return workspaceType;
}
}
@@ -1,37 +1,35 @@
import { Injectable, NotFoundException } from "@nestjs/common"; import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm"; import { InjectRepository } from "@nestjs/typeorm";
import { In, Repository } from "typeorm"; import { In, Not, Repository } from "typeorm";
import { WorkspaceRepository } from "../repositories/workspace.repository"; import { WorkspaceRepository } from "../repositories/workspace.repository";
import { TMWorkspace } from "../entities/workspace.entity"; import { TMWorkspace } from "../entities/workspace.entity";
import { CreateWorkspaceDto } from "../dto/workspace/create-workspace.dto"; import { CreateWorkspaceDto } from "../dto/workspace/create-workspace.dto";
import { UpdateWorkspaceDto } from "../dto/workspace/update-workspace.dto"; import { UpdateWorkspaceDto } from "../dto/workspace/update-workspace.dto";
import { User } from "../../users/entities/user.entity"; import { User } from "../../users/entities/user.entity";
import { WorkspaceTypeRepository } from "../repositories/workspace-type.repository"; import { WorkSpaceMessage } from "../../../common/enums/message.enum";
import { PermissionEnum } from "../../users/enums/permission.enum";
@Injectable() @Injectable()
export class WorkspaceService { export class WorkspaceService {
constructor( constructor(
private readonly workspaceRepository: WorkspaceRepository, private readonly workspaceRepository: WorkspaceRepository,
private readonly workspaceTypeRepository: WorkspaceTypeRepository,
@InjectRepository(User) @InjectRepository(User)
private readonly userRepository: Repository<User>, private readonly userRepository: Repository<User>,
) {} ) {}
findAll(): Promise<TMWorkspace[]> {
return this.workspaceRepository.findAll();
}
async findOneOrFail(id: string): Promise<TMWorkspace> { async findOneOrFail(id: string): Promise<TMWorkspace> {
const workspace = await this.workspaceRepository.findOneById(id); const workspace = await this.workspaceRepository.findOneWorkSpace(id);
if (!workspace) throw new NotFoundException(`Workspace #${id} not found`); if (!workspace) throw new NotFoundException(WorkSpaceMessage.WORKSPACE_NOT_FOUND);
return workspace; return workspace;
} }
async create(dto: CreateWorkspaceDto): Promise<TMWorkspace> { async create(dto: CreateWorkspaceDto): Promise<TMWorkspace> {
const { userIds, workspaceTypeId, ...rest } = dto; const { userIds, ...rest } = dto;
const workspaceType = await this.workspaceTypeRepository.findOneById(workspaceTypeId); const existingWorkspace = await this.workspaceRepository.findOne({ where: { name: rest.name } });
if (!workspaceType) throw new NotFoundException(`WorkSpaceType #${workspaceTypeId} not found!`); if (existingWorkspace) {
throw new BadRequestException(WorkSpaceMessage.WORKSPACE_EXISTS);
}
const workspace = this.workspaceRepository.create(rest); const workspace = this.workspaceRepository.create(rest);
@@ -44,11 +42,11 @@ export class WorkspaceService {
async update(id: string, dto: UpdateWorkspaceDto): Promise<TMWorkspace> { async update(id: string, dto: UpdateWorkspaceDto): Promise<TMWorkspace> {
const workspace = await this.findOneOrFail(id); const workspace = await this.findOneOrFail(id);
const { userIds, workspaceTypeId, ...rest } = dto; const { userIds, ...rest } = dto;
if (workspaceTypeId) { if (rest.name) {
const workspaceType = await this.workspaceTypeRepository.findOneById(workspaceTypeId); const existingWorkSpaceWithTheSameName = await this.workspaceRepository.findOne({ where: { name: rest.name, id: Not(id) } });
if (!workspaceType) throw new NotFoundException(`WorkSpaceType #${workspaceTypeId} not found!`); if (existingWorkSpaceWithTheSameName) throw new BadRequestException(WorkSpaceMessage.WORKSPACE_SAME_NAME_EXISTS);
} }
Object.assign(workspace, rest); Object.assign(workspace, rest);
@@ -66,4 +64,11 @@ export class WorkspaceService {
return workspace; return workspace;
} }
async findByUser(userId: string, permissions: PermissionEnum[]): Promise<TMWorkspace[]> {
if(permissions.includes(PermissionEnum.VIEW_ALL_WORKSPACES)) {
return await this.workspaceRepository.find();
}
return await this.workspaceRepository.findByUser(userId);
}
} }
@@ -0,0 +1,13 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { TMAttachment } from '../entities/attachment.entity';
@Injectable()
export class AttachmentRepository extends Repository<TMAttachment>{
constructor(
@InjectRepository(TMAttachment) attachmentRepository: Repository<TMAttachment>,
) {
super(attachmentRepository.target, attachmentRepository.manager, attachmentRepository.queryRunner);
}
}
@@ -4,44 +4,10 @@ import { Repository } from 'typeorm';
import { TMCheckListItem } from '../entities/check-list-item.entity'; import { TMCheckListItem } from '../entities/check-list-item.entity';
@Injectable() @Injectable()
export class CheckListItemRepository { export class CheckListItemRepository extends Repository<TMCheckListItem>{
constructor( constructor(
@InjectRepository(TMCheckListItem) @InjectRepository(TMCheckListItem) checkListItemRepository: Repository<TMCheckListItem>,
private readonly repository: Repository<TMCheckListItem>, ) {
) {} super(checkListItemRepository.target, checkListItemRepository.manager, checkListItemRepository.queryRunner);
findAll() {
return this.repository.find({
relations: ['task'],
});
}
findByTask(taskId: string) {
return this.repository.find({
where: { taskId },
});
}
findOneById(id: string) {
return this.repository.findOne({
where: { id },
relations: ['task'],
});
}
create(data: Partial<TMCheckListItem>) {
return this.repository.create(data);
}
save(checkListItem: TMCheckListItem) {
return this.repository.save(checkListItem);
}
update(id: string, data: Partial<TMCheckListItem>) {
return this.repository.update(id, data);
}
delete(id: string) {
return this.repository.delete(id);
} }
} }
@@ -4,45 +4,37 @@ import { Repository } from "typeorm";
import { TMProject } from "../entities/project.entity"; import { TMProject } from "../entities/project.entity";
@Injectable() @Injectable()
export class ProjectRepository { export class ProjectRepository extends Repository<TMProject> {
constructor( constructor(@InjectRepository(TMProject) projectRepository: Repository<TMProject>) {
@InjectRepository(TMProject) super(projectRepository.target, projectRepository.manager, projectRepository.queryRunner);
private readonly repository: Repository<TMProject>,
) {}
findAll() {
return this.repository.find({
relations: ["workspace", "taskPhases", "users"],
});
} }
findByWorkspace(workspaceId: string) { async findUserProjects(userId: string, wsId: string, skip?: number, take?: number): Promise<[TMProject[], number]> {
return this.repository.find({ return this.createQueryBuilder("project")
where: { workspaceId }, .innerJoin("project.workspace", "workspace")
relations: ["taskPhases", "users"], .innerJoin("project.users", "user")
}); .where("workspace.id = :wsId", { wsId })
.andWhere("user.id = :userId", { userId })
.skip(skip ?? 0)
.take(take ?? 10)
.getManyAndCount();
} }
findOneById(id: string) { async getProjectDetail(projectId: string): Promise<TMProject | null> {
return this.repository.findOne({ return this.createQueryBuilder("project")
where: { id }, .leftJoinAndSelect("project.taskPhases", "taskPhase")
relations: ["workspace", "taskPhases", "users"], .where("project.id = :projectId", { projectId })
});
}
create(data: Partial<TMProject>) { .select([
return this.repository.create(data); "project.id",
} "project.name",
"taskPhase.id",
"taskPhase.name",
"taskPhase.order",
])
save(project: TMProject) { .orderBy("taskPhase.order", "ASC")
return this.repository.save(project);
}
update(id: string, data: Partial<TMProject>) { .getOne();
return this.repository.update(id, data);
}
delete(id: string) {
return this.repository.delete(id);
} }
} }
@@ -0,0 +1,13 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { TMRemark } from "../entities/remark.entity";
@Injectable()
export class RemarkRepository extends Repository<TMRemark>{
constructor(
@InjectRepository(TMRemark) remarkRepository: Repository<TMRemark>,
) {
super(remarkRepository.target, remarkRepository.manager, remarkRepository.queryRunner);
}
}
@@ -4,47 +4,10 @@ import { Repository } from "typeorm";
import { TMTaskPhase } from "../entities/task-phase.entity"; import { TMTaskPhase } from "../entities/task-phase.entity";
@Injectable() @Injectable()
export class TaskPhaseRepository { export class TaskPhaseRepository extends Repository<TMTaskPhase> {
constructor( constructor(
@InjectRepository(TMTaskPhase) @InjectRepository(TMTaskPhase) taskPhaseRepository: Repository<TMTaskPhase>,
private readonly repository: Repository<TMTaskPhase>, ) {
) {} super(taskPhaseRepository.target, taskPhaseRepository.manager, taskPhaseRepository.queryRunner);
findAll() {
return this.repository.find({
relations: ["project", "tasks"],
order: { order: "ASC" },
});
}
findByProject(projectId: string) {
return this.repository.find({
where: { projectId },
relations: ["tasks"],
order: { order: "ASC" },
});
}
findOneById(id: string) {
return this.repository.findOne({
where: { id },
relations: ["project", "tasks"],
});
}
create(data: Partial<TMTaskPhase>) {
return this.repository.create(data);
}
save(taskPhase: TMTaskPhase) {
return this.repository.save(taskPhase);
}
update(id: string, data: Partial<TMTaskPhase>) {
return this.repository.update(id, data);
}
delete(id: string) {
return this.repository.delete(id);
} }
} }
@@ -2,47 +2,144 @@ import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm"; import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm"; import { Repository } from "typeorm";
import { TMTask } from "../entities/task.entity"; import { TMTask } from "../entities/task.entity";
import { TMTime } from "../entities/time.entity";
import { CursorDirection, CursorPaginationDto } from "../../../common/DTO/cursor-pagination.dto";
import { decodeCursor } from "../../utils/providers/cursor-pagination.utils";
@Injectable() @Injectable()
export class TaskRepository { export class TaskRepository extends Repository<TMTask> {
constructor( constructor(@InjectRepository(TMTask) taskRepository: Repository<TMTask>) {
@InjectRepository(TMTask) super(taskRepository.target, taskRepository.manager, taskRepository.queryRunner);
private readonly repository: Repository<TMTask>,
) {}
findAll() {
return this.repository.find({
relations: ["taskPhase", "remarks", "attachments", "checkListItems", "users"],
});
} }
findByPhase(taskPhaseId: string) { async getTaskDetail(taskId: string): Promise<TMTask | null> {
return this.repository.find({ const task = await this.createQueryBuilder("task")
where: { taskPhaseId }, .leftJoinAndSelect("task.remarks", "remark")
relations: ["remarks", "attachments", "checkListItems", "users"], .leftJoinAndSelect("task.attachments", "attachment")
}); .leftJoinAndSelect("task.checkListItems", "checkListItem")
.leftJoinAndSelect("task.users", "user")
.leftJoinAndSelect("task.times", "time")
.leftJoinAndSelect("time.admin", "timeAdmin")
.loadRelationCountAndMap("task.checkListItemCount", "task.checkListItems")
.loadRelationCountAndMap("task.completedCheckListItemCount", "task.checkListItems", "completedCheckListItems", (qb) =>
qb.where("completedCheckListItems.isDone = :isDone", { isDone: true }),
)
.select([
"task",
"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",
"time.id",
"time.startedAt",
"time.endedAt",
"time.duration",
"time.adminId",
"timeAdmin.id",
"timeAdmin.firstName",
"timeAdmin.lastName",
])
.where("task.id = :taskId", { taskId })
.orderBy("checkListItem.createdAt", "ASC")
.addOrderBy("time.createdAt", "ASC")
.getOne();
if (task) {
task.time = task.times?.reduce((sum, entry) => sum + (entry.duration ?? 0), 0) ?? 0;
} }
findOneById(id: string) { return task;
return this.repository.findOne({
where: { id },
relations: ["taskPhase", "remarks", "attachments", "checkListItems", "users"],
});
} }
create(data: Partial<TMTask>) { async findTasksByTaskPhase(taskPhaseId: string, pagination: CursorPaginationDto): Promise<{ items: TMTask[]; hasMore: boolean }> {
return this.repository.create(data); const limit = pagination.limit || 10;
const isBackward = pagination.direction === CursorDirection.PREV;
const createdAtExpr = `date_trunc('milliseconds', task.createdAt)`;
const seekQuery = this.createQueryBuilder("task")
.select(["task.id", "task.priority", "task.createdAt"])
.where("task.taskPhaseId = :taskPhaseId", { taskPhaseId })
.limit(limit + 1); // fetch one extra to check whether there's a further page
if (isBackward) {
seekQuery.orderBy("task.priority", "ASC").addOrderBy(createdAtExpr, "DESC").addOrderBy("task.id", "DESC");
} else {
seekQuery.orderBy("task.priority", "DESC").addOrderBy(createdAtExpr, "ASC").addOrderBy("task.id", "ASC");
} }
save(task: TMTask) { if (pagination.cursor) {
return this.repository.save(task); const { priority: cursorPriority, createdAt: cursorCreatedAt, id: cursorId } = decodeCursor(pagination.cursor);
const priorityOp = isBackward ? ">" : "<";
const seekOp = isBackward ? "<" : ">";
seekQuery.andWhere(
`(task.priority ${priorityOp} :cursorPriority
OR (task.priority = :cursorPriority AND ${createdAtExpr} ${seekOp} :cursorCreatedAt)
OR (task.priority = :cursorPriority AND ${createdAtExpr} = :cursorCreatedAt AND task.id ${seekOp} :cursorId))`,
{ cursorPriority, cursorCreatedAt, cursorId },
);
} }
update(id: string, data: Partial<TMTask>) { const seekRows = await seekQuery.getMany();
return this.repository.update(id, data); const hasMore = seekRows.length > limit;
} if (hasMore) seekRows.pop();
delete(id: string) { // backward queries fetch in reverse order to seek from the cursor, so flip back to
return this.repository.delete(id); // the normal display order before returning
const orderedRows = isBackward ? seekRows.reverse() : seekRows;
const ids = orderedRows.map((row) => row.id);
if (!ids.length) return { items: [], hasMore };
// Detail query: hydrates exactly the ids picked above with the relations/counts the
// response needs. IN (:...ids) doesn't preserve order, so re-order to match seekQuery.
const detailRows = await this.createQueryBuilder("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 }),
)
.select(["task", "remark.id", "remark.title", "remark.color"])
.where("task.id IN (:...ids)", { ids })
.getMany();
const timeSums = await this.manager
.createQueryBuilder(TMTime, "time")
.select("time.taskId", "taskId")
.addSelect("COALESCE(SUM(time.duration), 0)", "totalTime")
.where("time.taskId IN (:...ids)", { ids })
.groupBy("time.taskId")
.getRawMany<{ taskId: string; totalTime: string }>();
const timeByTaskId = new Map(timeSums.map((row) => [row.taskId, Number(row.totalTime)]));
const detailById = new Map(
detailRows.map((task) => {
task.time = timeByTaskId.get(task.id) ?? 0;
return [task.id, task];
}),
);
const items = ids.map((id) => detailById.get(id)).filter((task): task is TMTask => task !== undefined);
return { items, hasMore };
} }
} }
@@ -0,0 +1,11 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { TMTime } from "../entities/time.entity";
@Injectable()
export class TimeRepository extends Repository<TMTime> {
constructor(@InjectRepository(TMTime) timeRepository: Repository<TMTime>) {
super(timeRepository.target, timeRepository.manager, timeRepository.queryRunner);
}
}
@@ -1,42 +0,0 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { TMWorkspaceType } from "../entities/workspace-type.entity";
@Injectable()
export class WorkspaceTypeRepository {
constructor(
@InjectRepository(TMWorkspaceType)
private readonly repository: Repository<TMWorkspaceType>,
) {}
findAll() {
return this.repository.find({
order: { order: "ASC" },
});
}
findOneById(id: string) {
return this.repository.findOne({ where: { id } });
}
findOneByName(name: string) {
return this.repository.findOne({ where: { name } });
}
create(data: Partial<TMWorkspaceType>) {
return this.repository.create(data);
}
save(workspaceType: TMWorkspaceType) {
return this.repository.save(workspaceType);
}
update(id: string, data: Partial<TMWorkspaceType>) {
return this.repository.update(id, data);
}
delete(id: string) {
return this.repository.delete(id);
}
}
@@ -1,40 +1,30 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm"; import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm"; import { Repository } from 'typeorm';
import { TMWorkspace } from "../entities/workspace.entity"; import { TMWorkspace } from '../entities/workspace.entity';
@Injectable() @Injectable()
export class WorkspaceRepository { export class WorkspaceRepository extends Repository<TMWorkspace> {
constructor( constructor(@InjectRepository(TMWorkspace) workspaceRepository: Repository<TMWorkspace>) {
@InjectRepository(TMWorkspace) super(workspaceRepository.target, workspaceRepository.manager, workspaceRepository.queryRunner);
private readonly repository: Repository<TMWorkspace>,
) {}
findAll() {
return this.repository.find({
relations: ["workspaceType", "projects", "users"],
});
} }
findOneById(id: string) { async findOneWorkSpace(workspaceId: string): Promise<TMWorkspace | null> {
return this.repository.findOne({ return this.createQueryBuilder("workspace")
where: { id }, .leftJoinAndSelect('workspace.users', 'user')
}); .select([
'workspace',
'user.id',
'user.firstName',
'user.lastName'
]).where('workspace.id = :workspaceId', {workspaceId})
.getOne();
} }
create(data: Partial<TMWorkspace>) { async findByUser(userId: string): Promise<TMWorkspace[]> {
return this.repository.create(data); return this.createQueryBuilder("workspace")
} .innerJoin("workspace.users", "users")
.where("users.id = :userId", { userId })
save(workspace: TMWorkspace) { .getMany();
return this.repository.save(workspace);
}
update(id: string, data: Partial<TMWorkspace>) {
return this.repository.update(id, data);
}
delete(id: string) {
return this.repository.delete(id);
} }
} }
+21 -23
View File
@@ -1,7 +1,6 @@
import { Module } from "@nestjs/common"; import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm"; import { TypeOrmModule } from "@nestjs/typeorm";
import { TMWorkspaceType } from "./entities/workspace-type.entity";
import { TMWorkspace } from "./entities/workspace.entity"; import { TMWorkspace } from "./entities/workspace.entity";
import { TMProject } from "./entities/project.entity"; import { TMProject } from "./entities/project.entity";
import { TMTaskPhase } from "./entities/task-phase.entity"; import { TMTaskPhase } from "./entities/task-phase.entity";
@@ -9,13 +8,11 @@ import { TMTask } from "./entities/task.entity";
import { TMRemark } from "./entities/remark.entity"; import { TMRemark } from "./entities/remark.entity";
import { TMAttachment } from "./entities/attachment.entity"; import { TMAttachment } from "./entities/attachment.entity";
import { TMCheckListItem } from "./entities/check-list-item.entity"; import { TMCheckListItem } from "./entities/check-list-item.entity";
import { TMTime } from "./entities/time.entity";
import { WorkspaceRepository } from "./repositories/workspace.repository"; import { WorkspaceRepository } from "./repositories/workspace.repository";
import { WorkspaceController } from "./controllers/workspace.controller"; import { WorkspaceController } from "./controllers/workspace.controller";
import { WorkspaceService } from "./providers/workspace.service"; import { WorkspaceService } from "./providers/workspace.service";
import { User } from "../users/entities/user.entity"; import { User } from "../users/entities/user.entity";
import { WorkspaceTypeService } from "./providers/workspace-type.service";
import { WorkspaceTypeRepository } from "./repositories/workspace-type.repository";
import { WorkspaceTypeController } from "./controllers/workspace-type.controller";
import { ProjectController } from "./controllers/project.controller"; import { ProjectController } from "./controllers/project.controller";
import { ProjectRepository } from "./repositories/project.repository"; import { ProjectRepository } from "./repositories/project.repository";
import { ProjectService } from "./providers/project.service"; import { ProjectService } from "./providers/project.service";
@@ -28,24 +25,33 @@ import { CheckListItemService } from "./providers/check-list-item.service";
import { TaskPhaseController } from "./controllers/task-phase.controller"; import { TaskPhaseController } from "./controllers/task-phase.controller";
import { TaskController } from "./controllers/task.controller"; import { TaskController } from "./controllers/task.controller";
import { CheckListItemController } from "./controllers/check-list-item.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";
import { TimeController } from "./controllers/time.controller";
import { TimeRepository } from "./repositories/time.repository";
import { TimeService } from "./providers/time.service";
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([TMWorkspaceType, TMWorkspace, TMProject, TMTaskPhase, TMTask, TMRemark, TMAttachment, TMCheckListItem, User]), TypeOrmModule.forFeature([TMWorkspace, TMProject, TMTaskPhase, TMTask, TMRemark, TMAttachment, TMCheckListItem, TMTime, User]),
], ],
controllers: [ controllers: [
WorkspaceController, WorkspaceController,
WorkspaceTypeController,
ProjectController, ProjectController,
TaskPhaseController, TaskPhaseController,
TaskController, TaskController,
CheckListItemController, CheckListItemController,
RemarkController,
AttachmentController,
TimeController,
], ],
providers: [ providers: [
WorkspaceRepository, WorkspaceRepository,
WorkspaceService, WorkspaceService,
WorkspaceTypeRepository,
WorkspaceTypeService,
ProjectRepository, ProjectRepository,
ProjectService, ProjectService,
TaskPhaseRepository, TaskPhaseRepository,
@@ -54,20 +60,12 @@ import { CheckListItemController } from "./controllers/check-list-item.controlle
TaskService, TaskService,
CheckListItemRepository, CheckListItemRepository,
CheckListItemService, CheckListItemService,
], RemarkRepository,
exports: [ RemarkService,
WorkspaceRepository, AttachmentRepository,
WorkspaceService, AttachmentService,
WorkspaceTypeRepository, TimeRepository,
WorkspaceTypeService, TimeService,
ProjectRepository, ]
ProjectService,
TaskPhaseRepository,
TaskPhaseService,
TaskRepository,
TaskService,
CheckListItemRepository,
CheckListItemService,
],
}) })
export class TaskManagerModule {} export class TaskManagerModule {}
+7 -1
View File
@@ -24,5 +24,11 @@ export enum PermissionEnum {
DKALA = "dkala", DKALA = "dkala",
DPAGE = "dpage", DPAGE = "dpage",
DMAIL = 'dmail', DMAIL = 'dmail',
RESELLER="reseller" RESELLER="reseller",
WORKSPACE="workspace", // create, update, remove, read-one(for update purposes)
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",
} }
@@ -0,0 +1,28 @@
import { BadRequestException } from "@nestjs/common";
interface CursorPayload {
priority: number;
createdAt: Date;
id: string;
}
export function decodeCursor(cursor: string): CursorPayload {
try {
const decoded = Buffer.from(cursor, "base64").toString("utf-8");
const [priorityStr, timestamp, id] = decoded.split("|");
if (!priorityStr || !timestamp || !id) throw new Error("Invalid format");
const priority = parseInt(priorityStr, 10);
if (isNaN(priority)) throw new Error("Priority not a number");
const createdAt = new Date(timestamp);
if (isNaN(createdAt.getTime())) throw new Error("Invalid date");
return { priority, createdAt, id };
} catch {
throw new BadRequestException(
"Invalid cursor. Expected base64encoded string containing priority, ISO date and id (separated by '|').",
);
}
}
export function encodeCursor(task: { priority: number; createdAt: Date; id: string }): string {
return Buffer.from(`${task.priority}|${task.createdAt.toISOString()}|${task.id}`).toString("base64");
}