From e728303d530691f1da1d1160c8f249729130fef4 Mon Sep 17 00:00:00 2001 From: mahdi rafie Date: Mon, 29 Jun 2026 16:43:17 +0330 Subject: [PATCH 01/66] adding entities for taskmanager --- package-lock.json | 53 +++++----------- .../dto/workspace/create-workspace.dto.ts | 25 ++++++++ .../dto/workspace/update-workspace.dto.ts | 4 ++ .../entities/attachment.entity.ts | 33 ++++++++++ .../entities/check-list-item.entity.ts | 24 ++++++++ .../task-manager/entities/project.entity.ts | 36 +++++++++++ .../task-manager/entities/remark.entity.ts | 24 ++++++++ .../entities/task-phase.entity.ts | 33 ++++++++++ .../task-manager/entities/task.entity.ts | 60 +++++++++++++++++++ .../entities/workspace-type.entity.ts | 20 +++++++ .../task-manager/entities/workspace.entity.ts | 42 +++++++++++++ .../enums/attachment-type.enum.ts | 6 ++ .../providers/workspace.service.ts | 41 +++++++++++++ .../repositories/workspace.repository.ts | 41 +++++++++++++ .../task-manager/task-manager.controller.ts | 38 ++++++++++++ .../task-manager/task-manager.module.ts | 51 ++++++++++++++++ src/modules/users/entities/user.entity.ts | 18 ++++-- src/modules/users/users.module.ts | 2 + 18 files changed, 507 insertions(+), 44 deletions(-) create mode 100644 src/modules/task-manager/dto/workspace/create-workspace.dto.ts create mode 100644 src/modules/task-manager/dto/workspace/update-workspace.dto.ts create mode 100644 src/modules/task-manager/entities/attachment.entity.ts create mode 100644 src/modules/task-manager/entities/check-list-item.entity.ts create mode 100644 src/modules/task-manager/entities/project.entity.ts create mode 100644 src/modules/task-manager/entities/remark.entity.ts create mode 100644 src/modules/task-manager/entities/task-phase.entity.ts create mode 100644 src/modules/task-manager/entities/task.entity.ts create mode 100644 src/modules/task-manager/entities/workspace-type.entity.ts create mode 100644 src/modules/task-manager/entities/workspace.entity.ts create mode 100644 src/modules/task-manager/enums/attachment-type.enum.ts create mode 100644 src/modules/task-manager/providers/workspace.service.ts create mode 100644 src/modules/task-manager/repositories/workspace.repository.ts create mode 100644 src/modules/task-manager/task-manager.controller.ts create mode 100644 src/modules/task-manager/task-manager.module.ts diff --git a/package-lock.json b/package-lock.json index b5dab37..1b1b3b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1191,7 +1191,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2695,7 +2694,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@fastify/accept-negotiator": "^2.0.0", "@fastify/send": "^4.0.0", @@ -4461,7 +4459,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.8.tgz", "integrity": "sha512-bbsOqwld/GdBfiRNc4nnjyWWENDEicq4SH+R5AuYatvf++vf1x5JIsHB1i1KtfZMD3eRte0D4K9WXuAYil6XAg==", "license": "MIT", - "peer": true, "dependencies": { "file-type": "21.0.0", "iterare": "1.2.1", @@ -4521,7 +4518,6 @@ "integrity": "sha512-7riWfmTmMhCJHZ5ZiaG+crj4t85IPCq/wLRuOUSigBYyFT2JZj0lVHtAdf4Davp9ouNI8GINBDt9h9b5Gz9nTw==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", @@ -4605,7 +4601,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-fastify/-/platform-fastify-11.1.8.tgz", "integrity": "sha512-4XiiTiTkF9UbVDAuHDyVIzgr43L2sI3vLs9A52ov2lrOJcqyKwTYL/NiCQd4dtUQm1L6M0jOrJhzpYXobX5uMw==", "license": "MIT", - "peer": true, "dependencies": { "@fastify/cors": "11.1.0", "@fastify/formbody": "8.0.2", @@ -4808,7 +4803,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-6.4.0.tgz", "integrity": "sha512-osL67i0PUuwU5nqSuJjtUJZMkxAnYB4VldgYUMGzvYRJDCqGRFMWbsbzm/CkUtPLRL30I8T74Xgt/OQxnYokiA==", "license": "MIT", - "peer": true, "peerDependencies": { "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", "@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", @@ -4916,6 +4910,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -5937,7 +5932,6 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -6107,7 +6101,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -6299,7 +6292,6 @@ "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.4", "@typescript-eslint/types": "8.46.4", @@ -7007,7 +6999,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "devOptional": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7056,7 +7047,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -7483,7 +7473,6 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "license": "MIT", - "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -7736,7 +7725,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/bl": { "version": "4.1.0", @@ -7845,7 +7835,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -7933,7 +7922,6 @@ "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.63.0.tgz", "integrity": "sha512-HT1iM3Jt4bZeg3Ru/MxrOy2iIItxcl1Pz5Ync1Vrot70jBpVguMxFEiSaDU57BwYwR4iwnObDnzct2lirKkX5A==", "license": "MIT", - "peer": true, "dependencies": { "cron-parser": "^4.9.0", "ioredis": "^5.4.1", @@ -7962,7 +7950,6 @@ "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-6.4.3.tgz", "integrity": "sha512-VV5eq/QQ5rIVix7/aICO4JyvSeEv9eIQuKL5iFwgM2BrcYoE0A/D1mNsAHJAsB0WEbNdBlKkn6Tjz6fKzh/cKQ==", "license": "MIT", - "peer": true, "dependencies": { "keyv": "^5.3.3" } @@ -8152,7 +8139,6 @@ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -8200,15 +8186,13 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/class-validator": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz", "integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==", "license": "MIT", - "peer": true, "dependencies": { "@types/validator": "^13.11.8", "libphonenumber-js": "^1.11.1", @@ -8688,7 +8672,6 @@ "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -9735,7 +9718,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9796,7 +9778,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -9898,7 +9879,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -11825,7 +11805,6 @@ "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.2.tgz", "integrity": "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==", "license": "MIT", - "peer": true, "dependencies": { "@ioredis/commands": "1.4.0", "cluster-key-slot": "^1.1.0", @@ -12587,7 +12566,6 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -14131,7 +14109,6 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.3.tgz", "integrity": "sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==", "license": "MIT", - "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -16010,7 +15987,6 @@ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", "license": "MIT-0", - "peer": true, "engines": { "node": ">=6.0.0" } @@ -16720,7 +16696,8 @@ "node_modules/pause": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", - "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==", + "peer": true }, "node_modules/peberminta": { "version": "0.9.0", @@ -16748,7 +16725,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -17066,7 +17042,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -17659,8 +17634,7 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", @@ -18030,7 +18004,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -18207,7 +18180,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -19155,6 +19127,7 @@ "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", "license": "MIT", + "peer": true, "dependencies": { "bintrees": "1.0.2" } @@ -19164,7 +19137,6 @@ "resolved": "https://registry.npmjs.org/telegraf/-/telegraf-4.16.3.tgz", "integrity": "sha512-yjEu2NwkHlXu0OARWoNhJlIjX09dRktiMQFsM678BAH/PEPVwctzL67+tvXqLCRQQvm3SDtki2saGO9hLlz68w==", "license": "MIT", - "peer": true, "dependencies": { "@telegraf/types": "^7.1.0", "abort-controller": "^3.0.0", @@ -19765,7 +19737,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -20021,7 +19992,6 @@ "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.27.tgz", "integrity": "sha512-pNV1bn+1n8qEe8tUNsNdD8ejuPcMAg47u2lUGnbsajiNUr3p2Js1XLKQjBMH0yMRMDfdX8T+fIRejFmIwy9x4A==", "license": "MIT", - "peer": true, "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^3.17.0", @@ -20247,7 +20217,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -20499,6 +20468,7 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.4.0" } @@ -20814,6 +20784,7 @@ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -20832,6 +20803,7 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -20846,6 +20818,7 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=4.0" } @@ -20856,6 +20829,7 @@ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.8.x" } @@ -20866,6 +20840,7 @@ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", diff --git a/src/modules/task-manager/dto/workspace/create-workspace.dto.ts b/src/modules/task-manager/dto/workspace/create-workspace.dto.ts new file mode 100644 index 0000000..ac2ef19 --- /dev/null +++ b/src/modules/task-manager/dto/workspace/create-workspace.dto.ts @@ -0,0 +1,25 @@ +import { IsString, IsOptional, IsBoolean, IsNotEmpty, IsUUID, MaxLength } from "class-validator"; + +export class CreateWorkspaceDto { + @IsString() + @IsNotEmpty() + @MaxLength(100) + name: string; + + @IsString() + @IsOptional() + description?: string; + + @IsString() + @IsNotEmpty() + @MaxLength(20) + color: string; + + @IsBoolean() + @IsOptional() + isActive?: boolean; + + @IsUUID() + @IsNotEmpty() + workspaceTypeId: string; +} diff --git a/src/modules/task-manager/dto/workspace/update-workspace.dto.ts b/src/modules/task-manager/dto/workspace/update-workspace.dto.ts new file mode 100644 index 0000000..8d9eef1 --- /dev/null +++ b/src/modules/task-manager/dto/workspace/update-workspace.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from "@nestjs/mapped-types"; +import { CreateWorkspaceDto } from "./create-workspace.dto"; + +export class UpdateWorkspaceDto extends PartialType(CreateWorkspaceDto) {} diff --git a/src/modules/task-manager/entities/attachment.entity.ts b/src/modules/task-manager/entities/attachment.entity.ts new file mode 100644 index 0000000..ce2ef05 --- /dev/null +++ b/src/modules/task-manager/entities/attachment.entity.ts @@ -0,0 +1,33 @@ +import { + Column, + Entity, + JoinColumn, + ManyToOne, + } from 'typeorm'; + import { BaseEntity } from '../../../common/entities/base.entity'; + import { TMTask } from './task.entity'; + import { TMAttachmentType } from '../enums/attachment-type.enum'; + + @Entity('tm_attachments') + export class TMAttachment extends BaseEntity { + @Column({ type: 'varchar', length: 150 }) + title: string; + + @Column({ type: 'enum', enum: TMAttachmentType }) + type: TMAttachmentType; + + @Column({ type: 'text' }) + file: string; + + // --- Relations --- + + @ManyToOne(() => TMTask, (task) => task.attachments, { + nullable: false, + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'taskId' }) + task: TMTask; + + @Column({ type: 'uuid' }) + taskId: string; + } \ No newline at end of file diff --git a/src/modules/task-manager/entities/check-list-item.entity.ts b/src/modules/task-manager/entities/check-list-item.entity.ts new file mode 100644 index 0000000..870b438 --- /dev/null +++ b/src/modules/task-manager/entities/check-list-item.entity.ts @@ -0,0 +1,24 @@ +import { Column, Entity, JoinColumn, ManyToOne } from "typeorm"; +import { BaseEntity } from "../../../common/entities/base.entity"; +import { TMTask } from "./task.entity"; + +@Entity("tm_check_list_items") +export class TMCheckListItem extends BaseEntity { + @Column({ type: "varchar", length: 150 }) + title: string; + + @Column({ type: "boolean", default: false }) + isDone: boolean; + + // --- Relations --- + + @ManyToOne(() => TMTask, (task) => task.checkListItems, { + nullable: false, + onDelete: "CASCADE", + }) + @JoinColumn({ name: "taskId" }) + task: TMTask; + + @Column({ type: "uuid" }) + taskId: string; +} diff --git a/src/modules/task-manager/entities/project.entity.ts b/src/modules/task-manager/entities/project.entity.ts new file mode 100644 index 0000000..a4e761b --- /dev/null +++ b/src/modules/task-manager/entities/project.entity.ts @@ -0,0 +1,36 @@ +import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from "typeorm"; +import { BaseEntity } from "../../../common/entities/base.entity"; +import { TMWorkspace } from "./workspace.entity"; +import { TMTaskPhase } from "./task-phase.entity"; + +@Entity("tm_projects") +export class TMProject extends BaseEntity { + @Column({ type: "varchar", length: 150 }) + name: string; + + @Column({ type: "varchar", length: 150, nullable: true }) + ownerName: string; + + @Column({ type: "text", nullable: true }) + description: string; + + @Column({ type: "timestamptz", nullable: true }) + startDate: Date; + + // --- Relations --- + + @ManyToOne(() => TMWorkspace, (workspace) => workspace.projects, { + nullable: false, + onDelete: "RESTRICT", + }) + @JoinColumn({ name: "workspaceId" }) + workspace: TMWorkspace; + + @Column({ type: "uuid" }) + workspaceId: string; + + @OneToMany(() => TMTaskPhase, (taskPhase) => taskPhase.project, { + cascade: ["insert", "update"], + }) + taskPhases: TMTaskPhase[]; +} diff --git a/src/modules/task-manager/entities/remark.entity.ts b/src/modules/task-manager/entities/remark.entity.ts new file mode 100644 index 0000000..5705a20 --- /dev/null +++ b/src/modules/task-manager/entities/remark.entity.ts @@ -0,0 +1,24 @@ +import { Column, Entity, JoinColumn, ManyToOne } from "typeorm"; +import { BaseEntity } from "../../../common/entities/base.entity"; +import { TMTask } from "./task.entity"; + +@Entity("tm_remarks") +export class TMRemark extends BaseEntity { + @Column({ type: "varchar", length: 150 }) + title: string; + + @Column({ type: "varchar", length: 20, nullable: true }) + color: string; + + // --- Relations --- + + @ManyToOne(() => TMTask, (task) => task.remarks, { + nullable: false, + onDelete: "CASCADE", + }) + @JoinColumn({ name: "taskId" }) + task: TMTask; + + @Column({ type: "uuid" }) + taskId: string; +} diff --git a/src/modules/task-manager/entities/task-phase.entity.ts b/src/modules/task-manager/entities/task-phase.entity.ts new file mode 100644 index 0000000..644166d --- /dev/null +++ b/src/modules/task-manager/entities/task-phase.entity.ts @@ -0,0 +1,33 @@ +import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from "typeorm"; +import { BaseEntity } from "../../../common/entities/base.entity"; +import { TMProject } from "./project.entity"; +import { TMTask } from "./task.entity"; + +@Entity("tm_task_phases") +export class TMTaskPhase extends BaseEntity { + @Column({ type: "varchar", length: 100 }) + name: string; + + @Column({ type: "int", default: 0 }) + order: number; + + @Column({ type: "varchar", length: 20, nullable: true }) + color: string; + + // --- Relations --- + + @ManyToOne(() => TMProject, (project) => project.taskPhases, { + nullable: false, + onDelete: "CASCADE", + }) + @JoinColumn({ name: "projectId" }) + project: TMProject; + + @Column({ type: "uuid" }) + projectId: string; + + @OneToMany(() => TMTask, (task) => task.taskPhase, { + cascade: ["insert", "update"], + }) + tasks: TMTask[]; +} diff --git a/src/modules/task-manager/entities/task.entity.ts b/src/modules/task-manager/entities/task.entity.ts new file mode 100644 index 0000000..bf18fe2 --- /dev/null +++ b/src/modules/task-manager/entities/task.entity.ts @@ -0,0 +1,60 @@ +import { Column, Entity, JoinColumn, ManyToOne, ManyToMany, OneToMany, JoinTable } from "typeorm"; +import { BaseEntity } from "../../../common/entities/base.entity"; +import { TMTaskPhase } from "./task-phase.entity"; +import { TMRemark } from "./remark.entity"; +import { TMAttachment } from "./attachment.entity"; +import { TMCheckListItem } from "./check-list-item.entity"; +import { User } from "../../users/entities/user.entity"; + +@Entity("tm_tasks") +export class TMTask extends BaseEntity { + @Column({ type: "varchar", length: 150 }) + title: string; + + @Column({ type: "text", nullable: true }) + description: string; + + @Column({ type: "varchar", length: 20, nullable: true }) + color: string; + + @Column({ type: "timestamptz", nullable: true }) + startDate: Date; + + @Column({ type: "timestamptz", nullable: true }) + endDate: Date; + + // --- Relations --- + + @ManyToOne(() => TMTaskPhase, (taskPhase) => taskPhase.tasks, { + nullable: false, + onDelete: "RESTRICT", + }) + @JoinColumn({ name: "taskPhaseId" }) + taskPhase: TMTaskPhase; + + @Column({ type: "uuid" }) + taskPhaseId: string; + + @OneToMany(() => TMRemark, (remark) => remark.task, { + cascade: ["insert", "update"], + }) + remarks: TMRemark[]; + + @OneToMany(() => TMAttachment, (attachment) => attachment.task, { + cascade: ["insert", "update"], + }) + attachments: TMAttachment[]; + + @OneToMany(() => TMCheckListItem, (checkListItem) => checkListItem.task, { + cascade: ["insert", "update"], + }) + checkListItems: TMCheckListItem[]; + + @ManyToMany(() => User, (user) => user.tasks) + @JoinTable({ + name: "tm_task_users", + joinColumn: { name: "taskId", referencedColumnName: "id" }, + inverseJoinColumn: { name: "userId", referencedColumnName: "id" }, + }) + users: User[]; +} diff --git a/src/modules/task-manager/entities/workspace-type.entity.ts b/src/modules/task-manager/entities/workspace-type.entity.ts new file mode 100644 index 0000000..b840242 --- /dev/null +++ b/src/modules/task-manager/entities/workspace-type.entity.ts @@ -0,0 +1,20 @@ +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[]; +} diff --git a/src/modules/task-manager/entities/workspace.entity.ts b/src/modules/task-manager/entities/workspace.entity.ts new file mode 100644 index 0000000..c63366d --- /dev/null +++ b/src/modules/task-manager/entities/workspace.entity.ts @@ -0,0 +1,42 @@ +import { Column, Entity, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToMany } from "typeorm"; +import { BaseEntity } from "../../../common/entities/base.entity"; +import { TMWorkspaceType } from "./workspace-type.entity"; +import { TMProject } from "./project.entity"; +import { User } from "../../users/entities/user.entity"; + +@Entity("tm_workspaces") +export class TMWorkspace extends BaseEntity { + @Column({ type: "varchar", length: 100 }) + name: string; + + @Column({ type: "text", nullable: true }) + description: string; + + @Column({ type: "varchar", length: 20 }) + color: string; + + @Column({ type: "boolean", default: true }) + isActive: boolean; + + // --- Relations --- + + @ManyToOne(() => TMWorkspaceType, { nullable: false, eager: true }) + @JoinColumn({ name: "workspaceTypeId" }) + workspaceType: TMWorkspaceType; + + @Column({ type: "uuid" }) + workspaceTypeId: string; + + @OneToMany(() => TMProject, (project) => project.workspace, { + cascade: ["insert", "update"], + }) + projects: TMProject[]; + + @ManyToMany(() => User, (user) => user.workspaces) + @JoinTable({ + name: "tm_workspace_users", + joinColumn: { name: "workspaceId", referencedColumnName: "id" }, + inverseJoinColumn: { name: "userId", referencedColumnName: "id" }, + }) + users: User[]; +} diff --git a/src/modules/task-manager/enums/attachment-type.enum.ts b/src/modules/task-manager/enums/attachment-type.enum.ts new file mode 100644 index 0000000..e442e22 --- /dev/null +++ b/src/modules/task-manager/enums/attachment-type.enum.ts @@ -0,0 +1,6 @@ +export enum TMAttachmentType { + FILE = "file", + LINK = "link", + IMAGE = "image", + VIDEO = "video", +} diff --git a/src/modules/task-manager/providers/workspace.service.ts b/src/modules/task-manager/providers/workspace.service.ts new file mode 100644 index 0000000..259b4f1 --- /dev/null +++ b/src/modules/task-manager/providers/workspace.service.ts @@ -0,0 +1,41 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { CreateWorkspaceDto } from "../dto/workspace/create-workspace.dto"; +import { WorkspaceRepository } from "../repositories/workspace.repository"; +import { UpdateWorkspaceDto } from "../dto/workspace/update-workspace.dto"; + +@Injectable() +export class WorkspaceService { + constructor( + private readonly workspaceRepository: WorkspaceRepository, + ) {} + + findAll() { + return this.workspaceRepository.findAll(); + } + + async findOne(id: string) { + const workspace = await this.workspaceRepository.findOneById(id); + + if (!workspace) { + throw new NotFoundException(`Workspace #${id} not found`); + } + + return workspace; + } + + async create(dto: CreateWorkspaceDto) { + const workspace = this.workspaceRepository.create(dto); + return this.workspaceRepository.save(workspace); + } + + async update(id: string, dto: UpdateWorkspaceDto) { + await this.findOne(id); + await this.workspaceRepository.update(id, dto); + return this.findOne(id); + } + + async remove(id: string) { + await this.findOne(id); + await this.workspaceRepository.delete(id); + } +} \ No newline at end of file diff --git a/src/modules/task-manager/repositories/workspace.repository.ts b/src/modules/task-manager/repositories/workspace.repository.ts new file mode 100644 index 0000000..fd38d40 --- /dev/null +++ b/src/modules/task-manager/repositories/workspace.repository.ts @@ -0,0 +1,41 @@ +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; +import { TMWorkspace } from "../entities/workspace.entity"; + +@Injectable() +export class WorkspaceRepository { + constructor( + @InjectRepository(TMWorkspace) + private readonly repository: Repository, + ) {} + + findAll() { + return this.repository.find({ + relations: ["workspaceType", "projects", "users"], + }); + } + + findOneById(id: string) { + return this.repository.findOne({ + where: { id }, + relations: ["workspaceType", "projects", "users"], + }); + } + + create(data: Partial) { + return this.repository.create(data); + } + + save(workspace: TMWorkspace) { + return this.repository.save(workspace); + } + + update(id: string, data: Partial) { + return this.repository.update(id, data); + } + + delete(id: string) { + return this.repository.delete(id); + } +} diff --git a/src/modules/task-manager/task-manager.controller.ts b/src/modules/task-manager/task-manager.controller.ts new file mode 100644 index 0000000..1d7f245 --- /dev/null +++ b/src/modules/task-manager/task-manager.controller.ts @@ -0,0 +1,38 @@ +import { Controller, Get, Post, Patch, Delete, Param, Body } from '@nestjs/common'; +import { TMWorkspace } from './entities/workspace.entity'; +import { WorkspaceService } from './providers/workspace.service'; +import { CreateWorkspaceDto } from './dto/workspace/create-workspace.dto'; +import { UpdateWorkspaceDto } from './dto/workspace/update-workspace.dto'; + +@Controller('task-manager') +export class TaskManagerController { + constructor( + private readonly workspaceService: WorkspaceService, + ) {} + // --- Workspace --- + + @Get('workspaces') + findAllWorkspaces(): Promise { + return this.workspaceService.findAll(); + } + + @Get('workspaces/:id') + findOneWorkspace(@Param('id') id: string): Promise { + return this.workspaceService.findOne(id); + } + + @Post('workspaces') + createWorkspace(@Body() body: CreateWorkspaceDto): Promise { + return this.workspaceService.create(body); + } + + @Patch('workspaces/:id') + updateWorkspace(@Param('id') id: string, @Body() body: UpdateWorkspaceDto): Promise { + return this.workspaceService.update(id, body); + } + + @Delete('workspaces/:id') + removeWorkspace(@Param('id') id: string): Promise { + return this.workspaceService.remove(id); + } +} \ No newline at end of file diff --git a/src/modules/task-manager/task-manager.module.ts b/src/modules/task-manager/task-manager.module.ts new file mode 100644 index 0000000..d02d0c4 --- /dev/null +++ b/src/modules/task-manager/task-manager.module.ts @@ -0,0 +1,51 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { TMWorkspaceType } from './entities/workspace-type.entity'; +import { TMWorkspace } from './entities/workspace.entity'; +import { TMProject } from './entities/project.entity'; +import { TMTaskPhase } from './entities/task-phase.entity'; +import { TMTask } from './entities/task.entity'; +import { TMRemark } from './entities/remark.entity'; +import { TMAttachment } from './entities/attachment.entity'; +import { TMCheckListItem } from './entities/check-list-item.entity'; +import { WorkspaceRepository } from './repositories/workspace.repository'; +import { TaskManagerController } from './task-manager.controller'; +import { WorkspaceService } from './providers/workspace.service'; + +// import { WorkspaceTypeService } from './providers/workspace-type.service'; +// import { WorkspaceService } from './providers/workspace.service'; +// import { ProjectService } from './providers/project.service'; +// import { TaskPhaseService } from './providers/task-phase.service'; +// import { TaskService } from './providers/task.service'; + +// import { TaskManagerController } from './task-manager.controller'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + TMWorkspaceType, + TMWorkspace, + TMProject, + TMTaskPhase, + TMTask, + TMRemark, + TMAttachment, + TMCheckListItem, + ]), + ], + controllers: [TaskManagerController], + providers: [ + WorkspaceRepository, + WorkspaceService, + // WorkspaceTypeService, + // ProjectService, + // TaskPhaseService, + // TaskService, + ], + exports: [ + WorkspaceService, + WorkspaceRepository + ], +}) +export class TaskManagerModule {} \ No newline at end of file diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index 2237188..abcd099 100755 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -32,6 +32,8 @@ import { Wallet } from "../../wallets/entities/wallet.entity"; import { FinancialType } from "../enums/financial-type.enum"; import { Reseller } from "../../reseller/entities/reseller.entity"; import { UserBankAccount } from "../../reseller/entities/user-bank-account.entity"; +import { TMWorkspace } from "../../task-manager/entities/workspace.entity"; +import { TMTask } from "../../task-manager/entities/task.entity"; @Entity() export class User extends BaseEntity { @@ -167,14 +169,20 @@ export class User extends BaseEntity { //--------------------------------------- @ManyToOne(() => Reseller, (reseller) => reseller.agents, { nullable: true }) - reseller?: Reseller + reseller?: Reseller; - - @OneToOne(() => Reseller, reseller => reseller.owner, { nullable: true }) - ownedReseller?: Reseller + @OneToOne(() => Reseller, (reseller) => reseller.owner, { nullable: true }) + ownedReseller?: Reseller; //------------------------ @OneToOne(() => UserBankAccount, (account) => account.user, { nullable: true }) - bankAckount: UserBankAccount + bankAckount: UserBankAccount; + + // ----------------------- + @ManyToMany(() => TMWorkspace, (workspace) => workspace.users) + workspaces: TMWorkspace[]; + + @ManyToMany(() => TMTask, (task) => task.users) + tasks: TMTask[]; } diff --git a/src/modules/users/users.module.ts b/src/modules/users/users.module.ts index 1d24e7a..68afdc5 100755 --- a/src/modules/users/users.module.ts +++ b/src/modules/users/users.module.ts @@ -28,6 +28,7 @@ import { AdminsService } from "./providers/admins.service"; import { CustomersService } from "./providers/customers.service"; import { RefreshTokensRepository } from "./repositories/refresh-token.repository"; import { ReferralsModule } from "../referrals/referrals.module"; +import { TaskManagerModule } from "../task-manager/task-manager.module"; @Module({ @@ -38,6 +39,7 @@ import { ReferralsModule } from "../referrals/referrals.module"; AddressModule, ReferralsModule, AccessLogsModule, + TaskManagerModule ], providers: [ UsersService, From d24abe7ee47deae6e2957f9992db4806ef027074 Mon Sep 17 00:00:00 2001 From: realrafi Date: Tue, 30 Jun 2026 09:37:00 +0330 Subject: [PATCH 02/66] add create & update dto workspace-type --- .../workspace-type/create-workspace-type.dto.ts | 17 +++++++++++++++++ .../workspace-type/update-workspace-type.dto.ts | 4 ++++ 2 files changed, 21 insertions(+) create mode 100644 src/modules/task-manager/dto/workspace-type/create-workspace-type.dto.ts create mode 100644 src/modules/task-manager/dto/workspace-type/update-workspace-type.dto.ts diff --git a/src/modules/task-manager/dto/workspace-type/create-workspace-type.dto.ts b/src/modules/task-manager/dto/workspace-type/create-workspace-type.dto.ts new file mode 100644 index 0000000..9b00e3f --- /dev/null +++ b/src/modules/task-manager/dto/workspace-type/create-workspace-type.dto.ts @@ -0,0 +1,17 @@ +import { IsString, IsOptional, IsInt, IsNotEmpty, MaxLength, Min } from "class-validator"; + +export class CreateWorkspaceTypeDto { + @IsString() + @IsNotEmpty() + @MaxLength(100) + name: string; + + @IsString() + @IsOptional() + description?: string; + + @IsInt() + @IsOptional() + @Min(0) + order?: number; +} diff --git a/src/modules/task-manager/dto/workspace-type/update-workspace-type.dto.ts b/src/modules/task-manager/dto/workspace-type/update-workspace-type.dto.ts new file mode 100644 index 0000000..86e8b47 --- /dev/null +++ b/src/modules/task-manager/dto/workspace-type/update-workspace-type.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from "@nestjs/mapped-types"; +import { CreateWorkspaceTypeDto } from "./create-workspace-type.dto"; + +export class UpdateWorkspaceTypeDto extends PartialType(CreateWorkspaceTypeDto) {} From 9d8ff3603e8c2946533f021e42e9f396a14e48bb Mon Sep 17 00:00:00 2001 From: realrafi Date: Tue, 30 Jun 2026 10:44:50 +0330 Subject: [PATCH 03/66] fix & enhance workspace --- .../controllers/workspace.controller.ts | 53 +++++++++++++++ .../dto/workspace/create-workspace.dto.ts | 25 +++++-- .../dto/workspace/update-workspace.dto.ts | 2 +- .../providers/workspace.service.ts | 67 +++++++++++++------ .../task-manager/task-manager.controller.ts | 38 ----------- .../task-manager/task-manager.module.ts | 6 +- 6 files changed, 123 insertions(+), 68 deletions(-) create mode 100644 src/modules/task-manager/controllers/workspace.controller.ts delete mode 100644 src/modules/task-manager/task-manager.controller.ts diff --git a/src/modules/task-manager/controllers/workspace.controller.ts b/src/modules/task-manager/controllers/workspace.controller.ts new file mode 100644 index 0000000..93cd8eb --- /dev/null +++ b/src/modules/task-manager/controllers/workspace.controller.ts @@ -0,0 +1,53 @@ +import { Controller, Get, Post, Patch, Delete, Param, Body } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger'; +import { WorkspaceService } from '../providers/workspace.service'; +import { CreateWorkspaceDto } from '../dto/workspace/create-workspace.dto'; +import { UpdateWorkspaceDto } from '../dto/workspace/update-workspace.dto'; +import { TMWorkspace } from '../entities/workspace.entity'; + +@ApiTags('Task Manager - Workspaces') +@Controller('task-manager/workspaces') +export class WorkspaceController { + constructor(private readonly workspaceService: WorkspaceService) {} + + @Get() + @ApiOperation({ summary: 'Get all workspaces' }) + @ApiResponse({ status: 200, description: 'List of workspaces', type: [TMWorkspace] }) + findAll(): Promise { + return this.workspaceService.findAll(); + } + + @Get(':id') + @ApiOperation({ summary: 'Get a workspace by ID' }) + @ApiParam({ name: 'id', description: 'Workspace ID' }) + @ApiResponse({ status: 200, description: 'Workspace found', type: TMWorkspace }) + @ApiResponse({ status: 404, description: 'Workspace not found' }) + findOne(@Param('id') id: string): Promise { + return this.workspaceService.findOneOrFail(id); + } + + @Post() + @ApiOperation({ summary: 'Create a new workspace' }) + @ApiResponse({ status: 201, description: 'Workspace created', type: TMWorkspace }) + create(@Body() body: CreateWorkspaceDto): Promise { + return this.workspaceService.create(body); + } + + @Patch(':id') + @ApiOperation({ summary: 'Update a workspace' }) + @ApiParam({ name: 'id', description: 'Workspace ID' }) + @ApiResponse({ status: 200, description: 'Workspace updated', type: TMWorkspace }) + @ApiResponse({ status: 404, description: 'Workspace not found' }) + update(@Param('id') id: string, @Body() body: UpdateWorkspaceDto): Promise { + return this.workspaceService.update(id, body); + } + + @Delete(':id') + @ApiOperation({ summary: 'Delete a workspace' }) + @ApiParam({ name: 'id', description: 'Workspace ID' }) + @ApiResponse({ status: 200, description: 'Workspace deleted' }) + @ApiResponse({ status: 404, description: 'Workspace not found' }) + remove(@Param('id') id: string): Promise { + return this.workspaceService.remove(id); + } +} \ No newline at end of file diff --git a/src/modules/task-manager/dto/workspace/create-workspace.dto.ts b/src/modules/task-manager/dto/workspace/create-workspace.dto.ts index ac2ef19..0efdb4b 100644 --- a/src/modules/task-manager/dto/workspace/create-workspace.dto.ts +++ b/src/modules/task-manager/dto/workspace/create-workspace.dto.ts @@ -1,25 +1,40 @@ +import { ApiProperty } from "@nestjs/swagger"; import { IsString, IsOptional, IsBoolean, IsNotEmpty, IsUUID, MaxLength } from "class-validator"; export class CreateWorkspaceDto { + @ApiProperty({ description: "Name of the workspace", example: "Engineering" }) @IsString() @IsNotEmpty() @MaxLength(100) name: string; + @ApiProperty({ description: "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" }) @IsString() @IsOptional() description?: string; + @ApiProperty({ description: "Color code for the workspace (hex or named)", example: "#4F46E5" }) @IsString() @IsNotEmpty() @MaxLength(20) color: string; + @ApiProperty({ description: "Whether the workspace is active", required: false, default: true, example: true }) @IsBoolean() - @IsOptional() - isActive?: boolean; + isActive: boolean; - @IsUUID() - @IsNotEmpty() - workspaceTypeId: string; + @ApiProperty({ + description: "IDs of users to assign to the workspace", + required: false, + type: [String], + example: ["b2c3d4e5-f6a7-8901-bcde-f12345678901"], + }) + @IsUUID("4", { each: true }) + @IsOptional() + userIds?: string[]; } diff --git a/src/modules/task-manager/dto/workspace/update-workspace.dto.ts b/src/modules/task-manager/dto/workspace/update-workspace.dto.ts index 8d9eef1..9dd610e 100644 --- a/src/modules/task-manager/dto/workspace/update-workspace.dto.ts +++ b/src/modules/task-manager/dto/workspace/update-workspace.dto.ts @@ -1,4 +1,4 @@ -import { PartialType } from "@nestjs/mapped-types"; +import { PartialType } from "@nestjs/swagger"; import { CreateWorkspaceDto } from "./create-workspace.dto"; export class UpdateWorkspaceDto extends PartialType(CreateWorkspaceDto) {} diff --git a/src/modules/task-manager/providers/workspace.service.ts b/src/modules/task-manager/providers/workspace.service.ts index 259b4f1..8d8ea40 100644 --- a/src/modules/task-manager/providers/workspace.service.ts +++ b/src/modules/task-manager/providers/workspace.service.ts @@ -1,41 +1,64 @@ -import { Injectable, NotFoundException } from "@nestjs/common"; -import { CreateWorkspaceDto } from "../dto/workspace/create-workspace.dto"; -import { WorkspaceRepository } from "../repositories/workspace.repository"; -import { UpdateWorkspaceDto } from "../dto/workspace/update-workspace.dto"; +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { In, Repository } from 'typeorm'; +import { TMWorkspace } from '../entities/workspace.entity'; +import { CreateWorkspaceDto } from '../dto/workspace/create-workspace.dto'; +import { UpdateWorkspaceDto } from '../dto/workspace/update-workspace.dto'; +import { User } from '../../users/entities/user.entity'; @Injectable() export class WorkspaceService { constructor( - private readonly workspaceRepository: WorkspaceRepository, + @InjectRepository(TMWorkspace) + private readonly workspaceRepository: Repository, + @InjectRepository(User) + private readonly userRepository: Repository, ) {} - findAll() { - return this.workspaceRepository.findAll(); + findAll(): Promise { + return this.workspaceRepository.find({ + relations: ['workspaceType', 'projects', 'users'], + }); } - async findOne(id: string) { - const workspace = await this.workspaceRepository.findOneById(id); - - if (!workspace) { - throw new NotFoundException(`Workspace #${id} not found`); - } - + async findOneOrFail(id: string): Promise { + const workspace = await this.workspaceRepository.findOne({ + where: { id }, + relations: ['workspaceType', 'projects', 'users'], + }); + if (!workspace) throw new NotFoundException(`Workspace #${id} not found`); return workspace; } - async create(dto: CreateWorkspaceDto) { - const workspace = this.workspaceRepository.create(dto); + async create(dto: CreateWorkspaceDto): Promise { + const { userIds, ...rest } = dto; + + const workspace = this.workspaceRepository.create(rest); + + if (userIds?.length) { + workspace.users = await this.userRepository.findBy({ id: In(userIds) }); + } + return this.workspaceRepository.save(workspace); } - async update(id: string, dto: UpdateWorkspaceDto) { - await this.findOne(id); - await this.workspaceRepository.update(id, dto); - return this.findOne(id); + async update(id: string, dto: UpdateWorkspaceDto): Promise { + const workspace = await this.findOneOrFail(id); + const { userIds, ...rest } = dto; + + Object.assign(workspace, rest); + + if (userIds) { + workspace.users = userIds.length + ? await this.userRepository.findBy({ id: In(userIds) }) + : []; + } + + return this.workspaceRepository.save(workspace); } - async remove(id: string) { - await this.findOne(id); + async remove(id: string): Promise { + await this.findOneOrFail(id); await this.workspaceRepository.delete(id); } } \ No newline at end of file diff --git a/src/modules/task-manager/task-manager.controller.ts b/src/modules/task-manager/task-manager.controller.ts deleted file mode 100644 index 1d7f245..0000000 --- a/src/modules/task-manager/task-manager.controller.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Controller, Get, Post, Patch, Delete, Param, Body } from '@nestjs/common'; -import { TMWorkspace } from './entities/workspace.entity'; -import { WorkspaceService } from './providers/workspace.service'; -import { CreateWorkspaceDto } from './dto/workspace/create-workspace.dto'; -import { UpdateWorkspaceDto } from './dto/workspace/update-workspace.dto'; - -@Controller('task-manager') -export class TaskManagerController { - constructor( - private readonly workspaceService: WorkspaceService, - ) {} - // --- Workspace --- - - @Get('workspaces') - findAllWorkspaces(): Promise { - return this.workspaceService.findAll(); - } - - @Get('workspaces/:id') - findOneWorkspace(@Param('id') id: string): Promise { - return this.workspaceService.findOne(id); - } - - @Post('workspaces') - createWorkspace(@Body() body: CreateWorkspaceDto): Promise { - return this.workspaceService.create(body); - } - - @Patch('workspaces/:id') - updateWorkspace(@Param('id') id: string, @Body() body: UpdateWorkspaceDto): Promise { - return this.workspaceService.update(id, body); - } - - @Delete('workspaces/:id') - removeWorkspace(@Param('id') id: string): Promise { - return this.workspaceService.remove(id); - } -} \ No newline at end of file diff --git a/src/modules/task-manager/task-manager.module.ts b/src/modules/task-manager/task-manager.module.ts index d02d0c4..f580f2d 100644 --- a/src/modules/task-manager/task-manager.module.ts +++ b/src/modules/task-manager/task-manager.module.ts @@ -10,8 +10,9 @@ import { TMRemark } from './entities/remark.entity'; import { TMAttachment } from './entities/attachment.entity'; import { TMCheckListItem } from './entities/check-list-item.entity'; import { WorkspaceRepository } from './repositories/workspace.repository'; -import { TaskManagerController } from './task-manager.controller'; +import { WorkspaceController } from './controllers/workspace.controller'; import { WorkspaceService } from './providers/workspace.service'; +import { User } from '../users/entities/user.entity'; // import { WorkspaceTypeService } from './providers/workspace-type.service'; // import { WorkspaceService } from './providers/workspace.service'; @@ -32,9 +33,10 @@ import { WorkspaceService } from './providers/workspace.service'; TMRemark, TMAttachment, TMCheckListItem, + User ]), ], - controllers: [TaskManagerController], + controllers: [WorkspaceController], providers: [ WorkspaceRepository, WorkspaceService, From b0fba4bb846648d1d3a3e496110444184ef122ac Mon Sep 17 00:00:00 2001 From: realrafi Date: Tue, 30 Jun 2026 12:31:22 +0330 Subject: [PATCH 04/66] feat: add workspace-type DTOs --- .../migrations/1782807558167-taskManager.ts | 66 +++++++++++++++++++ .../create-workspace-type.dto.ts | 12 +++- .../update-workspace-type.dto.ts | 6 +- 3 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 database/migrations/1782807558167-taskManager.ts diff --git a/database/migrations/1782807558167-taskManager.ts b/database/migrations/1782807558167-taskManager.ts new file mode 100644 index 0000000..2aed74a --- /dev/null +++ b/database/migrations/1782807558167-taskManager.ts @@ -0,0 +1,66 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class TaskManager1782807558167 implements MigrationInterface { + name = 'TaskManager1782807558167' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "tm_workspace_types" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "name" character varying(100) NOT NULL, "description" text, "order" integer NOT NULL DEFAULT '0', CONSTRAINT "UQ_f6eb300d6dfa0f7dbfdf05e2592" UNIQUE ("name"), CONSTRAINT "PK_b3437ae89cc910f1f0ad3de0fc5" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "tm_remarks" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "title" character varying(150) NOT NULL, "color" character varying(20), "taskId" uuid NOT NULL, CONSTRAINT "PK_99be294f0b23c3823d9ee784ebf" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TYPE "public"."tm_attachments_type_enum" AS ENUM('file', 'link', 'image', 'video')`); + await queryRunner.query(`CREATE TABLE "tm_attachments" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "title" character varying(150) NOT NULL, "type" "public"."tm_attachments_type_enum" NOT NULL, "file" text NOT NULL, "taskId" uuid NOT NULL, CONSTRAINT "PK_85eb42d47a75319a1ff074ff39c" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "tm_check_list_items" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "title" character varying(150) NOT NULL, "isDone" boolean NOT NULL DEFAULT false, "taskId" uuid NOT NULL, CONSTRAINT "PK_0bb2fee42ccb8a7f4c5c054ea71" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "tm_tasks" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "title" character varying(150) NOT NULL, "description" text, "color" character varying(20), "startDate" TIMESTAMP WITH TIME ZONE, "endDate" TIMESTAMP WITH TIME ZONE, "taskPhaseId" uuid NOT NULL, CONSTRAINT "PK_f975c1e9cc0cfc054482cb36028" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "tm_task_phases" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "name" character varying(100) NOT NULL, "order" integer NOT NULL DEFAULT '0', "color" character varying(20), "projectId" uuid NOT NULL, CONSTRAINT "PK_045ad1778b77407b8e4017cc9b2" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "tm_projects" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "name" character varying(150) NOT NULL, "ownerName" character varying(150), "description" text, "startDate" TIMESTAMP WITH TIME ZONE, "workspaceId" uuid NOT NULL, CONSTRAINT "PK_0ceacba52c6bb5cec6481b910e4" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "tm_workspaces" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "name" character varying(100) NOT NULL, "description" text, "color" character varying(20) NOT NULL, "isActive" boolean NOT NULL DEFAULT true, "workspaceTypeId" uuid NOT NULL, CONSTRAINT "PK_fb4be955b5b4b11aacc5169e9d5" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "tm_task_users" ("taskId" uuid NOT NULL, "userId" uuid NOT NULL, CONSTRAINT "PK_bb8f94f4ea4715ad190bc4fb23c" PRIMARY KEY ("taskId", "userId"))`); + await queryRunner.query(`CREATE INDEX "IDX_739cfd1807f78bb2ebda02e7bc" ON "tm_task_users" ("taskId") `); + await queryRunner.query(`CREATE INDEX "IDX_3fe44ce1acd8a620affb55a750" ON "tm_task_users" ("userId") `); + await queryRunner.query(`CREATE TABLE "tm_workspace_users" ("workspaceId" uuid NOT NULL, "userId" uuid NOT NULL, CONSTRAINT "PK_7f8b37c58e5267bc16cf577fcbd" PRIMARY KEY ("workspaceId", "userId"))`); + await queryRunner.query(`CREATE INDEX "IDX_ca9b0a16dfec7c4753ea4910f0" ON "tm_workspace_users" ("workspaceId") `); + await queryRunner.query(`CREATE INDEX "IDX_1c9d677417216377d55a060011" ON "tm_workspace_users" ("userId") `); + await queryRunner.query(`ALTER TABLE "reward_withdraw_request" ADD CONSTRAINT "UQ_79526c10ecb097f553e0120a36e" UNIQUE ("transactionId")`); + await queryRunner.query(`ALTER TABLE "tm_remarks" ADD CONSTRAINT "FK_bc03cabb99c42245436fcbb4e6a" FOREIGN KEY ("taskId") REFERENCES "tm_tasks"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "tm_attachments" ADD CONSTRAINT "FK_1e9ab0f9ec5b898abea95d21fb2" FOREIGN KEY ("taskId") REFERENCES "tm_tasks"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "tm_check_list_items" ADD CONSTRAINT "FK_14730b0971bcdc5547889392606" FOREIGN KEY ("taskId") REFERENCES "tm_tasks"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "tm_tasks" ADD CONSTRAINT "FK_f7462cec045e564d5fd54d17177" FOREIGN KEY ("taskPhaseId") REFERENCES "tm_task_phases"("id") ON DELETE RESTRICT ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "tm_task_phases" ADD CONSTRAINT "FK_884b8c2fe724031aa61c8181f0b" FOREIGN KEY ("projectId") REFERENCES "tm_projects"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "tm_projects" ADD CONSTRAINT "FK_c3d997618910273be84d58faa8c" FOREIGN KEY ("workspaceId") REFERENCES "tm_workspaces"("id") ON DELETE RESTRICT ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "tm_workspaces" ADD CONSTRAINT "FK_253e36f065cfc1eca58abaefb22" FOREIGN KEY ("workspaceTypeId") REFERENCES "tm_workspace_types"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "tm_task_users" ADD CONSTRAINT "FK_739cfd1807f78bb2ebda02e7bce" FOREIGN KEY ("taskId") REFERENCES "tm_tasks"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "tm_task_users" ADD CONSTRAINT "FK_3fe44ce1acd8a620affb55a7500" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "tm_workspace_users" ADD CONSTRAINT "FK_ca9b0a16dfec7c4753ea4910f00" FOREIGN KEY ("workspaceId") REFERENCES "tm_workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "tm_workspace_users" ADD CONSTRAINT "FK_1c9d677417216377d55a060011d" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tm_workspace_users" DROP CONSTRAINT "FK_1c9d677417216377d55a060011d"`); + await queryRunner.query(`ALTER TABLE "tm_workspace_users" DROP CONSTRAINT "FK_ca9b0a16dfec7c4753ea4910f00"`); + await queryRunner.query(`ALTER TABLE "tm_task_users" DROP CONSTRAINT "FK_3fe44ce1acd8a620affb55a7500"`); + await queryRunner.query(`ALTER TABLE "tm_task_users" DROP CONSTRAINT "FK_739cfd1807f78bb2ebda02e7bce"`); + await queryRunner.query(`ALTER TABLE "tm_workspaces" DROP CONSTRAINT "FK_253e36f065cfc1eca58abaefb22"`); + await queryRunner.query(`ALTER TABLE "tm_projects" DROP CONSTRAINT "FK_c3d997618910273be84d58faa8c"`); + await queryRunner.query(`ALTER TABLE "tm_task_phases" DROP CONSTRAINT "FK_884b8c2fe724031aa61c8181f0b"`); + await queryRunner.query(`ALTER TABLE "tm_tasks" DROP CONSTRAINT "FK_f7462cec045e564d5fd54d17177"`); + await queryRunner.query(`ALTER TABLE "tm_check_list_items" DROP CONSTRAINT "FK_14730b0971bcdc5547889392606"`); + await queryRunner.query(`ALTER TABLE "tm_attachments" DROP CONSTRAINT "FK_1e9ab0f9ec5b898abea95d21fb2"`); + await queryRunner.query(`ALTER TABLE "tm_remarks" DROP CONSTRAINT "FK_bc03cabb99c42245436fcbb4e6a"`); + await queryRunner.query(`ALTER TABLE "reward_withdraw_request" DROP CONSTRAINT "UQ_79526c10ecb097f553e0120a36e"`); + await queryRunner.query(`DROP INDEX "public"."IDX_1c9d677417216377d55a060011"`); + await queryRunner.query(`DROP INDEX "public"."IDX_ca9b0a16dfec7c4753ea4910f0"`); + await queryRunner.query(`DROP TABLE "tm_workspace_users"`); + await queryRunner.query(`DROP INDEX "public"."IDX_3fe44ce1acd8a620affb55a750"`); + await queryRunner.query(`DROP INDEX "public"."IDX_739cfd1807f78bb2ebda02e7bc"`); + await queryRunner.query(`DROP TABLE "tm_task_users"`); + await queryRunner.query(`DROP TABLE "tm_workspaces"`); + await queryRunner.query(`DROP TABLE "tm_projects"`); + await queryRunner.query(`DROP TABLE "tm_task_phases"`); + await queryRunner.query(`DROP TABLE "tm_tasks"`); + await queryRunner.query(`DROP TABLE "tm_check_list_items"`); + await queryRunner.query(`DROP TABLE "tm_attachments"`); + await queryRunner.query(`DROP TYPE "public"."tm_attachments_type_enum"`); + await queryRunner.query(`DROP TABLE "tm_remarks"`); + await queryRunner.query(`DROP TABLE "tm_workspace_types"`); + } + +} diff --git a/src/modules/task-manager/dto/workspace-type/create-workspace-type.dto.ts b/src/modules/task-manager/dto/workspace-type/create-workspace-type.dto.ts index 9b00e3f..d0d28c4 100644 --- a/src/modules/task-manager/dto/workspace-type/create-workspace-type.dto.ts +++ b/src/modules/task-manager/dto/workspace-type/create-workspace-type.dto.ts @@ -1,17 +1,25 @@ +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() - @IsOptional() + @IsNotEmpty() @Min(0) - order?: number; + order: number; } diff --git a/src/modules/task-manager/dto/workspace-type/update-workspace-type.dto.ts b/src/modules/task-manager/dto/workspace-type/update-workspace-type.dto.ts index 86e8b47..cc4a3ea 100644 --- a/src/modules/task-manager/dto/workspace-type/update-workspace-type.dto.ts +++ b/src/modules/task-manager/dto/workspace-type/update-workspace-type.dto.ts @@ -1,4 +1,4 @@ -import { PartialType } from "@nestjs/mapped-types"; -import { CreateWorkspaceTypeDto } from "./create-workspace-type.dto"; +import { PartialType } from '@nestjs/swagger'; +import { CreateWorkspaceTypeDto } from './create-workspace-type.dto'; -export class UpdateWorkspaceTypeDto extends PartialType(CreateWorkspaceTypeDto) {} +export class UpdateWorkspaceTypeDto extends PartialType(CreateWorkspaceTypeDto) {} \ No newline at end of file From 838f2bc4b727bb9918a958ef0e94eeef37dcc8c2 Mon Sep 17 00:00:00 2001 From: realrafi Date: Tue, 30 Jun 2026 13:07:23 +0330 Subject: [PATCH 05/66] feat: add workspace-type service --- .../providers/workspace-type.service.ts | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/modules/task-manager/providers/workspace-type.service.ts diff --git a/src/modules/task-manager/providers/workspace-type.service.ts b/src/modules/task-manager/providers/workspace-type.service.ts new file mode 100644 index 0000000..d2502dc --- /dev/null +++ b/src/modules/task-manager/providers/workspace-type.service.ts @@ -0,0 +1,51 @@ +import { Injectable, NotFoundException, ConflictException } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; +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( + @InjectRepository(TMWorkspaceType) + private readonly workspaceTypeRepository: Repository, + ) {} + + findAll(): Promise { + return this.workspaceTypeRepository.find({ + order: { order: "ASC" }, + }); + } + + async findOne(id: string): Promise { + const workspaceType = await this.workspaceTypeRepository.findOne({ where: { id } }); + if (!workspaceType) throw new NotFoundException(`WorkspaceType #${id} not found`); + return workspaceType; + } + + async create(dto: CreateWorkspaceTypeDto): Promise { + const existing = await this.workspaceTypeRepository.findOne({ where: { name: 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 { + await this.findOne(id); + + if (dto.name) { + const existing = await this.workspaceTypeRepository.findOne({ where: { name: 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 { + await this.findOne(id); + await this.workspaceTypeRepository.delete(id); + } +} From 2077477445b5929f355dc980c306a767997a60a1 Mon Sep 17 00:00:00 2001 From: realrafi Date: Tue, 30 Jun 2026 14:43:41 +0330 Subject: [PATCH 06/66] feat: add workspacetype controller --- .../controllers/workspace-type.controller.ts | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/modules/task-manager/controllers/workspace-type.controller.ts diff --git a/src/modules/task-manager/controllers/workspace-type.controller.ts b/src/modules/task-manager/controllers/workspace-type.controller.ts new file mode 100644 index 0000000..1ee357b --- /dev/null +++ b/src/modules/task-manager/controllers/workspace-type.controller.ts @@ -0,0 +1,55 @@ +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 - Workspace Types") +@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 { + 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 { + 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 { + 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 { + 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 { + return this.workspaceTypeService.remove(id); + } +} From a134429fa46d11df036be5eafb87d44557709367 Mon Sep 17 00:00:00 2001 From: realrafi Date: Tue, 30 Jun 2026 14:46:17 +0330 Subject: [PATCH 07/66] refactor: change return type of remove funcs --- .../task-manager/controllers/workspace-type.controller.ts | 2 +- .../task-manager/controllers/workspace.controller.ts | 2 +- .../task-manager/providers/workspace-type.service.ts | 6 ++++-- src/modules/task-manager/providers/workspace.service.ts | 6 ++++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/modules/task-manager/controllers/workspace-type.controller.ts b/src/modules/task-manager/controllers/workspace-type.controller.ts index 1ee357b..1f99a98 100644 --- a/src/modules/task-manager/controllers/workspace-type.controller.ts +++ b/src/modules/task-manager/controllers/workspace-type.controller.ts @@ -49,7 +49,7 @@ export class WorkspaceTypeController { @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 { + remove(@Param("id") id: string): Promise { return this.workspaceTypeService.remove(id); } } diff --git a/src/modules/task-manager/controllers/workspace.controller.ts b/src/modules/task-manager/controllers/workspace.controller.ts index 93cd8eb..3b0ce51 100644 --- a/src/modules/task-manager/controllers/workspace.controller.ts +++ b/src/modules/task-manager/controllers/workspace.controller.ts @@ -47,7 +47,7 @@ export class WorkspaceController { @ApiParam({ name: 'id', description: 'Workspace ID' }) @ApiResponse({ status: 200, description: 'Workspace deleted' }) @ApiResponse({ status: 404, description: 'Workspace not found' }) - remove(@Param('id') id: string): Promise { + remove(@Param('id') id: string): Promise { return this.workspaceService.remove(id); } } \ No newline at end of file diff --git a/src/modules/task-manager/providers/workspace-type.service.ts b/src/modules/task-manager/providers/workspace-type.service.ts index d2502dc..f084175 100644 --- a/src/modules/task-manager/providers/workspace-type.service.ts +++ b/src/modules/task-manager/providers/workspace-type.service.ts @@ -44,8 +44,10 @@ export class WorkspaceTypeService { return this.findOne(id); } - async remove(id: string): Promise { - await this.findOne(id); + async remove(id: string): Promise { + const workspaceType = await this.findOne(id); await this.workspaceTypeRepository.delete(id); + + return workspaceType; } } diff --git a/src/modules/task-manager/providers/workspace.service.ts b/src/modules/task-manager/providers/workspace.service.ts index 8d8ea40..0b81880 100644 --- a/src/modules/task-manager/providers/workspace.service.ts +++ b/src/modules/task-manager/providers/workspace.service.ts @@ -57,8 +57,10 @@ export class WorkspaceService { return this.workspaceRepository.save(workspace); } - async remove(id: string): Promise { - await this.findOneOrFail(id); + async remove(id: string): Promise { + const workspace = await this.findOneOrFail(id); await this.workspaceRepository.delete(id); + + return workspace; } } \ No newline at end of file From 62fd234eb48cf97adf7d774b163cb7e24854658c Mon Sep 17 00:00:00 2001 From: realrafi Date: Tue, 30 Jun 2026 14:53:50 +0330 Subject: [PATCH 08/66] feat: added workspace-type repository --- .../repositories/workspace-type.repository.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/modules/task-manager/repositories/workspace-type.repository.ts diff --git a/src/modules/task-manager/repositories/workspace-type.repository.ts b/src/modules/task-manager/repositories/workspace-type.repository.ts new file mode 100644 index 0000000..bfd0c85 --- /dev/null +++ b/src/modules/task-manager/repositories/workspace-type.repository.ts @@ -0,0 +1,42 @@ +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, + ) {} + + 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) { + return this.repository.create(data); + } + + save(workspaceType: TMWorkspaceType) { + return this.repository.save(workspaceType); + } + + update(id: string, data: Partial) { + return this.repository.update(id, data); + } + + delete(id: string) { + return this.repository.delete(id); + } +} From e886adbcbc19c8a3c2074e758c89e4b0c5c27fe8 Mon Sep 17 00:00:00 2001 From: realrafi Date: Tue, 30 Jun 2026 14:58:24 +0330 Subject: [PATCH 09/66] refactor: isolated the repository and logic in services --- .../providers/workspace-type.service.ts | 18 ++++++------------ .../providers/workspace.service.ts | 13 ++++--------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/src/modules/task-manager/providers/workspace-type.service.ts b/src/modules/task-manager/providers/workspace-type.service.ts index f084175..1e0f0ed 100644 --- a/src/modules/task-manager/providers/workspace-type.service.ts +++ b/src/modules/task-manager/providers/workspace-type.service.ts @@ -1,31 +1,25 @@ import { Injectable, NotFoundException, ConflictException } from "@nestjs/common"; -import { InjectRepository } from "@nestjs/typeorm"; -import { Repository } from "typeorm"; +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( - @InjectRepository(TMWorkspaceType) - private readonly workspaceTypeRepository: Repository, - ) {} + constructor(private readonly workspaceTypeRepository: WorkspaceTypeRepository) {} findAll(): Promise { - return this.workspaceTypeRepository.find({ - order: { order: "ASC" }, - }); + return this.workspaceTypeRepository.findAll(); } async findOne(id: string): Promise { - const workspaceType = await this.workspaceTypeRepository.findOne({ where: { id } }); + const workspaceType = await this.workspaceTypeRepository.findOneById(id); if (!workspaceType) throw new NotFoundException(`WorkspaceType #${id} not found`); return workspaceType; } async create(dto: CreateWorkspaceTypeDto): Promise { - const existing = await this.workspaceTypeRepository.findOne({ where: { name: dto.name } }); + 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); @@ -36,7 +30,7 @@ export class WorkspaceTypeService { await this.findOne(id); if (dto.name) { - const existing = await this.workspaceTypeRepository.findOne({ where: { name: 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`); } diff --git a/src/modules/task-manager/providers/workspace.service.ts b/src/modules/task-manager/providers/workspace.service.ts index 0b81880..b6d408e 100644 --- a/src/modules/task-manager/providers/workspace.service.ts +++ b/src/modules/task-manager/providers/workspace.service.ts @@ -1,6 +1,7 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { In, Repository } from 'typeorm'; +import { WorkspaceRepository } from '../repositories/workspace.repository'; import { TMWorkspace } from '../entities/workspace.entity'; import { CreateWorkspaceDto } from '../dto/workspace/create-workspace.dto'; import { UpdateWorkspaceDto } from '../dto/workspace/update-workspace.dto'; @@ -9,23 +10,17 @@ import { User } from '../../users/entities/user.entity'; @Injectable() export class WorkspaceService { constructor( - @InjectRepository(TMWorkspace) - private readonly workspaceRepository: Repository, + private readonly workspaceRepository: WorkspaceRepository, @InjectRepository(User) private readonly userRepository: Repository, ) {} findAll(): Promise { - return this.workspaceRepository.find({ - relations: ['workspaceType', 'projects', 'users'], - }); + return this.workspaceRepository.findAll(); } async findOneOrFail(id: string): Promise { - const workspace = await this.workspaceRepository.findOne({ - where: { id }, - relations: ['workspaceType', 'projects', 'users'], - }); + const workspace = await this.workspaceRepository.findOneById(id); if (!workspace) throw new NotFoundException(`Workspace #${id} not found`); return workspace; } From 77e365a259ed9eb0e7aa2544c47d31088a03259e Mon Sep 17 00:00:00 2001 From: realrafi Date: Tue, 30 Jun 2026 15:27:32 +0330 Subject: [PATCH 10/66] refactor: swagger tag --- .../controllers/workspace-type.controller.ts | 2 +- .../controllers/workspace.controller.ts | 2 +- .../task-manager/task-manager.module.ts | 55 ++++++++----------- 3 files changed, 25 insertions(+), 34 deletions(-) diff --git a/src/modules/task-manager/controllers/workspace-type.controller.ts b/src/modules/task-manager/controllers/workspace-type.controller.ts index 1f99a98..8b7e9bf 100644 --- a/src/modules/task-manager/controllers/workspace-type.controller.ts +++ b/src/modules/task-manager/controllers/workspace-type.controller.ts @@ -5,7 +5,7 @@ import { CreateWorkspaceTypeDto } from "../dto/workspace-type/create-workspace-t import { UpdateWorkspaceTypeDto } from "../dto/workspace-type/update-workspace-type.dto"; import { TMWorkspaceType } from "../entities/workspace-type.entity"; -@ApiTags("Task Manager - Workspace Types") +@ApiTags("Task Manager") @Controller("task-manager/workspace-types") export class WorkspaceTypeController { constructor(private readonly workspaceTypeService: WorkspaceTypeService) {} diff --git a/src/modules/task-manager/controllers/workspace.controller.ts b/src/modules/task-manager/controllers/workspace.controller.ts index 3b0ce51..bb9fcc5 100644 --- a/src/modules/task-manager/controllers/workspace.controller.ts +++ b/src/modules/task-manager/controllers/workspace.controller.ts @@ -5,7 +5,7 @@ import { CreateWorkspaceDto } from '../dto/workspace/create-workspace.dto'; import { UpdateWorkspaceDto } from '../dto/workspace/update-workspace.dto'; import { TMWorkspace } from '../entities/workspace.entity'; -@ApiTags('Task Manager - Workspaces') +@ApiTags('Task Manager') @Controller('task-manager/workspaces') export class WorkspaceController { constructor(private readonly workspaceService: WorkspaceService) {} diff --git a/src/modules/task-manager/task-manager.module.ts b/src/modules/task-manager/task-manager.module.ts index f580f2d..86007d4 100644 --- a/src/modules/task-manager/task-manager.module.ts +++ b/src/modules/task-manager/task-manager.module.ts @@ -1,18 +1,21 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; +import { Module } from "@nestjs/common"; +import { TypeOrmModule } from "@nestjs/typeorm"; -import { TMWorkspaceType } from './entities/workspace-type.entity'; -import { TMWorkspace } from './entities/workspace.entity'; -import { TMProject } from './entities/project.entity'; -import { TMTaskPhase } from './entities/task-phase.entity'; -import { TMTask } from './entities/task.entity'; -import { TMRemark } from './entities/remark.entity'; -import { TMAttachment } from './entities/attachment.entity'; -import { TMCheckListItem } from './entities/check-list-item.entity'; -import { WorkspaceRepository } from './repositories/workspace.repository'; -import { WorkspaceController } from './controllers/workspace.controller'; -import { WorkspaceService } from './providers/workspace.service'; -import { User } from '../users/entities/user.entity'; +import { TMWorkspaceType } from "./entities/workspace-type.entity"; +import { TMWorkspace } from "./entities/workspace.entity"; +import { TMProject } from "./entities/project.entity"; +import { TMTaskPhase } from "./entities/task-phase.entity"; +import { TMTask } from "./entities/task.entity"; +import { TMRemark } from "./entities/remark.entity"; +import { TMAttachment } from "./entities/attachment.entity"; +import { TMCheckListItem } from "./entities/check-list-item.entity"; +import { WorkspaceRepository } from "./repositories/workspace.repository"; +import { WorkspaceController } from "./controllers/workspace.controller"; +import { WorkspaceService } from "./providers/workspace.service"; +import { User } from "../users/entities/user.entity"; +import { WorkspaceTypeService } from "./providers/workspace-type.service"; +import { WorkspaceTypeRepository } from "./repositories/workspace-type.repository"; +import { WorkspaceTypeController } from "./controllers/workspace-type.controller"; // import { WorkspaceTypeService } from './providers/workspace-type.service'; // import { WorkspaceService } from './providers/workspace.service'; @@ -24,30 +27,18 @@ import { User } from '../users/entities/user.entity'; @Module({ imports: [ - TypeOrmModule.forFeature([ - TMWorkspaceType, - TMWorkspace, - TMProject, - TMTaskPhase, - TMTask, - TMRemark, - TMAttachment, - TMCheckListItem, - User - ]), + TypeOrmModule.forFeature([TMWorkspaceType, TMWorkspace, TMProject, TMTaskPhase, TMTask, TMRemark, TMAttachment, TMCheckListItem, User]), ], - controllers: [WorkspaceController], + controllers: [WorkspaceController, WorkspaceTypeController], providers: [ WorkspaceRepository, WorkspaceService, - // WorkspaceTypeService, + WorkspaceTypeRepository, + WorkspaceTypeService, // ProjectService, // TaskPhaseService, // TaskService, ], - exports: [ - WorkspaceService, - WorkspaceRepository - ], + exports: [WorkspaceService, WorkspaceRepository], }) -export class TaskManagerModule {} \ No newline at end of file +export class TaskManagerModule {} From a4aba43bc1a65fb8e67fac2aa97869d16de59c2a Mon Sep 17 00:00:00 2001 From: realrafi Date: Tue, 30 Jun 2026 16:37:45 +0330 Subject: [PATCH 11/66] refactor: add validation to workspace create --- .../providers/workspace.service.ts | 36 +++++++++++-------- .../repositories/workspace.repository.ts | 1 - .../task-manager/task-manager.module.ts | 2 +- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/modules/task-manager/providers/workspace.service.ts b/src/modules/task-manager/providers/workspace.service.ts index b6d408e..06e68f4 100644 --- a/src/modules/task-manager/providers/workspace.service.ts +++ b/src/modules/task-manager/providers/workspace.service.ts @@ -1,16 +1,18 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { In, Repository } from 'typeorm'; -import { WorkspaceRepository } from '../repositories/workspace.repository'; -import { TMWorkspace } from '../entities/workspace.entity'; -import { CreateWorkspaceDto } from '../dto/workspace/create-workspace.dto'; -import { UpdateWorkspaceDto } from '../dto/workspace/update-workspace.dto'; -import { User } from '../../users/entities/user.entity'; +import { Injectable, NotFoundException } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { In, Repository } from "typeorm"; +import { WorkspaceRepository } from "../repositories/workspace.repository"; +import { TMWorkspace } from "../entities/workspace.entity"; +import { CreateWorkspaceDto } from "../dto/workspace/create-workspace.dto"; +import { UpdateWorkspaceDto } from "../dto/workspace/update-workspace.dto"; +import { User } from "../../users/entities/user.entity"; +import { WorkspaceTypeRepository } from "../repositories/workspace-type.repository"; @Injectable() export class WorkspaceService { constructor( private readonly workspaceRepository: WorkspaceRepository, + private readonly workspaceTypeRepository: WorkspaceTypeRepository, @InjectRepository(User) private readonly userRepository: Repository, ) {} @@ -26,7 +28,10 @@ export class WorkspaceService { } async create(dto: CreateWorkspaceDto): Promise { - const { userIds, ...rest } = dto; + const { userIds, workspaceTypeId, ...rest } = dto; + + const workspaceType = await this.workspaceTypeRepository.findOneById(workspaceTypeId); + if (!workspaceType) throw new NotFoundException(`WorkSpaceType #${workspaceTypeId} not found!`); const workspace = this.workspaceRepository.create(rest); @@ -39,14 +44,17 @@ export class WorkspaceService { async update(id: string, dto: UpdateWorkspaceDto): Promise { const workspace = await this.findOneOrFail(id); - const { userIds, ...rest } = dto; + const { userIds, workspaceTypeId, ...rest } = dto; + + if (workspaceTypeId) { + const workspaceType = await this.workspaceTypeRepository.findOneById(workspaceTypeId); + if (!workspaceType) throw new NotFoundException(`WorkSpaceType #${workspaceTypeId} not found!`); + } Object.assign(workspace, rest); if (userIds) { - workspace.users = userIds.length - ? await this.userRepository.findBy({ id: In(userIds) }) - : []; + workspace.users = userIds.length ? await this.userRepository.findBy({ id: In(userIds) }) : []; } return this.workspaceRepository.save(workspace); @@ -58,4 +66,4 @@ export class WorkspaceService { return workspace; } -} \ No newline at end of file +} diff --git a/src/modules/task-manager/repositories/workspace.repository.ts b/src/modules/task-manager/repositories/workspace.repository.ts index fd38d40..2d63626 100644 --- a/src/modules/task-manager/repositories/workspace.repository.ts +++ b/src/modules/task-manager/repositories/workspace.repository.ts @@ -19,7 +19,6 @@ export class WorkspaceRepository { findOneById(id: string) { return this.repository.findOne({ where: { id }, - relations: ["workspaceType", "projects", "users"], }); } diff --git a/src/modules/task-manager/task-manager.module.ts b/src/modules/task-manager/task-manager.module.ts index 86007d4..35a1c5c 100644 --- a/src/modules/task-manager/task-manager.module.ts +++ b/src/modules/task-manager/task-manager.module.ts @@ -39,6 +39,6 @@ import { WorkspaceTypeController } from "./controllers/workspace-type.controller // TaskPhaseService, // TaskService, ], - exports: [WorkspaceService, WorkspaceRepository], + exports: [WorkspaceService, WorkspaceRepository, WorkspaceTypeService, WorkspaceTypeRepository], }) export class TaskManagerModule {} From 9b32f35582fe4a61f703d8b3758fcbc6a337723d Mon Sep 17 00:00:00 2001 From: realrafi Date: Tue, 30 Jun 2026 17:00:54 +0330 Subject: [PATCH 12/66] feat: add project DTOs --- .../dto/project/create-project.dto.ts | 40 +++++++++++++++++++ .../dto/project/update-project.dto.ts | 4 ++ .../task-manager/entities/project.entity.ts | 3 ++ 3 files changed, 47 insertions(+) create mode 100644 src/modules/task-manager/dto/project/create-project.dto.ts create mode 100644 src/modules/task-manager/dto/project/update-project.dto.ts diff --git a/src/modules/task-manager/dto/project/create-project.dto.ts b/src/modules/task-manager/dto/project/create-project.dto.ts new file mode 100644 index 0000000..7c9c2c1 --- /dev/null +++ b/src/modules/task-manager/dto/project/create-project.dto.ts @@ -0,0 +1,40 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsString, IsOptional, IsNotEmpty, IsUUID, IsDateString, MaxLength } from "class-validator"; + +export class CreateProjectDto { + @ApiProperty({ description: "Name of the project", example: "Mobile App Redesign" }) + @IsString() + @IsNotEmpty() + @MaxLength(150) + name: string; + + @ApiProperty({ description: "Name of the project owner", example: "Jane Doe" }) + @IsString() + @IsNotEmpty() + @MaxLength(150) + ownerName: string; + + @ApiProperty({ description: "Description of the project", required: false, example: "Redesign of the mobile app UI/UX" }) + @IsString() + @IsOptional() + description?: string; + + @ApiProperty({ description: "Start date of the project", example: "2026-07-01" }) + @IsDateString() + @IsNotEmpty() + startDate: string; + + @ApiProperty({ + description: "Background picture URL of the project", + required: false, + example: "https://cdn.example.com/projects/bg1.png", + }) + @IsString() + @IsOptional() + backgroundPicture?: string; + + @ApiProperty({ description: "ID of the workspace this project belongs to", example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }) + @IsUUID() + @IsNotEmpty() + workspaceId: string; +} diff --git a/src/modules/task-manager/dto/project/update-project.dto.ts b/src/modules/task-manager/dto/project/update-project.dto.ts new file mode 100644 index 0000000..df437f1 --- /dev/null +++ b/src/modules/task-manager/dto/project/update-project.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateProjectDto } from './create-project.dto'; + +export class UpdateProjectDto extends PartialType(CreateProjectDto) {} \ No newline at end of file diff --git a/src/modules/task-manager/entities/project.entity.ts b/src/modules/task-manager/entities/project.entity.ts index a4e761b..890870c 100644 --- a/src/modules/task-manager/entities/project.entity.ts +++ b/src/modules/task-manager/entities/project.entity.ts @@ -17,6 +17,9 @@ export class TMProject extends BaseEntity { @Column({ type: "timestamptz", nullable: true }) startDate: Date; + @Column({ type: "text", nullable: true }) + backgroundPicture: string; + // --- Relations --- @ManyToOne(() => TMWorkspace, (workspace) => workspace.projects, { From 89d85e6d2401aca62901463427763d78542187cd Mon Sep 17 00:00:00 2001 From: realrafi Date: Wed, 1 Jul 2026 10:39:12 +0330 Subject: [PATCH 13/66] feat: added project section of TM with some refactors --- .../1782889623829-taskmanager-project.ts | 28 ++++++ .../controllers/project.controller.ts | 58 ++++++++++++ .../dto/project/create-project.dto.ts | 23 ++++- .../dto/project/update-project.dto.ts | 6 +- .../task-manager/entities/project.entity.ts | 17 +++- .../task-manager/providers/project.service.ts | 92 +++++++++++++++++++ .../repositories/project.repository.ts | 48 ++++++++++ .../task-manager/task-manager.module.ts | 18 ++-- src/modules/users/entities/user.entity.ts | 4 + 9 files changed, 278 insertions(+), 16 deletions(-) create mode 100644 database/migrations/1782889623829-taskmanager-project.ts create mode 100644 src/modules/task-manager/controllers/project.controller.ts create mode 100644 src/modules/task-manager/providers/project.service.ts create mode 100644 src/modules/task-manager/repositories/project.repository.ts diff --git a/database/migrations/1782889623829-taskmanager-project.ts b/database/migrations/1782889623829-taskmanager-project.ts new file mode 100644 index 0000000..95869e2 --- /dev/null +++ b/database/migrations/1782889623829-taskmanager-project.ts @@ -0,0 +1,28 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class TaskmanagerProject1782889623829 implements MigrationInterface { + name = 'TaskmanagerProject1782889623829' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "tm_project_users" ("projectId" uuid NOT NULL, "userId" uuid NOT NULL, CONSTRAINT "PK_5969064e19a1efa76b91de6a065" PRIMARY KEY ("projectId", "userId"))`); + await queryRunner.query(`CREATE INDEX "IDX_fcc8e135e4891925d288dbb14e" ON "tm_project_users" ("projectId") `); + await queryRunner.query(`CREATE INDEX "IDX_8e2d2c9d639d5fd806159f8698" ON "tm_project_users" ("userId") `); + await queryRunner.query(`ALTER TABLE "tm_projects" ADD "backgroundPicture" text`); + await queryRunner.query(`ALTER TABLE "tm_projects" ADD "backgroundColor" character varying(20)`); + await queryRunner.query(`ALTER TABLE "tm_projects" ADD "isActive" boolean NOT NULL DEFAULT true`); + await queryRunner.query(`ALTER TABLE "tm_project_users" ADD CONSTRAINT "FK_fcc8e135e4891925d288dbb14e5" FOREIGN KEY ("projectId") REFERENCES "tm_projects"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "tm_project_users" ADD CONSTRAINT "FK_8e2d2c9d639d5fd806159f86980" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tm_project_users" DROP CONSTRAINT "FK_8e2d2c9d639d5fd806159f86980"`); + await queryRunner.query(`ALTER TABLE "tm_project_users" DROP CONSTRAINT "FK_fcc8e135e4891925d288dbb14e5"`); + await queryRunner.query(`ALTER TABLE "tm_projects" DROP COLUMN "isActive"`); + await queryRunner.query(`ALTER TABLE "tm_projects" DROP COLUMN "backgroundColor"`); + await queryRunner.query(`ALTER TABLE "tm_projects" DROP COLUMN "backgroundPicture"`); + await queryRunner.query(`DROP INDEX "public"."IDX_8e2d2c9d639d5fd806159f8698"`); + await queryRunner.query(`DROP INDEX "public"."IDX_fcc8e135e4891925d288dbb14e"`); + await queryRunner.query(`DROP TABLE "tm_project_users"`); + } + +} diff --git a/src/modules/task-manager/controllers/project.controller.ts b/src/modules/task-manager/controllers/project.controller.ts new file mode 100644 index 0000000..5f59e4d --- /dev/null +++ b/src/modules/task-manager/controllers/project.controller.ts @@ -0,0 +1,58 @@ +import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from "@nestjs/common"; +import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from "@nestjs/swagger"; +import { ProjectService } from "../providers/project.service"; +import { CreateProjectDto } from "../dto/project/create-project.dto"; +import { UpdateProjectDto } from "../dto/project/update-project.dto"; +import { TMProject } from "../entities/project.entity"; + +@ApiTags("Task Manager - Projects") +@Controller("task-manager/projects") +export class ProjectController { + constructor(private readonly projectService: ProjectService) {} + + @Get() + @ApiOperation({ summary: "Get all projects, optionally filtered by workspace" }) + @ApiQuery({ name: "workspaceId", required: false, description: "Filter projects by workspace ID" }) + @ApiResponse({ status: 200, description: "List of projects", type: [TMProject] }) + findAll(@Query("workspaceId") workspaceId?: string): Promise { + if (workspaceId) return this.projectService.findByWorkspace(workspaceId); + return this.projectService.findAll(); + } + + @Get(":id") + @ApiOperation({ summary: "Get a project by ID" }) + @ApiParam({ name: "id", description: "Project ID" }) + @ApiResponse({ status: 200, description: "Project found", type: TMProject }) + @ApiResponse({ status: 404, description: "Project not found" }) + findOne(@Param("id") id: string): Promise { + return this.projectService.findOne(id); + } + + @Post() + @ApiOperation({ summary: "Create a new project" }) + @ApiResponse({ status: 201, description: "Project created", type: TMProject }) + @ApiResponse({ status: 400, description: "One or more users are not members of the workspace" }) + @ApiResponse({ status: 404, description: "Workspace not found" }) + create(@Body() body: CreateProjectDto): Promise { + return this.projectService.create(body); + } + + @Patch(":id") + @ApiOperation({ summary: "Update a project" }) + @ApiParam({ name: "id", description: "Project ID" }) + @ApiResponse({ status: 200, description: "Project updated", type: TMProject }) + @ApiResponse({ status: 400, description: "One or more users are not members of the workspace" }) + @ApiResponse({ status: 404, description: "Project not found" }) + update(@Param("id") id: string, @Body() body: UpdateProjectDto): Promise { + return this.projectService.update(id, body); + } + + @Delete(":id") + @ApiOperation({ summary: "Delete a project" }) + @ApiParam({ name: "id", description: "Project ID" }) + @ApiResponse({ status: 200, description: "Project deleted" }) + @ApiResponse({ status: 404, description: "Project not found" }) + remove(@Param("id") id: string): Promise { + return this.projectService.remove(id); + } +} diff --git a/src/modules/task-manager/dto/project/create-project.dto.ts b/src/modules/task-manager/dto/project/create-project.dto.ts index 7c9c2c1..588fcb9 100644 --- a/src/modules/task-manager/dto/project/create-project.dto.ts +++ b/src/modules/task-manager/dto/project/create-project.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsString, IsOptional, IsNotEmpty, IsUUID, IsDateString, MaxLength } from "class-validator"; +import { IsString, IsOptional, IsNotEmpty, IsUUID, IsDateString, IsBoolean, MaxLength } from "class-validator"; export class CreateProjectDto { @ApiProperty({ description: "Name of the project", example: "Mobile App Redesign" }) @@ -33,8 +33,29 @@ export class CreateProjectDto { @IsOptional() backgroundPicture?: string; + @ApiProperty({ description: "Background color of the project (hex or named)", required: false, example: "#4F46E5" }) + @IsString() + @IsOptional() + @MaxLength(20) + backgroundColor?: string; + + @ApiProperty({ description: "Whether the project is active", required: false, default: true, example: true }) + @IsBoolean() + @IsOptional() + isActive?: boolean; + @ApiProperty({ description: "ID of the workspace this project belongs to", example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }) @IsUUID() @IsNotEmpty() workspaceId: string; + + @ApiProperty({ + description: "IDs of users to assign to the project (must be members of the workspace)", + required: false, + type: [String], + example: ["b2c3d4e5-f6a7-8901-bcde-f12345678901"], + }) + @IsUUID("4", { each: true }) + @IsOptional() + userIds?: string[]; } diff --git a/src/modules/task-manager/dto/project/update-project.dto.ts b/src/modules/task-manager/dto/project/update-project.dto.ts index df437f1..2b845b1 100644 --- a/src/modules/task-manager/dto/project/update-project.dto.ts +++ b/src/modules/task-manager/dto/project/update-project.dto.ts @@ -1,4 +1,4 @@ -import { PartialType } from '@nestjs/swagger'; -import { CreateProjectDto } from './create-project.dto'; +import { PartialType } from "@nestjs/swagger"; +import { CreateProjectDto } from "./create-project.dto"; -export class UpdateProjectDto extends PartialType(CreateProjectDto) {} \ No newline at end of file +export class UpdateProjectDto extends PartialType(CreateProjectDto) {} diff --git a/src/modules/task-manager/entities/project.entity.ts b/src/modules/task-manager/entities/project.entity.ts index 890870c..0166a5c 100644 --- a/src/modules/task-manager/entities/project.entity.ts +++ b/src/modules/task-manager/entities/project.entity.ts @@ -1,7 +1,8 @@ -import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from "typeorm"; +import { Column, Entity, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToMany } from "typeorm"; import { BaseEntity } from "../../../common/entities/base.entity"; import { TMWorkspace } from "./workspace.entity"; import { TMTaskPhase } from "./task-phase.entity"; +import { User } from "../../users/entities/user.entity"; @Entity("tm_projects") export class TMProject extends BaseEntity { @@ -20,6 +21,12 @@ export class TMProject extends BaseEntity { @Column({ type: "text", nullable: true }) backgroundPicture: string; + @Column({ type: "varchar", length: 20, nullable: true }) + backgroundColor: string; + + @Column({ type: "boolean", default: true }) + isActive: boolean; + // --- Relations --- @ManyToOne(() => TMWorkspace, (workspace) => workspace.projects, { @@ -36,4 +43,12 @@ export class TMProject extends BaseEntity { cascade: ["insert", "update"], }) taskPhases: TMTaskPhase[]; + + @ManyToMany(() => User, (user) => user.projects) + @JoinTable({ + name: "tm_project_users", + joinColumn: { name: "projectId", referencedColumnName: "id" }, + inverseJoinColumn: { name: "userId", referencedColumnName: "id" }, + }) + users: User[]; } diff --git a/src/modules/task-manager/providers/project.service.ts b/src/modules/task-manager/providers/project.service.ts new file mode 100644 index 0000000..752aede --- /dev/null +++ b/src/modules/task-manager/providers/project.service.ts @@ -0,0 +1,92 @@ +import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { In, Repository } from "typeorm"; +import { ProjectRepository } from "../repositories/project.repository"; +import { TMProject } from "../entities/project.entity"; +import { CreateProjectDto } from "../dto/project/create-project.dto"; +import { UpdateProjectDto } from "../dto/project/update-project.dto"; +import { User } from "../../users/entities/user.entity"; +import { WorkspaceRepository } from "../repositories/workspace.repository"; + +@Injectable() +export class ProjectService { + constructor( + private readonly projectRepository: ProjectRepository, + private readonly workspaceRepository: WorkspaceRepository, + @InjectRepository(User) + private readonly userRepository: Repository, + ) {} + + findAll(): Promise { + return this.projectRepository.findAll(); + } + + findByWorkspace(workspaceId: string): Promise { + return this.projectRepository.findByWorkspace(workspaceId); + } + + async findOne(id: string): Promise { + const project = await this.projectRepository.findOneById(id); + if (!project) throw new NotFoundException(`Project #${id} not found`); + return project; + } + + async create(dto: CreateProjectDto): Promise { + const { userIds, startDate, ...rest } = dto; + + const workspace = await this.workspaceRepository.findOneById(dto.workspaceId); + if (!workspace) throw new NotFoundException(`Workspace #${dto.workspaceId} not found`); + + const project = this.projectRepository.create({ + ...rest, + startDate: new Date(startDate), + }); + + if (userIds?.length) { + const workspaceUserIds = workspace.users.map((u) => u.id); + const invalidUsers = userIds.filter((id) => !workspaceUserIds.includes(id)); + if (invalidUsers.length) { + throw new BadRequestException(`Users [${invalidUsers.join(", ")}] are not members of this workspace`); + } + project.users = await this.userRepository.findBy({ id: In(userIds) }); + } + + return this.projectRepository.save(project); + } + + async update(id: string, dto: UpdateProjectDto): Promise { + const project = await this.findOne(id); + const { userIds, startDate, ...rest } = dto; + + Object.assign(project, { + ...rest, + startDate: startDate ? new Date(startDate) : project.startDate, + }); + + if (userIds) { + if (userIds.length) { + const workspaceId = dto.workspaceId ?? project.workspaceId; + const workspace = await this.workspaceRepository.findOneById(workspaceId); + if (!workspace) throw new NotFoundException(`Workspace #${workspaceId} not found`); + + const workspaceUserIds = workspace.users.map((u) => u.id); + const invalidUsers = userIds.filter((uid) => !workspaceUserIds.includes(uid)); + if (invalidUsers.length) { + throw new BadRequestException(`Users [${invalidUsers.join(", ")}] are not members of this workspace`); + } + project.users = await this.userRepository.findBy({ id: In(userIds) }); + } else { + project.users = []; + } + } + + return this.projectRepository.save(project); + } + + async remove(id: string): Promise { + const project = await this.findOne(id); + await this.projectRepository.delete(id); + + return project; + } +} diff --git a/src/modules/task-manager/repositories/project.repository.ts b/src/modules/task-manager/repositories/project.repository.ts new file mode 100644 index 0000000..0a4a1c4 --- /dev/null +++ b/src/modules/task-manager/repositories/project.repository.ts @@ -0,0 +1,48 @@ +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; +import { TMProject } from "../entities/project.entity"; + +@Injectable() +export class ProjectRepository { + constructor( + @InjectRepository(TMProject) + private readonly repository: Repository, + ) {} + + findAll() { + return this.repository.find({ + relations: ["workspace", "taskPhases", "users"], + }); + } + + findByWorkspace(workspaceId: string) { + return this.repository.find({ + where: { workspaceId }, + relations: ["taskPhases", "users"], + }); + } + + findOneById(id: string) { + return this.repository.findOne({ + where: { id }, + relations: ["workspace", "taskPhases", "users"], + }); + } + + create(data: Partial) { + return this.repository.create(data); + } + + save(project: TMProject) { + return this.repository.save(project); + } + + update(id: string, data: Partial) { + return this.repository.update(id, data); + } + + delete(id: string) { + return this.repository.delete(id); + } +} diff --git a/src/modules/task-manager/task-manager.module.ts b/src/modules/task-manager/task-manager.module.ts index 35a1c5c..d92c7c2 100644 --- a/src/modules/task-manager/task-manager.module.ts +++ b/src/modules/task-manager/task-manager.module.ts @@ -16,29 +16,25 @@ import { User } from "../users/entities/user.entity"; import { WorkspaceTypeService } from "./providers/workspace-type.service"; import { WorkspaceTypeRepository } from "./repositories/workspace-type.repository"; import { WorkspaceTypeController } from "./controllers/workspace-type.controller"; - -// import { WorkspaceTypeService } from './providers/workspace-type.service'; -// import { WorkspaceService } from './providers/workspace.service'; -// import { ProjectService } from './providers/project.service'; -// import { TaskPhaseService } from './providers/task-phase.service'; -// import { TaskService } from './providers/task.service'; - -// import { TaskManagerController } from './task-manager.controller'; +import { ProjectController } from "./controllers/project.controller"; +import { ProjectRepository } from "./repositories/project.repository"; +import { ProjectService } from "./providers/project.service"; @Module({ imports: [ TypeOrmModule.forFeature([TMWorkspaceType, TMWorkspace, TMProject, TMTaskPhase, TMTask, TMRemark, TMAttachment, TMCheckListItem, User]), ], - controllers: [WorkspaceController, WorkspaceTypeController], + controllers: [WorkspaceController, WorkspaceTypeController, ProjectController], providers: [ WorkspaceRepository, WorkspaceService, WorkspaceTypeRepository, WorkspaceTypeService, - // ProjectService, + ProjectRepository, + ProjectService, // TaskPhaseService, // TaskService, ], - exports: [WorkspaceService, WorkspaceRepository, WorkspaceTypeService, WorkspaceTypeRepository], + exports: [WorkspaceService, WorkspaceRepository, WorkspaceTypeService, WorkspaceTypeRepository, ProjectService, ProjectRepository], }) export class TaskManagerModule {} diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index abcd099..5b85278 100755 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -34,6 +34,7 @@ import { Reseller } from "../../reseller/entities/reseller.entity"; import { UserBankAccount } from "../../reseller/entities/user-bank-account.entity"; import { TMWorkspace } from "../../task-manager/entities/workspace.entity"; import { TMTask } from "../../task-manager/entities/task.entity"; +import { TMProject } from "../../task-manager/entities/project.entity"; @Entity() export class User extends BaseEntity { @@ -185,4 +186,7 @@ export class User extends BaseEntity { @ManyToMany(() => TMTask, (task) => task.users) tasks: TMTask[]; + + @ManyToMany(() => TMProject, (project) => project.users) + projects: TMProject[]; } From bf98b8b9ced6e57735d0f8ead3737b0503a1e8eb Mon Sep 17 00:00:00 2001 From: realrafi Date: Wed, 1 Jul 2026 12:53:42 +0330 Subject: [PATCH 14/66] feat: added task phase APIs --- .../controllers/project.controller.ts | 72 +++++++++---------- .../controllers/task-phase.controller.ts | 56 +++++++++++++++ .../dto/task-phase/create-task-phase.dto.ts | 27 +++++++ .../dto/task-phase/update-task-phase.dto.ts | 4 ++ .../providers/task-phase.service.ts | 53 ++++++++++++++ .../repositories/task-phase.repository.ts | 50 +++++++++++++ 6 files changed, 226 insertions(+), 36 deletions(-) create mode 100644 src/modules/task-manager/controllers/task-phase.controller.ts create mode 100644 src/modules/task-manager/dto/task-phase/create-task-phase.dto.ts create mode 100644 src/modules/task-manager/dto/task-phase/update-task-phase.dto.ts create mode 100644 src/modules/task-manager/providers/task-phase.service.ts create mode 100644 src/modules/task-manager/repositories/task-phase.repository.ts diff --git a/src/modules/task-manager/controllers/project.controller.ts b/src/modules/task-manager/controllers/project.controller.ts index 5f59e4d..3fae2a2 100644 --- a/src/modules/task-manager/controllers/project.controller.ts +++ b/src/modules/task-manager/controllers/project.controller.ts @@ -1,58 +1,58 @@ -import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from "@nestjs/common"; -import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from "@nestjs/swagger"; -import { ProjectService } from "../providers/project.service"; -import { CreateProjectDto } from "../dto/project/create-project.dto"; -import { UpdateProjectDto } from "../dto/project/update-project.dto"; -import { TMProject } from "../entities/project.entity"; +import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger'; +import { ProjectService } from '../providers/project.service'; +import { CreateProjectDto } from '../dto/project/create-project.dto'; +import { UpdateProjectDto } from '../dto/project/update-project.dto'; +import { TMProject } from '../entities/project.entity'; -@ApiTags("Task Manager - Projects") -@Controller("task-manager/projects") +@ApiTags('Task Manager - Projects') +@Controller('task-manager/projects') export class ProjectController { constructor(private readonly projectService: ProjectService) {} @Get() - @ApiOperation({ summary: "Get all projects, optionally filtered by workspace" }) - @ApiQuery({ name: "workspaceId", required: false, description: "Filter projects by workspace ID" }) - @ApiResponse({ status: 200, description: "List of projects", type: [TMProject] }) - findAll(@Query("workspaceId") workspaceId?: string): Promise { + @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 { if (workspaceId) return this.projectService.findByWorkspace(workspaceId); return this.projectService.findAll(); } - @Get(":id") - @ApiOperation({ summary: "Get a project by ID" }) - @ApiParam({ name: "id", description: "Project ID" }) - @ApiResponse({ status: 200, description: "Project found", type: TMProject }) - @ApiResponse({ status: 404, description: "Project not found" }) - findOne(@Param("id") id: string): Promise { + @Get(':id') + @ApiOperation({ summary: 'Get a project by ID' }) + @ApiParam({ name: 'id', description: 'Project ID' }) + @ApiResponse({ status: 200, description: 'Project found', type: TMProject }) + @ApiResponse({ status: 404, description: 'Project not found' }) + findOne(@Param('id') id: string): Promise { return this.projectService.findOne(id); } @Post() - @ApiOperation({ summary: "Create a new project" }) - @ApiResponse({ status: 201, description: "Project created", type: TMProject }) - @ApiResponse({ status: 400, description: "One or more users are not members of the workspace" }) - @ApiResponse({ status: 404, description: "Workspace not found" }) + @ApiOperation({ summary: 'Create a new project' }) + @ApiResponse({ status: 201, description: 'Project created', type: TMProject }) + @ApiResponse({ status: 400, description: 'One or more users are not members of the workspace' }) + @ApiResponse({ status: 404, description: 'Workspace not found' }) create(@Body() body: CreateProjectDto): Promise { return this.projectService.create(body); } - @Patch(":id") - @ApiOperation({ summary: "Update a project" }) - @ApiParam({ name: "id", description: "Project ID" }) - @ApiResponse({ status: 200, description: "Project updated", type: TMProject }) - @ApiResponse({ status: 400, description: "One or more users are not members of the workspace" }) - @ApiResponse({ status: 404, description: "Project not found" }) - update(@Param("id") id: string, @Body() body: UpdateProjectDto): Promise { + @Patch(':id') + @ApiOperation({ summary: 'Update a project' }) + @ApiParam({ name: 'id', description: 'Project ID' }) + @ApiResponse({ status: 200, description: 'Project updated', type: TMProject }) + @ApiResponse({ status: 400, description: 'One or more users are not members of the workspace' }) + @ApiResponse({ status: 404, description: 'Project not found' }) + update(@Param('id') id: string, @Body() body: UpdateProjectDto): Promise { return this.projectService.update(id, body); } - @Delete(":id") - @ApiOperation({ summary: "Delete a project" }) - @ApiParam({ name: "id", description: "Project ID" }) - @ApiResponse({ status: 200, description: "Project deleted" }) - @ApiResponse({ status: 404, description: "Project not found" }) - remove(@Param("id") id: string): Promise { + @Delete(':id') + @ApiOperation({ summary: 'Delete a project' }) + @ApiParam({ name: 'id', description: 'Project ID' }) + @ApiResponse({ status: 200, description: 'Project deleted' }) + @ApiResponse({ status: 404, description: 'Project not found' }) + remove(@Param('id') id: string): Promise { return this.projectService.remove(id); } -} +} \ No newline at end of file diff --git a/src/modules/task-manager/controllers/task-phase.controller.ts b/src/modules/task-manager/controllers/task-phase.controller.ts new file mode 100644 index 0000000..e38c41c --- /dev/null +++ b/src/modules/task-manager/controllers/task-phase.controller.ts @@ -0,0 +1,56 @@ +import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from "@nestjs/common"; +import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from "@nestjs/swagger"; +import { TaskPhaseService } from "../providers/task-phase.service"; +import { CreateTaskPhaseDto } from "../dto/task-phase/create-task-phase.dto"; +import { UpdateTaskPhaseDto } from "../dto/task-phase/update-task-phase.dto"; +import { TMTaskPhase } from "../entities/task-phase.entity"; + +@ApiTags("Task Manager - Task Phases") +@Controller("task-manager/task-phases") +export class TaskPhaseController { + constructor(private readonly taskPhaseService: TaskPhaseService) {} + + @Get() + @ApiOperation({ summary: "Get all task phases, optionally filtered by project" }) + @ApiQuery({ name: "projectId", required: false, description: "Filter task phases by project ID" }) + @ApiResponse({ status: 200, description: "List of task phases", type: [TMTaskPhase] }) + findAll(@Query("projectId") projectId?: string): Promise { + 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 { + return this.taskPhaseService.findOne(id); + } + + @Post() + @ApiOperation({ summary: "Create a new task phase" }) + @ApiResponse({ status: 201, description: "TaskPhase created", type: TMTaskPhase }) + @ApiResponse({ status: 404, description: "Project not found" }) + create(@Body() body: CreateTaskPhaseDto): Promise { + return this.taskPhaseService.create(body); + } + + @Patch(":id") + @ApiOperation({ summary: "Update a task phase" }) + @ApiParam({ name: "id", description: "TaskPhase ID" }) + @ApiResponse({ status: 200, description: "TaskPhase updated", type: TMTaskPhase }) + @ApiResponse({ status: 404, description: "TaskPhase or Project not found" }) + update(@Param("id") id: string, @Body() body: UpdateTaskPhaseDto): Promise { + return this.taskPhaseService.update(id, body); + } + + @Delete(":id") + @ApiOperation({ summary: "Delete a task phase" }) + @ApiParam({ name: "id", description: "TaskPhase ID" }) + @ApiResponse({ status: 200, description: "TaskPhase deleted" }) + @ApiResponse({ status: 404, description: "TaskPhase not found" }) + remove(@Param("id") id: string): Promise { + return this.taskPhaseService.remove(id); + } +} diff --git a/src/modules/task-manager/dto/task-phase/create-task-phase.dto.ts b/src/modules/task-manager/dto/task-phase/create-task-phase.dto.ts new file mode 100644 index 0000000..69dc0f2 --- /dev/null +++ b/src/modules/task-manager/dto/task-phase/create-task-phase.dto.ts @@ -0,0 +1,27 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsString, IsOptional, IsNotEmpty, IsUUID, IsInt, MaxLength, Min } from "class-validator"; + +export class CreateTaskPhaseDto { + @ApiProperty({ description: "Name of the task phase", example: "In Progress" }) + @IsString() + @IsNotEmpty() + @MaxLength(100) + name: string; + + @ApiProperty({ description: "Display order of the task phase", required: false, example: 1 }) + @IsInt() + @IsOptional() + @Min(0) + order?: number; + + @ApiProperty({ description: "Color code for the task phase", required: false, example: "#4F46E5" }) + @IsString() + @IsOptional() + @MaxLength(20) + color?: string; + + @ApiProperty({ description: "ID of the project this phase belongs to", example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }) + @IsUUID() + @IsNotEmpty() + projectId: string; +} \ No newline at end of file diff --git a/src/modules/task-manager/dto/task-phase/update-task-phase.dto.ts b/src/modules/task-manager/dto/task-phase/update-task-phase.dto.ts new file mode 100644 index 0000000..45ebc93 --- /dev/null +++ b/src/modules/task-manager/dto/task-phase/update-task-phase.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from "@nestjs/swagger"; +import { CreateTaskPhaseDto } from "./create-task-phase.dto"; + +export class UpdateTaskPhaseDto extends PartialType(CreateTaskPhaseDto) {} diff --git a/src/modules/task-manager/providers/task-phase.service.ts b/src/modules/task-manager/providers/task-phase.service.ts new file mode 100644 index 0000000..570ac19 --- /dev/null +++ b/src/modules/task-manager/providers/task-phase.service.ts @@ -0,0 +1,53 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { TaskPhaseRepository } from "../repositories/task-phase.repository"; +import { ProjectRepository } from "../repositories/project.repository"; +import { TMTaskPhase } from "../entities/task-phase.entity"; +import { CreateTaskPhaseDto } from "../dto/task-phase/create-task-phase.dto"; +import { UpdateTaskPhaseDto } from "../dto/task-phase/update-task-phase.dto"; + +@Injectable() +export class TaskPhaseService { + constructor( + private readonly taskPhaseRepository: TaskPhaseRepository, + private readonly projectRepository: ProjectRepository, + ) {} + + findAll(): Promise { + return this.taskPhaseRepository.findAll(); + } + + findByProject(projectId: string): Promise { + return this.taskPhaseRepository.findByProject(projectId); + } + + async findOne(id: string): Promise { + const taskPhase = await this.taskPhaseRepository.findOneById(id); + if (!taskPhase) throw new NotFoundException(`TaskPhase #${id} not found`); + return taskPhase; + } + + async create(dto: CreateTaskPhaseDto): Promise { + const project = await this.projectRepository.findOneById(dto.projectId); + if (!project) throw new NotFoundException(`Project #${dto.projectId} not found`); + + const taskPhase = this.taskPhaseRepository.create(dto); + return this.taskPhaseRepository.save(taskPhase); + } + + async update(id: string, dto: UpdateTaskPhaseDto): Promise { + const taskPhase = await this.findOne(id); + + if (dto.projectId) { + const project = await this.projectRepository.findOneById(dto.projectId); + if (!project) throw new NotFoundException(`Project #${dto.projectId} not found`); + } + + Object.assign(taskPhase, dto); + return this.taskPhaseRepository.save(taskPhase); + } + + async remove(id: string): Promise { + await this.findOne(id); + await this.taskPhaseRepository.delete(id); + } +} diff --git a/src/modules/task-manager/repositories/task-phase.repository.ts b/src/modules/task-manager/repositories/task-phase.repository.ts new file mode 100644 index 0000000..1023f9a --- /dev/null +++ b/src/modules/task-manager/repositories/task-phase.repository.ts @@ -0,0 +1,50 @@ +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; +import { TMTaskPhase } from "../entities/task-phase.entity"; + +@Injectable() +export class TaskPhaseRepository { + constructor( + @InjectRepository(TMTaskPhase) + private readonly repository: Repository, + ) {} + + 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) { + return this.repository.create(data); + } + + save(taskPhase: TMTaskPhase) { + return this.repository.save(taskPhase); + } + + update(id: string, data: Partial) { + return this.repository.update(id, data); + } + + delete(id: string) { + return this.repository.delete(id); + } +} From 3d59a2fa40972d8253b4ecbc25438012223c2247 Mon Sep 17 00:00:00 2001 From: realrafi Date: Wed, 1 Jul 2026 13:10:05 +0330 Subject: [PATCH 15/66] feat: added task DTOs --- .../task-manager/dto/task/create-task.dto.ts | 46 +++++++++++++++++++ .../task-manager/dto/task/update-task.dto.ts | 4 ++ 2 files changed, 50 insertions(+) create mode 100644 src/modules/task-manager/dto/task/create-task.dto.ts create mode 100644 src/modules/task-manager/dto/task/update-task.dto.ts diff --git a/src/modules/task-manager/dto/task/create-task.dto.ts b/src/modules/task-manager/dto/task/create-task.dto.ts new file mode 100644 index 0000000..5d0a148 --- /dev/null +++ b/src/modules/task-manager/dto/task/create-task.dto.ts @@ -0,0 +1,46 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsString, IsOptional, IsNotEmpty, IsUUID, IsDateString, MaxLength } from "class-validator"; + +export class CreateTaskDto { + @ApiProperty({ description: "Title of the task", example: "Implement login page" }) + @IsString() + @IsNotEmpty() + @MaxLength(150) + title: string; + + @ApiProperty({ description: "Description of the task", required: false, example: "Implement the login page with JWT auth" }) + @IsString() + @IsOptional() + description?: string; + + @ApiProperty({ description: "Color code for the task", required: false, example: "#4F46E5" }) + @IsString() + @IsOptional() + @MaxLength(20) + color?: string; + + @ApiProperty({ description: "Start date of the task", required: false, example: "2026-07-01" }) + @IsDateString() + @IsOptional() + startDate?: string; + + @ApiProperty({ description: "End date of the task", required: false, example: "2026-07-15" }) + @IsDateString() + @IsOptional() + endDate?: string; + + @ApiProperty({ description: "ID of the task phase this task belongs to", example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }) + @IsUUID() + @IsNotEmpty() + taskPhaseId: string; + + @ApiProperty({ + description: "IDs of users to assign to the task", + required: false, + type: [String], + example: ["b2c3d4e5-f6a7-8901-bcde-f12345678901"], + }) + @IsUUID("4", { each: true }) + @IsOptional() + userIds?: string[]; +} diff --git a/src/modules/task-manager/dto/task/update-task.dto.ts b/src/modules/task-manager/dto/task/update-task.dto.ts new file mode 100644 index 0000000..bee1042 --- /dev/null +++ b/src/modules/task-manager/dto/task/update-task.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from "@nestjs/swagger"; +import { CreateTaskDto } from "./create-task.dto"; + +export class UpdateTaskDto extends PartialType(CreateTaskDto) {} From 26b8268f73ebb733dea74a25e6b64bc8ca3f0c9e Mon Sep 17 00:00:00 2001 From: realrafi Date: Wed, 1 Jul 2026 13:17:21 +0330 Subject: [PATCH 16/66] feat: added change task phase DTO for task --- .../task-manager/dto/task/change-task-phase.dto.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/modules/task-manager/dto/task/change-task-phase.dto.ts diff --git a/src/modules/task-manager/dto/task/change-task-phase.dto.ts b/src/modules/task-manager/dto/task/change-task-phase.dto.ts new file mode 100644 index 0000000..c98872d --- /dev/null +++ b/src/modules/task-manager/dto/task/change-task-phase.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsUUID, IsNotEmpty } from 'class-validator'; + +export class ChangeTaskPhaseDto { + @ApiProperty({ description: 'ID of the new task phase', example: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' }) + @IsUUID() + @IsNotEmpty() + taskPhaseId: string; +} \ No newline at end of file From 87772f46dd20ddad10c4745b6d157288c30113b0 Mon Sep 17 00:00:00 2001 From: realrafi Date: Wed, 1 Jul 2026 13:25:45 +0330 Subject: [PATCH 17/66] feat: added task repository --- .../repositories/task.repository.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/modules/task-manager/repositories/task.repository.ts diff --git a/src/modules/task-manager/repositories/task.repository.ts b/src/modules/task-manager/repositories/task.repository.ts new file mode 100644 index 0000000..f419845 --- /dev/null +++ b/src/modules/task-manager/repositories/task.repository.ts @@ -0,0 +1,48 @@ +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; +import { TMTask } from "../entities/task.entity"; + +@Injectable() +export class TaskRepository { + constructor( + @InjectRepository(TMTask) + private readonly repository: Repository, + ) {} + + findAll() { + return this.repository.find({ + relations: ["taskPhase", "remarks", "attachments", "checkListItems", "users"], + }); + } + + findByPhase(taskPhaseId: string) { + return this.repository.find({ + where: { taskPhaseId }, + relations: ["remarks", "attachments", "checkListItems", "users"], + }); + } + + findOneById(id: string) { + return this.repository.findOne({ + where: { id }, + relations: ["taskPhase", "remarks", "attachments", "checkListItems", "users"], + }); + } + + create(data: Partial) { + return this.repository.create(data); + } + + save(task: TMTask) { + return this.repository.save(task); + } + + update(id: string, data: Partial) { + return this.repository.update(id, data); + } + + delete(id: string) { + return this.repository.delete(id); + } +} From ff4a8dedbae2651dd1b35dec6c64742b6a0a52d9 Mon Sep 17 00:00:00 2001 From: realrafi Date: Wed, 1 Jul 2026 15:41:59 +0330 Subject: [PATCH 18/66] feat: add task service --- .../task-manager/providers/task.service.ts | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 src/modules/task-manager/providers/task.service.ts diff --git a/src/modules/task-manager/providers/task.service.ts b/src/modules/task-manager/providers/task.service.ts new file mode 100644 index 0000000..9e635e3 --- /dev/null +++ b/src/modules/task-manager/providers/task.service.ts @@ -0,0 +1,100 @@ +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { In, Repository } from 'typeorm'; +import { TaskRepository } from '../repositories/task.repository'; +import { TaskPhaseRepository } from '../repositories/task-phase.repository'; +import { TMTask } from '../entities/task.entity'; +import { CreateTaskDto } from '../dto/task/create-task.dto'; +import { UpdateTaskDto } from '../dto/task/update-task.dto'; +import { ChangeTaskPhaseDto } from '../dto/task/change-task-phase.dto'; +import { User } from '../../users/entities/user.entity'; + +@Injectable() +export class TaskService { + constructor( + private readonly taskRepository: TaskRepository, + private readonly taskPhaseRepository: TaskPhaseRepository, + @InjectRepository(User) + private readonly userRepository: Repository, + ) {} + + findAll(): Promise { + return this.taskRepository.findAll(); + } + + findByPhase(taskPhaseId: string): Promise { + return this.taskRepository.findByPhase(taskPhaseId); + } + + async findOne(id: string): Promise { + const task = await this.taskRepository.findOneById(id); + if (!task) throw new NotFoundException(`Task #${id} not found`); + return task; + } + + async create(dto: CreateTaskDto): Promise { + const { userIds, startDate, endDate, ...rest } = dto; + + const taskPhase = await this.taskPhaseRepository.findOneById(dto.taskPhaseId); + if (!taskPhase) throw new NotFoundException(`TaskPhase #${dto.taskPhaseId} not found`); + + const task = this.taskRepository.create({ + ...rest, + startDate: startDate ? new Date(startDate) : undefined, + endDate: endDate ? new Date(endDate) : undefined, + }); + + if (userIds?.length) { + task.users = await this.userRepository.findBy({ id: In(userIds) }); + } + + return this.taskRepository.save(task); + } + + async update(id: string, dto: UpdateTaskDto): Promise { + const task = await this.findOne(id); + const { userIds, startDate, endDate, ...rest } = dto; + + if (dto.taskPhaseId) { + const taskPhase = await this.taskPhaseRepository.findOneById(dto.taskPhaseId); + if (!taskPhase) throw new NotFoundException(`TaskPhase #${dto.taskPhaseId} not found`); + } + + Object.assign(task, { + ...rest, + startDate: startDate ? new Date(startDate) : task.startDate, + endDate: endDate ? new Date(endDate) : task.endDate, + }); + + if (userIds) { + task.users = userIds.length + ? await this.userRepository.findBy({ id: In(userIds) }) + : []; + } + + return this.taskRepository.save(task); + } + + async changePhase(id: string, dto: ChangeTaskPhaseDto): Promise { + const task = await this.findOne(id); + + const newPhase = await this.taskPhaseRepository.findOneById(dto.taskPhaseId); + if (!newPhase) throw new NotFoundException(`TaskPhase #${dto.taskPhaseId} not found`); + + if (task.taskPhase.projectId !== newPhase.projectId) { + throw new BadRequestException( + `TaskPhase #${dto.taskPhaseId} does not belong to the same project as the current phase`, + ); + } + + task.taskPhaseId = dto.taskPhaseId; + task.taskPhase = newPhase; + + return this.taskRepository.save(task); + } + + async remove(id: string): Promise { + await this.findOne(id); + await this.taskRepository.delete(id); + } +} \ No newline at end of file From 31e3b5f00dd39e94e7428c51700dbedafc2349c9 Mon Sep 17 00:00:00 2001 From: realrafi Date: Wed, 1 Jul 2026 15:54:35 +0330 Subject: [PATCH 19/66] feat: added task controller --- .../controllers/task.controller.ts | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/modules/task-manager/controllers/task.controller.ts diff --git a/src/modules/task-manager/controllers/task.controller.ts b/src/modules/task-manager/controllers/task.controller.ts new file mode 100644 index 0000000..9e356b3 --- /dev/null +++ b/src/modules/task-manager/controllers/task.controller.ts @@ -0,0 +1,67 @@ +import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger'; +import { TaskService } from '../providers/task.service'; +import { CreateTaskDto } from '../dto/task/create-task.dto'; +import { UpdateTaskDto } from '../dto/task/update-task.dto'; +import { ChangeTaskPhaseDto } from '../dto/task/change-task-phase.dto'; +import { TMTask } from '../entities/task.entity'; + +@ApiTags('Task Manager - Tasks') +@Controller('task-manager/tasks') +export class TaskController { + constructor(private readonly taskService: TaskService) {} + + @Get() + @ApiOperation({ summary: 'Get all tasks, optionally filtered by phase' }) + @ApiQuery({ name: 'taskPhaseId', required: false, description: 'Filter tasks by task phase ID' }) + @ApiResponse({ status: 200, description: 'List of tasks', type: [TMTask] }) + findAll(@Query('taskPhaseId') taskPhaseId?: string): Promise { + 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 { + return this.taskService.findOne(id); + } + + @Post() + @ApiOperation({ summary: 'Create a new task' }) + @ApiResponse({ status: 201, description: 'Task created', type: TMTask }) + @ApiResponse({ status: 404, description: 'TaskPhase not found' }) + create(@Body() body: CreateTaskDto): Promise { + return this.taskService.create(body); + } + + @Patch(':id') + @ApiOperation({ summary: 'Update a task' }) + @ApiParam({ name: 'id', description: 'Task ID' }) + @ApiResponse({ status: 200, description: 'Task updated', type: TMTask }) + @ApiResponse({ status: 404, description: 'Task or TaskPhase not found' }) + update(@Param('id') id: string, @Body() body: UpdateTaskDto): Promise { + return this.taskService.update(id, body); + } + + @Patch(':id/change-phase') + @ApiOperation({ summary: 'Move a task to a different phase within the same project' }) + @ApiParam({ name: 'id', description: 'Task ID' }) + @ApiResponse({ status: 200, description: 'Task phase changed', type: TMTask }) + @ApiResponse({ status: 400, description: 'Target phase belongs to a different project' }) + @ApiResponse({ status: 404, description: 'Task or TaskPhase not found' }) + changePhase(@Param('id') id: string, @Body() body: ChangeTaskPhaseDto): Promise { + return this.taskService.changePhase(id, body); + } + + @Delete(':id') + @ApiOperation({ summary: 'Delete a task' }) + @ApiParam({ name: 'id', description: 'Task ID' }) + @ApiResponse({ status: 200, description: 'Task deleted' }) + @ApiResponse({ status: 404, description: 'Task not found' }) + remove(@Param('id') id: string): Promise { + return this.taskService.remove(id); + } +} \ No newline at end of file From db61030b44c8e1c261b56c9e832a672535964f0d Mon Sep 17 00:00:00 2001 From: realrafi Date: Wed, 1 Jul 2026 16:16:39 +0330 Subject: [PATCH 20/66] feat: added check list item DTOs --- .../create-check-list-item.dto.ts | 20 +++++++++++++++++++ .../update-check-list-item.dto.ts | 4 ++++ 2 files changed, 24 insertions(+) create mode 100644 src/modules/task-manager/dto/check-list-item/create-check-list-item.dto.ts create mode 100644 src/modules/task-manager/dto/check-list-item/update-check-list-item.dto.ts diff --git a/src/modules/task-manager/dto/check-list-item/create-check-list-item.dto.ts b/src/modules/task-manager/dto/check-list-item/create-check-list-item.dto.ts new file mode 100644 index 0000000..8966881 --- /dev/null +++ b/src/modules/task-manager/dto/check-list-item/create-check-list-item.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsString, IsNotEmpty, IsUUID, IsBoolean, MaxLength } from "class-validator"; + +export class CreateCheckListItemDto { + @ApiProperty({ description: "Title of the check list item", example: "Write unit tests" }) + @IsString() + @IsNotEmpty() + @MaxLength(150) + title: string; + + @ApiProperty({ description: "Whether the check list item is done", example: false }) + @IsBoolean() + @IsNotEmpty() + isDone: boolean; + + @ApiProperty({ description: "ID of the task this check list item belongs to", example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }) + @IsUUID() + @IsNotEmpty() + taskId: string; +} diff --git a/src/modules/task-manager/dto/check-list-item/update-check-list-item.dto.ts b/src/modules/task-manager/dto/check-list-item/update-check-list-item.dto.ts new file mode 100644 index 0000000..c171e29 --- /dev/null +++ b/src/modules/task-manager/dto/check-list-item/update-check-list-item.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateCheckListItemDto } from './create-check-list-item.dto'; + +export class UpdateCheckListItemDto extends PartialType(CreateCheckListItemDto) {} \ No newline at end of file From 6ad18d8f3e83427904d51ac4026c570daa061570 Mon Sep 17 00:00:00 2001 From: realrafi Date: Wed, 1 Jul 2026 16:40:55 +0330 Subject: [PATCH 21/66] feat: added checklistitem repo --- .../check-list-item.repository.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/modules/task-manager/repositories/check-list-item.repository.ts diff --git a/src/modules/task-manager/repositories/check-list-item.repository.ts b/src/modules/task-manager/repositories/check-list-item.repository.ts new file mode 100644 index 0000000..0d8c3ee --- /dev/null +++ b/src/modules/task-manager/repositories/check-list-item.repository.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { TMCheckListItem } from '../entities/check-list-item.entity'; + +@Injectable() +export class CheckListItemRepository { + constructor( + @InjectRepository(TMCheckListItem) + private readonly repository: Repository, + ) {} + + 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) { + return this.repository.create(data); + } + + save(checkListItem: TMCheckListItem) { + return this.repository.save(checkListItem); + } + + update(id: string, data: Partial) { + return this.repository.update(id, data); + } + + delete(id: string) { + return this.repository.delete(id); + } +} \ No newline at end of file From c2e0cb1bc64c7ca8ec99cdae660a47a20fbb1048 Mon Sep 17 00:00:00 2001 From: realrafi Date: Wed, 1 Jul 2026 17:10:01 +0330 Subject: [PATCH 22/66] feat: added checklistitem service --- .../providers/check-list-item.service.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/modules/task-manager/providers/check-list-item.service.ts diff --git a/src/modules/task-manager/providers/check-list-item.service.ts b/src/modules/task-manager/providers/check-list-item.service.ts new file mode 100644 index 0000000..f421db4 --- /dev/null +++ b/src/modules/task-manager/providers/check-list-item.service.ts @@ -0,0 +1,53 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { CheckListItemRepository } from "../repositories/check-list-item.repository"; +import { TaskRepository } from "../repositories/task.repository"; +import { TMCheckListItem } from "../entities/check-list-item.entity"; +import { CreateCheckListItemDto } from "../dto/check-list-item/create-check-list-item.dto"; +import { UpdateCheckListItemDto } from "../dto/check-list-item/update-check-list-item.dto"; + +@Injectable() +export class CheckListItemService { + constructor( + private readonly checkListItemRepository: CheckListItemRepository, + private readonly taskRepository: TaskRepository, + ) {} + + findAll(): Promise { + return this.checkListItemRepository.findAll(); + } + + findByTask(taskId: string): Promise { + return this.checkListItemRepository.findByTask(taskId); + } + + async findOne(id: string): Promise { + const checkListItem = await this.checkListItemRepository.findOneById(id); + if (!checkListItem) throw new NotFoundException(`CheckListItem #${id} not found`); + return checkListItem; + } + + async create(dto: CreateCheckListItemDto): Promise { + const task = await this.taskRepository.findOneById(dto.taskId); + if (!task) throw new NotFoundException(`Task #${dto.taskId} not found`); + + const checkListItem = this.checkListItemRepository.create(dto); + return this.checkListItemRepository.save(checkListItem); + } + + async update(id: string, dto: UpdateCheckListItemDto): Promise { + const checkListItem = await this.findOne(id); + + if (dto.taskId) { + const task = await this.taskRepository.findOneById(dto.taskId); + if (!task) throw new NotFoundException(`Task #${dto.taskId} not found`); + } + + Object.assign(checkListItem, dto); + return this.checkListItemRepository.save(checkListItem); + } + + async remove(id: string): Promise { + await this.findOne(id); + await this.checkListItemRepository.delete(id); + } +} From 743d1eb79d8dc818723481a4ff478185acd2baf7 Mon Sep 17 00:00:00 2001 From: realrafi Date: Wed, 1 Jul 2026 17:28:43 +0330 Subject: [PATCH 23/66] feat: added checklistitem controller --- .../controllers/check-list-item.controller.ts | 56 +++++++++++++++++++ .../task-manager/task-manager.module.ts | 41 ++++++++++++-- 2 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 src/modules/task-manager/controllers/check-list-item.controller.ts diff --git a/src/modules/task-manager/controllers/check-list-item.controller.ts b/src/modules/task-manager/controllers/check-list-item.controller.ts new file mode 100644 index 0000000..c7529d3 --- /dev/null +++ b/src/modules/task-manager/controllers/check-list-item.controller.ts @@ -0,0 +1,56 @@ +import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger'; +import { CheckListItemService } from '../providers/check-list-item.service'; +import { CreateCheckListItemDto } from '../dto/check-list-item/create-check-list-item.dto'; +import { UpdateCheckListItemDto } from '../dto/check-list-item/update-check-list-item.dto'; +import { TMCheckListItem } from '../entities/check-list-item.entity'; + +@ApiTags('Task Manager - Check List Items') +@Controller('task-manager/check-list-items') +export class CheckListItemController { + constructor(private readonly checkListItemService: CheckListItemService) {} + + @Get() + @ApiOperation({ summary: 'Get all check list items, optionally filtered by task' }) + @ApiQuery({ name: 'taskId', required: false, description: 'Filter check list items by task ID' }) + @ApiResponse({ status: 200, description: 'List of check list items', type: [TMCheckListItem] }) + findAll(@Query('taskId') taskId?: string): Promise { + 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 { + return this.checkListItemService.findOne(id); + } + + @Post() + @ApiOperation({ summary: 'Create a new check list item' }) + @ApiResponse({ status: 201, description: 'CheckListItem created', type: TMCheckListItem }) + @ApiResponse({ status: 404, description: 'Task not found' }) + create(@Body() body: CreateCheckListItemDto): Promise { + return this.checkListItemService.create(body); + } + + @Patch(':id') + @ApiOperation({ summary: 'Update a check list item' }) + @ApiParam({ name: 'id', description: 'CheckListItem ID' }) + @ApiResponse({ status: 200, description: 'CheckListItem updated', type: TMCheckListItem }) + @ApiResponse({ status: 404, description: 'CheckListItem or Task not found' }) + update(@Param('id') id: string, @Body() body: UpdateCheckListItemDto): Promise { + return this.checkListItemService.update(id, body); + } + + @Delete(':id') + @ApiOperation({ summary: 'Delete a check list item' }) + @ApiParam({ name: 'id', description: 'CheckListItem ID' }) + @ApiResponse({ status: 200, description: 'CheckListItem deleted' }) + @ApiResponse({ status: 404, description: 'CheckListItem not found' }) + remove(@Param('id') id: string): Promise { + return this.checkListItemService.remove(id); + } +} \ No newline at end of file diff --git a/src/modules/task-manager/task-manager.module.ts b/src/modules/task-manager/task-manager.module.ts index d92c7c2..95b01e5 100644 --- a/src/modules/task-manager/task-manager.module.ts +++ b/src/modules/task-manager/task-manager.module.ts @@ -19,12 +19,28 @@ import { WorkspaceTypeController } from "./controllers/workspace-type.controller import { ProjectController } from "./controllers/project.controller"; import { ProjectRepository } from "./repositories/project.repository"; import { ProjectService } from "./providers/project.service"; +import { TaskPhaseService } from "./providers/task-phase.service"; +import { TaskPhaseRepository } from "./repositories/task-phase.repository"; +import { TaskRepository } from "./repositories/task.repository"; +import { TaskService } from "./providers/task.service"; +import { CheckListItemRepository } from "./repositories/check-list-item.repository"; +import { CheckListItemService } from "./providers/check-list-item.service"; +import { TaskPhaseController } from "./controllers/task-phase.controller"; +import { TaskController } from "./controllers/task.controller"; +import { CheckListItemController } from "./controllers/check-list-item.controller"; @Module({ imports: [ TypeOrmModule.forFeature([TMWorkspaceType, TMWorkspace, TMProject, TMTaskPhase, TMTask, TMRemark, TMAttachment, TMCheckListItem, User]), ], - controllers: [WorkspaceController, WorkspaceTypeController, ProjectController], + controllers: [ + WorkspaceController, + WorkspaceTypeController, + ProjectController, + TaskPhaseController, + TaskController, + CheckListItemController, + ], providers: [ WorkspaceRepository, WorkspaceService, @@ -32,9 +48,26 @@ import { ProjectService } from "./providers/project.service"; WorkspaceTypeService, ProjectRepository, ProjectService, - // TaskPhaseService, - // TaskService, + TaskPhaseRepository, + TaskPhaseService, + TaskRepository, + TaskService, + CheckListItemRepository, + CheckListItemService, + ], + exports: [ + WorkspaceRepository, + WorkspaceService, + WorkspaceTypeRepository, + WorkspaceTypeService, + ProjectRepository, + ProjectService, + TaskPhaseRepository, + TaskPhaseService, + TaskRepository, + TaskService, + CheckListItemRepository, + CheckListItemService, ], - exports: [WorkspaceService, WorkspaceRepository, WorkspaceTypeService, WorkspaceTypeRepository, ProjectService, ProjectRepository], }) export class TaskManagerModule {} From c99a8a435cb2c5beba53efd237dc6fad7c86465a Mon Sep 17 00:00:00 2001 From: realrafi Date: Thu, 2 Jul 2026 10:56:34 +0330 Subject: [PATCH 24/66] feat: added remark DTOs --- .../dto/remark/create-remark.dto.ts | 21 +++++++++++++++++++ .../dto/remark/update-remark.dto.ts | 4 ++++ 2 files changed, 25 insertions(+) create mode 100644 src/modules/task-manager/dto/remark/create-remark.dto.ts create mode 100644 src/modules/task-manager/dto/remark/update-remark.dto.ts diff --git a/src/modules/task-manager/dto/remark/create-remark.dto.ts b/src/modules/task-manager/dto/remark/create-remark.dto.ts new file mode 100644 index 0000000..b735dfe --- /dev/null +++ b/src/modules/task-manager/dto/remark/create-remark.dto.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsString, IsNotEmpty, IsUUID, IsOptional, MaxLength } from "class-validator"; + +export class CreateRemarkDto { + @ApiProperty({ description: "Title of the remark", example: "This task needs review" }) + @IsString() + @IsNotEmpty() + @MaxLength(150) + title: string; + + @ApiProperty({ description: "Color code for the remark", required: false, example: "#F59E0B" }) + @IsString() + @IsOptional() + @MaxLength(20) + color?: string; + + @ApiProperty({ description: "ID of the task this remark belongs to", example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }) + @IsUUID() + @IsNotEmpty() + taskId: string; +} diff --git a/src/modules/task-manager/dto/remark/update-remark.dto.ts b/src/modules/task-manager/dto/remark/update-remark.dto.ts new file mode 100644 index 0000000..06610d3 --- /dev/null +++ b/src/modules/task-manager/dto/remark/update-remark.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from "@nestjs/swagger"; +import { CreateRemarkDto } from "./create-remark.dto"; + +export class UpdateRemarkDto extends PartialType(CreateRemarkDto) {} From 27e9ad76a506bf061a623647ae7b12263e253b9a Mon Sep 17 00:00:00 2001 From: realrafi Date: Thu, 2 Jul 2026 11:21:46 +0330 Subject: [PATCH 25/66] feat: added remark repository --- .../repositories/remark.repository.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/modules/task-manager/repositories/remark.repository.ts diff --git a/src/modules/task-manager/repositories/remark.repository.ts b/src/modules/task-manager/repositories/remark.repository.ts new file mode 100644 index 0000000..4881a3d --- /dev/null +++ b/src/modules/task-manager/repositories/remark.repository.ts @@ -0,0 +1,47 @@ +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; +import { TMRemark } from "../entities/remark.entity"; + +@Injectable() +export class RemarkRepository { + constructor( + @InjectRepository(TMRemark) + private readonly repository: Repository, + ) {} + + 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) { + return this.repository.create(data); + } + + save(remark: TMRemark) { + return this.repository.save(remark); + } + + update(id: string, data: Partial) { + return this.repository.update(id, data); + } + + delete(id: string) { + return this.repository.delete(id); + } +} From 4f76af87a60d31c0feb7e81d54fe2a735a83760a Mon Sep 17 00:00:00 2001 From: realrafi Date: Thu, 2 Jul 2026 11:34:35 +0330 Subject: [PATCH 26/66] feat: added remark service --- .../task-manager/providers/remark.service.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/modules/task-manager/providers/remark.service.ts diff --git a/src/modules/task-manager/providers/remark.service.ts b/src/modules/task-manager/providers/remark.service.ts new file mode 100644 index 0000000..b30d40b --- /dev/null +++ b/src/modules/task-manager/providers/remark.service.ts @@ -0,0 +1,53 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { RemarkRepository } from "../repositories/remark.repository"; +import { TaskRepository } from "../repositories/task.repository"; +import { TMRemark } from "../entities/remark.entity"; +import { CreateRemarkDto } from "../dto/remark/create-remark.dto"; +import { UpdateRemarkDto } from "../dto/remark/update-remark.dto"; + +@Injectable() +export class RemarkService { + constructor( + private readonly remarkRepository: RemarkRepository, + private readonly taskRepository: TaskRepository, + ) {} + + findAll(): Promise { + return this.remarkRepository.findAll(); + } + + findByTask(taskId: string): Promise { + return this.remarkRepository.findByTask(taskId); + } + + async findOne(id: string): Promise { + const remark = await this.remarkRepository.findOneById(id); + if (!remark) throw new NotFoundException(`Remark #${id} not found`); + return remark; + } + + async create(dto: CreateRemarkDto): Promise { + const task = await this.taskRepository.findOneById(dto.taskId); + if (!task) throw new NotFoundException(`Task #${dto.taskId} not found`); + + const remark = this.remarkRepository.create(dto); + return this.remarkRepository.save(remark); + } + + async update(id: string, dto: UpdateRemarkDto): Promise { + const remark = await this.findOne(id); + + if (dto.taskId) { + const task = await this.taskRepository.findOneById(dto.taskId); + if (!task) throw new NotFoundException(`Task #${dto.taskId} not found`); + } + + Object.assign(remark, dto); + return this.remarkRepository.save(remark); + } + + async remove(id: string): Promise { + await this.findOne(id); + await this.remarkRepository.delete(id); + } +} From a199623eb4c764df32870a44ea28ecda6db51414 Mon Sep 17 00:00:00 2001 From: realrafi Date: Thu, 2 Jul 2026 11:58:01 +0330 Subject: [PATCH 27/66] feat: added remark controller --- .../controllers/remark.controller.ts | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/modules/task-manager/controllers/remark.controller.ts diff --git a/src/modules/task-manager/controllers/remark.controller.ts b/src/modules/task-manager/controllers/remark.controller.ts new file mode 100644 index 0000000..7c4168a --- /dev/null +++ b/src/modules/task-manager/controllers/remark.controller.ts @@ -0,0 +1,56 @@ +import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from "@nestjs/common"; +import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from "@nestjs/swagger"; +import { 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"; + +@ApiTags("Task Manager - Remarks") +@Controller("task-manager/remarks") +export class RemarkController { + constructor(private readonly remarkService: RemarkService) {} + + @Get() + @ApiOperation({ summary: "Get all remarks, optionally filtered by task" }) + @ApiQuery({ name: "taskId", required: false, description: "Filter remarks by task ID" }) + @ApiResponse({ status: 200, description: "List of remarks", type: [TMRemark] }) + findAll(@Query("taskId") taskId?: string): Promise { + if (taskId) return this.remarkService.findByTask(taskId); + return this.remarkService.findAll(); + } + + @Get(":id") + @ApiOperation({ summary: "Get a remark by ID" }) + @ApiParam({ name: "id", description: "Remark ID" }) + @ApiResponse({ status: 200, description: "Remark found", type: TMRemark }) + @ApiResponse({ status: 404, description: "Remark not found" }) + findOne(@Param("id") id: string): Promise { + return this.remarkService.findOne(id); + } + + @Post() + @ApiOperation({ summary: "Create a new remark" }) + @ApiResponse({ status: 201, description: "Remark created", type: TMRemark }) + @ApiResponse({ status: 404, description: "Task not found" }) + create(@Body() body: CreateRemarkDto): Promise { + return this.remarkService.create(body); + } + + @Patch(":id") + @ApiOperation({ summary: "Update a remark" }) + @ApiParam({ name: "id", description: "Remark ID" }) + @ApiResponse({ status: 200, description: "Remark updated", type: TMRemark }) + @ApiResponse({ status: 404, description: "Remark or Task not found" }) + update(@Param("id") id: string, @Body() body: UpdateRemarkDto): Promise { + return this.remarkService.update(id, body); + } + + @Delete(":id") + @ApiOperation({ summary: "Delete a remark" }) + @ApiParam({ name: "id", description: "Remark ID" }) + @ApiResponse({ status: 200, description: "Remark deleted" }) + @ApiResponse({ status: 404, description: "Remark not found" }) + remove(@Param("id") id: string): Promise { + return this.remarkService.remove(id); + } +} From a51b19f85ce2ddd94c249dd7d02e745b549fc14b Mon Sep 17 00:00:00 2001 From: realrafi Date: Thu, 2 Jul 2026 12:02:54 +0330 Subject: [PATCH 28/66] refactor: refactoring task manger module --- .../task-manager/task-manager.module.ts | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/modules/task-manager/task-manager.module.ts b/src/modules/task-manager/task-manager.module.ts index 95b01e5..c5704cd 100644 --- a/src/modules/task-manager/task-manager.module.ts +++ b/src/modules/task-manager/task-manager.module.ts @@ -28,6 +28,9 @@ import { CheckListItemService } from "./providers/check-list-item.service"; import { TaskPhaseController } from "./controllers/task-phase.controller"; import { TaskController } from "./controllers/task.controller"; import { CheckListItemController } from "./controllers/check-list-item.controller"; +import { RemarkController } from "./controllers/remark.controller"; +import { RemarkRepository } from "./repositories/remark.repository"; +import { RemarkService } from "./providers/remark.service"; @Module({ imports: [ @@ -40,6 +43,7 @@ import { CheckListItemController } from "./controllers/check-list-item.controlle TaskPhaseController, TaskController, CheckListItemController, + RemarkController ], providers: [ WorkspaceRepository, @@ -54,20 +58,8 @@ import { CheckListItemController } from "./controllers/check-list-item.controlle TaskService, CheckListItemRepository, CheckListItemService, - ], - exports: [ - WorkspaceRepository, - WorkspaceService, - WorkspaceTypeRepository, - WorkspaceTypeService, - ProjectRepository, - ProjectService, - TaskPhaseRepository, - TaskPhaseService, - TaskRepository, - TaskService, - CheckListItemRepository, - CheckListItemService, - ], + RemarkRepository, + RemarkService + ] }) export class TaskManagerModule {} From 3ad73a5271e12361b255ce24256cc9e16aef2088 Mon Sep 17 00:00:00 2001 From: realrafi Date: Thu, 2 Jul 2026 12:11:44 +0330 Subject: [PATCH 29/66] feat: added attachment DTOs --- .../dto/attachment/create-attachment.dto.ts | 26 +++++++++++++++++++ .../dto/attachment/update-attachment.dto.ts | 4 +++ 2 files changed, 30 insertions(+) create mode 100644 src/modules/task-manager/dto/attachment/create-attachment.dto.ts create mode 100644 src/modules/task-manager/dto/attachment/update-attachment.dto.ts diff --git a/src/modules/task-manager/dto/attachment/create-attachment.dto.ts b/src/modules/task-manager/dto/attachment/create-attachment.dto.ts new file mode 100644 index 0000000..dcca8d4 --- /dev/null +++ b/src/modules/task-manager/dto/attachment/create-attachment.dto.ts @@ -0,0 +1,26 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsString, IsNotEmpty, IsUUID, IsEnum, MaxLength } from "class-validator"; +import { TMAttachmentType } from "../../enums/attachment-type.enum"; + +export class CreateAttachmentDto { + @ApiProperty({ description: "Title of the attachment", example: "Project Requirements Doc" }) + @IsString() + @IsNotEmpty() + @MaxLength(150) + title: string; + + @ApiProperty({ description: "Type of the attachment", enum: TMAttachmentType, example: TMAttachmentType.FILE }) + @IsEnum(TMAttachmentType) + @IsNotEmpty() + type: TMAttachmentType; + + @ApiProperty({ description: "File URL or path of the attachment", example: "https://cdn.example.com/files/requirements.pdf" }) + @IsString() + @IsNotEmpty() + file: string; + + @ApiProperty({ description: "ID of the task this attachment belongs to", example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }) + @IsUUID() + @IsNotEmpty() + taskId: string; +} diff --git a/src/modules/task-manager/dto/attachment/update-attachment.dto.ts b/src/modules/task-manager/dto/attachment/update-attachment.dto.ts new file mode 100644 index 0000000..977fed9 --- /dev/null +++ b/src/modules/task-manager/dto/attachment/update-attachment.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from "@nestjs/swagger"; +import { CreateAttachmentDto } from "./create-attachment.dto"; + +export class UpdateAttachmentDto extends PartialType(CreateAttachmentDto) {} From 1b70e49d934287a5c521fca569cc7d0e0c0eddd3 Mon Sep 17 00:00:00 2001 From: realrafi Date: Thu, 2 Jul 2026 12:17:22 +0330 Subject: [PATCH 30/66] feat: added attachment repo --- .../repositories/attachment.repository.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/modules/task-manager/repositories/attachment.repository.ts diff --git a/src/modules/task-manager/repositories/attachment.repository.ts b/src/modules/task-manager/repositories/attachment.repository.ts new file mode 100644 index 0000000..9fd3f26 --- /dev/null +++ b/src/modules/task-manager/repositories/attachment.repository.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { TMAttachment } from '../entities/attachment.entity'; + +@Injectable() +export class AttachmentRepository { + constructor( + @InjectRepository(TMAttachment) + private readonly repository: Repository, + ) {} + + 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) { + return this.repository.create(data); + } + + save(attachment: TMAttachment) { + return this.repository.save(attachment); + } + + update(id: string, data: Partial) { + return this.repository.update(id, data); + } + + delete(id: string) { + return this.repository.delete(id); + } +} \ No newline at end of file From fc23a94fb6e9893d2aff84e28532d04a76c3ecd5 Mon Sep 17 00:00:00 2001 From: realrafi Date: Thu, 2 Jul 2026 12:35:59 +0330 Subject: [PATCH 31/66] feat: added attachment service --- .../providers/attachment.service.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/modules/task-manager/providers/attachment.service.ts diff --git a/src/modules/task-manager/providers/attachment.service.ts b/src/modules/task-manager/providers/attachment.service.ts new file mode 100644 index 0000000..a22b76b --- /dev/null +++ b/src/modules/task-manager/providers/attachment.service.ts @@ -0,0 +1,53 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { AttachmentRepository } from "../repositories/attachment.repository"; +import { TaskRepository } from "../repositories/task.repository"; +import { TMAttachment } from "../entities/attachment.entity"; +import { CreateAttachmentDto } from "../dto/attachment/create-attachment.dto"; +import { UpdateAttachmentDto } from "../dto/attachment/update-attachment.dto"; + +@Injectable() +export class AttachmentService { + constructor( + private readonly attachmentRepository: AttachmentRepository, + private readonly taskRepository: TaskRepository, + ) {} + + findAll(): Promise { + return this.attachmentRepository.findAll(); + } + + findByTask(taskId: string): Promise { + return this.attachmentRepository.findByTask(taskId); + } + + async findOne(id: string): Promise { + const attachment = await this.attachmentRepository.findOneById(id); + if (!attachment) throw new NotFoundException(`Attachment #${id} not found`); + return attachment; + } + + async create(dto: CreateAttachmentDto): Promise { + const task = await this.taskRepository.findOneById(dto.taskId); + if (!task) throw new NotFoundException(`Task #${dto.taskId} not found`); + + const attachment = this.attachmentRepository.create(dto); + return this.attachmentRepository.save(attachment); + } + + async update(id: string, dto: UpdateAttachmentDto): Promise { + const attachment = await this.findOne(id); + + if (dto.taskId) { + const task = await this.taskRepository.findOneById(dto.taskId); + if (!task) throw new NotFoundException(`Task #${dto.taskId} not found`); + } + + Object.assign(attachment, dto); + return this.attachmentRepository.save(attachment); + } + + async remove(id: string): Promise { + await this.findOne(id); + await this.attachmentRepository.delete(id); + } +} From 4b95d35d874a88db1955c950f9e0f0866cd77c40 Mon Sep 17 00:00:00 2001 From: realrafi Date: Thu, 2 Jul 2026 12:59:33 +0330 Subject: [PATCH 32/66] feat: added attachment controller --- .../controllers/attachment.controller.ts | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/modules/task-manager/controllers/attachment.controller.ts diff --git a/src/modules/task-manager/controllers/attachment.controller.ts b/src/modules/task-manager/controllers/attachment.controller.ts new file mode 100644 index 0000000..7c004a7 --- /dev/null +++ b/src/modules/task-manager/controllers/attachment.controller.ts @@ -0,0 +1,56 @@ +import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger'; +import { 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'; + +@ApiTags('Task Manager - Attachments') +@Controller('task-manager/attachments') +export class AttachmentController { + constructor(private readonly attachmentService: AttachmentService) {} + + @Get() + @ApiOperation({ summary: 'Get all attachments, optionally filtered by task' }) + @ApiQuery({ name: 'taskId', required: false, description: 'Filter attachments by task ID' }) + @ApiResponse({ status: 200, description: 'List of attachments', type: [TMAttachment] }) + findAll(@Query('taskId') taskId?: string): Promise { + if (taskId) return this.attachmentService.findByTask(taskId); + return this.attachmentService.findAll(); + } + + @Get(':id') + @ApiOperation({ summary: 'Get an attachment by ID' }) + @ApiParam({ name: 'id', description: 'Attachment ID' }) + @ApiResponse({ status: 200, description: 'Attachment found', type: TMAttachment }) + @ApiResponse({ status: 404, description: 'Attachment not found' }) + findOne(@Param('id') id: string): Promise { + return this.attachmentService.findOne(id); + } + + @Post() + @ApiOperation({ summary: 'Create a new attachment' }) + @ApiResponse({ status: 201, description: 'Attachment created', type: TMAttachment }) + @ApiResponse({ status: 404, description: 'Task not found' }) + create(@Body() body: CreateAttachmentDto): Promise { + return this.attachmentService.create(body); + } + + @Patch(':id') + @ApiOperation({ summary: 'Update an attachment' }) + @ApiParam({ name: 'id', description: 'Attachment ID' }) + @ApiResponse({ status: 200, description: 'Attachment updated', type: TMAttachment }) + @ApiResponse({ status: 404, description: 'Attachment or Task not found' }) + update(@Param('id') id: string, @Body() body: UpdateAttachmentDto): Promise { + return this.attachmentService.update(id, body); + } + + @Delete(':id') + @ApiOperation({ summary: 'Delete an attachment' }) + @ApiParam({ name: 'id', description: 'Attachment ID' }) + @ApiResponse({ status: 200, description: 'Attachment deleted' }) + @ApiResponse({ status: 404, description: 'Attachment not found' }) + remove(@Param('id') id: string): Promise { + return this.attachmentService.remove(id); + } +} \ No newline at end of file From c22a41d29b62dde56de6f140f5f5fc02442e511b Mon Sep 17 00:00:00 2001 From: realrafi Date: Thu, 2 Jul 2026 16:51:11 +0330 Subject: [PATCH 33/66] refactor: made project service to use pagination --- src/common/helpers/pagination.helper.ts | 29 +++++++++ .../interfaces/paginated-result.interface.ts | 6 ++ .../controllers/project.controller.ts | 19 ++++-- .../task-manager/providers/project.service.ts | 62 ++++++++++++++----- .../repositories/project.repository.ts | 28 +++++---- 5 files changed, 109 insertions(+), 35 deletions(-) create mode 100644 src/common/helpers/pagination.helper.ts create mode 100644 src/common/interfaces/paginated-result.interface.ts diff --git a/src/common/helpers/pagination.helper.ts b/src/common/helpers/pagination.helper.ts new file mode 100644 index 0000000..df195a4 --- /dev/null +++ b/src/common/helpers/pagination.helper.ts @@ -0,0 +1,29 @@ +import { IPageFormat } from "../interfaces/IPagination"; + +export function buildPageFormat( + page: number, + limit: number, + totalItems: number, + baseUrl: string, +): IPageFormat { + const totalPages = Math.ceil(totalItems / limit); + + const prevPage = + page > 1 + ? `${baseUrl}?page=${page - 1}&limit=${limit}` + : false; + + const nextPage = + page < totalPages + ? `${baseUrl}?page=${page + 1}&limit=${limit}` + : false; + + return { + page, + limit, + totalItems, + totalPages, + prevPage, + nextPage, + }; +} \ No newline at end of file diff --git a/src/common/interfaces/paginated-result.interface.ts b/src/common/interfaces/paginated-result.interface.ts new file mode 100644 index 0000000..b9247fb --- /dev/null +++ b/src/common/interfaces/paginated-result.interface.ts @@ -0,0 +1,6 @@ +import { IPageFormat } from "./IPagination"; + +export interface PaginatedResult { + data: T[]; + meta: IPageFormat; +} \ No newline at end of file diff --git a/src/modules/task-manager/controllers/project.controller.ts b/src/modules/task-manager/controllers/project.controller.ts index 3fae2a2..55c95bd 100644 --- a/src/modules/task-manager/controllers/project.controller.ts +++ b/src/modules/task-manager/controllers/project.controller.ts @@ -1,8 +1,10 @@ -import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from '@nestjs/common'; +import { Controller, Get, Post, Patch, Delete, Param, Body, Query, Req } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger'; +import { Request } from 'express'; import { ProjectService } from '../providers/project.service'; import { CreateProjectDto } from '../dto/project/create-project.dto'; import { UpdateProjectDto } from '../dto/project/update-project.dto'; +import { PaginationDto } from '../../../common/DTO/pagination.dto'; import { TMProject } from '../entities/project.entity'; @ApiTags('Task Manager - Projects') @@ -13,10 +15,15 @@ export class ProjectController { @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 { - if (workspaceId) return this.projectService.findByWorkspace(workspaceId); - return this.projectService.findAll(); + @ApiResponse({ status: 200, description: 'Paginated list of projects' }) + findAll( + @Query() pagination: PaginationDto, + @Query('workspaceId') workspaceId: string, + @Req() req: Request, + ) { + const baseUrl = `${req.protocol}://${req.get('host')}${req.path}`; + if (workspaceId) return this.projectService.findByWorkspace(workspaceId, pagination, baseUrl); + return this.projectService.findAll(pagination, baseUrl); } @Get(':id') @@ -50,7 +57,7 @@ export class ProjectController { @Delete(':id') @ApiOperation({ summary: 'Delete a project' }) @ApiParam({ name: 'id', description: 'Project ID' }) - @ApiResponse({ status: 200, description: 'Project deleted' }) + @ApiResponse({ status: 200, description: 'Project deleted', type: TMProject }) @ApiResponse({ status: 404, description: 'Project not found' }) remove(@Param('id') id: string): Promise { return this.projectService.remove(id); diff --git a/src/modules/task-manager/providers/project.service.ts b/src/modules/task-manager/providers/project.service.ts index 752aede..771916c 100644 --- a/src/modules/task-manager/providers/project.service.ts +++ b/src/modules/task-manager/providers/project.service.ts @@ -1,12 +1,15 @@ -import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; -import { InjectRepository } from "@nestjs/typeorm"; -import { In, Repository } from "typeorm"; -import { ProjectRepository } from "../repositories/project.repository"; -import { TMProject } from "../entities/project.entity"; -import { CreateProjectDto } from "../dto/project/create-project.dto"; -import { UpdateProjectDto } from "../dto/project/update-project.dto"; -import { User } from "../../users/entities/user.entity"; -import { WorkspaceRepository } from "../repositories/workspace.repository"; +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { In, Repository } from 'typeorm'; +import { ProjectRepository } from '../repositories/project.repository'; +import { WorkspaceRepository } from '../repositories/workspace.repository'; +import { TMProject } from '../entities/project.entity'; +import { CreateProjectDto } from '../dto/project/create-project.dto'; +import { UpdateProjectDto } from '../dto/project/update-project.dto'; +import { PaginationDto } from '../../../common/DTO/pagination.dto'; +import { PaginatedResult } from '../../../common/interfaces/paginated-result.interface'; +import { buildPageFormat } from '../../../common/helpers/pagination.helper'; +import { User } from '../../users/entities/user.entity'; @Injectable() export class ProjectService { @@ -17,12 +20,34 @@ export class ProjectService { private readonly userRepository: Repository, ) {} - findAll(): Promise { - return this.projectRepository.findAll(); + async findAll(pagination: PaginationDto, baseUrl: string): Promise> { + const page = pagination.page ?? 1; + const limit = pagination.limit ?? 10; + const skip = (page - 1) * limit; + + const [data, totalItems] = await this.projectRepository.findAll(skip, limit); + + return { + data, + meta: buildPageFormat(page, limit, totalItems, baseUrl), + }; } - findByWorkspace(workspaceId: string): Promise { - return this.projectRepository.findByWorkspace(workspaceId); + async findByWorkspace( + workspaceId: string, + pagination: PaginationDto, + baseUrl: string, + ): Promise> { + const page = pagination.page ?? 1; + const limit = pagination.limit ?? 10; + const skip = (page - 1) * limit; + + const [data, totalItems] = await this.projectRepository.findByWorkspace(workspaceId, skip, limit); + + return { + data, + meta: buildPageFormat(page, limit, totalItems, baseUrl), + }; } async findOne(id: string): Promise { @@ -46,7 +71,9 @@ export class ProjectService { const workspaceUserIds = workspace.users.map((u) => u.id); const invalidUsers = userIds.filter((id) => !workspaceUserIds.includes(id)); if (invalidUsers.length) { - throw new BadRequestException(`Users [${invalidUsers.join(", ")}] are not members of this workspace`); + throw new BadRequestException( + `Users [${invalidUsers.join(', ')}] are not members of this workspace`, + ); } project.users = await this.userRepository.findBy({ id: In(userIds) }); } @@ -72,7 +99,9 @@ export class ProjectService { const workspaceUserIds = workspace.users.map((u) => u.id); const invalidUsers = userIds.filter((uid) => !workspaceUserIds.includes(uid)); if (invalidUsers.length) { - throw new BadRequestException(`Users [${invalidUsers.join(", ")}] are not members of this workspace`); + throw new BadRequestException( + `Users [${invalidUsers.join(', ')}] are not members of this workspace`, + ); } project.users = await this.userRepository.findBy({ id: In(userIds) }); } else { @@ -86,7 +115,6 @@ export class ProjectService { async remove(id: string): Promise { const project = await this.findOne(id); await this.projectRepository.delete(id); - return project; } -} +} \ No newline at end of file diff --git a/src/modules/task-manager/repositories/project.repository.ts b/src/modules/task-manager/repositories/project.repository.ts index 0a4a1c4..6b0535a 100644 --- a/src/modules/task-manager/repositories/project.repository.ts +++ b/src/modules/task-manager/repositories/project.repository.ts @@ -1,7 +1,7 @@ -import { Injectable } from "@nestjs/common"; -import { InjectRepository } from "@nestjs/typeorm"; -import { Repository } from "typeorm"; -import { TMProject } from "../entities/project.entity"; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { TMProject } from '../entities/project.entity'; @Injectable() export class ProjectRepository { @@ -10,23 +10,27 @@ export class ProjectRepository { private readonly repository: Repository, ) {} - findAll() { - return this.repository.find({ - relations: ["workspace", "taskPhases", "users"], + findAll(skip: number, take: number) { + return this.repository.findAndCount({ + relations: ['workspace', 'taskPhases', 'users'], + skip, + take, }); } - findByWorkspace(workspaceId: string) { - return this.repository.find({ + findByWorkspace(workspaceId: string, skip: number, take: number) { + return this.repository.findAndCount({ where: { workspaceId }, - relations: ["taskPhases", "users"], + relations: ['taskPhases', 'users'], + skip, + take, }); } findOneById(id: string) { return this.repository.findOne({ where: { id }, - relations: ["workspace", "taskPhases", "users"], + relations: ['workspace', 'taskPhases', 'users'], }); } @@ -45,4 +49,4 @@ export class ProjectRepository { delete(id: string) { return this.repository.delete(id); } -} +} \ No newline at end of file From 06ec73be9249fa1cf94131a62d39726907432d74 Mon Sep 17 00:00:00 2001 From: realrafi Date: Sat, 4 Jul 2026 10:14:33 +0330 Subject: [PATCH 34/66] feat: added cursor-based pagination to checklistitems --- src/common/DTO/cursor-pagination.dto.ts | 18 +++++ src/common/helpers/cursor-paginated.helper.ts | 25 +++++++ .../cursor-paginated-result.interface.ts | 9 +++ .../controllers/check-list-item.controller.ts | 75 ++++++++++--------- .../providers/check-list-item.service.ts | 43 +++++++---- .../check-list-item.repository.ts | 27 +++++-- 6 files changed, 142 insertions(+), 55 deletions(-) create mode 100644 src/common/DTO/cursor-pagination.dto.ts create mode 100644 src/common/helpers/cursor-paginated.helper.ts create mode 100644 src/common/interfaces/cursor-paginated-result.interface.ts diff --git a/src/common/DTO/cursor-pagination.dto.ts b/src/common/DTO/cursor-pagination.dto.ts new file mode 100644 index 0000000..2b2faf4 --- /dev/null +++ b/src/common/DTO/cursor-pagination.dto.ts @@ -0,0 +1,18 @@ +import { ApiPropertyOptional } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { IsInt, IsISO8601, IsOptional, Max, Min } from "class-validator"; + +export class CursorPaginationDto { + @IsOptional() + @IsISO8601() + @ApiPropertyOptional({ type: "string", description: "Cursor (createdAt of last seen item)", example: "2026-07-01T10:00:00.000Z" }) + cursor?: string; + + @IsOptional() + @IsInt() + @Min(1) + @Max(50) + @Type(() => Number) + @ApiPropertyOptional({ type: "number", required: false, default: 10 }) + limit?: number; +} diff --git a/src/common/helpers/cursor-paginated.helper.ts b/src/common/helpers/cursor-paginated.helper.ts new file mode 100644 index 0000000..ec0a8b3 --- /dev/null +++ b/src/common/helpers/cursor-paginated.helper.ts @@ -0,0 +1,25 @@ +import { CursorPaginatedResult } from "../interfaces/cursor-paginated-result.interface"; + +export function buildCursorPageFormat( + data: T[], + limit: number, + previousCursor?: Date, +): CursorPaginatedResult { + const hasMore = data.length > limit; + + if (hasMore) data.pop(); + + const nextCursor = hasMore ? data[data.length - 1].createdAt.toISOString() : null; + + const prevCursor = previousCursor ? previousCursor.toISOString() : null; + + return { + data, + meta: { + nextCursor, + prevCursor, + hasMore, + limit, + }, + }; +} diff --git a/src/common/interfaces/cursor-paginated-result.interface.ts b/src/common/interfaces/cursor-paginated-result.interface.ts new file mode 100644 index 0000000..6605514 --- /dev/null +++ b/src/common/interfaces/cursor-paginated-result.interface.ts @@ -0,0 +1,9 @@ +export interface CursorPaginatedResult { + data: T[]; + meta: { + nextCursor: string | null; + prevCursor: string | null; + hasMore: boolean; + limit: number; + }; +} diff --git a/src/modules/task-manager/controllers/check-list-item.controller.ts b/src/modules/task-manager/controllers/check-list-item.controller.ts index c7529d3..5d4cb29 100644 --- a/src/modules/task-manager/controllers/check-list-item.controller.ts +++ b/src/modules/task-manager/controllers/check-list-item.controller.ts @@ -1,56 +1,59 @@ -import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger'; -import { CheckListItemService } from '../providers/check-list-item.service'; -import { CreateCheckListItemDto } from '../dto/check-list-item/create-check-list-item.dto'; -import { UpdateCheckListItemDto } from '../dto/check-list-item/update-check-list-item.dto'; -import { TMCheckListItem } from '../entities/check-list-item.entity'; +import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from "@nestjs/common"; +import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from "@nestjs/swagger"; +import { CheckListItemService } from "../providers/check-list-item.service"; +import { CreateCheckListItemDto } from "../dto/check-list-item/create-check-list-item.dto"; +import { UpdateCheckListItemDto } from "../dto/check-list-item/update-check-list-item.dto"; +import { CursorPaginationDto } from "../../../common/DTO/cursor-pagination.dto"; +import { TMCheckListItem } from "../entities/check-list-item.entity"; -@ApiTags('Task Manager - Check List Items') -@Controller('task-manager/check-list-items') +@ApiTags("Task Manager - Check List Items") +@Controller("task-manager/check-list-items") export class CheckListItemController { constructor(private readonly checkListItemService: CheckListItemService) {} @Get() - @ApiOperation({ summary: 'Get all check list items, optionally filtered by task' }) - @ApiQuery({ name: 'taskId', required: false, description: 'Filter check list items by task ID' }) - @ApiResponse({ status: 200, description: 'List of check list items', type: [TMCheckListItem] }) - findAll(@Query('taskId') taskId?: string): Promise { - if (taskId) return this.checkListItemService.findByTask(taskId); - return this.checkListItemService.findAll(); + @ApiOperation({ summary: "Get all check list items, optionally filtered by task" }) + @ApiQuery({ name: "taskId", required: false, description: "Filter check list items by task ID" }) + @ApiQuery({ name: "cursor", required: false, description: "Cursor for pagination (createdAt ISO string)" }) + @ApiQuery({ name: "limit", required: false, description: "Number of records to return (max 50)" }) + @ApiResponse({ status: 200, description: "Cursor-paginated list of check list items" }) + findAll(@Query() pagination: CursorPaginationDto, @Query("taskId") taskId?: string) { + if (taskId) return this.checkListItemService.findByTask(taskId, pagination); + return this.checkListItemService.findAll(pagination); } - @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 { + @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 { return this.checkListItemService.findOne(id); } @Post() - @ApiOperation({ summary: 'Create a new check list item' }) - @ApiResponse({ status: 201, description: 'CheckListItem created', type: TMCheckListItem }) - @ApiResponse({ status: 404, description: 'Task not found' }) + @ApiOperation({ summary: "Create a new check list item" }) + @ApiResponse({ status: 201, description: "CheckListItem created", type: TMCheckListItem }) + @ApiResponse({ status: 404, description: "Task not found" }) create(@Body() body: CreateCheckListItemDto): Promise { return this.checkListItemService.create(body); } - @Patch(':id') - @ApiOperation({ summary: 'Update a check list item' }) - @ApiParam({ name: 'id', description: 'CheckListItem ID' }) - @ApiResponse({ status: 200, description: 'CheckListItem updated', type: TMCheckListItem }) - @ApiResponse({ status: 404, description: 'CheckListItem or Task not found' }) - update(@Param('id') id: string, @Body() body: UpdateCheckListItemDto): Promise { + @Patch(":id") + @ApiOperation({ summary: "Update a check list item" }) + @ApiParam({ name: "id", description: "CheckListItem ID" }) + @ApiResponse({ status: 200, description: "CheckListItem updated", type: TMCheckListItem }) + @ApiResponse({ status: 404, description: "CheckListItem or Task not found" }) + update(@Param("id") id: string, @Body() body: UpdateCheckListItemDto): Promise { return this.checkListItemService.update(id, body); } - @Delete(':id') - @ApiOperation({ summary: 'Delete a check list item' }) - @ApiParam({ name: 'id', description: 'CheckListItem ID' }) - @ApiResponse({ status: 200, description: 'CheckListItem deleted' }) - @ApiResponse({ status: 404, description: 'CheckListItem not found' }) - remove(@Param('id') id: string): Promise { + @Delete(":id") + @ApiOperation({ summary: "Delete a check list item" }) + @ApiParam({ name: "id", description: "CheckListItem ID" }) + @ApiResponse({ status: 200, description: "CheckListItem deleted", type: TMCheckListItem }) + @ApiResponse({ status: 404, description: "CheckListItem not found" }) + remove(@Param("id") id: string): Promise { return this.checkListItemService.remove(id); } -} \ No newline at end of file +} diff --git a/src/modules/task-manager/providers/check-list-item.service.ts b/src/modules/task-manager/providers/check-list-item.service.ts index f421db4..d02a78f 100644 --- a/src/modules/task-manager/providers/check-list-item.service.ts +++ b/src/modules/task-manager/providers/check-list-item.service.ts @@ -1,9 +1,12 @@ -import { Injectable, NotFoundException } from "@nestjs/common"; -import { CheckListItemRepository } from "../repositories/check-list-item.repository"; -import { TaskRepository } from "../repositories/task.repository"; -import { TMCheckListItem } from "../entities/check-list-item.entity"; -import { CreateCheckListItemDto } from "../dto/check-list-item/create-check-list-item.dto"; -import { UpdateCheckListItemDto } from "../dto/check-list-item/update-check-list-item.dto"; +import { Injectable, NotFoundException } from '@nestjs/common'; +import { CheckListItemRepository } from '../repositories/check-list-item.repository'; +import { TaskRepository } from '../repositories/task.repository'; +import { TMCheckListItem } from '../entities/check-list-item.entity'; +import { CreateCheckListItemDto } from '../dto/check-list-item/create-check-list-item.dto'; +import { UpdateCheckListItemDto } from '../dto/check-list-item/update-check-list-item.dto'; +import { CursorPaginationDto } from '../../../common/DTO/cursor-pagination.dto'; +import { CursorPaginatedResult } from '../../../common/interfaces/cursor-paginated-result.interface'; +import { buildCursorPageFormat } from '../../../common/helpers/cursor-paginated.helper'; @Injectable() export class CheckListItemService { @@ -12,12 +15,25 @@ export class CheckListItemService { private readonly taskRepository: TaskRepository, ) {} - findAll(): Promise { - return this.checkListItemRepository.findAll(); + async findAll(pagination: CursorPaginationDto): Promise> { + const limit = pagination.limit ?? 10; + const cursorDate = pagination.cursor ? new Date(pagination.cursor) : undefined; + + const [data] = await this.checkListItemRepository.findAll(limit, cursorDate); + + return buildCursorPageFormat(data, limit, cursorDate); } - findByTask(taskId: string): Promise { - return this.checkListItemRepository.findByTask(taskId); + async findByTask( + taskId: string, + pagination: CursorPaginationDto, + ): Promise> { + const limit = pagination.limit ?? 10; + const cursorDate = pagination.cursor ? new Date(pagination.cursor) : undefined; + + const [data] = await this.checkListItemRepository.findByTask(taskId, limit, cursorDate); + + return buildCursorPageFormat(data, limit, cursorDate); } async findOne(id: string): Promise { @@ -46,8 +62,9 @@ export class CheckListItemService { return this.checkListItemRepository.save(checkListItem); } - async remove(id: string): Promise { - await this.findOne(id); + async remove(id: string): Promise { + const checkListItem = await this.findOne(id); await this.checkListItemRepository.delete(id); + return checkListItem; } -} +} \ No newline at end of file diff --git a/src/modules/task-manager/repositories/check-list-item.repository.ts b/src/modules/task-manager/repositories/check-list-item.repository.ts index 0d8c3ee..2b5d931 100644 --- a/src/modules/task-manager/repositories/check-list-item.repository.ts +++ b/src/modules/task-manager/repositories/check-list-item.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { Repository, MoreThan } from 'typeorm'; import { TMCheckListItem } from '../entities/check-list-item.entity'; @Injectable() @@ -10,15 +10,23 @@ export class CheckListItemRepository { private readonly repository: Repository, ) {} - findAll() { - return this.repository.find({ + findAll(limit: number, cursor?: Date) { + return this.repository.findAndCount({ + where: cursor ? { createdAt: MoreThan(cursor) } : {}, + order: { createdAt: 'ASC' }, + take: limit + 1, relations: ['task'], }); } - findByTask(taskId: string) { - return this.repository.find({ - where: { taskId }, + findByTask(taskId: string, limit: number, cursor?: Date) { + return this.repository.findAndCount({ + where: cursor + ? { taskId, createdAt: MoreThan(cursor) } + : { taskId }, + order: { createdAt: 'ASC' }, + take: limit + 1, + relations: ['task'], }); } @@ -29,6 +37,13 @@ export class CheckListItemRepository { }); } + findOneByCreatedAt(createdAt: Date) { + return this.repository.findOne({ + where: { createdAt }, + order: { createdAt: 'ASC' }, + }); + } + create(data: Partial) { return this.repository.create(data); } From 8d962ea6ee3b78dd6c91a5792d4e36383b1e909c Mon Sep 17 00:00:00 2001 From: realrafi Date: Sat, 4 Jul 2026 10:52:44 +0330 Subject: [PATCH 35/66] fix: fixing some errors in project service and controller --- .../controllers/project.controller.ts | 81 ++++++++++--------- .../repositories/workspace.repository.ts | 1 + .../task-manager/task-manager.module.ts | 10 ++- 3 files changed, 50 insertions(+), 42 deletions(-) diff --git a/src/modules/task-manager/controllers/project.controller.ts b/src/modules/task-manager/controllers/project.controller.ts index 55c95bd..249858c 100644 --- a/src/modules/task-manager/controllers/project.controller.ts +++ b/src/modules/task-manager/controllers/project.controller.ts @@ -1,65 +1,66 @@ -import { Controller, Get, Post, Patch, Delete, Param, Body, Query, Req } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger'; -import { Request } from 'express'; -import { ProjectService } from '../providers/project.service'; -import { CreateProjectDto } from '../dto/project/create-project.dto'; -import { UpdateProjectDto } from '../dto/project/update-project.dto'; -import { PaginationDto } from '../../../common/DTO/pagination.dto'; -import { TMProject } from '../entities/project.entity'; +import { Controller, Get, Post, Patch, Delete, Param, Body, Query, Headers } from "@nestjs/common"; +import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from "@nestjs/swagger"; +import { ProjectService } from "../providers/project.service"; +import { CreateProjectDto } from "../dto/project/create-project.dto"; +import { UpdateProjectDto } from "../dto/project/update-project.dto"; +import { PaginationDto } from "../../../common/DTO/pagination.dto"; +import { TMProject } from "../entities/project.entity"; -@ApiTags('Task Manager - Projects') -@Controller('task-manager/projects') +@ApiTags("Task Manager - Projects") +@Controller("task-manager/projects") export class ProjectController { constructor(private readonly projectService: ProjectService) {} @Get() - @ApiOperation({ summary: 'Get all projects, optionally filtered by workspace' }) - @ApiQuery({ name: 'workspaceId', required: false, description: 'Filter projects by workspace ID' }) - @ApiResponse({ status: 200, description: 'Paginated list of projects' }) + @ApiOperation({ summary: "Get all projects, optionally filtered by workspace" }) + @ApiQuery({ name: "workspaceId", required: false, description: "Filter projects by workspace ID" }) + @ApiResponse({ status: 200, description: "Paginated list of projects" }) findAll( @Query() pagination: PaginationDto, - @Query('workspaceId') workspaceId: string, - @Req() req: Request, + @Query("workspaceId") workspaceId: string, + @Headers("host") host: string, + @Headers("x-forwarded-proto") proto?: string, ) { - const baseUrl = `${req.protocol}://${req.get('host')}${req.path}`; + const protocol = proto ?? "http"; + const baseUrl = `${protocol}://${host}/task-manager/projects`; if (workspaceId) return this.projectService.findByWorkspace(workspaceId, pagination, baseUrl); return this.projectService.findAll(pagination, baseUrl); } - @Get(':id') - @ApiOperation({ summary: 'Get a project by ID' }) - @ApiParam({ name: 'id', description: 'Project ID' }) - @ApiResponse({ status: 200, description: 'Project found', type: TMProject }) - @ApiResponse({ status: 404, description: 'Project not found' }) - findOne(@Param('id') id: string): Promise { + @Get(":id") + @ApiOperation({ summary: "Get a project by ID" }) + @ApiParam({ name: "id", description: "Project ID" }) + @ApiResponse({ status: 200, description: "Project found", type: TMProject }) + @ApiResponse({ status: 404, description: "Project not found" }) + findOne(@Param("id") id: string): Promise { return this.projectService.findOne(id); } @Post() - @ApiOperation({ summary: 'Create a new project' }) - @ApiResponse({ status: 201, description: 'Project created', type: TMProject }) - @ApiResponse({ status: 400, description: 'One or more users are not members of the workspace' }) - @ApiResponse({ status: 404, description: 'Workspace not found' }) + @ApiOperation({ summary: "Create a new project" }) + @ApiResponse({ status: 201, description: "Project created", type: TMProject }) + @ApiResponse({ status: 400, description: "One or more users are not members of the workspace" }) + @ApiResponse({ status: 404, description: "Workspace not found" }) create(@Body() body: CreateProjectDto): Promise { return this.projectService.create(body); } - @Patch(':id') - @ApiOperation({ summary: 'Update a project' }) - @ApiParam({ name: 'id', description: 'Project ID' }) - @ApiResponse({ status: 200, description: 'Project updated', type: TMProject }) - @ApiResponse({ status: 400, description: 'One or more users are not members of the workspace' }) - @ApiResponse({ status: 404, description: 'Project not found' }) - update(@Param('id') id: string, @Body() body: UpdateProjectDto): Promise { + @Patch(":id") + @ApiOperation({ summary: "Update a project" }) + @ApiParam({ name: "id", description: "Project ID" }) + @ApiResponse({ status: 200, description: "Project updated", type: TMProject }) + @ApiResponse({ status: 400, description: "One or more users are not members of the workspace" }) + @ApiResponse({ status: 404, description: "Project not found" }) + update(@Param("id") id: string, @Body() body: UpdateProjectDto): Promise { return this.projectService.update(id, body); } - @Delete(':id') - @ApiOperation({ summary: 'Delete a project' }) - @ApiParam({ name: 'id', description: 'Project ID' }) - @ApiResponse({ status: 200, description: 'Project deleted', type: TMProject }) - @ApiResponse({ status: 404, description: 'Project not found' }) - remove(@Param('id') id: string): Promise { + @Delete(":id") + @ApiOperation({ summary: "Delete a project" }) + @ApiParam({ name: "id", description: "Project ID" }) + @ApiResponse({ status: 200, description: "Project deleted", type: TMProject }) + @ApiResponse({ status: 404, description: "Project not found" }) + remove(@Param("id") id: string): Promise { return this.projectService.remove(id); } -} \ No newline at end of file +} diff --git a/src/modules/task-manager/repositories/workspace.repository.ts b/src/modules/task-manager/repositories/workspace.repository.ts index 2d63626..fd38d40 100644 --- a/src/modules/task-manager/repositories/workspace.repository.ts +++ b/src/modules/task-manager/repositories/workspace.repository.ts @@ -19,6 +19,7 @@ export class WorkspaceRepository { findOneById(id: string) { return this.repository.findOne({ where: { id }, + relations: ["workspaceType", "projects", "users"], }); } diff --git a/src/modules/task-manager/task-manager.module.ts b/src/modules/task-manager/task-manager.module.ts index c5704cd..e6de5b0 100644 --- a/src/modules/task-manager/task-manager.module.ts +++ b/src/modules/task-manager/task-manager.module.ts @@ -31,6 +31,9 @@ import { CheckListItemController } from "./controllers/check-list-item.controlle import { RemarkController } from "./controllers/remark.controller"; import { RemarkRepository } from "./repositories/remark.repository"; import { RemarkService } from "./providers/remark.service"; +import { AttachmentController } from "./controllers/attachment.controller"; +import { AttachmentRepository } from "./repositories/attachment.repository"; +import { AttachmentService } from "./providers/attachment.service"; @Module({ imports: [ @@ -43,7 +46,8 @@ import { RemarkService } from "./providers/remark.service"; TaskPhaseController, TaskController, CheckListItemController, - RemarkController + RemarkController, + AttachmentController ], providers: [ WorkspaceRepository, @@ -59,7 +63,9 @@ import { RemarkService } from "./providers/remark.service"; CheckListItemRepository, CheckListItemService, RemarkRepository, - RemarkService + RemarkService, + AttachmentRepository, + AttachmentService ] }) export class TaskManagerModule {} From e57dd84ebcf0d1861243ef167e3f7e04dc1c3de0 Mon Sep 17 00:00:00 2001 From: realrafi Date: Sat, 4 Jul 2026 11:47:36 +0330 Subject: [PATCH 36/66] refactor: removing the workspace type entirely --- .../migrations/1783150384434-taskmanager.ts | 0 .../migrations/1783150480192-taskmanager.ts | 16 ++++++ .../controllers/workspace-type.controller.ts | 55 ------------------- .../create-workspace-type.dto.ts | 25 --------- .../update-workspace-type.dto.ts | 4 -- .../dto/workspace/create-workspace.dto.ts | 5 -- .../entities/workspace-type.entity.ts | 20 ------- .../task-manager/entities/workspace.entity.ts | 10 +--- .../providers/workspace-type.service.ts | 47 ---------------- .../providers/workspace.service.ts | 14 +---- .../repositories/workspace-type.repository.ts | 42 -------------- .../repositories/workspace.repository.ts | 4 +- .../task-manager/task-manager.module.ts | 9 +-- 13 files changed, 22 insertions(+), 229 deletions(-) create mode 100644 database/migrations/1783150384434-taskmanager.ts create mode 100644 database/migrations/1783150480192-taskmanager.ts delete mode 100644 src/modules/task-manager/controllers/workspace-type.controller.ts delete mode 100644 src/modules/task-manager/dto/workspace-type/create-workspace-type.dto.ts delete mode 100644 src/modules/task-manager/dto/workspace-type/update-workspace-type.dto.ts delete mode 100644 src/modules/task-manager/entities/workspace-type.entity.ts delete mode 100644 src/modules/task-manager/providers/workspace-type.service.ts delete mode 100644 src/modules/task-manager/repositories/workspace-type.repository.ts diff --git a/database/migrations/1783150384434-taskmanager.ts b/database/migrations/1783150384434-taskmanager.ts new file mode 100644 index 0000000..e69de29 diff --git a/database/migrations/1783150480192-taskmanager.ts b/database/migrations/1783150480192-taskmanager.ts new file mode 100644 index 0000000..5627d12 --- /dev/null +++ b/database/migrations/1783150480192-taskmanager.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Taskmanager1783150480192 implements MigrationInterface { + name = 'Taskmanager1783150480192' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tm_workspaces" DROP CONSTRAINT "FK_253e36f065cfc1eca58abaefb22"`); + await queryRunner.query(`ALTER TABLE "tm_workspaces" DROP COLUMN "workspaceTypeId"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tm_workspaces" ADD "workspaceTypeId" uuid NOT NULL`); + await queryRunner.query(`ALTER TABLE "tm_workspaces" ADD CONSTRAINT "FK_253e36f065cfc1eca58abaefb22" FOREIGN KEY ("workspaceTypeId") REFERENCES "tm_workspace_types"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + +} diff --git a/src/modules/task-manager/controllers/workspace-type.controller.ts b/src/modules/task-manager/controllers/workspace-type.controller.ts deleted file mode 100644 index 8b7e9bf..0000000 --- a/src/modules/task-manager/controllers/workspace-type.controller.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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 { - 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 { - return this.workspaceTypeService.remove(id); - } -} diff --git a/src/modules/task-manager/dto/workspace-type/create-workspace-type.dto.ts b/src/modules/task-manager/dto/workspace-type/create-workspace-type.dto.ts deleted file mode 100644 index d0d28c4..0000000 --- a/src/modules/task-manager/dto/workspace-type/create-workspace-type.dto.ts +++ /dev/null @@ -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; -} diff --git a/src/modules/task-manager/dto/workspace-type/update-workspace-type.dto.ts b/src/modules/task-manager/dto/workspace-type/update-workspace-type.dto.ts deleted file mode 100644 index cc4a3ea..0000000 --- a/src/modules/task-manager/dto/workspace-type/update-workspace-type.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PartialType } from '@nestjs/swagger'; -import { CreateWorkspaceTypeDto } from './create-workspace-type.dto'; - -export class UpdateWorkspaceTypeDto extends PartialType(CreateWorkspaceTypeDto) {} \ No newline at end of file diff --git a/src/modules/task-manager/dto/workspace/create-workspace.dto.ts b/src/modules/task-manager/dto/workspace/create-workspace.dto.ts index 0efdb4b..99c8bf5 100644 --- a/src/modules/task-manager/dto/workspace/create-workspace.dto.ts +++ b/src/modules/task-manager/dto/workspace/create-workspace.dto.ts @@ -8,11 +8,6 @@ export class CreateWorkspaceDto { @MaxLength(100) 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" }) @IsString() @IsOptional() diff --git a/src/modules/task-manager/entities/workspace-type.entity.ts b/src/modules/task-manager/entities/workspace-type.entity.ts deleted file mode 100644 index b840242..0000000 --- a/src/modules/task-manager/entities/workspace-type.entity.ts +++ /dev/null @@ -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[]; -} diff --git a/src/modules/task-manager/entities/workspace.entity.ts b/src/modules/task-manager/entities/workspace.entity.ts index c63366d..417c1fd 100644 --- a/src/modules/task-manager/entities/workspace.entity.ts +++ b/src/modules/task-manager/entities/workspace.entity.ts @@ -1,6 +1,5 @@ -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 { TMWorkspaceType } from "./workspace-type.entity"; import { TMProject } from "./project.entity"; import { User } from "../../users/entities/user.entity"; @@ -20,13 +19,6 @@ export class TMWorkspace extends BaseEntity { // --- Relations --- - @ManyToOne(() => TMWorkspaceType, { nullable: false, eager: true }) - @JoinColumn({ name: "workspaceTypeId" }) - workspaceType: TMWorkspaceType; - - @Column({ type: "uuid" }) - workspaceTypeId: string; - @OneToMany(() => TMProject, (project) => project.workspace, { cascade: ["insert", "update"], }) diff --git a/src/modules/task-manager/providers/workspace-type.service.ts b/src/modules/task-manager/providers/workspace-type.service.ts deleted file mode 100644 index 1e0f0ed..0000000 --- a/src/modules/task-manager/providers/workspace-type.service.ts +++ /dev/null @@ -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 { - return this.workspaceTypeRepository.findAll(); - } - - async findOne(id: string): Promise { - const workspaceType = await this.workspaceTypeRepository.findOneById(id); - if (!workspaceType) throw new NotFoundException(`WorkspaceType #${id} not found`); - return workspaceType; - } - - async create(dto: CreateWorkspaceTypeDto): Promise { - 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 { - 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 { - const workspaceType = await this.findOne(id); - await this.workspaceTypeRepository.delete(id); - - return workspaceType; - } -} diff --git a/src/modules/task-manager/providers/workspace.service.ts b/src/modules/task-manager/providers/workspace.service.ts index 06e68f4..5e90079 100644 --- a/src/modules/task-manager/providers/workspace.service.ts +++ b/src/modules/task-manager/providers/workspace.service.ts @@ -6,13 +6,11 @@ import { TMWorkspace } from "../entities/workspace.entity"; import { CreateWorkspaceDto } from "../dto/workspace/create-workspace.dto"; import { UpdateWorkspaceDto } from "../dto/workspace/update-workspace.dto"; import { User } from "../../users/entities/user.entity"; -import { WorkspaceTypeRepository } from "../repositories/workspace-type.repository"; @Injectable() export class WorkspaceService { constructor( private readonly workspaceRepository: WorkspaceRepository, - private readonly workspaceTypeRepository: WorkspaceTypeRepository, @InjectRepository(User) private readonly userRepository: Repository, ) {} @@ -28,10 +26,7 @@ export class WorkspaceService { } async create(dto: CreateWorkspaceDto): Promise { - const { userIds, workspaceTypeId, ...rest } = dto; - - const workspaceType = await this.workspaceTypeRepository.findOneById(workspaceTypeId); - if (!workspaceType) throw new NotFoundException(`WorkSpaceType #${workspaceTypeId} not found!`); + const { userIds, ...rest } = dto; const workspace = this.workspaceRepository.create(rest); @@ -44,12 +39,7 @@ export class WorkspaceService { async update(id: string, dto: UpdateWorkspaceDto): Promise { const workspace = await this.findOneOrFail(id); - const { userIds, workspaceTypeId, ...rest } = dto; - - if (workspaceTypeId) { - const workspaceType = await this.workspaceTypeRepository.findOneById(workspaceTypeId); - if (!workspaceType) throw new NotFoundException(`WorkSpaceType #${workspaceTypeId} not found!`); - } + const { userIds, ...rest } = dto; Object.assign(workspace, rest); diff --git a/src/modules/task-manager/repositories/workspace-type.repository.ts b/src/modules/task-manager/repositories/workspace-type.repository.ts deleted file mode 100644 index bfd0c85..0000000 --- a/src/modules/task-manager/repositories/workspace-type.repository.ts +++ /dev/null @@ -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, - ) {} - - 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) { - return this.repository.create(data); - } - - save(workspaceType: TMWorkspaceType) { - return this.repository.save(workspaceType); - } - - update(id: string, data: Partial) { - return this.repository.update(id, data); - } - - delete(id: string) { - return this.repository.delete(id); - } -} diff --git a/src/modules/task-manager/repositories/workspace.repository.ts b/src/modules/task-manager/repositories/workspace.repository.ts index fd38d40..175678a 100644 --- a/src/modules/task-manager/repositories/workspace.repository.ts +++ b/src/modules/task-manager/repositories/workspace.repository.ts @@ -12,14 +12,14 @@ export class WorkspaceRepository { findAll() { return this.repository.find({ - relations: ["workspaceType", "projects", "users"], + relations: ["projects", "users"], }); } findOneById(id: string) { return this.repository.findOne({ where: { id }, - relations: ["workspaceType", "projects", "users"], + relations: ["projects", "users"], }); } diff --git a/src/modules/task-manager/task-manager.module.ts b/src/modules/task-manager/task-manager.module.ts index e6de5b0..43bbb5b 100644 --- a/src/modules/task-manager/task-manager.module.ts +++ b/src/modules/task-manager/task-manager.module.ts @@ -1,7 +1,6 @@ import { Module } from "@nestjs/common"; import { TypeOrmModule } from "@nestjs/typeorm"; -import { TMWorkspaceType } from "./entities/workspace-type.entity"; import { TMWorkspace } from "./entities/workspace.entity"; import { TMProject } from "./entities/project.entity"; import { TMTaskPhase } from "./entities/task-phase.entity"; @@ -13,9 +12,6 @@ import { WorkspaceRepository } from "./repositories/workspace.repository"; import { WorkspaceController } from "./controllers/workspace.controller"; import { WorkspaceService } from "./providers/workspace.service"; import { User } from "../users/entities/user.entity"; -import { 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 { ProjectRepository } from "./repositories/project.repository"; import { ProjectService } from "./providers/project.service"; @@ -37,11 +33,10 @@ import { AttachmentService } from "./providers/attachment.service"; @Module({ imports: [ - TypeOrmModule.forFeature([TMWorkspaceType, TMWorkspace, TMProject, TMTaskPhase, TMTask, TMRemark, TMAttachment, TMCheckListItem, User]), + TypeOrmModule.forFeature([TMWorkspace, TMProject, TMTaskPhase, TMTask, TMRemark, TMAttachment, TMCheckListItem, User]), ], controllers: [ WorkspaceController, - WorkspaceTypeController, ProjectController, TaskPhaseController, TaskController, @@ -52,8 +47,6 @@ import { AttachmentService } from "./providers/attachment.service"; providers: [ WorkspaceRepository, WorkspaceService, - WorkspaceTypeRepository, - WorkspaceTypeService, ProjectRepository, ProjectService, TaskPhaseRepository, From c5b2cb93f0b2dd10e0fedce4a59fa87c25151840 Mon Sep 17 00:00:00 2001 From: realrafi Date: Sat, 4 Jul 2026 17:32:02 +0330 Subject: [PATCH 37/66] feat: added get workspaces by user and some refactors --- src/common/enums/message.enum.ts | 4 ++ .../controllers/workspace.controller.ts | 23 ++++++++++- .../task-manager/entities/workspace.entity.ts | 2 +- .../providers/workspace.service.ts | 18 +++++++-- .../repositories/workspace.repository.ts | 39 ++++--------------- src/modules/users/enums/permission.enum.ts | 3 +- 6 files changed, 52 insertions(+), 37 deletions(-) diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 1b28777..c9f2f19 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -915,3 +915,7 @@ export const enum UploaderMessage { export const enum AccessLogMessage { ACCESS_LOG_NOT_FOUND = "لاگ دسترسی مورد نظر یافت نشد", } + +export const enum WorkSpaceMessage { + WORKSPACE_EXISTS = "فضای کار با این اسم وجود دارد!" +} \ No newline at end of file diff --git a/src/modules/task-manager/controllers/workspace.controller.ts b/src/modules/task-manager/controllers/workspace.controller.ts index bb9fcc5..ed8a810 100644 --- a/src/modules/task-manager/controllers/workspace.controller.ts +++ b/src/modules/task-manager/controllers/workspace.controller.ts @@ -4,13 +4,22 @@ import { WorkspaceService } from '../providers/workspace.service'; import { CreateWorkspaceDto } from '../dto/workspace/create-workspace.dto'; import { UpdateWorkspaceDto } from '../dto/workspace/update-workspace.dto'; import { TMWorkspace } from '../entities/workspace.entity'; +// import { 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'; +import { UserDec } from '../../../common/decorators/user.decorator'; +import { AuthGuards } from '../../../common/decorators/auth-guard.decorator'; -@ApiTags('Task Manager') +@AuthGuards() +@AdminRoute() +@ApiTags('Task Manager - Workspaces') @Controller('task-manager/workspaces') export class WorkspaceController { constructor(private readonly workspaceService: WorkspaceService) {} @Get() + @PermissionsDec(PermissionEnum.WORKSPACE) @ApiOperation({ summary: 'Get all workspaces' }) @ApiResponse({ status: 200, description: 'List of workspaces', type: [TMWorkspace] }) findAll(): Promise { @@ -18,6 +27,7 @@ export class WorkspaceController { } @Get(':id') + @PermissionsDec(PermissionEnum.WORKSPACE) @ApiOperation({ summary: 'Get a workspace by ID' }) @ApiParam({ name: 'id', description: 'Workspace ID' }) @ApiResponse({ status: 200, description: 'Workspace found', type: TMWorkspace }) @@ -27,6 +37,7 @@ export class WorkspaceController { } @Post() + @PermissionsDec(PermissionEnum.WORKSPACE) @ApiOperation({ summary: 'Create a new workspace' }) @ApiResponse({ status: 201, description: 'Workspace created', type: TMWorkspace }) create(@Body() body: CreateWorkspaceDto): Promise { @@ -34,6 +45,7 @@ export class WorkspaceController { } @Patch(':id') + @PermissionsDec(PermissionEnum.WORKSPACE) @ApiOperation({ summary: 'Update a workspace' }) @ApiParam({ name: 'id', description: 'Workspace ID' }) @ApiResponse({ status: 200, description: 'Workspace updated', type: TMWorkspace }) @@ -43,6 +55,7 @@ export class WorkspaceController { } @Delete(':id') + @PermissionsDec(PermissionEnum.WORKSPACE) @ApiOperation({ summary: 'Delete a workspace' }) @ApiParam({ name: 'id', description: 'Workspace ID' }) @ApiResponse({ status: 200, description: 'Workspace deleted' }) @@ -50,4 +63,12 @@ export class WorkspaceController { remove(@Param('id') id: string): Promise { return this.workspaceService.remove(id); } + + @Get('/byuser') + @ApiOperation({summary: 'Get User WorkSpaces'}) + @ApiResponse({ status: 200, description: 'Got Workspaces successfully!' }) + @ApiResponse({ status: 404, description: 'Bad Request!' }) + findByUser(@UserDec('id') userId: string) { + return this.workspaceService.findByUser(userId); + } } \ No newline at end of file diff --git a/src/modules/task-manager/entities/workspace.entity.ts b/src/modules/task-manager/entities/workspace.entity.ts index 417c1fd..c2feb0c 100644 --- a/src/modules/task-manager/entities/workspace.entity.ts +++ b/src/modules/task-manager/entities/workspace.entity.ts @@ -5,7 +5,7 @@ import { User } from "../../users/entities/user.entity"; @Entity("tm_workspaces") export class TMWorkspace extends BaseEntity { - @Column({ type: "varchar", length: 100 }) + @Column({ type: "varchar", length: 100, unique: true }) name: string; @Column({ type: "text", nullable: true }) diff --git a/src/modules/task-manager/providers/workspace.service.ts b/src/modules/task-manager/providers/workspace.service.ts index 5e90079..8ea8257 100644 --- a/src/modules/task-manager/providers/workspace.service.ts +++ b/src/modules/task-manager/providers/workspace.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException } from "@nestjs/common"; +import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { In, Repository } from "typeorm"; import { WorkspaceRepository } from "../repositories/workspace.repository"; @@ -6,6 +6,7 @@ import { TMWorkspace } from "../entities/workspace.entity"; import { CreateWorkspaceDto } from "../dto/workspace/create-workspace.dto"; import { UpdateWorkspaceDto } from "../dto/workspace/update-workspace.dto"; import { User } from "../../users/entities/user.entity"; +import { WorkSpaceMessage } from "../../../common/enums/message.enum"; @Injectable() export class WorkspaceService { @@ -16,11 +17,13 @@ export class WorkspaceService { ) {} findAll(): Promise { - return this.workspaceRepository.findAll(); + return this.workspaceRepository.find({ + relations: ["projects", "users"], + }); } async findOneOrFail(id: string): Promise { - const workspace = await this.workspaceRepository.findOneById(id); + const workspace = await this.workspaceRepository.findOne({ where: { id } }); if (!workspace) throw new NotFoundException(`Workspace #${id} not found`); return workspace; } @@ -28,6 +31,11 @@ export class WorkspaceService { async create(dto: CreateWorkspaceDto): Promise { const { userIds, ...rest } = dto; + const existingWorkspace = await this.workspaceRepository.findOne({where: {name: rest.name}}); + if(existingWorkspace) { + throw new BadRequestException(WorkSpaceMessage.WORKSPACE_EXISTS); + } + const workspace = this.workspaceRepository.create(rest); if (userIds?.length) { @@ -56,4 +64,8 @@ export class WorkspaceService { return workspace; } + + async findByUser(userId: string): Promise { + return await this.workspaceRepository.findByUser(userId); + } } diff --git a/src/modules/task-manager/repositories/workspace.repository.ts b/src/modules/task-manager/repositories/workspace.repository.ts index 175678a..aa8eb3f 100644 --- a/src/modules/task-manager/repositories/workspace.repository.ts +++ b/src/modules/task-manager/repositories/workspace.repository.ts @@ -4,38 +4,15 @@ import { Repository } from "typeorm"; import { TMWorkspace } from "../entities/workspace.entity"; @Injectable() -export class WorkspaceRepository { - constructor( - @InjectRepository(TMWorkspace) - private readonly repository: Repository, - ) {} - - findAll() { - return this.repository.find({ - relations: ["projects", "users"], - }); +export class WorkspaceRepository extends Repository { + constructor(@InjectRepository(TMWorkspace) workspaceRepository: Repository) { + super(workspaceRepository.target, workspaceRepository.manager, workspaceRepository.queryRunner); } - findOneById(id: string) { - return this.repository.findOne({ - where: { id }, - relations: ["projects", "users"], - }); - } - - create(data: Partial) { - return this.repository.create(data); - } - - save(workspace: TMWorkspace) { - return this.repository.save(workspace); - } - - update(id: string, data: Partial) { - return this.repository.update(id, data); - } - - delete(id: string) { - return this.repository.delete(id); + async findByUser(userId: string): Promise { + return this.createQueryBuilder("workspace") + .innerJoin("workspace.users", "users") + .where("user.id = :userId", { userId }) + .getMany(); } } diff --git a/src/modules/users/enums/permission.enum.ts b/src/modules/users/enums/permission.enum.ts index 5a42f37..fd8f92a 100755 --- a/src/modules/users/enums/permission.enum.ts +++ b/src/modules/users/enums/permission.enum.ts @@ -24,5 +24,6 @@ export enum PermissionEnum { DKALA = "dkala", DPAGE = "dpage", DMAIL = 'dmail', - RESELLER="reseller" + RESELLER="reseller", + WORKSPACE="workspace" } From d4bd593f5b0c6154239e5302e9c66ff473c82fc7 Mon Sep 17 00:00:00 2001 From: realrafi Date: Sun, 5 Jul 2026 14:17:24 +0330 Subject: [PATCH 38/66] fix: fixing some errors in workspace and seeding --- database/1783244654421-migrations.ts | 11 +++++++++++ database/migrations/1783244669371-taskmanager3.ts | 14 ++++++++++++++ database/seeder.runner.ts | 12 ++++++------ database/seeders/role.seeder.ts | 8 ++++---- .../repositories/workspace.repository.ts | 2 +- 5 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 database/1783244654421-migrations.ts create mode 100644 database/migrations/1783244669371-taskmanager3.ts diff --git a/database/1783244654421-migrations.ts b/database/1783244654421-migrations.ts new file mode 100644 index 0000000..ffb3d29 --- /dev/null +++ b/database/1783244654421-migrations.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Migrations1783244654421 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + } + + public async down(queryRunner: QueryRunner): Promise { + } + +} diff --git a/database/migrations/1783244669371-taskmanager3.ts b/database/migrations/1783244669371-taskmanager3.ts new file mode 100644 index 0000000..6cce1f1 --- /dev/null +++ b/database/migrations/1783244669371-taskmanager3.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Taskmanager31783244669371 implements MigrationInterface { + name = 'Taskmanager31783244669371' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tm_workspaces" ADD CONSTRAINT "UQ_6496168de3e6c5f006d494ebac5" UNIQUE ("name")`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tm_workspaces" DROP CONSTRAINT "UQ_6496168de3e6c5f006d494ebac5"`); + } + +} diff --git a/database/seeder.runner.ts b/database/seeder.runner.ts index 16446f6..20d9d81 100755 --- a/database/seeder.runner.ts +++ b/database/seeder.runner.ts @@ -5,9 +5,9 @@ import { connectionSource } from "./connection"; // import { seedCityAndProvince } from "./seeders/iran-city.seeder"; // import { seedNotifSettings } from "./seeders/notif-setting.seeder"; // import { seedPaymentGateways } from "./seeders/payment-gateway.seeder"; -// import { seedPermissionsAndRoles } from "./seeders/role.seeder"; -import { seedBlogCommentReplies } from "./seeders/blog-comment-reply.seeder"; -import { seedBlogComments } from "./seeders/blog-comment.seeder"; +import { seedPermissionsAndRoles } from "./seeders/role.seeder"; +// import { seedBlogCommentReplies } from "./seeders/blog-comment-reply.seeder"; +// import { seedBlogComments } from "./seeders/blog-comment.seeder"; const logger = new Logger("Seeder"); @@ -15,14 +15,14 @@ export const runSeeder = async () => { await connectionSource.initialize(); logger.log("start seeding database"); // - // await seedPermissionsAndRoles(connectionSource, logger); + await seedPermissionsAndRoles(connectionSource, logger); // await seedAdmin(connectionSource, logger); // await seedNotifSettings(connectionSource, logger); // await seedPaymentGateways(connectionSource, logger); // await seedCityAndProvince(connectionSource, logger); - await seedBlogComments(connectionSource, logger); - await seedBlogCommentReplies(connectionSource, logger); + // await seedBlogComments(connectionSource, logger); + // await seedBlogCommentReplies(connectionSource, logger); logger.log("seeding completed"); process.exit(0); diff --git a/database/seeders/role.seeder.ts b/database/seeders/role.seeder.ts index f731d12..747354b 100755 --- a/database/seeders/role.seeder.ts +++ b/database/seeders/role.seeder.ts @@ -1,8 +1,8 @@ import { Logger } from "@nestjs/common"; import { DataSource } from "typeorm"; -import { Permission } from "../../src/modules/users/entities/permission.entity"; -import { Role } from "../../src/modules/users/entities/role.entity"; +// import { Permission } from "../../src/modules/users/entities/permission.entity"; +// import { Role } from "../../src/modules/users/entities/role.entity"; import { PermissionEnum } from "../../src/modules/users/enums/permission.enum"; import { RoleEnum } from "../../src/modules/users/enums/role.enum"; @@ -23,8 +23,8 @@ const rolesWithPermissions: { name: RoleEnum; permissions: PermissionEnum[] }[] export const seedPermissionsAndRoles = async (dataSource: DataSource, logger: Logger) => { try { - const permissionRepo = dataSource.getRepository(Permission); - const roleRepo = dataSource.getRepository(Role); + const permissionRepo = dataSource.getRepository('Permission'); + const roleRepo = dataSource.getRepository('Role'); // Seed Permissions logger.log("Seeding Permissions..."); diff --git a/src/modules/task-manager/repositories/workspace.repository.ts b/src/modules/task-manager/repositories/workspace.repository.ts index aa8eb3f..df73f29 100644 --- a/src/modules/task-manager/repositories/workspace.repository.ts +++ b/src/modules/task-manager/repositories/workspace.repository.ts @@ -12,7 +12,7 @@ export class WorkspaceRepository extends Repository { async findByUser(userId: string): Promise { return this.createQueryBuilder("workspace") .innerJoin("workspace.users", "users") - .where("user.id = :userId", { userId }) + .where("users.id = :userId", { userId }) .getMany(); } } From 35d0d9f7cda241620bde41bc1b39f5376db9fd3e Mon Sep 17 00:00:00 2001 From: realrafi Date: Sun, 5 Jul 2026 14:23:09 +0330 Subject: [PATCH 39/66] refactor: refactoring the project repository --- .../repositories/project.repository.ts | 57 +++---------------- 1 file changed, 9 insertions(+), 48 deletions(-) diff --git a/src/modules/task-manager/repositories/project.repository.ts b/src/modules/task-manager/repositories/project.repository.ts index 6b0535a..8f11e27 100644 --- a/src/modules/task-manager/repositories/project.repository.ts +++ b/src/modules/task-manager/repositories/project.repository.ts @@ -1,52 +1,13 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { TMProject } from '../entities/project.entity'; +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; +import { TMProject } from "../entities/project.entity"; @Injectable() -export class ProjectRepository { +export class ProjectRepository extends Repository { constructor( - @InjectRepository(TMProject) - private readonly repository: Repository, - ) {} - - findAll(skip: number, take: number) { - return this.repository.findAndCount({ - relations: ['workspace', 'taskPhases', 'users'], - skip, - take, - }); + @InjectRepository(TMProject) projectRepository: Repository, + ) { + super(projectRepository.target, projectRepository.manager, projectRepository.queryRunner); } - - findByWorkspace(workspaceId: string, skip: number, take: number) { - return this.repository.findAndCount({ - where: { workspaceId }, - relations: ['taskPhases', 'users'], - skip, - take, - }); - } - - findOneById(id: string) { - return this.repository.findOne({ - where: { id }, - relations: ['workspace', 'taskPhases', 'users'], - }); - } - - create(data: Partial) { - return this.repository.create(data); - } - - save(project: TMProject) { - return this.repository.save(project); - } - - update(id: string, data: Partial) { - return this.repository.update(id, data); - } - - delete(id: string) { - return this.repository.delete(id); - } -} \ No newline at end of file +} From 5763cb7d6c0e1a917fa0d383071ee8c2732425a9 Mon Sep 17 00:00:00 2001 From: realrafi Date: Sun, 5 Jul 2026 14:43:05 +0330 Subject: [PATCH 40/66] refactor: refactoring some project service functions --- .../task-manager/providers/project.service.ts | 48 ++++++++----------- .../repositories/project.repository.ts | 12 ++++- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/modules/task-manager/providers/project.service.ts b/src/modules/task-manager/providers/project.service.ts index 771916c..c0b60d1 100644 --- a/src/modules/task-manager/providers/project.service.ts +++ b/src/modules/task-manager/providers/project.service.ts @@ -1,15 +1,15 @@ -import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { In, Repository } from 'typeorm'; -import { ProjectRepository } from '../repositories/project.repository'; -import { WorkspaceRepository } from '../repositories/workspace.repository'; -import { TMProject } from '../entities/project.entity'; -import { CreateProjectDto } from '../dto/project/create-project.dto'; -import { UpdateProjectDto } from '../dto/project/update-project.dto'; -import { PaginationDto } from '../../../common/DTO/pagination.dto'; -import { PaginatedResult } from '../../../common/interfaces/paginated-result.interface'; -import { buildPageFormat } from '../../../common/helpers/pagination.helper'; -import { User } from '../../users/entities/user.entity'; +import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { In, Repository } from "typeorm"; +import { ProjectRepository } from "../repositories/project.repository"; +import { WorkspaceRepository } from "../repositories/workspace.repository"; +import { TMProject } from "../entities/project.entity"; +import { CreateProjectDto } from "../dto/project/create-project.dto"; +import { UpdateProjectDto } from "../dto/project/update-project.dto"; +import { PaginationDto } from "../../../common/DTO/pagination.dto"; +import { PaginatedResult } from "../../../common/interfaces/paginated-result.interface"; +import { buildPageFormat } from "../../../common/helpers/pagination.helper"; +import { User } from "../../users/entities/user.entity"; @Injectable() export class ProjectService { @@ -25,7 +25,7 @@ export class ProjectService { const limit = pagination.limit ?? 10; const skip = (page - 1) * limit; - const [data, totalItems] = await this.projectRepository.findAll(skip, limit); + const [data, totalItems] = await this.projectRepository.findAndCount({ skip, take: limit }); return { data, @@ -33,11 +33,7 @@ export class ProjectService { }; } - async findByWorkspace( - workspaceId: string, - pagination: PaginationDto, - baseUrl: string, - ): Promise> { + async findByWorkspace(workspaceId: string, pagination: PaginationDto, baseUrl: string): Promise> { const page = pagination.page ?? 1; const limit = pagination.limit ?? 10; const skip = (page - 1) * limit; @@ -51,7 +47,7 @@ export class ProjectService { } async findOne(id: string): Promise { - const project = await this.projectRepository.findOneById(id); + const project = await this.projectRepository.findOne({ where: { id } }); if (!project) throw new NotFoundException(`Project #${id} not found`); return project; } @@ -59,7 +55,7 @@ export class ProjectService { async create(dto: CreateProjectDto): Promise { const { userIds, startDate, ...rest } = dto; - const workspace = await this.workspaceRepository.findOneById(dto.workspaceId); + const workspace = await this.workspaceRepository.findOne({ where: { id: dto.workspaceId } }); if (!workspace) throw new NotFoundException(`Workspace #${dto.workspaceId} not found`); const project = this.projectRepository.create({ @@ -71,9 +67,7 @@ export class ProjectService { const workspaceUserIds = workspace.users.map((u) => u.id); const invalidUsers = userIds.filter((id) => !workspaceUserIds.includes(id)); if (invalidUsers.length) { - throw new BadRequestException( - `Users [${invalidUsers.join(', ')}] are not members of this workspace`, - ); + throw new BadRequestException(`Users [${invalidUsers.join(", ")}] are not members of this workspace`); } project.users = await this.userRepository.findBy({ id: In(userIds) }); } @@ -93,15 +87,13 @@ export class ProjectService { if (userIds) { if (userIds.length) { const workspaceId = dto.workspaceId ?? project.workspaceId; - const workspace = await this.workspaceRepository.findOneById(workspaceId); + const workspace = await this.workspaceRepository.findOne({ where: { id: workspaceId } }); if (!workspace) throw new NotFoundException(`Workspace #${workspaceId} not found`); const workspaceUserIds = workspace.users.map((u) => u.id); const invalidUsers = userIds.filter((uid) => !workspaceUserIds.includes(uid)); if (invalidUsers.length) { - throw new BadRequestException( - `Users [${invalidUsers.join(', ')}] are not members of this workspace`, - ); + throw new BadRequestException(`Users [${invalidUsers.join(", ")}] are not members of this workspace`); } project.users = await this.userRepository.findBy({ id: In(userIds) }); } else { @@ -117,4 +109,4 @@ export class ProjectService { await this.projectRepository.delete(id); return project; } -} \ No newline at end of file +} diff --git a/src/modules/task-manager/repositories/project.repository.ts b/src/modules/task-manager/repositories/project.repository.ts index 8f11e27..5e90209 100644 --- a/src/modules/task-manager/repositories/project.repository.ts +++ b/src/modules/task-manager/repositories/project.repository.ts @@ -6,8 +6,18 @@ import { TMProject } from "../entities/project.entity"; @Injectable() export class ProjectRepository extends Repository { constructor( - @InjectRepository(TMProject) projectRepository: Repository, + @InjectRepository(TMProject) + private readonly projectRepository: Repository, ) { super(projectRepository.target, projectRepository.manager, projectRepository.queryRunner); } + + findByWorkspace(workspaceId: string, skip: number, take: number) { + return this.projectRepository.findAndCount({ + where: { workspaceId }, + relations: ["taskPhases", "users"], + skip, + take, + }); + } } From 47a5a2abb490083b105fe6c7a6ca52a38954f197 Mon Sep 17 00:00:00 2001 From: realrafi Date: Sun, 5 Jul 2026 16:22:36 +0330 Subject: [PATCH 41/66] refactor: refactored project --- src/common/enums/message.enum.ts | 9 ++++- .../controllers/project.controller.ts | 20 ++++++++++- .../task-manager/providers/project.service.ts | 33 +++++++++++-------- .../repositories/project.repository.ts | 12 +------ src/modules/users/enums/permission.enum.ts | 3 +- 5 files changed, 50 insertions(+), 27 deletions(-) diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index c9f2f19..f6c39f5 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -917,5 +917,12 @@ export const enum AccessLogMessage { } export const enum WorkSpaceMessage { - WORKSPACE_EXISTS = "فضای کار با این اسم وجود دارد!" + WORKSPACE_EXISTS = "فضای کار با این اسم وجود دارد!", + WORKSPACE_NOT_FOUND = "فضای کار مورد نظر شما وجود ندارد!", + USERS_DONT_BELONG_TO_THIS_WORKSPACE = "کاربران تعیین شده برای این فضای کاری نیستند!" +} + +export const enum ProjectMessage { + PROJECT_EXISTS = "پروژه مورد نظر شما از قبل وجود دارد!", + PROJECT_NOT_FOUND = "پروژه مورد نظر شما وجود ندارد!" } \ No newline at end of file diff --git a/src/modules/task-manager/controllers/project.controller.ts b/src/modules/task-manager/controllers/project.controller.ts index 249858c..27f3788 100644 --- a/src/modules/task-manager/controllers/project.controller.ts +++ b/src/modules/task-manager/controllers/project.controller.ts @@ -5,13 +5,18 @@ import { CreateProjectDto } from "../dto/project/create-project.dto"; import { UpdateProjectDto } from "../dto/project/update-project.dto"; import { PaginationDto } from "../../../common/DTO/pagination.dto"; import { TMProject } from "../entities/project.entity"; +import { AdminRoute } from "../../../common/decorators/admin.decorator"; +import { PermissionsDec } from "../../../common/decorators/permission.decorator"; +import { PermissionEnum } from "../../users/enums/permission.enum"; +@AdminRoute() @ApiTags("Task Manager - Projects") @Controller("task-manager/projects") export class ProjectController { constructor(private readonly projectService: ProjectService) {} @Get() + @PermissionsDec(PermissionEnum.PROJECT) @ApiOperation({ summary: "Get all projects, optionally filtered by workspace" }) @ApiQuery({ name: "workspaceId", required: false, description: "Filter projects by workspace ID" }) @ApiResponse({ status: 200, description: "Paginated list of projects" }) @@ -28,15 +33,17 @@ export class ProjectController { } @Get(":id") + @PermissionsDec(PermissionEnum.PROJECT) @ApiOperation({ summary: "Get a project by ID" }) @ApiParam({ name: "id", description: "Project ID" }) @ApiResponse({ status: 200, description: "Project found", type: TMProject }) @ApiResponse({ status: 404, description: "Project not found" }) findOne(@Param("id") id: string): Promise { - return this.projectService.findOne(id); + return this.projectService.findOneOrFail(id); } @Post() + @PermissionsDec(PermissionEnum.PROJECT) @ApiOperation({ summary: "Create a new project" }) @ApiResponse({ status: 201, description: "Project created", type: TMProject }) @ApiResponse({ status: 400, description: "One or more users are not members of the workspace" }) @@ -46,6 +53,7 @@ export class ProjectController { } @Patch(":id") + @PermissionsDec(PermissionEnum.PROJECT) @ApiOperation({ summary: "Update a project" }) @ApiParam({ name: "id", description: "Project ID" }) @ApiResponse({ status: 200, description: "Project updated", type: TMProject }) @@ -56,6 +64,7 @@ export class ProjectController { } @Delete(":id") + @PermissionsDec(PermissionEnum.PROJECT) @ApiOperation({ summary: "Delete a project" }) @ApiParam({ name: "id", description: "Project ID" }) @ApiResponse({ status: 200, description: "Project deleted", type: TMProject }) @@ -63,4 +72,13 @@ export class ProjectController { remove(@Param("id") id: string): Promise { return this.projectService.remove(id); } + + @Get("/workspace/:wsId") + @ApiOperation({summary: "Get projects of a specific workspace"}) + @ApiParam({ name: "wsId", description: "Workspace ID" }) + @ApiResponse({ status: 200, description: "received projects successfully!"}) + @ApiResponse({ status: 404, description: "WorkSpace not found" }) + findByWorkspace(@Param("wsId") wsId: string): Promise { + return this.findByWorkspace(wsId); + } } diff --git a/src/modules/task-manager/providers/project.service.ts b/src/modules/task-manager/providers/project.service.ts index c0b60d1..72cdb7b 100644 --- a/src/modules/task-manager/providers/project.service.ts +++ b/src/modules/task-manager/providers/project.service.ts @@ -2,7 +2,6 @@ import { BadRequestException, Injectable, NotFoundException } from "@nestjs/comm import { InjectRepository } from "@nestjs/typeorm"; import { In, Repository } from "typeorm"; import { ProjectRepository } from "../repositories/project.repository"; -import { WorkspaceRepository } from "../repositories/workspace.repository"; import { TMProject } from "../entities/project.entity"; import { CreateProjectDto } from "../dto/project/create-project.dto"; import { UpdateProjectDto } from "../dto/project/update-project.dto"; @@ -10,12 +9,14 @@ import { PaginationDto } from "../../../common/DTO/pagination.dto"; import { PaginatedResult } from "../../../common/interfaces/paginated-result.interface"; import { buildPageFormat } from "../../../common/helpers/pagination.helper"; import { User } from "../../users/entities/user.entity"; +import { ProjectMessage, WorkSpaceMessage } from "../../../common/enums/message.enum"; +import { WorkspaceService } from "./workspace.service"; @Injectable() export class ProjectService { constructor( private readonly projectRepository: ProjectRepository, - private readonly workspaceRepository: WorkspaceRepository, + private readonly workspaceService: WorkspaceService, @InjectRepository(User) private readonly userRepository: Repository, ) {} @@ -34,11 +35,18 @@ export class ProjectService { } async findByWorkspace(workspaceId: string, pagination: PaginationDto, baseUrl: string): Promise> { + await this.workspaceService.findOneOrFail(workspaceId); + const page = pagination.page ?? 1; const limit = pagination.limit ?? 10; const skip = (page - 1) * limit; - const [data, totalItems] = await this.projectRepository.findByWorkspace(workspaceId, skip, limit); + const [data, totalItems] = await this.projectRepository.findAndCount({ + where: { workspaceId }, + relations: ["taskPhases", "users"], + skip, + take: limit, + }); return { data, @@ -46,17 +54,16 @@ export class ProjectService { }; } - async findOne(id: string): Promise { + async findOneOrFail(id: string): Promise { const project = await this.projectRepository.findOne({ where: { id } }); - if (!project) throw new NotFoundException(`Project #${id} not found`); + if (!project) throw new NotFoundException(ProjectMessage.PROJECT_NOT_FOUND); return project; } async create(dto: CreateProjectDto): Promise { const { userIds, startDate, ...rest } = dto; - const workspace = await this.workspaceRepository.findOne({ where: { id: dto.workspaceId } }); - if (!workspace) throw new NotFoundException(`Workspace #${dto.workspaceId} not found`); + const workspace = await this.workspaceService.findOneOrFail(dto.workspaceId); const project = this.projectRepository.create({ ...rest, @@ -67,7 +74,7 @@ export class ProjectService { const workspaceUserIds = workspace.users.map((u) => u.id); const invalidUsers = userIds.filter((id) => !workspaceUserIds.includes(id)); if (invalidUsers.length) { - throw new BadRequestException(`Users [${invalidUsers.join(", ")}] are not members of this workspace`); + throw new BadRequestException(WorkSpaceMessage.USERS_DONT_BELONG_TO_THIS_WORKSPACE); } project.users = await this.userRepository.findBy({ id: In(userIds) }); } @@ -76,7 +83,8 @@ export class ProjectService { } async update(id: string, dto: UpdateProjectDto): Promise { - const project = await this.findOne(id); + const project = await this.findOneOrFail(id); + const { userIds, startDate, ...rest } = dto; Object.assign(project, { @@ -87,13 +95,12 @@ export class ProjectService { if (userIds) { if (userIds.length) { const workspaceId = dto.workspaceId ?? project.workspaceId; - const workspace = await this.workspaceRepository.findOne({ where: { id: workspaceId } }); - if (!workspace) throw new NotFoundException(`Workspace #${workspaceId} not found`); + const workspace = await this.workspaceService.findOneOrFail(workspaceId); const workspaceUserIds = workspace.users.map((u) => u.id); const invalidUsers = userIds.filter((uid) => !workspaceUserIds.includes(uid)); if (invalidUsers.length) { - throw new BadRequestException(`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) }); } else { @@ -105,7 +112,7 @@ export class ProjectService { } async remove(id: string): Promise { - const project = await this.findOne(id); + const project = await this.findOneOrFail(id); await this.projectRepository.delete(id); return project; } diff --git a/src/modules/task-manager/repositories/project.repository.ts b/src/modules/task-manager/repositories/project.repository.ts index 5e90209..8f11e27 100644 --- a/src/modules/task-manager/repositories/project.repository.ts +++ b/src/modules/task-manager/repositories/project.repository.ts @@ -6,18 +6,8 @@ import { TMProject } from "../entities/project.entity"; @Injectable() export class ProjectRepository extends Repository { constructor( - @InjectRepository(TMProject) - private readonly projectRepository: Repository, + @InjectRepository(TMProject) projectRepository: Repository, ) { super(projectRepository.target, projectRepository.manager, projectRepository.queryRunner); } - - findByWorkspace(workspaceId: string, skip: number, take: number) { - return this.projectRepository.findAndCount({ - where: { workspaceId }, - relations: ["taskPhases", "users"], - skip, - take, - }); - } } diff --git a/src/modules/users/enums/permission.enum.ts b/src/modules/users/enums/permission.enum.ts index fd8f92a..1748c29 100755 --- a/src/modules/users/enums/permission.enum.ts +++ b/src/modules/users/enums/permission.enum.ts @@ -25,5 +25,6 @@ export enum PermissionEnum { DPAGE = "dpage", DMAIL = 'dmail', RESELLER="reseller", - WORKSPACE="workspace" + WORKSPACE="workspace", + PROJECT="project" } From a14bd490be4d656f07261091df1ae706dda34131 Mon Sep 17 00:00:00 2001 From: realrafi Date: Sun, 5 Jul 2026 17:12:30 +0330 Subject: [PATCH 42/66] fix: fixed the update func in workspace service to include same name checking --- src/common/enums/message.enum.ts | 3 ++- .../providers/workspace.service.ts | 19 +++++++++++-------- .../repositories/workspace.repository.ts | 16 ++++++++++++++-- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index f6c39f5..8759cba 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -919,7 +919,8 @@ export const enum AccessLogMessage { export const enum WorkSpaceMessage { WORKSPACE_EXISTS = "فضای کار با این اسم وجود دارد!", WORKSPACE_NOT_FOUND = "فضای کار مورد نظر شما وجود ندارد!", - USERS_DONT_BELONG_TO_THIS_WORKSPACE = "کاربران تعیین شده برای این فضای کاری نیستند!" + USERS_DONT_BELONG_TO_THIS_WORKSPACE = "کاربران تعیین شده برای این فضای کاری نیستند!", + WORKSPACE_SAME_NAME_EXISTS = "فضای کاری با این اسم از قبل وجود دارد!" } export const enum ProjectMessage { diff --git a/src/modules/task-manager/providers/workspace.service.ts b/src/modules/task-manager/providers/workspace.service.ts index 8ea8257..73940fc 100644 --- a/src/modules/task-manager/providers/workspace.service.ts +++ b/src/modules/task-manager/providers/workspace.service.ts @@ -1,6 +1,6 @@ import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; -import { In, Repository } from "typeorm"; +import { In, Not, Repository } from "typeorm"; import { WorkspaceRepository } from "../repositories/workspace.repository"; import { TMWorkspace } from "../entities/workspace.entity"; import { CreateWorkspaceDto } from "../dto/workspace/create-workspace.dto"; @@ -17,22 +17,20 @@ export class WorkspaceService { ) {} findAll(): Promise { - return this.workspaceRepository.find({ - relations: ["projects", "users"], - }); + return this.workspaceRepository.find({}); } async findOneOrFail(id: string): Promise { - const workspace = await this.workspaceRepository.findOne({ where: { id } }); - if (!workspace) throw new NotFoundException(`Workspace #${id} not found`); + const workspace = await this.workspaceRepository.findOneWorkSpace(id); + if (!workspace) throw new NotFoundException(WorkSpaceMessage.WORKSPACE_NOT_FOUND); return workspace; } async create(dto: CreateWorkspaceDto): Promise { const { userIds, ...rest } = dto; - const existingWorkspace = await this.workspaceRepository.findOne({where: {name: rest.name}}); - if(existingWorkspace) { + const existingWorkspace = await this.workspaceRepository.findOne({ where: { name: rest.name } }); + if (existingWorkspace) { throw new BadRequestException(WorkSpaceMessage.WORKSPACE_EXISTS); } @@ -49,6 +47,11 @@ export class WorkspaceService { const workspace = await this.findOneOrFail(id); const { userIds, ...rest } = dto; + if (rest.name) { + const existingWorkSpaceWithTheSameName = await this.workspaceRepository.findOne({ where: { name: rest.name, id: Not(id) } }); + if (existingWorkSpaceWithTheSameName) throw new BadRequestException(WorkSpaceMessage.WORKSPACE_SAME_NAME_EXISTS); + } + Object.assign(workspace, rest); if (userIds) { diff --git a/src/modules/task-manager/repositories/workspace.repository.ts b/src/modules/task-manager/repositories/workspace.repository.ts index df73f29..04b4697 100644 --- a/src/modules/task-manager/repositories/workspace.repository.ts +++ b/src/modules/task-manager/repositories/workspace.repository.ts @@ -1,7 +1,7 @@ import { Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; -import { Repository } from "typeorm"; -import { TMWorkspace } from "../entities/workspace.entity"; +import { Repository } from 'typeorm'; +import { TMWorkspace } from '../entities/workspace.entity'; @Injectable() export class WorkspaceRepository extends Repository { @@ -9,6 +9,18 @@ export class WorkspaceRepository extends Repository { super(workspaceRepository.target, workspaceRepository.manager, workspaceRepository.queryRunner); } + async findOneWorkSpace(workspaceId: string): Promise { + return this.createQueryBuilder("workspace") + .leftJoinAndSelect('workspace.users', 'user') + .select([ + 'workspace', + 'user.id', + 'user.firstName', + 'user.lastName' + ]).where('workspace.id = :workspaceId', {workspaceId}) + .getOne(); + } + async findByUser(userId: string): Promise { return this.createQueryBuilder("workspace") .innerJoin("workspace.users", "users") From 9f61152d94fb003e44b9d1097209827e92f8197e Mon Sep 17 00:00:00 2001 From: realrafi Date: Sun, 5 Jul 2026 19:20:08 +0330 Subject: [PATCH 43/66] refactor: changed functions to use pagination logic correctly --- src/common/enums/message.enum.ts | 3 +- src/common/helpers/pagination.helper.ts | 29 --------- .../interfaces/paginated-result.interface.ts | 6 -- .../controllers/project.controller.ts | 29 ++++----- .../task-manager/providers/project.service.ts | 65 ++++++++++++++----- 5 files changed, 63 insertions(+), 69 deletions(-) delete mode 100644 src/common/helpers/pagination.helper.ts delete mode 100644 src/common/interfaces/paginated-result.interface.ts diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 8759cba..cb1ac06 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -925,5 +925,6 @@ export const enum WorkSpaceMessage { export const enum ProjectMessage { PROJECT_EXISTS = "پروژه مورد نظر شما از قبل وجود دارد!", - PROJECT_NOT_FOUND = "پروژه مورد نظر شما وجود ندارد!" + PROJECT_NOT_FOUND = "پروژه مورد نظر شما وجود ندارد!", + PROJECT_CONTAINS_TASK_PHASES = "پروژه شامل فاز تسک است. لطفا ابتدا فاز تسک های این پروژه را حذف کنید." } \ No newline at end of file diff --git a/src/common/helpers/pagination.helper.ts b/src/common/helpers/pagination.helper.ts deleted file mode 100644 index df195a4..0000000 --- a/src/common/helpers/pagination.helper.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { IPageFormat } from "../interfaces/IPagination"; - -export function buildPageFormat( - page: number, - limit: number, - totalItems: number, - baseUrl: string, -): IPageFormat { - const totalPages = Math.ceil(totalItems / limit); - - const prevPage = - page > 1 - ? `${baseUrl}?page=${page - 1}&limit=${limit}` - : false; - - const nextPage = - page < totalPages - ? `${baseUrl}?page=${page + 1}&limit=${limit}` - : false; - - return { - page, - limit, - totalItems, - totalPages, - prevPage, - nextPage, - }; -} \ No newline at end of file diff --git a/src/common/interfaces/paginated-result.interface.ts b/src/common/interfaces/paginated-result.interface.ts deleted file mode 100644 index b9247fb..0000000 --- a/src/common/interfaces/paginated-result.interface.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { IPageFormat } from "./IPagination"; - -export interface PaginatedResult { - data: T[]; - meta: IPageFormat; -} \ No newline at end of file diff --git a/src/modules/task-manager/controllers/project.controller.ts b/src/modules/task-manager/controllers/project.controller.ts index 27f3788..2c0d43e 100644 --- a/src/modules/task-manager/controllers/project.controller.ts +++ b/src/modules/task-manager/controllers/project.controller.ts @@ -1,5 +1,5 @@ -import { Controller, Get, Post, Patch, Delete, Param, Body, Query, Headers } from "@nestjs/common"; -import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from "@nestjs/swagger"; +import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from "@nestjs/common"; +import { ApiTags, ApiOperation, ApiResponse, ApiParam } from "@nestjs/swagger"; import { ProjectService } from "../providers/project.service"; import { CreateProjectDto } from "../dto/project/create-project.dto"; import { UpdateProjectDto } from "../dto/project/update-project.dto"; @@ -18,18 +18,9 @@ export class ProjectController { @Get() @PermissionsDec(PermissionEnum.PROJECT) @ApiOperation({ summary: "Get all projects, optionally filtered by workspace" }) - @ApiQuery({ name: "workspaceId", required: false, description: "Filter projects by workspace ID" }) @ApiResponse({ status: 200, description: "Paginated list of projects" }) - findAll( - @Query() pagination: PaginationDto, - @Query("workspaceId") workspaceId: string, - @Headers("host") host: string, - @Headers("x-forwarded-proto") proto?: string, - ) { - const protocol = proto ?? "http"; - const baseUrl = `${protocol}://${host}/task-manager/projects`; - if (workspaceId) return this.projectService.findByWorkspace(workspaceId, pagination, baseUrl); - return this.projectService.findAll(pagination, baseUrl); + findAll(@Query() pagination: PaginationDto): Promise<{ projects: TMProject[]; count: number; paginate: true }> { + return this.projectService.findAll(pagination); } @Get(":id") @@ -74,11 +65,15 @@ export class ProjectController { } @Get("/workspace/:wsId") - @ApiOperation({summary: "Get projects of a specific workspace"}) + @PermissionsDec(PermissionEnum.PROJECT) + @ApiOperation({ summary: "Get projects of a specific workspace" }) @ApiParam({ name: "wsId", description: "Workspace ID" }) - @ApiResponse({ status: 200, description: "received projects successfully!"}) + @ApiResponse({ status: 200, description: "received projects successfully!" }) @ApiResponse({ status: 404, description: "WorkSpace not found" }) - findByWorkspace(@Param("wsId") wsId: string): Promise { - return this.findByWorkspace(wsId); + findByWorkspace( + @Param("wsId") wsId: string, + @Query() pagination: PaginationDto, + ): Promise<{ projects: TMProject[]; count: number; paginate: true }> { + return this.projectService.findByWorkspace(wsId, pagination); } } diff --git a/src/modules/task-manager/providers/project.service.ts b/src/modules/task-manager/providers/project.service.ts index 72cdb7b..523fd15 100644 --- a/src/modules/task-manager/providers/project.service.ts +++ b/src/modules/task-manager/providers/project.service.ts @@ -6,8 +6,6 @@ import { TMProject } from "../entities/project.entity"; import { CreateProjectDto } from "../dto/project/create-project.dto"; import { UpdateProjectDto } from "../dto/project/update-project.dto"; import { PaginationDto } from "../../../common/DTO/pagination.dto"; -import { PaginatedResult } from "../../../common/interfaces/paginated-result.interface"; -import { buildPageFormat } from "../../../common/helpers/pagination.helper"; import { User } from "../../users/entities/user.entity"; import { ProjectMessage, WorkSpaceMessage } from "../../../common/enums/message.enum"; import { WorkspaceService } from "./workspace.service"; @@ -21,37 +19,53 @@ export class ProjectService { private readonly userRepository: Repository, ) {} - async findAll(pagination: PaginationDto, baseUrl: string): Promise> { + async findAll(pagination: PaginationDto): Promise<{ projects: TMProject[]; count: number; paginate: true }> { const page = pagination.page ?? 1; const limit = pagination.limit ?? 10; const skip = (page - 1) * limit; - const [data, totalItems] = await this.projectRepository.findAndCount({ skip, take: limit }); + const [projects, count] = await this.projectRepository.findAndCount({ + skip: skip, + take: limit, + relations: { + users: true, + }, + select: { + users: { + id: true, + firstName: true, + lastName: true, + }, + }, + }); - return { - data, - meta: buildPageFormat(page, limit, totalItems, baseUrl), - }; + return { projects, count, paginate: true }; } - async findByWorkspace(workspaceId: string, pagination: PaginationDto, baseUrl: string): Promise> { + async findByWorkspace(workspaceId: string, pagination: PaginationDto): Promise<{ projects: TMProject[]; count: number; paginate: true }> { await this.workspaceService.findOneOrFail(workspaceId); const page = pagination.page ?? 1; const limit = pagination.limit ?? 10; const skip = (page - 1) * limit; - const [data, totalItems] = await this.projectRepository.findAndCount({ + const [projects, count] = await this.projectRepository.findAndCount({ where: { workspaceId }, - relations: ["taskPhases", "users"], + relations: { + users: true, + }, + select: { + users: { + id: true, + firstName: true, + lastName: true, + }, + }, skip, take: limit, }); - return { - data, - meta: buildPageFormat(page, limit, totalItems, baseUrl), - }; + return { projects, count, paginate: true }; } async findOneOrFail(id: string): Promise { @@ -112,7 +126,26 @@ export class ProjectService { } async remove(id: string): Promise { - const project = await this.findOneOrFail(id); + const project = await this.projectRepository.findOne({ + where: { + id, + }, + relations: { + taskPhases: true, + }, + select: { + taskPhases: { + id: true, + }, + }, + }); + + if(!project) throw new NotFoundException(ProjectMessage.PROJECT_NOT_FOUND); + + const taskPhases = project.taskPhases; + if (taskPhases !== undefined && taskPhases.length) { + throw new BadRequestException(ProjectMessage.PROJECT_CONTAINS_TASK_PHASES); + } await this.projectRepository.delete(id); return project; } From 16a0c1414936506c061f9a55b64f516d61f07f2c Mon Sep 17 00:00:00 2001 From: realrafi Date: Mon, 6 Jul 2026 12:06:33 +0330 Subject: [PATCH 44/66] feat: added get user projects function in project --- database/1783244654421-migrations.ts | 11 ---------- .../migrations/1783150384434-taskmanager.ts | 0 ...0480192-taskmanagerRemoveWorkspaceType.ts} | 0 ...371-taskmanagerMakeWorkspaceNameUnique.ts} | 0 ...AddingNewEnumValueForPermissionEnumType.ts | 20 +++++++++++++++++++ .../controllers/project.controller.ts | 13 ++++++++++++ .../task-manager/providers/project.service.ts | 14 +++++++++++-- .../repositories/project.repository.ts | 9 +++++++++ 8 files changed, 54 insertions(+), 13 deletions(-) delete mode 100644 database/1783244654421-migrations.ts delete mode 100644 database/migrations/1783150384434-taskmanager.ts rename database/migrations/{1783150480192-taskmanager.ts => 1783150480192-taskmanagerRemoveWorkspaceType.ts} (100%) rename database/migrations/{1783244669371-taskmanager3.ts => 1783244669371-taskmanagerMakeWorkspaceNameUnique.ts} (100%) create mode 100644 database/migrations/1783324039378-taskmanagerAddingNewEnumValueForPermissionEnumType.ts diff --git a/database/1783244654421-migrations.ts b/database/1783244654421-migrations.ts deleted file mode 100644 index ffb3d29..0000000 --- a/database/1783244654421-migrations.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class Migrations1783244654421 implements MigrationInterface { - - public async up(queryRunner: QueryRunner): Promise { - } - - public async down(queryRunner: QueryRunner): Promise { - } - -} diff --git a/database/migrations/1783150384434-taskmanager.ts b/database/migrations/1783150384434-taskmanager.ts deleted file mode 100644 index e69de29..0000000 diff --git a/database/migrations/1783150480192-taskmanager.ts b/database/migrations/1783150480192-taskmanagerRemoveWorkspaceType.ts similarity index 100% rename from database/migrations/1783150480192-taskmanager.ts rename to database/migrations/1783150480192-taskmanagerRemoveWorkspaceType.ts diff --git a/database/migrations/1783244669371-taskmanager3.ts b/database/migrations/1783244669371-taskmanagerMakeWorkspaceNameUnique.ts similarity index 100% rename from database/migrations/1783244669371-taskmanager3.ts rename to database/migrations/1783244669371-taskmanagerMakeWorkspaceNameUnique.ts diff --git a/database/migrations/1783324039378-taskmanagerAddingNewEnumValueForPermissionEnumType.ts b/database/migrations/1783324039378-taskmanagerAddingNewEnumValueForPermissionEnumType.ts new file mode 100644 index 0000000..4f0e81a --- /dev/null +++ b/database/migrations/1783324039378-taskmanagerAddingNewEnumValueForPermissionEnumType.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class TaskmanagerAddingNewEnumValueForPermissionEnumType1783324039378 implements MigrationInterface { + name = 'TaskmanagerAddingNewEnumValueForPermissionEnumType1783324039378' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TYPE "public"."permission_name_enum" RENAME TO "permission_name_enum_old"`); + await queryRunner.query(`CREATE TYPE "public"."permission_name_enum" AS ENUM('services', 'customers', 'agents', 'developers', 'invoices', 'transactions', 'discounts', 'admins', 'tickets', 'criticisms', 'contacts_us', 'advertisements', 'announcements', 'blogs', 'learnings', 'logs', 'settings', 'bank_accounts', 'payments', 'manage_sso_clients', 'support_plan', 'dmenu', 'dkala', 'dpage', 'dmail', 'reseller', 'workspace', 'project')`); + await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum" USING "name"::"text"::"public"."permission_name_enum"`); + await queryRunner.query(`DROP TYPE "public"."permission_name_enum_old"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TYPE "public"."permission_name_enum_old" AS ENUM('services', 'customers', 'agents', 'developers', 'invoices', 'transactions', 'discounts', 'admins', 'tickets', 'criticisms', 'contacts_us', 'advertisements', 'announcements', 'blogs', 'learnings', 'logs', 'settings', 'bank_accounts', 'payments', 'manage_sso_clients', 'support_plan', 'dmenu', 'dkala', 'dpage', 'dmail', 'reseller', 'workspace')`); + await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum_old" USING "name"::"text"::"public"."permission_name_enum_old"`); + await queryRunner.query(`DROP TYPE "public"."permission_name_enum"`); + await queryRunner.query(`ALTER TYPE "public"."permission_name_enum_old" RENAME TO "permission_name_enum"`); + } + +} diff --git a/src/modules/task-manager/controllers/project.controller.ts b/src/modules/task-manager/controllers/project.controller.ts index 2c0d43e..fd712c5 100644 --- a/src/modules/task-manager/controllers/project.controller.ts +++ b/src/modules/task-manager/controllers/project.controller.ts @@ -8,7 +8,10 @@ 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"; +@AuthGuards() @AdminRoute() @ApiTags("Task Manager - Projects") @Controller("task-manager/projects") @@ -76,4 +79,14 @@ export class ProjectController { ): Promise<{ projects: TMProject[]; count: number; paginate: true }> { return this.projectService.findByWorkspace(wsId, pagination); } + + @Get("/user") + @ApiOperation({ summary: "Get user projects!" }) + @ApiResponse({ status: 200, description: "Got user projects successfully!" }) + findUserProjects( + @UserDec("id") userId: string, + @Query() pagination: PaginationDto, + ): Promise<{ projects: TMProject[]; count: number; paginate: true }> { + return this.projectService.findUserProjects(userId, pagination); + } } diff --git a/src/modules/task-manager/providers/project.service.ts b/src/modules/task-manager/providers/project.service.ts index 523fd15..c9875cd 100644 --- a/src/modules/task-manager/providers/project.service.ts +++ b/src/modules/task-manager/providers/project.service.ts @@ -140,8 +140,8 @@ export class ProjectService { }, }); - if(!project) throw new NotFoundException(ProjectMessage.PROJECT_NOT_FOUND); - + 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); @@ -149,4 +149,14 @@ export class ProjectService { await this.projectRepository.delete(id); return project; } + + async findUserProjects(userId: string, pagination: PaginationDto): Promise<{ projects: TMProject[]; count: number; paginate: true }> { + const page = pagination.page ?? 1; + const limit = pagination.limit ?? 10; + const skip = (page - 1) * limit; + + const [projects, count] = await this.projectRepository.findUserProjects(userId, skip, limit); + + return { projects, count, paginate: true }; + } } diff --git a/src/modules/task-manager/repositories/project.repository.ts b/src/modules/task-manager/repositories/project.repository.ts index 8f11e27..addeea8 100644 --- a/src/modules/task-manager/repositories/project.repository.ts +++ b/src/modules/task-manager/repositories/project.repository.ts @@ -10,4 +10,13 @@ export class ProjectRepository extends Repository { ) { super(projectRepository.target, projectRepository.manager, projectRepository.queryRunner); } + + async findUserProjects(userId: string, skip?: number, take?: number): Promise<[TMProject[], number]> { + return this.createQueryBuilder('project') + .innerJoin('project.users', 'user') + .where('user.id = :userId', {userId}) + .skip(skip ?? 0) + .take(take ?? 0) + .getManyAndCount(); + } } From 2590a222b481fda20830c7bbad43d33ae06b14c8 Mon Sep 17 00:00:00 2001 From: realrafi Date: Mon, 6 Jul 2026 12:16:23 +0330 Subject: [PATCH 45/66] refactor: removing useless get projects by workspace function --- .../controllers/project.controller.ts | 13 ---------- .../task-manager/providers/project.service.ts | 26 ------------------- 2 files changed, 39 deletions(-) diff --git a/src/modules/task-manager/controllers/project.controller.ts b/src/modules/task-manager/controllers/project.controller.ts index fd712c5..3ee9db6 100644 --- a/src/modules/task-manager/controllers/project.controller.ts +++ b/src/modules/task-manager/controllers/project.controller.ts @@ -67,19 +67,6 @@ export class ProjectController { return this.projectService.remove(id); } - @Get("/workspace/:wsId") - @PermissionsDec(PermissionEnum.PROJECT) - @ApiOperation({ summary: "Get projects of a specific workspace" }) - @ApiParam({ name: "wsId", description: "Workspace ID" }) - @ApiResponse({ status: 200, description: "received projects successfully!" }) - @ApiResponse({ status: 404, description: "WorkSpace not found" }) - findByWorkspace( - @Param("wsId") wsId: string, - @Query() pagination: PaginationDto, - ): Promise<{ projects: TMProject[]; count: number; paginate: true }> { - return this.projectService.findByWorkspace(wsId, pagination); - } - @Get("/user") @ApiOperation({ summary: "Get user projects!" }) @ApiResponse({ status: 200, description: "Got user projects successfully!" }) diff --git a/src/modules/task-manager/providers/project.service.ts b/src/modules/task-manager/providers/project.service.ts index c9875cd..f9ef36b 100644 --- a/src/modules/task-manager/providers/project.service.ts +++ b/src/modules/task-manager/providers/project.service.ts @@ -42,32 +42,6 @@ export class ProjectService { return { projects, count, paginate: true }; } - async findByWorkspace(workspaceId: string, pagination: PaginationDto): Promise<{ projects: TMProject[]; count: number; paginate: true }> { - await this.workspaceService.findOneOrFail(workspaceId); - - const page = pagination.page ?? 1; - const limit = pagination.limit ?? 10; - const skip = (page - 1) * limit; - - const [projects, count] = await this.projectRepository.findAndCount({ - where: { workspaceId }, - relations: { - users: true, - }, - select: { - users: { - id: true, - firstName: true, - lastName: true, - }, - }, - skip, - take: limit, - }); - - return { projects, count, paginate: true }; - } - async findOneOrFail(id: string): Promise { const project = await this.projectRepository.findOne({ where: { id } }); if (!project) throw new NotFoundException(ProjectMessage.PROJECT_NOT_FOUND); From 5715cbe9b1afc13317cdc8d15b7ea1ee4051e5ef Mon Sep 17 00:00:00 2001 From: realrafi Date: Mon, 6 Jul 2026 13:24:36 +0330 Subject: [PATCH 46/66] refactor: changed the getUserProjects to get projects in a specific workspace --- src/common/enums/message.enum.ts | 6 ++++-- .../task-manager/controllers/project.controller.ts | 7 +++++-- src/modules/task-manager/providers/project.service.ts | 4 ++-- .../task-manager/repositories/project.repository.ts | 8 +++++--- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index cb1ac06..7658080 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -920,11 +920,13 @@ export const enum WorkSpaceMessage { WORKSPACE_EXISTS = "فضای کار با این اسم وجود دارد!", WORKSPACE_NOT_FOUND = "فضای کار مورد نظر شما وجود ندارد!", USERS_DONT_BELONG_TO_THIS_WORKSPACE = "کاربران تعیین شده برای این فضای کاری نیستند!", - WORKSPACE_SAME_NAME_EXISTS = "فضای کاری با این اسم از قبل وجود دارد!" + WORKSPACE_SAME_NAME_EXISTS = "فضای کاری با این اسم از قبل وجود دارد!", + WORKSPACE_ID_PARAM_NOT_EMPTY = "پارامتر شناسه فضای کار نباید خالی باشد!", + WORKSPACE_ID_UUID = "شناسه فضای کار باید uuid باشد" } export const enum ProjectMessage { PROJECT_EXISTS = "پروژه مورد نظر شما از قبل وجود دارد!", PROJECT_NOT_FOUND = "پروژه مورد نظر شما وجود ندارد!", - PROJECT_CONTAINS_TASK_PHASES = "پروژه شامل فاز تسک است. لطفا ابتدا فاز تسک های این پروژه را حذف کنید." + PROJECT_CONTAINS_TASK_PHASES = "پروژه شامل فاز تسک است. لطفا ابتدا فاز تسک های این پروژه را حذف کنید.", } \ No newline at end of file diff --git a/src/modules/task-manager/controllers/project.controller.ts b/src/modules/task-manager/controllers/project.controller.ts index 3ee9db6..9199203 100644 --- a/src/modules/task-manager/controllers/project.controller.ts +++ b/src/modules/task-manager/controllers/project.controller.ts @@ -10,6 +10,7 @@ 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"; @AuthGuards() @AdminRoute() @@ -67,13 +68,15 @@ export class ProjectController { return this.projectService.remove(id); } - @Get("/user") + @Get("/user/workspace/:id") @ApiOperation({ summary: "Get user projects!" }) @ApiResponse({ status: 200, description: "Got user projects successfully!" }) + @ApiResponse({status: 400, description: "Bad Request!"}) findUserProjects( @UserDec("id") userId: string, + @Param() paramDto : ParamDto, @Query() pagination: PaginationDto, ): Promise<{ projects: TMProject[]; count: number; paginate: true }> { - return this.projectService.findUserProjects(userId, pagination); + return this.projectService.findUserProjects(userId, paramDto.id, pagination); } } diff --git a/src/modules/task-manager/providers/project.service.ts b/src/modules/task-manager/providers/project.service.ts index f9ef36b..8a192b5 100644 --- a/src/modules/task-manager/providers/project.service.ts +++ b/src/modules/task-manager/providers/project.service.ts @@ -124,12 +124,12 @@ export class ProjectService { return project; } - async findUserProjects(userId: string, pagination: PaginationDto): Promise<{ projects: TMProject[]; count: number; paginate: true }> { + async findUserProjects(userId: string, wsId: string, pagination: PaginationDto): Promise<{ projects: TMProject[]; count: number; paginate: true }> { const page = pagination.page ?? 1; const limit = pagination.limit ?? 10; const skip = (page - 1) * limit; - const [projects, count] = await this.projectRepository.findUserProjects(userId, skip, limit); + const [projects, count] = await this.projectRepository.findUserProjects(userId, wsId, skip, limit); return { projects, count, paginate: true }; } diff --git a/src/modules/task-manager/repositories/project.repository.ts b/src/modules/task-manager/repositories/project.repository.ts index addeea8..ad87f20 100644 --- a/src/modules/task-manager/repositories/project.repository.ts +++ b/src/modules/task-manager/repositories/project.repository.ts @@ -11,12 +11,14 @@ export class ProjectRepository extends Repository { super(projectRepository.target, projectRepository.manager, projectRepository.queryRunner); } - async findUserProjects(userId: string, skip?: number, take?: number): Promise<[TMProject[], number]> { + async findUserProjects(userId: string, wsId: string, skip?: number, take?: number): Promise<[TMProject[], number]> { return this.createQueryBuilder('project') + .innerJoin('project.workspace', 'workspace') .innerJoin('project.users', 'user') - .where('user.id = :userId', {userId}) + .where('workspace.id = :wsId', {wsId}) + .andWhere('user.id = :userId', {userId}) .skip(skip ?? 0) - .take(take ?? 0) + .take(take ?? 10) .getManyAndCount(); } } From 5964a9b20de4d580e6a8e30c3fa8e428190026dc Mon Sep 17 00:00:00 2001 From: realrafi Date: Mon, 6 Jul 2026 14:05:38 +0330 Subject: [PATCH 47/66] feat: added project detail API --- src/common/enums/message.enum.ts | 1 + .../controllers/project.controller.ts | 12 +++++++++ .../task-manager/providers/project.service.ts | 25 +++++++++++++++++-- .../repositories/project.repository.ts | 15 +++++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 7658080..9489ec2 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -929,4 +929,5 @@ export const enum ProjectMessage { PROJECT_EXISTS = "پروژه مورد نظر شما از قبل وجود دارد!", PROJECT_NOT_FOUND = "پروژه مورد نظر شما وجود ندارد!", PROJECT_CONTAINS_TASK_PHASES = "پروژه شامل فاز تسک است. لطفا ابتدا فاز تسک های این پروژه را حذف کنید.", + NOT_MEMBER_OF_PROJECT = "شما عضو این پروژه نیستید!" } \ No newline at end of file diff --git a/src/modules/task-manager/controllers/project.controller.ts b/src/modules/task-manager/controllers/project.controller.ts index 9199203..783b99e 100644 --- a/src/modules/task-manager/controllers/project.controller.ts +++ b/src/modules/task-manager/controllers/project.controller.ts @@ -79,4 +79,16 @@ export class ProjectController { ): Promise<{ projects: TMProject[]; count: number; paginate: true }> { return this.projectService.findUserProjects(userId, paramDto.id, pagination); } + + @Get("project/:id") + @ApiOperation({summary: "Get project detail"}) + @ApiResponse({status: 200, description: "Got Project Detail Successfully!"}) + @ApiResponse({status: 400, description: "Bad request!"}) + @ApiResponse({status: 404, description: "The project was not found!"}) + getProjectDetail(@Param() paramDto: ParamDto, + @UserDec('id') userId: string +): Promise { + return this.projectService.getProjectDetail(paramDto.id, userId); + } + } diff --git a/src/modules/task-manager/providers/project.service.ts b/src/modules/task-manager/providers/project.service.ts index 8a192b5..df8054e 100644 --- a/src/modules/task-manager/providers/project.service.ts +++ b/src/modules/task-manager/providers/project.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; +import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { In, Repository } from "typeorm"; import { ProjectRepository } from "../repositories/project.repository"; @@ -124,7 +124,11 @@ export class ProjectService { return project; } - async findUserProjects(userId: string, wsId: string, pagination: PaginationDto): Promise<{ projects: TMProject[]; count: number; paginate: true }> { + async findUserProjects( + userId: string, + wsId: string, + pagination: PaginationDto, + ): Promise<{ projects: TMProject[]; count: number; paginate: true }> { const page = pagination.page ?? 1; const limit = pagination.limit ?? 10; const skip = (page - 1) * limit; @@ -133,4 +137,21 @@ export class ProjectService { return { projects, count, paginate: true }; } + + async getProjectDetail(projectId: string, userId: string): Promise { + const project = await this.projectRepository.findOne({ + where: { id: projectId }, + relations: { + users: true, + }, + }); + if (!project) throw new NotFoundException(ProjectMessage.PROJECT_NOT_FOUND); + + const isUserMember = project.users.some(user => user.id === userId); + if(!isUserMember) throw new ForbiddenException(ProjectMessage.NOT_MEMBER_OF_PROJECT); + + const projectDetail = await this.projectRepository.getProjectDetail(projectId); + + return projectDetail; + } } diff --git a/src/modules/task-manager/repositories/project.repository.ts b/src/modules/task-manager/repositories/project.repository.ts index ad87f20..4175ab4 100644 --- a/src/modules/task-manager/repositories/project.repository.ts +++ b/src/modules/task-manager/repositories/project.repository.ts @@ -21,4 +21,19 @@ export class ProjectRepository extends Repository { .take(take ?? 10) .getManyAndCount(); } + + async getProjectDetail(projectId: string): Promise { + return this.createQueryBuilder('project') + .leftJoinAndSelect('project.taskPhases', 'taskPhase') + .leftJoinAndSelect('taskPhase.tasks', 'task') + .where('project.id = :projectId', {projectId}) + .select([ + "project.id", + "project.name", + "taskPhase.id", + "taskPhase.name", + "task.id", + "task.title" + ]).getOne(); + } } From d9283e3a904a492e7ac8a3204121de5d003a7534 Mon Sep 17 00:00:00 2001 From: realrafi Date: Mon, 6 Jul 2026 14:26:03 +0330 Subject: [PATCH 48/66] refactor: refactored the taskphase --- ...TaskPhasePermissionToPermissionEnumType.ts | 20 +++++++++ src/common/enums/message.enum.ts | 4 ++ .../controllers/task-phase.controller.ts | 7 +++ .../providers/task-phase.service.ts | 22 ++++----- .../repositories/task-phase.repository.ts | 45 ++----------------- src/modules/users/enums/permission.enum.ts | 3 +- 6 files changed, 48 insertions(+), 53 deletions(-) create mode 100644 database/migrations/1783335279945-TMAddingTaskPhasePermissionToPermissionEnumType.ts diff --git a/database/migrations/1783335279945-TMAddingTaskPhasePermissionToPermissionEnumType.ts b/database/migrations/1783335279945-TMAddingTaskPhasePermissionToPermissionEnumType.ts new file mode 100644 index 0000000..c1191db --- /dev/null +++ b/database/migrations/1783335279945-TMAddingTaskPhasePermissionToPermissionEnumType.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class TMAddingTaskPhasePermissionToPermissionEnumType1783335279945 implements MigrationInterface { + name = 'TMAddingTaskPhasePermissionToPermissionEnumType1783335279945' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TYPE "public"."permission_name_enum" RENAME TO "permission_name_enum_old"`); + await queryRunner.query(`CREATE TYPE "public"."permission_name_enum" AS ENUM('services', 'customers', 'agents', 'developers', 'invoices', 'transactions', 'discounts', 'admins', 'tickets', 'criticisms', 'contacts_us', 'advertisements', 'announcements', 'blogs', 'learnings', 'logs', 'settings', 'bank_accounts', 'payments', 'manage_sso_clients', 'support_plan', 'dmenu', 'dkala', 'dpage', 'dmail', 'reseller', 'workspace', 'project', 'task_phase')`); + await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum" USING "name"::"text"::"public"."permission_name_enum"`); + await queryRunner.query(`DROP TYPE "public"."permission_name_enum_old"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TYPE "public"."permission_name_enum_old" AS ENUM('services', 'customers', 'agents', 'developers', 'invoices', 'transactions', 'discounts', 'admins', 'tickets', 'criticisms', 'contacts_us', 'advertisements', 'announcements', 'blogs', 'learnings', 'logs', 'settings', 'bank_accounts', 'payments', 'manage_sso_clients', 'support_plan', 'dmenu', 'dkala', 'dpage', 'dmail', 'reseller', 'workspace', 'project')`); + await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum_old" USING "name"::"text"::"public"."permission_name_enum_old"`); + await queryRunner.query(`DROP TYPE "public"."permission_name_enum"`); + await queryRunner.query(`ALTER TYPE "public"."permission_name_enum_old" RENAME TO "permission_name_enum"`); + } + +} diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 9489ec2..e992f6f 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -930,4 +930,8 @@ export const enum ProjectMessage { PROJECT_NOT_FOUND = "پروژه مورد نظر شما وجود ندارد!", PROJECT_CONTAINS_TASK_PHASES = "پروژه شامل فاز تسک است. لطفا ابتدا فاز تسک های این پروژه را حذف کنید.", NOT_MEMBER_OF_PROJECT = "شما عضو این پروژه نیستید!" +} + +export const enum TaskPhaseMessage { + TASK_PHASE_NOT_FOUND = "فاز تسک مورد نظر پیدا نشد!" } \ No newline at end of file diff --git a/src/modules/task-manager/controllers/task-phase.controller.ts b/src/modules/task-manager/controllers/task-phase.controller.ts index e38c41c..32f83c9 100644 --- a/src/modules/task-manager/controllers/task-phase.controller.ts +++ b/src/modules/task-manager/controllers/task-phase.controller.ts @@ -4,7 +4,14 @@ import { TaskPhaseService } from "../providers/task-phase.service"; import { CreateTaskPhaseDto } from "../dto/task-phase/create-task-phase.dto"; import { UpdateTaskPhaseDto } from "../dto/task-phase/update-task-phase.dto"; import { TMTaskPhase } from "../entities/task-phase.entity"; +import { AuthGuards } from "../../../common/decorators/auth-guard.decorator"; +import { AdminRoute } from "../../../common/decorators/admin.decorator"; +import { PermissionsDec } from "../../../common/decorators/permission.decorator"; +import { PermissionEnum } from "../../users/enums/permission.enum"; +@AuthGuards() +@AdminRoute() +@PermissionsDec(PermissionEnum.TASK_PHASE) @ApiTags("Task Manager - Task Phases") @Controller("task-manager/task-phases") export class TaskPhaseController { diff --git a/src/modules/task-manager/providers/task-phase.service.ts b/src/modules/task-manager/providers/task-phase.service.ts index 570ac19..00bcf63 100644 --- a/src/modules/task-manager/providers/task-phase.service.ts +++ b/src/modules/task-manager/providers/task-phase.service.ts @@ -1,35 +1,36 @@ import { Injectable, NotFoundException } from "@nestjs/common"; import { TaskPhaseRepository } from "../repositories/task-phase.repository"; -import { ProjectRepository } from "../repositories/project.repository"; import { TMTaskPhase } from "../entities/task-phase.entity"; import { CreateTaskPhaseDto } from "../dto/task-phase/create-task-phase.dto"; import { UpdateTaskPhaseDto } from "../dto/task-phase/update-task-phase.dto"; +import { TaskPhaseMessage } from "../../../common/enums/message.enum"; +import { ProjectService } from "./project.service"; @Injectable() export class TaskPhaseService { constructor( private readonly taskPhaseRepository: TaskPhaseRepository, - private readonly projectRepository: ProjectRepository, + private readonly projectService: ProjectService ) {} findAll(): Promise { - return this.taskPhaseRepository.findAll(); + return this.taskPhaseRepository.find(); } findByProject(projectId: string): Promise { - return this.taskPhaseRepository.findByProject(projectId); + return this.taskPhaseRepository.find({ + where: {projectId} + }); } async findOne(id: string): Promise { - const taskPhase = await this.taskPhaseRepository.findOneById(id); - if (!taskPhase) throw new NotFoundException(`TaskPhase #${id} not found`); + const taskPhase = await this.taskPhaseRepository.findOne({where: {id}}); + if (!taskPhase) throw new NotFoundException(TaskPhaseMessage.TASK_PHASE_NOT_FOUND); return taskPhase; } async create(dto: CreateTaskPhaseDto): Promise { - const project = await this.projectRepository.findOneById(dto.projectId); - if (!project) throw new NotFoundException(`Project #${dto.projectId} not found`); - + await this.projectService.findOneOrFail(dto.projectId); const taskPhase = this.taskPhaseRepository.create(dto); return this.taskPhaseRepository.save(taskPhase); } @@ -38,8 +39,7 @@ export class TaskPhaseService { const taskPhase = await this.findOne(id); if (dto.projectId) { - const project = await this.projectRepository.findOneById(dto.projectId); - if (!project) throw new NotFoundException(`Project #${dto.projectId} not found`); + await this.projectService.findOneOrFail(dto.projectId); } Object.assign(taskPhase, dto); diff --git a/src/modules/task-manager/repositories/task-phase.repository.ts b/src/modules/task-manager/repositories/task-phase.repository.ts index 1023f9a..b283229 100644 --- a/src/modules/task-manager/repositories/task-phase.repository.ts +++ b/src/modules/task-manager/repositories/task-phase.repository.ts @@ -4,47 +4,10 @@ import { Repository } from "typeorm"; import { TMTaskPhase } from "../entities/task-phase.entity"; @Injectable() -export class TaskPhaseRepository { +export class TaskPhaseRepository extends Repository { constructor( - @InjectRepository(TMTaskPhase) - private readonly repository: Repository, - ) {} - - 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) { - return this.repository.create(data); - } - - save(taskPhase: TMTaskPhase) { - return this.repository.save(taskPhase); - } - - update(id: string, data: Partial) { - return this.repository.update(id, data); - } - - delete(id: string) { - return this.repository.delete(id); + @InjectRepository(TMTaskPhase) taskPhaseRepository: Repository, + ) { + super(taskPhaseRepository.target, taskPhaseRepository.manager, taskPhaseRepository.queryRunner); } } diff --git a/src/modules/users/enums/permission.enum.ts b/src/modules/users/enums/permission.enum.ts index 1748c29..363e730 100755 --- a/src/modules/users/enums/permission.enum.ts +++ b/src/modules/users/enums/permission.enum.ts @@ -26,5 +26,6 @@ export enum PermissionEnum { DMAIL = 'dmail', RESELLER="reseller", WORKSPACE="workspace", - PROJECT="project" + PROJECT="project", + TASK_PHASE="task_phase" } From 263ede68b256ccb4428b39299633489f228ba20e Mon Sep 17 00:00:00 2001 From: realrafi Date: Mon, 6 Jul 2026 14:30:41 +0330 Subject: [PATCH 49/66] refactor: removed typo forward slash from API paths --- src/modules/task-manager/controllers/project.controller.ts | 2 +- src/modules/task-manager/controllers/workspace.controller.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/task-manager/controllers/project.controller.ts b/src/modules/task-manager/controllers/project.controller.ts index 783b99e..3da243d 100644 --- a/src/modules/task-manager/controllers/project.controller.ts +++ b/src/modules/task-manager/controllers/project.controller.ts @@ -68,7 +68,7 @@ export class ProjectController { return this.projectService.remove(id); } - @Get("/user/workspace/:id") + @Get("user/workspace/:id") @ApiOperation({ summary: "Get user projects!" }) @ApiResponse({ status: 200, description: "Got user projects successfully!" }) @ApiResponse({status: 400, description: "Bad Request!"}) diff --git a/src/modules/task-manager/controllers/workspace.controller.ts b/src/modules/task-manager/controllers/workspace.controller.ts index ed8a810..c92c142 100644 --- a/src/modules/task-manager/controllers/workspace.controller.ts +++ b/src/modules/task-manager/controllers/workspace.controller.ts @@ -64,7 +64,7 @@ export class WorkspaceController { return this.workspaceService.remove(id); } - @Get('/byuser') + @Get('byuser') @ApiOperation({summary: 'Get User WorkSpaces'}) @ApiResponse({ status: 200, description: 'Got Workspaces successfully!' }) @ApiResponse({ status: 404, description: 'Bad Request!' }) From da71c20d6d057d51bd5fc27bcd2b563513abc654 Mon Sep 17 00:00:00 2001 From: realrafi Date: Mon, 6 Jul 2026 15:21:34 +0330 Subject: [PATCH 50/66] refactor: refactored task APIs --- src/common/enums/message.enum.ts | 8 +- .../controllers/task-phase.controller.ts | 2 +- .../controllers/task.controller.ts | 2 +- .../providers/task-phase.service.ts | 6 +- .../task-manager/providers/task.service.ts | 92 ++++++++++++------- .../repositories/task.repository.ts | 43 +-------- 6 files changed, 75 insertions(+), 78 deletions(-) diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index e992f6f..8bf57e4 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -933,5 +933,11 @@ export const enum ProjectMessage { } export const enum TaskPhaseMessage { - TASK_PHASE_NOT_FOUND = "فاز تسک مورد نظر پیدا نشد!" + TASK_PHASE_NOT_FOUND = "فاز تسک مورد نظر پیدا نشد!", + TASK_PHASE_NOT_SAME_PROJECT = "فاز تسک جدید با فاز تسک قبلی در یک پروژه نیستند!" +} + +export const enum TaskMessage { + TASK_NOT_FOUND = "تسک مورد نظر پیدا نشد!", + USERS_ASSIGNED_TO_TASK_NOT_PROJECT_MEMBER = "حداقل یکی از کاربران تعیین شده عضو پروژه نیستند!" } \ No newline at end of file diff --git a/src/modules/task-manager/controllers/task-phase.controller.ts b/src/modules/task-manager/controllers/task-phase.controller.ts index 32f83c9..5e3fd3d 100644 --- a/src/modules/task-manager/controllers/task-phase.controller.ts +++ b/src/modules/task-manager/controllers/task-phase.controller.ts @@ -32,7 +32,7 @@ export class TaskPhaseController { @ApiResponse({ status: 200, description: "TaskPhase found", type: TMTaskPhase }) @ApiResponse({ status: 404, description: "TaskPhase not found" }) findOne(@Param("id") id: string): Promise { - return this.taskPhaseService.findOne(id); + return this.taskPhaseService.findOneOrFail(id); } @Post() diff --git a/src/modules/task-manager/controllers/task.controller.ts b/src/modules/task-manager/controllers/task.controller.ts index 9e356b3..c0fa93d 100644 --- a/src/modules/task-manager/controllers/task.controller.ts +++ b/src/modules/task-manager/controllers/task.controller.ts @@ -26,7 +26,7 @@ export class TaskController { @ApiResponse({ status: 200, description: 'Task found', type: TMTask }) @ApiResponse({ status: 404, description: 'Task not found' }) findOne(@Param('id') id: string): Promise { - return this.taskService.findOne(id); + return this.taskService.findOneOrFail(id); } @Post() diff --git a/src/modules/task-manager/providers/task-phase.service.ts b/src/modules/task-manager/providers/task-phase.service.ts index 00bcf63..11233ee 100644 --- a/src/modules/task-manager/providers/task-phase.service.ts +++ b/src/modules/task-manager/providers/task-phase.service.ts @@ -23,7 +23,7 @@ export class TaskPhaseService { }); } - async findOne(id: string): Promise { + async findOneOrFail(id: string): Promise { const taskPhase = await this.taskPhaseRepository.findOne({where: {id}}); if (!taskPhase) throw new NotFoundException(TaskPhaseMessage.TASK_PHASE_NOT_FOUND); return taskPhase; @@ -36,7 +36,7 @@ export class TaskPhaseService { } async update(id: string, dto: UpdateTaskPhaseDto): Promise { - const taskPhase = await this.findOne(id); + const taskPhase = await this.findOneOrFail(id); if (dto.projectId) { await this.projectService.findOneOrFail(dto.projectId); @@ -47,7 +47,7 @@ export class TaskPhaseService { } async remove(id: string): Promise { - await this.findOne(id); + await this.findOneOrFail(id); await this.taskPhaseRepository.delete(id); } } diff --git a/src/modules/task-manager/providers/task.service.ts b/src/modules/task-manager/providers/task.service.ts index 9e635e3..2be9e15 100644 --- a/src/modules/task-manager/providers/task.service.ts +++ b/src/modules/task-manager/providers/task.service.ts @@ -1,13 +1,15 @@ -import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { In, Repository } from 'typeorm'; -import { TaskRepository } from '../repositories/task.repository'; -import { TaskPhaseRepository } from '../repositories/task-phase.repository'; -import { TMTask } from '../entities/task.entity'; -import { CreateTaskDto } from '../dto/task/create-task.dto'; -import { UpdateTaskDto } from '../dto/task/update-task.dto'; -import { ChangeTaskPhaseDto } from '../dto/task/change-task-phase.dto'; -import { User } from '../../users/entities/user.entity'; +import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { In, Repository } from "typeorm"; +import { TaskRepository } from "../repositories/task.repository"; +import { TMTask } from "../entities/task.entity"; +import { CreateTaskDto } from "../dto/task/create-task.dto"; +import { UpdateTaskDto } from "../dto/task/update-task.dto"; +import { ChangeTaskPhaseDto } from "../dto/task/change-task-phase.dto"; +import { User } from "../../users/entities/user.entity"; +import { TaskMessage, TaskPhaseMessage } from "../../../common/enums/message.enum"; +import { TaskPhaseRepository } from "../repositories/task-phase.repository"; +import { TMTaskPhase } from "../entities/task-phase.entity"; @Injectable() export class TaskService { @@ -18,25 +20,50 @@ export class TaskService { private readonly userRepository: Repository, ) {} - findAll(): Promise { - return this.taskRepository.findAll(); + async findAll(): Promise { + return this.taskRepository.find({ + relations: ["taskPhase", "remarks", "attachments", "checkListItems", "users"], + }); } - findByPhase(taskPhaseId: string): Promise { - return this.taskRepository.findByPhase(taskPhaseId); + async findByPhase(taskPhaseId: string): Promise { + return this.taskRepository.find({ where: { taskPhaseId } }); } - async findOne(id: string): Promise { - const task = await this.taskRepository.findOneById(id); - if (!task) throw new NotFoundException(`Task #${id} not found`); + async findOneOrFail(id: string): Promise { + const task = await this.taskRepository.findOne({ + where: { id }, + relations: { + taskPhase: true, + }, + }); + if (!task) throw new NotFoundException(TaskMessage.TASK_NOT_FOUND); return task; } + async findTaskPhaseOrFail(taskPhaseId: string): Promise { + const taskPhase = await this.taskPhaseRepository.findOne({ + where: { id: taskPhaseId }, + relations: { + project: { + users: true, + }, + }, + }); + if (!taskPhase) throw new NotFoundException(TaskPhaseMessage.TASK_PHASE_NOT_FOUND); + + return taskPhase; + } + async create(dto: CreateTaskDto): Promise { const { userIds, startDate, endDate, ...rest } = dto; - const taskPhase = await this.taskPhaseRepository.findOneById(dto.taskPhaseId); - if (!taskPhase) throw new NotFoundException(`TaskPhase #${dto.taskPhaseId} not found`); + const taskPhase = await this.findTaskPhaseOrFail(dto.taskPhaseId); + + if (userIds?.length) { + const isUsersValid = userIds.every((userId) => taskPhase.project.users.some((user) => user.id === userId)); + if (!isUsersValid) throw new BadRequestException(TaskMessage.USERS_ASSIGNED_TO_TASK_NOT_PROJECT_MEMBER); + } const task = this.taskRepository.create({ ...rest, @@ -52,12 +79,16 @@ export class TaskService { } async update(id: string, dto: UpdateTaskDto): Promise { - const task = await this.findOne(id); + const task = await this.findOneOrFail(id); const { userIds, startDate, endDate, ...rest } = dto; if (dto.taskPhaseId) { - const taskPhase = await this.taskPhaseRepository.findOneById(dto.taskPhaseId); - if (!taskPhase) throw new NotFoundException(`TaskPhase #${dto.taskPhaseId} not found`); + const taskPhase = await this.findTaskPhaseOrFail(dto.taskPhaseId); + + if (userIds?.length) { + const isUsersValid = userIds.every((userId) => taskPhase.project.users.some((user) => user.id === userId)); + if (!isUsersValid) throw new BadRequestException(TaskMessage.USERS_ASSIGNED_TO_TASK_NOT_PROJECT_MEMBER); + } } Object.assign(task, { @@ -67,24 +98,19 @@ export class TaskService { }); if (userIds) { - task.users = userIds.length - ? await this.userRepository.findBy({ id: In(userIds) }) - : []; + task.users = userIds.length ? await this.userRepository.findBy({ id: In(userIds) }) : []; } return this.taskRepository.save(task); } async changePhase(id: string, dto: ChangeTaskPhaseDto): Promise { - const task = await this.findOne(id); + const task = await this.findOneOrFail(id); - const newPhase = await this.taskPhaseRepository.findOneById(dto.taskPhaseId); - if (!newPhase) throw new NotFoundException(`TaskPhase #${dto.taskPhaseId} not found`); + const newPhase = await this.findTaskPhaseOrFail(dto.taskPhaseId); if (task.taskPhase.projectId !== newPhase.projectId) { - throw new BadRequestException( - `TaskPhase #${dto.taskPhaseId} does not belong to the same project as the current phase`, - ); + throw new BadRequestException(TaskPhaseMessage.TASK_PHASE_NOT_SAME_PROJECT); } task.taskPhaseId = dto.taskPhaseId; @@ -94,7 +120,7 @@ export class TaskService { } async remove(id: string): Promise { - await this.findOne(id); + await this.findOneOrFail(id); await this.taskRepository.delete(id); } -} \ No newline at end of file +} diff --git a/src/modules/task-manager/repositories/task.repository.ts b/src/modules/task-manager/repositories/task.repository.ts index f419845..5f07518 100644 --- a/src/modules/task-manager/repositories/task.repository.ts +++ b/src/modules/task-manager/repositories/task.repository.ts @@ -4,45 +4,10 @@ import { Repository } from "typeorm"; import { TMTask } from "../entities/task.entity"; @Injectable() -export class TaskRepository { +export class TaskRepository extends Repository { constructor( - @InjectRepository(TMTask) - private readonly repository: Repository, - ) {} - - findAll() { - return this.repository.find({ - relations: ["taskPhase", "remarks", "attachments", "checkListItems", "users"], - }); - } - - findByPhase(taskPhaseId: string) { - return this.repository.find({ - where: { taskPhaseId }, - relations: ["remarks", "attachments", "checkListItems", "users"], - }); - } - - findOneById(id: string) { - return this.repository.findOne({ - where: { id }, - relations: ["taskPhase", "remarks", "attachments", "checkListItems", "users"], - }); - } - - create(data: Partial) { - return this.repository.create(data); - } - - save(task: TMTask) { - return this.repository.save(task); - } - - update(id: string, data: Partial) { - return this.repository.update(id, data); - } - - delete(id: string) { - return this.repository.delete(id); + @InjectRepository(TMTask) taskRepository: Repository, + ) { + super(taskRepository.target, taskRepository.manager, taskRepository.queryRunner) } } From 0cd5a6a0cc64bd688bfe21318449dd1212d6c134 Mon Sep 17 00:00:00 2001 From: realrafi Date: Mon, 6 Jul 2026 16:01:36 +0330 Subject: [PATCH 51/66] feat: added getTaskDetail API --- src/common/enums/message.enum.ts | 3 ++- .../controllers/task.controller.ts | 18 ++++++++++++++++++ .../task-manager/providers/task.service.ts | 13 ++++++++++++- .../repositories/task.repository.ts | 9 +++++++++ 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 8bf57e4..64b887c 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -939,5 +939,6 @@ export const enum TaskPhaseMessage { export const enum TaskMessage { TASK_NOT_FOUND = "تسک مورد نظر پیدا نشد!", - USERS_ASSIGNED_TO_TASK_NOT_PROJECT_MEMBER = "حداقل یکی از کاربران تعیین شده عضو پروژه نیستند!" + USERS_ASSIGNED_TO_TASK_NOT_PROJECT_MEMBER = "حداقل یکی از کاربران تعیین شده عضو پروژه نیستند!", + USER_NOT_TASK_MEMBER = "شما عضو این تسک نیستید!" } \ No newline at end of file diff --git a/src/modules/task-manager/controllers/task.controller.ts b/src/modules/task-manager/controllers/task.controller.ts index c0fa93d..0b1e045 100644 --- a/src/modules/task-manager/controllers/task.controller.ts +++ b/src/modules/task-manager/controllers/task.controller.ts @@ -5,7 +5,13 @@ import { CreateTaskDto } from '../dto/task/create-task.dto'; import { UpdateTaskDto } from '../dto/task/update-task.dto'; import { ChangeTaskPhaseDto } from '../dto/task/change-task-phase.dto'; import { TMTask } from '../entities/task.entity'; +import { AuthGuards } from '../../../common/decorators/auth-guard.decorator'; +import { AdminRoute } from '../../../common/decorators/admin.decorator'; +import { UserDec } from '../../../common/decorators/user.decorator'; +import { ParamDto } from '../../../common/DTO/param.dto'; +@AuthGuards() +@AdminRoute() @ApiTags('Task Manager - Tasks') @Controller('task-manager/tasks') export class TaskController { @@ -64,4 +70,16 @@ export class TaskController { remove(@Param('id') id: string): Promise { return this.taskService.remove(id); } + + @Get('detail/:id') + @ApiOperation({summary: 'Get Task Detail'}) + @ApiParam({ name: 'id', description: 'Task ID' }) + @ApiResponse({ status: 200, description: 'Task detail received!' }) + @ApiResponse({ status: 404, description: 'Task not found' }) + getTaskDetail( + @UserDec('id') userId: string, + @Param() paramDto: ParamDto + ): Promise { + return this.taskService.getTaskDetail(userId, paramDto.id); + } } \ No newline at end of file diff --git a/src/modules/task-manager/providers/task.service.ts b/src/modules/task-manager/providers/task.service.ts index 2be9e15..e46f02e 100644 --- a/src/modules/task-manager/providers/task.service.ts +++ b/src/modules/task-manager/providers/task.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; +import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { In, Repository } from "typeorm"; import { TaskRepository } from "../repositories/task.repository"; @@ -35,6 +35,7 @@ export class TaskService { where: { id }, relations: { taskPhase: true, + users: true }, }); if (!task) throw new NotFoundException(TaskMessage.TASK_NOT_FOUND); @@ -123,4 +124,14 @@ export class TaskService { await this.findOneOrFail(id); await this.taskRepository.delete(id); } + + async getTaskDetail(userId: string, taskId: string): Promise { + const task = await this.findOneOrFail(taskId); + + const isTaskMember = task.users.some((user) => user.id === userId); + if(!isTaskMember) throw new ForbiddenException(TaskMessage.USER_NOT_TASK_MEMBER); + + const taskDetail = await this.taskRepository.getTaskDetail(taskId); + return taskDetail; + } } diff --git a/src/modules/task-manager/repositories/task.repository.ts b/src/modules/task-manager/repositories/task.repository.ts index 5f07518..4a5dff0 100644 --- a/src/modules/task-manager/repositories/task.repository.ts +++ b/src/modules/task-manager/repositories/task.repository.ts @@ -10,4 +10,13 @@ export class TaskRepository extends Repository { ) { super(taskRepository.target, taskRepository.manager, taskRepository.queryRunner) } + + async getTaskDetail(taskId: string): Promise { + return this.createQueryBuilder('task') + .leftJoinAndSelect('task.remarks', 'remark') + .leftJoinAndSelect('task.attachments', 'attachment') + .leftJoinAndSelect('task.checkListItems', 'checkListItem') + .where('task.id = :taskId', {taskId}) + .getOne(); + } } From e310411a4d7b6b5abcbe5309015e5670bbb196b6 Mon Sep 17 00:00:00 2001 From: realrafi Date: Mon, 6 Jul 2026 16:17:02 +0330 Subject: [PATCH 52/66] refactor: refactored the getProjectDetail function to include more values --- .../task-manager/entities/task.entity.ts | 5 ++ .../repositories/project.repository.ts | 50 ++++++++++++++----- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/modules/task-manager/entities/task.entity.ts b/src/modules/task-manager/entities/task.entity.ts index bf18fe2..5461d83 100644 --- a/src/modules/task-manager/entities/task.entity.ts +++ b/src/modules/task-manager/entities/task.entity.ts @@ -57,4 +57,9 @@ export class TMTask extends BaseEntity { inverseJoinColumn: { name: "userId", referencedColumnName: "id" }, }) users: User[]; + + // deriven(calculated) attributes. not database columns + attachmentCount?: number; + checkListItemCount?: number; + completedCheckListItemCount?: number; } diff --git a/src/modules/task-manager/repositories/project.repository.ts b/src/modules/task-manager/repositories/project.repository.ts index 4175ab4..a7bfbf4 100644 --- a/src/modules/task-manager/repositories/project.repository.ts +++ b/src/modules/task-manager/repositories/project.repository.ts @@ -23,17 +23,43 @@ export class ProjectRepository extends Repository { } async getProjectDetail(projectId: string): Promise { - return this.createQueryBuilder('project') - .leftJoinAndSelect('project.taskPhases', 'taskPhase') - .leftJoinAndSelect('taskPhase.tasks', 'task') - .where('project.id = :projectId', {projectId}) - .select([ - "project.id", - "project.name", - "taskPhase.id", - "taskPhase.name", - "task.id", - "task.title" - ]).getOne(); + return this.createQueryBuilder("project") + .leftJoinAndSelect("project.taskPhases", "taskPhase") + .leftJoinAndSelect("taskPhase.tasks", "task") + .leftJoinAndSelect("task.remarks", "remark") + + .loadRelationCountAndMap( + "task.attachmentCount", + "task.attachments", + ) + + .loadRelationCountAndMap( + "task.checkListItemCount", + "task.checkListItems", + ) + + .loadRelationCountAndMap( + "task.completedCheckListItemCount", + "task.checkListItems", + "completedCheckListItems", + (qb) => qb.where("completedCheckListItems.isDone = :isDone", { isDone: true }), + ) + + .where("project.id = :projectId", { projectId }) + + .select([ + "project.id", + "project.name", + "taskPhase.id", + "taskPhase.name", + "task.id", + "task.title", + "task.startDate", + "task.endDate", + "remark.title", + "remark.color", + ]) + + .getOne(); } } From e5c14bf5cc1c67096f4268e52db0efcc4f67b74c Mon Sep 17 00:00:00 2001 From: realrafi Date: Mon, 6 Jul 2026 16:44:19 +0330 Subject: [PATCH 53/66] refactor: refactored taskDetailAPI by adding some values --- .../controllers/task.controller.ts | 8 ++++ .../repositories/task.repository.ts | 41 ++++++++++++++----- src/modules/users/enums/permission.enum.ts | 3 +- 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/modules/task-manager/controllers/task.controller.ts b/src/modules/task-manager/controllers/task.controller.ts index 0b1e045..32160ce 100644 --- a/src/modules/task-manager/controllers/task.controller.ts +++ b/src/modules/task-manager/controllers/task.controller.ts @@ -9,6 +9,8 @@ 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'; @AuthGuards() @AdminRoute() @@ -18,6 +20,7 @@ export class TaskController { constructor(private readonly taskService: TaskService) {} @Get() + @PermissionsDec(PermissionEnum.TASK) @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] }) @@ -27,6 +30,7 @@ export class TaskController { } @Get(':id') + @PermissionsDec(PermissionEnum.TASK) @ApiOperation({ summary: 'Get a task by ID' }) @ApiParam({ name: 'id', description: 'Task ID' }) @ApiResponse({ status: 200, description: 'Task found', type: TMTask }) @@ -36,6 +40,7 @@ export class TaskController { } @Post() + @PermissionsDec(PermissionEnum.TASK) @ApiOperation({ summary: 'Create a new task' }) @ApiResponse({ status: 201, description: 'Task created', type: TMTask }) @ApiResponse({ status: 404, description: 'TaskPhase not found' }) @@ -44,6 +49,7 @@ export class TaskController { } @Patch(':id') + @PermissionsDec(PermissionEnum.TASK) @ApiOperation({ summary: 'Update a task' }) @ApiParam({ name: 'id', description: 'Task ID' }) @ApiResponse({ status: 200, description: 'Task updated', type: TMTask }) @@ -53,6 +59,7 @@ export class TaskController { } @Patch(':id/change-phase') + @PermissionsDec(PermissionEnum.TASK) @ApiOperation({ summary: 'Move a task to a different phase within the same project' }) @ApiParam({ name: 'id', description: 'Task ID' }) @ApiResponse({ status: 200, description: 'Task phase changed', type: TMTask }) @@ -63,6 +70,7 @@ export class TaskController { } @Delete(':id') + @PermissionsDec(PermissionEnum.TASK) @ApiOperation({ summary: 'Delete a task' }) @ApiParam({ name: 'id', description: 'Task ID' }) @ApiResponse({ status: 200, description: 'Task deleted' }) diff --git a/src/modules/task-manager/repositories/task.repository.ts b/src/modules/task-manager/repositories/task.repository.ts index 4a5dff0..26e76b6 100644 --- a/src/modules/task-manager/repositories/task.repository.ts +++ b/src/modules/task-manager/repositories/task.repository.ts @@ -5,18 +5,39 @@ import { TMTask } from "../entities/task.entity"; @Injectable() export class TaskRepository extends Repository { - constructor( - @InjectRepository(TMTask) taskRepository: Repository, - ) { - super(taskRepository.target, taskRepository.manager, taskRepository.queryRunner) + constructor(@InjectRepository(TMTask) taskRepository: Repository) { + super(taskRepository.target, taskRepository.manager, taskRepository.queryRunner); } async getTaskDetail(taskId: string): Promise { - return this.createQueryBuilder('task') - .leftJoinAndSelect('task.remarks', 'remark') - .leftJoinAndSelect('task.attachments', 'attachment') - .leftJoinAndSelect('task.checkListItems', 'checkListItem') - .where('task.id = :taskId', {taskId}) - .getOne(); + return this.createQueryBuilder("task") + .leftJoinAndSelect("task.remarks", "remark") + .leftJoinAndSelect("task.attachments", "attachment") + .leftJoinAndSelect("task.checkListItems", "checkListItem") + .leftJoinAndSelect("task.users", "user") + + .loadRelationCountAndMap("task.checkListItemCount", "task.checkListItems") + + .loadRelationCountAndMap("task.completedCheckListItemCount", "task.checkListItems", "completedCheckListItems", (qb) => + qb.where("completedCheckListItems.isDone = :isDone", { isDone: true }), + ) + + .select(["remark.id", + "remark.title", + "remark.color", + "checkListItem.id", + "checkListItem.title", + "checkListItem.isDone", + "attachment.id", + "attachment.title", + "attachment.type", + "attachment.file", + + "user.id", + "user.firstName", + "user.lastName" + ]) + .where("task.id = :taskId", { taskId }) + .getOne(); } } diff --git a/src/modules/users/enums/permission.enum.ts b/src/modules/users/enums/permission.enum.ts index 363e730..67c3dd8 100755 --- a/src/modules/users/enums/permission.enum.ts +++ b/src/modules/users/enums/permission.enum.ts @@ -27,5 +27,6 @@ export enum PermissionEnum { RESELLER="reseller", WORKSPACE="workspace", PROJECT="project", - TASK_PHASE="task_phase" + TASK_PHASE="task_phase", + TASK="task" } From 853368230356b57cbcdd9f798e32c572cc83ccf2 Mon Sep 17 00:00:00 2001 From: realrafi Date: Mon, 6 Jul 2026 17:00:44 +0330 Subject: [PATCH 54/66] refactor: refactored remark entirely --- src/common/enums/message.enum.ts | 4 ++ .../controllers/remark.controller.ts | 9 +++- .../task-manager/providers/remark.service.ts | 30 +++++++------ .../repositories/remark.repository.ts | 42 ++----------------- src/modules/users/enums/permission.enum.ts | 3 +- 5 files changed, 35 insertions(+), 53 deletions(-) diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 64b887c..2c456d5 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -941,4 +941,8 @@ export const enum TaskMessage { TASK_NOT_FOUND = "تسک مورد نظر پیدا نشد!", USERS_ASSIGNED_TO_TASK_NOT_PROJECT_MEMBER = "حداقل یکی از کاربران تعیین شده عضو پروژه نیستند!", USER_NOT_TASK_MEMBER = "شما عضو این تسک نیستید!" +} + +export const enum remarkMessage { + REMARK_NOT_FOUND = "برچسب مورد نظر پیدا نشد!" } \ No newline at end of file diff --git a/src/modules/task-manager/controllers/remark.controller.ts b/src/modules/task-manager/controllers/remark.controller.ts index 7c4168a..d5933b9 100644 --- a/src/modules/task-manager/controllers/remark.controller.ts +++ b/src/modules/task-manager/controllers/remark.controller.ts @@ -4,7 +4,14 @@ 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"; +import { PermissionsDec } from "../../../common/decorators/permission.decorator"; +import { PermissionEnum } from "../../users/enums/permission.enum"; +@AuthGuards() +@AdminRoute() +@PermissionsDec(PermissionEnum.REMARK) @ApiTags("Task Manager - Remarks") @Controller("task-manager/remarks") export class RemarkController { @@ -25,7 +32,7 @@ export class RemarkController { @ApiResponse({ status: 200, description: "Remark found", type: TMRemark }) @ApiResponse({ status: 404, description: "Remark not found" }) findOne(@Param("id") id: string): Promise { - return this.remarkService.findOne(id); + return this.remarkService.findOneOrFail(id); } @Post() diff --git a/src/modules/task-manager/providers/remark.service.ts b/src/modules/task-manager/providers/remark.service.ts index b30d40b..f48ed1f 100644 --- a/src/modules/task-manager/providers/remark.service.ts +++ b/src/modules/task-manager/providers/remark.service.ts @@ -1,45 +1,49 @@ import { Injectable, NotFoundException } from "@nestjs/common"; import { RemarkRepository } from "../repositories/remark.repository"; -import { TaskRepository } from "../repositories/task.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 taskRepository: TaskRepository, + private readonly taskService: TaskService, ) {} findAll(): Promise { - return this.remarkRepository.findAll(); + return this.remarkRepository.find({ relations: { task: true } }); } findByTask(taskId: string): Promise { - return this.remarkRepository.findByTask(taskId); + return this.remarkRepository.find({ + where: { taskId }, + relations: { + task: true, + }, + }); } - async findOne(id: string): Promise { - const remark = await this.remarkRepository.findOneById(id); - if (!remark) throw new NotFoundException(`Remark #${id} not found`); + async findOneOrFail(id: string): Promise { + const remark = await this.remarkRepository.findOne({ where: { id } }); + if (!remark) throw new NotFoundException(remarkMessage.REMARK_NOT_FOUND); return remark; } async create(dto: CreateRemarkDto): Promise { - const task = await this.taskRepository.findOneById(dto.taskId); - if (!task) throw new NotFoundException(`Task #${dto.taskId} not found`); + await this.taskService.findOneOrFail(dto.taskId); const remark = this.remarkRepository.create(dto); return this.remarkRepository.save(remark); } async update(id: string, dto: UpdateRemarkDto): Promise { - const remark = await this.findOne(id); + const remark = await this.findOneOrFail(id); if (dto.taskId) { - const task = await this.taskRepository.findOneById(dto.taskId); - if (!task) throw new NotFoundException(`Task #${dto.taskId} not found`); + await this.taskService.findOneOrFail(dto.taskId); } Object.assign(remark, dto); @@ -47,7 +51,7 @@ export class RemarkService { } async remove(id: string): Promise { - await this.findOne(id); + await this.findOneOrFail(id); await this.remarkRepository.delete(id); } } diff --git a/src/modules/task-manager/repositories/remark.repository.ts b/src/modules/task-manager/repositories/remark.repository.ts index 4881a3d..8b71ea3 100644 --- a/src/modules/task-manager/repositories/remark.repository.ts +++ b/src/modules/task-manager/repositories/remark.repository.ts @@ -4,44 +4,10 @@ import { Repository } from "typeorm"; import { TMRemark } from "../entities/remark.entity"; @Injectable() -export class RemarkRepository { +export class RemarkRepository extends Repository{ constructor( - @InjectRepository(TMRemark) - private readonly repository: Repository, - ) {} - - 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) { - return this.repository.create(data); - } - - save(remark: TMRemark) { - return this.repository.save(remark); - } - - update(id: string, data: Partial) { - return this.repository.update(id, data); - } - - delete(id: string) { - return this.repository.delete(id); + @InjectRepository(TMRemark) remarkRepository: Repository, + ) { + super(remarkRepository.target, remarkRepository.manager, remarkRepository.queryRunner); } } diff --git a/src/modules/users/enums/permission.enum.ts b/src/modules/users/enums/permission.enum.ts index 67c3dd8..779f6c9 100755 --- a/src/modules/users/enums/permission.enum.ts +++ b/src/modules/users/enums/permission.enum.ts @@ -28,5 +28,6 @@ export enum PermissionEnum { WORKSPACE="workspace", PROJECT="project", TASK_PHASE="task_phase", - TASK="task" + TASK="task", + REMARK="remark" } From 9565eda0fd1b8a5570925c55e19fa7a778b5b2a5 Mon Sep 17 00:00:00 2001 From: realrafi Date: Tue, 7 Jul 2026 11:51:42 +0330 Subject: [PATCH 55/66] refactor: refactored the checkListItem --- .../controllers/check-list-item.controller.ts | 4 +++ .../providers/check-list-item.service.ts | 2 +- .../check-list-item.repository.ts | 35 ++++++------------- 3 files changed, 15 insertions(+), 26 deletions(-) diff --git a/src/modules/task-manager/controllers/check-list-item.controller.ts b/src/modules/task-manager/controllers/check-list-item.controller.ts index 5d4cb29..45a9f40 100644 --- a/src/modules/task-manager/controllers/check-list-item.controller.ts +++ b/src/modules/task-manager/controllers/check-list-item.controller.ts @@ -5,7 +5,11 @@ import { CreateCheckListItemDto } from "../dto/check-list-item/create-check-list import { UpdateCheckListItemDto } from "../dto/check-list-item/update-check-list-item.dto"; import { CursorPaginationDto } from "../../../common/DTO/cursor-pagination.dto"; import { TMCheckListItem } from "../entities/check-list-item.entity"; +import { AuthGuards } from "../../../common/decorators/auth-guard.decorator"; +import { AdminRoute } from "../../../common/decorators/admin.decorator"; +@AuthGuards() +@AdminRoute() @ApiTags("Task Manager - Check List Items") @Controller("task-manager/check-list-items") export class CheckListItemController { diff --git a/src/modules/task-manager/providers/check-list-item.service.ts b/src/modules/task-manager/providers/check-list-item.service.ts index d02a78f..95cc633 100644 --- a/src/modules/task-manager/providers/check-list-item.service.ts +++ b/src/modules/task-manager/providers/check-list-item.service.ts @@ -19,7 +19,7 @@ export class CheckListItemService { const limit = pagination.limit ?? 10; const cursorDate = pagination.cursor ? new Date(pagination.cursor) : undefined; - const [data] = await this.checkListItemRepository.findAll(limit, cursorDate); + const [data] = await this.checkListItemRepository.findAllItems(limit, cursorDate); return buildCursorPageFormat(data, limit, cursorDate); } diff --git a/src/modules/task-manager/repositories/check-list-item.repository.ts b/src/modules/task-manager/repositories/check-list-item.repository.ts index 2b5d931..6f0eae3 100644 --- a/src/modules/task-manager/repositories/check-list-item.repository.ts +++ b/src/modules/task-manager/repositories/check-list-item.repository.ts @@ -4,14 +4,15 @@ import { Repository, MoreThan } from 'typeorm'; import { TMCheckListItem } from '../entities/check-list-item.entity'; @Injectable() -export class CheckListItemRepository { +export class CheckListItemRepository extends Repository{ constructor( - @InjectRepository(TMCheckListItem) - private readonly repository: Repository, - ) {} + @InjectRepository(TMCheckListItem) checkListItemRepository: Repository, + ) { + super(checkListItemRepository.target, checkListItemRepository.manager, checkListItemRepository.queryRunner); + } - findAll(limit: number, cursor?: Date) { - return this.repository.findAndCount({ + findAllItems(limit: number, cursor?: Date) { + return this.findAndCount({ where: cursor ? { createdAt: MoreThan(cursor) } : {}, order: { createdAt: 'ASC' }, take: limit + 1, @@ -20,7 +21,7 @@ export class CheckListItemRepository { } findByTask(taskId: string, limit: number, cursor?: Date) { - return this.repository.findAndCount({ + return this.findAndCount({ where: cursor ? { taskId, createdAt: MoreThan(cursor) } : { taskId }, @@ -31,32 +32,16 @@ export class CheckListItemRepository { } findOneById(id: string) { - return this.repository.findOne({ + return this.findOne({ where: { id }, relations: ['task'], }); } findOneByCreatedAt(createdAt: Date) { - return this.repository.findOne({ + return this.findOne({ where: { createdAt }, order: { createdAt: 'ASC' }, }); } - - create(data: Partial) { - return this.repository.create(data); - } - - save(checkListItem: TMCheckListItem) { - return this.repository.save(checkListItem); - } - - update(id: string, data: Partial) { - return this.repository.update(id, data); - } - - delete(id: string) { - return this.repository.delete(id); - } } \ No newline at end of file From eb6fbc3ce51f88743acb01ef41aeff5251f7d2ce Mon Sep 17 00:00:00 2001 From: realrafi Date: Tue, 7 Jul 2026 11:59:05 +0330 Subject: [PATCH 56/66] refactor: refactored attachment entirely --- src/common/enums/message.enum.ts | 4 +++ .../controllers/attachment.controller.ts | 6 +++- .../providers/attachment.service.ts | 19 ++++++------ .../repositories/attachment.repository.ts | 31 +++++-------------- 4 files changed, 26 insertions(+), 34 deletions(-) diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 2c456d5..467513c 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -945,4 +945,8 @@ export const enum TaskMessage { export const enum remarkMessage { REMARK_NOT_FOUND = "برچسب مورد نظر پیدا نشد!" +} + +export const enum attachmentMessage { + ATTACHMENT_NOT_FOUND = "ضمیمه پیدا نشد!" } \ No newline at end of file diff --git a/src/modules/task-manager/controllers/attachment.controller.ts b/src/modules/task-manager/controllers/attachment.controller.ts index 7c004a7..8a4fe1d 100644 --- a/src/modules/task-manager/controllers/attachment.controller.ts +++ b/src/modules/task-manager/controllers/attachment.controller.ts @@ -4,7 +4,11 @@ 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 { @@ -25,7 +29,7 @@ export class AttachmentController { @ApiResponse({ status: 200, description: 'Attachment found', type: TMAttachment }) @ApiResponse({ status: 404, description: 'Attachment not found' }) findOne(@Param('id') id: string): Promise { - return this.attachmentService.findOne(id); + return this.attachmentService.findOneOrFail(id); } @Post() diff --git a/src/modules/task-manager/providers/attachment.service.ts b/src/modules/task-manager/providers/attachment.service.ts index a22b76b..6854d30 100644 --- a/src/modules/task-manager/providers/attachment.service.ts +++ b/src/modules/task-manager/providers/attachment.service.ts @@ -1,15 +1,16 @@ import { Injectable, NotFoundException } from "@nestjs/common"; import { AttachmentRepository } from "../repositories/attachment.repository"; -import { TaskRepository } from "../repositories/task.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 taskRepository: TaskRepository, + private readonly taskService: TaskService, ) {} findAll(): Promise { @@ -20,26 +21,24 @@ export class AttachmentService { return this.attachmentRepository.findByTask(taskId); } - async findOne(id: string): Promise { + async findOneOrFail(id: string): Promise { const attachment = await this.attachmentRepository.findOneById(id); - if (!attachment) throw new NotFoundException(`Attachment #${id} not found`); + if (!attachment) throw new NotFoundException(attachmentMessage.ATTACHMENT_NOT_FOUND); return attachment; } async create(dto: CreateAttachmentDto): Promise { - const task = await this.taskRepository.findOneById(dto.taskId); - if (!task) throw new NotFoundException(`Task #${dto.taskId} not found`); + await this.taskService.findOneOrFail(dto.taskId); const attachment = this.attachmentRepository.create(dto); return this.attachmentRepository.save(attachment); } async update(id: string, dto: UpdateAttachmentDto): Promise { - const attachment = await this.findOne(id); + const attachment = await this.findOneOrFail(id); if (dto.taskId) { - const task = await this.taskRepository.findOneById(dto.taskId); - if (!task) throw new NotFoundException(`Task #${dto.taskId} not found`); + await this.taskService.findOneOrFail(dto.taskId); } Object.assign(attachment, dto); @@ -47,7 +46,7 @@ export class AttachmentService { } async remove(id: string): Promise { - await this.findOne(id); + await this.findOneOrFail(id); await this.attachmentRepository.delete(id); } } diff --git a/src/modules/task-manager/repositories/attachment.repository.ts b/src/modules/task-manager/repositories/attachment.repository.ts index 9fd3f26..c8b2c9d 100644 --- a/src/modules/task-manager/repositories/attachment.repository.ts +++ b/src/modules/task-manager/repositories/attachment.repository.ts @@ -4,44 +4,29 @@ import { Repository } from 'typeorm'; import { TMAttachment } from '../entities/attachment.entity'; @Injectable() -export class AttachmentRepository { +export class AttachmentRepository extends Repository{ constructor( - @InjectRepository(TMAttachment) - private readonly repository: Repository, - ) {} + @InjectRepository(TMAttachment) attachmentRepository: Repository, + ) { + super(attachmentRepository.target, attachmentRepository.manager, attachmentRepository.queryRunner); + } findAll() { - return this.repository.find({ + return this.find({ relations: ['task'], }); } findByTask(taskId: string) { - return this.repository.find({ + return this.find({ where: { taskId }, }); } findOneById(id: string) { - return this.repository.findOne({ + return this.findOne({ where: { id }, relations: ['task'], }); } - - create(data: Partial) { - return this.repository.create(data); - } - - save(attachment: TMAttachment) { - return this.repository.save(attachment); - } - - update(id: string, data: Partial) { - return this.repository.update(id, data); - } - - delete(id: string) { - return this.repository.delete(id); - } } \ No newline at end of file From a22d6ceb8162b18fa29dc01aebb2bcee74eb4975 Mon Sep 17 00:00:00 2001 From: realrafi Date: Tue, 7 Jul 2026 12:03:42 +0330 Subject: [PATCH 57/66] refactor: making the checklistitem service to use enum messages --- src/common/enums/message.enum.ts | 4 ++++ .../controllers/check-list-item.controller.ts | 2 +- .../providers/check-list-item.service.ts | 19 +++++++++---------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 467513c..3cd2337 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -947,6 +947,10 @@ export const enum remarkMessage { REMARK_NOT_FOUND = "برچسب مورد نظر پیدا نشد!" } +export const enum checkListItemMessage { + CHECKLISTITEM_NOT_FOUND = "آیتم چک لیست پیدا نشد!" +} + export const enum attachmentMessage { ATTACHMENT_NOT_FOUND = "ضمیمه پیدا نشد!" } \ No newline at end of file diff --git a/src/modules/task-manager/controllers/check-list-item.controller.ts b/src/modules/task-manager/controllers/check-list-item.controller.ts index 45a9f40..2e38072 100644 --- a/src/modules/task-manager/controllers/check-list-item.controller.ts +++ b/src/modules/task-manager/controllers/check-list-item.controller.ts @@ -32,7 +32,7 @@ export class CheckListItemController { @ApiResponse({ status: 200, description: "CheckListItem found", type: TMCheckListItem }) @ApiResponse({ status: 404, description: "CheckListItem not found" }) findOne(@Param("id") id: string): Promise { - return this.checkListItemService.findOne(id); + return this.checkListItemService.findOneOrFail(id); } @Post() diff --git a/src/modules/task-manager/providers/check-list-item.service.ts b/src/modules/task-manager/providers/check-list-item.service.ts index 95cc633..0a8fb8e 100644 --- a/src/modules/task-manager/providers/check-list-item.service.ts +++ b/src/modules/task-manager/providers/check-list-item.service.ts @@ -1,18 +1,19 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { CheckListItemRepository } from '../repositories/check-list-item.repository'; -import { TaskRepository } from '../repositories/task.repository'; import { TMCheckListItem } from '../entities/check-list-item.entity'; import { CreateCheckListItemDto } from '../dto/check-list-item/create-check-list-item.dto'; import { UpdateCheckListItemDto } from '../dto/check-list-item/update-check-list-item.dto'; import { CursorPaginationDto } from '../../../common/DTO/cursor-pagination.dto'; import { CursorPaginatedResult } from '../../../common/interfaces/cursor-paginated-result.interface'; import { buildCursorPageFormat } from '../../../common/helpers/cursor-paginated.helper'; +import { checkListItemMessage } from '../../../common/enums/message.enum'; +import { TaskService } from './task.service'; @Injectable() export class CheckListItemService { constructor( private readonly checkListItemRepository: CheckListItemRepository, - private readonly taskRepository: TaskRepository, + private readonly taskService: TaskService, ) {} async findAll(pagination: CursorPaginationDto): Promise> { @@ -36,26 +37,24 @@ export class CheckListItemService { return buildCursorPageFormat(data, limit, cursorDate); } - async findOne(id: string): Promise { + async findOneOrFail(id: string): Promise { 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; } async create(dto: CreateCheckListItemDto): Promise { - const task = await this.taskRepository.findOneById(dto.taskId); - if (!task) throw new NotFoundException(`Task #${dto.taskId} not found`); + await this.taskService.findOneOrFail(dto.taskId); const checkListItem = this.checkListItemRepository.create(dto); return this.checkListItemRepository.save(checkListItem); } async update(id: string, dto: UpdateCheckListItemDto): Promise { - const checkListItem = await this.findOne(id); + const checkListItem = await this.findOneOrFail(id); if (dto.taskId) { - const task = await this.taskRepository.findOneById(dto.taskId); - if (!task) throw new NotFoundException(`Task #${dto.taskId} not found`); + await this.taskService.findOneOrFail(dto.taskId); } Object.assign(checkListItem, dto); @@ -63,7 +62,7 @@ export class CheckListItemService { } async remove(id: string): Promise { - const checkListItem = await this.findOne(id); + const checkListItem = await this.findOneOrFail(id); await this.checkListItemRepository.delete(id); return checkListItem; } From 29d647556c08b31a6adf79c0daa3c63710943600 Mon Sep 17 00:00:00 2001 From: realrafi Date: Tue, 7 Jul 2026 15:53:18 +0330 Subject: [PATCH 58/66] refactor: changed the get workspaces by user logic --- .../controllers/workspace.controller.ts | 24 +++---------------- .../providers/workspace.service.ts | 6 ++++- src/modules/users/enums/permission.enum.ts | 4 +++- 3 files changed, 11 insertions(+), 23 deletions(-) diff --git a/src/modules/task-manager/controllers/workspace.controller.ts b/src/modules/task-manager/controllers/workspace.controller.ts index c92c142..8f53df3 100644 --- a/src/modules/task-manager/controllers/workspace.controller.ts +++ b/src/modules/task-manager/controllers/workspace.controller.ts @@ -4,12 +4,12 @@ import { WorkspaceService } from '../providers/workspace.service'; import { CreateWorkspaceDto } from '../dto/workspace/create-workspace.dto'; import { UpdateWorkspaceDto } from '../dto/workspace/update-workspace.dto'; import { TMWorkspace } from '../entities/workspace.entity'; -// import { 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'; import { UserDec } from '../../../common/decorators/user.decorator'; import { AuthGuards } from '../../../common/decorators/auth-guard.decorator'; +import { ITokenPayload } from '../../auth/interfaces/IToken-payload'; @AuthGuards() @AdminRoute() @@ -18,24 +18,6 @@ import { AuthGuards } from '../../../common/decorators/auth-guard.decorator'; export class WorkspaceController { constructor(private readonly workspaceService: WorkspaceService) {} - @Get() - @PermissionsDec(PermissionEnum.WORKSPACE) - @ApiOperation({ summary: 'Get all workspaces' }) - @ApiResponse({ status: 200, description: 'List of workspaces', type: [TMWorkspace] }) - findAll(): Promise { - return this.workspaceService.findAll(); - } - - @Get(':id') - @PermissionsDec(PermissionEnum.WORKSPACE) - @ApiOperation({ summary: 'Get a workspace by ID' }) - @ApiParam({ name: 'id', description: 'Workspace ID' }) - @ApiResponse({ status: 200, description: 'Workspace found', type: TMWorkspace }) - @ApiResponse({ status: 404, description: 'Workspace not found' }) - findOne(@Param('id') id: string): Promise { - return this.workspaceService.findOneOrFail(id); - } - @Post() @PermissionsDec(PermissionEnum.WORKSPACE) @ApiOperation({ summary: 'Create a new workspace' }) @@ -68,7 +50,7 @@ export class WorkspaceController { @ApiOperation({summary: 'Get User WorkSpaces'}) @ApiResponse({ status: 200, description: 'Got Workspaces successfully!' }) @ApiResponse({ status: 404, description: 'Bad Request!' }) - findByUser(@UserDec('id') userId: string) { - return this.workspaceService.findByUser(userId); + findByUser(@UserDec() user: ITokenPayload) { + return this.workspaceService.findByUser(user.id, user.permissions); } } \ No newline at end of file diff --git a/src/modules/task-manager/providers/workspace.service.ts b/src/modules/task-manager/providers/workspace.service.ts index 73940fc..d983d02 100644 --- a/src/modules/task-manager/providers/workspace.service.ts +++ b/src/modules/task-manager/providers/workspace.service.ts @@ -7,6 +7,7 @@ import { CreateWorkspaceDto } from "../dto/workspace/create-workspace.dto"; import { UpdateWorkspaceDto } from "../dto/workspace/update-workspace.dto"; import { User } from "../../users/entities/user.entity"; import { WorkSpaceMessage } from "../../../common/enums/message.enum"; +import { PermissionEnum } from "../../users/enums/permission.enum"; @Injectable() export class WorkspaceService { @@ -68,7 +69,10 @@ export class WorkspaceService { return workspace; } - async findByUser(userId: string): Promise { + async findByUser(userId: string, permissions: PermissionEnum[]): Promise { + if(permissions.includes(PermissionEnum.WORKSPACE)) { + return await this.workspaceRepository.find(); + } return await this.workspaceRepository.findByUser(userId); } } diff --git a/src/modules/users/enums/permission.enum.ts b/src/modules/users/enums/permission.enum.ts index 779f6c9..82043ae 100755 --- a/src/modules/users/enums/permission.enum.ts +++ b/src/modules/users/enums/permission.enum.ts @@ -26,8 +26,10 @@ export enum PermissionEnum { DMAIL = 'dmail', RESELLER="reseller", WORKSPACE="workspace", + VIEW_ALL_WORKSPACES="view_all_workspaces", PROJECT="project", - TASK_PHASE="task_phase", + VIEW_ALL_PROJECTS="view_all_projects", TASK="task", + TASK_PHASE="task_phase", REMARK="remark" } From ff08da133009b07f6b2decc9a7fbb8c6db5f9d1a Mon Sep 17 00:00:00 2001 From: realrafi Date: Wed, 8 Jul 2026 10:27:01 +0330 Subject: [PATCH 59/66] refactor: changed project functions logic of permissions --- ...772-TMaddingSomeNewPermissionsToTheEnum.ts | 20 ++++++++ src/common/enums/message.enum.ts | 5 +- .../controllers/project.controller.ts | 46 +++++------------ .../task-manager/providers/project.service.ts | 50 ++++++++----------- .../providers/workspace.service.ts | 2 +- src/modules/users/enums/permission.enum.ts | 4 +- 6 files changed, 61 insertions(+), 66 deletions(-) create mode 100644 database/migrations/1783428145772-TMaddingSomeNewPermissionsToTheEnum.ts diff --git a/database/migrations/1783428145772-TMaddingSomeNewPermissionsToTheEnum.ts b/database/migrations/1783428145772-TMaddingSomeNewPermissionsToTheEnum.ts new file mode 100644 index 0000000..f0ee3a6 --- /dev/null +++ b/database/migrations/1783428145772-TMaddingSomeNewPermissionsToTheEnum.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class TMaddingSomeNewPermissionsToTheEnum1783428145772 implements MigrationInterface { + name = 'TMaddingSomeNewPermissionsToTheEnum1783428145772' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TYPE "public"."permission_name_enum" RENAME TO "permission_name_enum_old"`); + await queryRunner.query(`CREATE TYPE "public"."permission_name_enum" AS ENUM('services', 'customers', 'agents', 'developers', 'invoices', 'transactions', 'discounts', 'admins', 'tickets', 'criticisms', 'contacts_us', 'advertisements', 'announcements', 'blogs', 'learnings', 'logs', 'settings', 'bank_accounts', 'payments', 'manage_sso_clients', 'support_plan', 'dmenu', 'dkala', 'dpage', 'dmail', 'reseller', 'workspace', 'view_all_workspaces', 'project', 'view_all_projects', 'task', 'task_phase', 'remark')`); + await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum" USING "name"::"text"::"public"."permission_name_enum"`); + await queryRunner.query(`DROP TYPE "public"."permission_name_enum_old"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TYPE "public"."permission_name_enum_old" AS ENUM('services', 'customers', 'agents', 'developers', 'invoices', 'transactions', 'discounts', 'admins', 'tickets', 'criticisms', 'contacts_us', 'advertisements', 'announcements', 'blogs', 'learnings', 'logs', 'settings', 'bank_accounts', 'payments', 'manage_sso_clients', 'support_plan', 'dmenu', 'dkala', 'dpage', 'dmail', 'reseller', 'workspace', 'project', 'task_phase')`); + await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum_old" USING "name"::"text"::"public"."permission_name_enum_old"`); + await queryRunner.query(`DROP TYPE "public"."permission_name_enum"`); + await queryRunner.query(`ALTER TYPE "public"."permission_name_enum_old" RENAME TO "permission_name_enum"`); + } + +} diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 3cd2337..a8cd15e 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -922,14 +922,15 @@ export const enum WorkSpaceMessage { USERS_DONT_BELONG_TO_THIS_WORKSPACE = "کاربران تعیین شده برای این فضای کاری نیستند!", WORKSPACE_SAME_NAME_EXISTS = "فضای کاری با این اسم از قبل وجود دارد!", WORKSPACE_ID_PARAM_NOT_EMPTY = "پارامتر شناسه فضای کار نباید خالی باشد!", - WORKSPACE_ID_UUID = "شناسه فضای کار باید uuid باشد" + WORKSPACE_ID_UUID = "شناسه فضای کار باید uuid باشد", + FORBIDDEN = "شما اجازه دسترسی به این فضای کار را ندارید و یا عضو آن نیستید!" } export const enum ProjectMessage { PROJECT_EXISTS = "پروژه مورد نظر شما از قبل وجود دارد!", PROJECT_NOT_FOUND = "پروژه مورد نظر شما وجود ندارد!", PROJECT_CONTAINS_TASK_PHASES = "پروژه شامل فاز تسک است. لطفا ابتدا فاز تسک های این پروژه را حذف کنید.", - NOT_MEMBER_OF_PROJECT = "شما عضو این پروژه نیستید!" + NOT_MEMBER_OF_PROJECT = "شما عضو این پروژه نیستید!", } export const enum TaskPhaseMessage { diff --git a/src/modules/task-manager/controllers/project.controller.ts b/src/modules/task-manager/controllers/project.controller.ts index 3da243d..d63207b 100644 --- a/src/modules/task-manager/controllers/project.controller.ts +++ b/src/modules/task-manager/controllers/project.controller.ts @@ -11,6 +11,7 @@ import { PermissionEnum } from "../../users/enums/permission.enum"; import { UserDec } from "../../../common/decorators/user.decorator"; import { AuthGuards } from "../../../common/decorators/auth-guard.decorator"; import { ParamDto } from "../../../common/DTO/param.dto"; +import { ITokenPayload } from "../../auth/interfaces/IToken-payload"; @AuthGuards() @AdminRoute() @@ -19,24 +20,6 @@ import { ParamDto } from "../../../common/DTO/param.dto"; export class ProjectController { constructor(private readonly projectService: ProjectService) {} - @Get() - @PermissionsDec(PermissionEnum.PROJECT) - @ApiOperation({ summary: "Get all projects, optionally filtered by workspace" }) - @ApiResponse({ status: 200, description: "Paginated list of projects" }) - findAll(@Query() pagination: PaginationDto): Promise<{ projects: TMProject[]; count: number; paginate: true }> { - return this.projectService.findAll(pagination); - } - - @Get(":id") - @PermissionsDec(PermissionEnum.PROJECT) - @ApiOperation({ summary: "Get a project by ID" }) - @ApiParam({ name: "id", description: "Project ID" }) - @ApiResponse({ status: 200, description: "Project found", type: TMProject }) - @ApiResponse({ status: 404, description: "Project not found" }) - findOne(@Param("id") id: string): Promise { - return this.projectService.findOneOrFail(id); - } - @Post() @PermissionsDec(PermissionEnum.PROJECT) @ApiOperation({ summary: "Create a new project" }) @@ -69,26 +52,23 @@ export class ProjectController { } @Get("user/workspace/:id") - @ApiOperation({ summary: "Get user projects!" }) + @ApiOperation({ summary: "Get user projects in a specific workspace!" }) @ApiResponse({ status: 200, description: "Got user projects successfully!" }) - @ApiResponse({status: 400, description: "Bad Request!"}) - findUserProjects( - @UserDec("id") userId: string, - @Param() paramDto : ParamDto, + @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.findUserProjects(userId, paramDto.id, pagination); + return this.projectService.findUserProjectsByWorkspace(user.id, user.permissions, paramDto.id, pagination); } @Get("project/:id") - @ApiOperation({summary: "Get project detail"}) - @ApiResponse({status: 200, description: "Got Project Detail Successfully!"}) - @ApiResponse({status: 400, description: "Bad request!"}) - @ApiResponse({status: 404, description: "The project was not found!"}) - getProjectDetail(@Param() paramDto: ParamDto, - @UserDec('id') userId: string -): Promise { - return this.projectService.getProjectDetail(paramDto.id, userId); + @ApiOperation({ summary: "Get project detail" }) + @ApiResponse({ status: 200, description: "Got Project Detail Successfully!" }) + @ApiResponse({ status: 400, description: "Bad request!" }) + @ApiResponse({ status: 404, description: "The project was not found!" }) + getProjectDetail(@Param() paramDto: ParamDto, @UserDec() user: ITokenPayload): Promise { + return this.projectService.getProjectDetail(paramDto.id, user.id, user.permissions); } - } diff --git a/src/modules/task-manager/providers/project.service.ts b/src/modules/task-manager/providers/project.service.ts index df8054e..384d6f6 100644 --- a/src/modules/task-manager/providers/project.service.ts +++ b/src/modules/task-manager/providers/project.service.ts @@ -9,6 +9,7 @@ import { PaginationDto } from "../../../common/DTO/pagination.dto"; import { User } from "../../users/entities/user.entity"; import { ProjectMessage, WorkSpaceMessage } from "../../../common/enums/message.enum"; import { WorkspaceService } from "./workspace.service"; +import { PermissionEnum } from "../../users/enums/permission.enum"; @Injectable() export class ProjectService { @@ -19,29 +20,6 @@ export class ProjectService { private readonly userRepository: Repository, ) {} - async findAll(pagination: PaginationDto): Promise<{ projects: TMProject[]; count: number; paginate: true }> { - const page = pagination.page ?? 1; - const limit = pagination.limit ?? 10; - const skip = (page - 1) * limit; - - const [projects, count] = await this.projectRepository.findAndCount({ - skip: skip, - take: limit, - relations: { - users: true, - }, - select: { - users: { - id: true, - firstName: true, - lastName: true, - }, - }, - }); - - return { projects, count, paginate: true }; - } - async findOneOrFail(id: string): Promise { const project = await this.projectRepository.findOne({ where: { id } }); if (!project) throw new NotFoundException(ProjectMessage.PROJECT_NOT_FOUND); @@ -124,21 +102,36 @@ export class ProjectService { return project; } - async findUserProjects( + 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 [projects, count] = await this.projectRepository.findUserProjects(userId, wsId, skip, limit); + const isPermissionIncluded = permissions.includes(PermissionEnum.VIEW_ALL_PROJECTS); + const isUserMemeberOfWorkspace = workspace.users.some((user) => user.id === userId); + + var res: [TMProject[], number]; + if (isUserMemeberOfWorkspace) { + res = await this.projectRepository.findUserProjects(userId, wsId, skip, limit); + } else if (isPermissionIncluded) { + res = await this.projectRepository.findAndCount({ where: { workspaceId: wsId } }); + } else { + throw new ForbiddenException(WorkSpaceMessage.FORBIDDEN); + } + + const [projects, count] = res; return { projects, count, paginate: true }; } - async getProjectDetail(projectId: string, userId: string): Promise { + async getProjectDetail(projectId: string, userId: string, permissions: PermissionEnum[]): Promise { const project = await this.projectRepository.findOne({ where: { id: projectId }, relations: { @@ -147,8 +140,9 @@ export class ProjectService { }); if (!project) throw new NotFoundException(ProjectMessage.PROJECT_NOT_FOUND); - const isUserMember = project.users.some(user => user.id === userId); - if(!isUserMember) throw new ForbiddenException(ProjectMessage.NOT_MEMBER_OF_PROJECT); + 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); diff --git a/src/modules/task-manager/providers/workspace.service.ts b/src/modules/task-manager/providers/workspace.service.ts index d983d02..931ecf7 100644 --- a/src/modules/task-manager/providers/workspace.service.ts +++ b/src/modules/task-manager/providers/workspace.service.ts @@ -70,7 +70,7 @@ export class WorkspaceService { } async findByUser(userId: string, permissions: PermissionEnum[]): Promise { - if(permissions.includes(PermissionEnum.WORKSPACE)) { + if(permissions.includes(PermissionEnum.VIEW_ALL_WORKSPACES)) { return await this.workspaceRepository.find(); } return await this.workspaceRepository.findByUser(userId); diff --git a/src/modules/users/enums/permission.enum.ts b/src/modules/users/enums/permission.enum.ts index 82043ae..7abceb7 100755 --- a/src/modules/users/enums/permission.enum.ts +++ b/src/modules/users/enums/permission.enum.ts @@ -25,9 +25,9 @@ export enum PermissionEnum { DPAGE = "dpage", DMAIL = 'dmail', RESELLER="reseller", - WORKSPACE="workspace", + WORKSPACE="workspace", // create, update, remove VIEW_ALL_WORKSPACES="view_all_workspaces", - PROJECT="project", + PROJECT="project", // create, update, remove VIEW_ALL_PROJECTS="view_all_projects", TASK="task", TASK_PHASE="task_phase", From b0668c7b2ce744546a27bb9f34f60c68b50c2f6d Mon Sep 17 00:00:00 2001 From: realrafi Date: Wed, 8 Jul 2026 10:32:45 +0330 Subject: [PATCH 60/66] refactor: removed unnecessary functions from taskphase --- .../controllers/task-phase.controller.ts | 22 ++----------------- .../providers/task-phase.service.ts | 10 --------- 2 files changed, 2 insertions(+), 30 deletions(-) diff --git a/src/modules/task-manager/controllers/task-phase.controller.ts b/src/modules/task-manager/controllers/task-phase.controller.ts index 5e3fd3d..97ff1ac 100644 --- a/src/modules/task-manager/controllers/task-phase.controller.ts +++ b/src/modules/task-manager/controllers/task-phase.controller.ts @@ -1,5 +1,5 @@ -import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from "@nestjs/common"; -import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from "@nestjs/swagger"; +import { Controller, Post, Patch, Delete, Param, Body } from "@nestjs/common"; +import { ApiTags, ApiOperation, ApiResponse, ApiParam } from "@nestjs/swagger"; import { TaskPhaseService } from "../providers/task-phase.service"; import { CreateTaskPhaseDto } from "../dto/task-phase/create-task-phase.dto"; import { UpdateTaskPhaseDto } from "../dto/task-phase/update-task-phase.dto"; @@ -17,24 +17,6 @@ import { PermissionEnum } from "../../users/enums/permission.enum"; export class TaskPhaseController { constructor(private readonly taskPhaseService: TaskPhaseService) {} - @Get() - @ApiOperation({ summary: "Get all task phases, optionally filtered by project" }) - @ApiQuery({ name: "projectId", required: false, description: "Filter task phases by project ID" }) - @ApiResponse({ status: 200, description: "List of task phases", type: [TMTaskPhase] }) - findAll(@Query("projectId") projectId?: string): Promise { - 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 { - return this.taskPhaseService.findOneOrFail(id); - } - @Post() @ApiOperation({ summary: "Create a new task phase" }) @ApiResponse({ status: 201, description: "TaskPhase created", type: TMTaskPhase }) diff --git a/src/modules/task-manager/providers/task-phase.service.ts b/src/modules/task-manager/providers/task-phase.service.ts index 11233ee..222d2c4 100644 --- a/src/modules/task-manager/providers/task-phase.service.ts +++ b/src/modules/task-manager/providers/task-phase.service.ts @@ -13,16 +13,6 @@ export class TaskPhaseService { private readonly projectService: ProjectService ) {} - findAll(): Promise { - return this.taskPhaseRepository.find(); - } - - findByProject(projectId: string): Promise { - return this.taskPhaseRepository.find({ - where: {projectId} - }); - } - async findOneOrFail(id: string): Promise { const taskPhase = await this.taskPhaseRepository.findOne({where: {id}}); if (!taskPhase) throw new NotFoundException(TaskPhaseMessage.TASK_PHASE_NOT_FOUND); From b32d613a1b16af1ab912e90a6a393bdda20c2fa1 Mon Sep 17 00:00:00 2001 From: realrafi Date: Wed, 8 Jul 2026 10:48:03 +0330 Subject: [PATCH 61/66] refactor: removed unnecessary functions from task --- .../controllers/task.controller.ts | 29 +++-------------- .../task-manager/providers/task.service.ts | 18 +++-------- .../repositories/task.repository.ts | 31 ++++++++++--------- src/modules/users/enums/permission.enum.ts | 2 +- 4 files changed, 27 insertions(+), 53 deletions(-) diff --git a/src/modules/task-manager/controllers/task.controller.ts b/src/modules/task-manager/controllers/task.controller.ts index 32160ce..e4253b4 100644 --- a/src/modules/task-manager/controllers/task.controller.ts +++ b/src/modules/task-manager/controllers/task.controller.ts @@ -1,5 +1,5 @@ -import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger'; +import { Controller, Get, Post, Patch, Delete, Param, Body } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger'; import { TaskService } from '../providers/task.service'; import { CreateTaskDto } from '../dto/task/create-task.dto'; import { UpdateTaskDto } from '../dto/task/update-task.dto'; @@ -11,6 +11,7 @@ import { UserDec } from '../../../common/decorators/user.decorator'; import { ParamDto } from '../../../common/DTO/param.dto'; import { PermissionsDec } from '../../../common/decorators/permission.decorator'; import { PermissionEnum } from '../../users/enums/permission.enum'; +import { ITokenPayload } from '../../auth/interfaces/IToken-payload'; @AuthGuards() @AdminRoute() @@ -19,26 +20,6 @@ import { PermissionEnum } from '../../users/enums/permission.enum'; export class TaskController { constructor(private readonly taskService: TaskService) {} - @Get() - @PermissionsDec(PermissionEnum.TASK) - @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 { - if (taskPhaseId) return this.taskService.findByPhase(taskPhaseId); - return this.taskService.findAll(); - } - - @Get(':id') - @PermissionsDec(PermissionEnum.TASK) - @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 { - return this.taskService.findOneOrFail(id); - } - @Post() @PermissionsDec(PermissionEnum.TASK) @ApiOperation({ summary: 'Create a new task' }) @@ -85,9 +66,9 @@ export class TaskController { @ApiResponse({ status: 200, description: 'Task detail received!' }) @ApiResponse({ status: 404, description: 'Task not found' }) getTaskDetail( - @UserDec('id') userId: string, + @UserDec() user: ITokenPayload, @Param() paramDto: ParamDto ): Promise { - return this.taskService.getTaskDetail(userId, paramDto.id); + return this.taskService.getTaskDetail(user.id, user.permissions, paramDto.id); } } \ No newline at end of file diff --git a/src/modules/task-manager/providers/task.service.ts b/src/modules/task-manager/providers/task.service.ts index e46f02e..a5d840c 100644 --- a/src/modules/task-manager/providers/task.service.ts +++ b/src/modules/task-manager/providers/task.service.ts @@ -10,6 +10,7 @@ import { User } from "../../users/entities/user.entity"; import { TaskMessage, TaskPhaseMessage } from "../../../common/enums/message.enum"; import { TaskPhaseRepository } from "../repositories/task-phase.repository"; import { TMTaskPhase } from "../entities/task-phase.entity"; +import { PermissionEnum } from "../../users/enums/permission.enum"; @Injectable() export class TaskService { @@ -20,22 +21,12 @@ export class TaskService { private readonly userRepository: Repository, ) {} - async findAll(): Promise { - return this.taskRepository.find({ - relations: ["taskPhase", "remarks", "attachments", "checkListItems", "users"], - }); - } - - async findByPhase(taskPhaseId: string): Promise { - return this.taskRepository.find({ where: { taskPhaseId } }); - } - async findOneOrFail(id: string): Promise { const task = await this.taskRepository.findOne({ where: { id }, relations: { taskPhase: true, - users: true + users: true, }, }); if (!task) throw new NotFoundException(TaskMessage.TASK_NOT_FOUND); @@ -125,11 +116,12 @@ export class TaskService { await this.taskRepository.delete(id); } - async getTaskDetail(userId: string, taskId: string): Promise { + async getTaskDetail(userId: string, permissions: PermissionEnum[], taskId: string): Promise { const task = await this.findOneOrFail(taskId); const isTaskMember = task.users.some((user) => user.id === userId); - if(!isTaskMember) throw new ForbiddenException(TaskMessage.USER_NOT_TASK_MEMBER); + const hasPermission = permissions.includes(PermissionEnum.TASK); + if (!isTaskMember && !hasPermission) throw new ForbiddenException(TaskMessage.USER_NOT_TASK_MEMBER); const taskDetail = await this.taskRepository.getTaskDetail(taskId); return taskDetail; diff --git a/src/modules/task-manager/repositories/task.repository.ts b/src/modules/task-manager/repositories/task.repository.ts index 26e76b6..68af884 100644 --- a/src/modules/task-manager/repositories/task.repository.ts +++ b/src/modules/task-manager/repositories/task.repository.ts @@ -22,21 +22,22 @@ export class TaskRepository extends Repository { qb.where("completedCheckListItems.isDone = :isDone", { isDone: true }), ) - .select(["remark.id", - "remark.title", - "remark.color", - "checkListItem.id", - "checkListItem.title", - "checkListItem.isDone", - "attachment.id", - "attachment.title", - "attachment.type", - "attachment.file", - - "user.id", - "user.firstName", - "user.lastName" - ]) + .select([ + "remark.id", + "remark.title", + "remark.color", + "checkListItem.id", + "checkListItem.title", + "checkListItem.isDone", + "attachment.id", + "attachment.title", + "attachment.type", + "attachment.file", + + "user.id", + "user.firstName", + "user.lastName", + ]) .where("task.id = :taskId", { taskId }) .getOne(); } diff --git a/src/modules/users/enums/permission.enum.ts b/src/modules/users/enums/permission.enum.ts index 7abceb7..2605a6f 100755 --- a/src/modules/users/enums/permission.enum.ts +++ b/src/modules/users/enums/permission.enum.ts @@ -29,7 +29,7 @@ export enum PermissionEnum { VIEW_ALL_WORKSPACES="view_all_workspaces", PROJECT="project", // create, update, remove VIEW_ALL_PROJECTS="view_all_projects", - TASK="task", + TASK="task", // read, create, update, remove TASK_PHASE="task_phase", REMARK="remark" } From 8481a0e86fe5ec397b8afe9ce5a35c293d3d7a8a Mon Sep 17 00:00:00 2001 From: realrafi Date: Wed, 8 Jul 2026 10:50:40 +0330 Subject: [PATCH 62/66] refactor: removed unnecessary functions from remark --- .../controllers/remark.controller.ts | 22 ++----------------- .../task-manager/providers/remark.service.ts | 13 ----------- 2 files changed, 2 insertions(+), 33 deletions(-) diff --git a/src/modules/task-manager/controllers/remark.controller.ts b/src/modules/task-manager/controllers/remark.controller.ts index d5933b9..9818f58 100644 --- a/src/modules/task-manager/controllers/remark.controller.ts +++ b/src/modules/task-manager/controllers/remark.controller.ts @@ -1,5 +1,5 @@ -import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from "@nestjs/common"; -import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from "@nestjs/swagger"; +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"; @@ -17,24 +17,6 @@ import { PermissionEnum } from "../../users/enums/permission.enum"; export class RemarkController { constructor(private readonly remarkService: RemarkService) {} - @Get() - @ApiOperation({ summary: "Get all remarks, optionally filtered by task" }) - @ApiQuery({ name: "taskId", required: false, description: "Filter remarks by task ID" }) - @ApiResponse({ status: 200, description: "List of remarks", type: [TMRemark] }) - findAll(@Query("taskId") taskId?: string): Promise { - if (taskId) return this.remarkService.findByTask(taskId); - return this.remarkService.findAll(); - } - - @Get(":id") - @ApiOperation({ summary: "Get a remark by ID" }) - @ApiParam({ name: "id", description: "Remark ID" }) - @ApiResponse({ status: 200, description: "Remark found", type: TMRemark }) - @ApiResponse({ status: 404, description: "Remark not found" }) - findOne(@Param("id") id: string): Promise { - return this.remarkService.findOneOrFail(id); - } - @Post() @ApiOperation({ summary: "Create a new remark" }) @ApiResponse({ status: 201, description: "Remark created", type: TMRemark }) diff --git a/src/modules/task-manager/providers/remark.service.ts b/src/modules/task-manager/providers/remark.service.ts index f48ed1f..50fe024 100644 --- a/src/modules/task-manager/providers/remark.service.ts +++ b/src/modules/task-manager/providers/remark.service.ts @@ -13,19 +13,6 @@ export class RemarkService { private readonly taskService: TaskService, ) {} - findAll(): Promise { - return this.remarkRepository.find({ relations: { task: true } }); - } - - findByTask(taskId: string): Promise { - return this.remarkRepository.find({ - where: { taskId }, - relations: { - task: true, - }, - }); - } - async findOneOrFail(id: string): Promise { const remark = await this.remarkRepository.findOne({ where: { id } }); if (!remark) throw new NotFoundException(remarkMessage.REMARK_NOT_FOUND); From 677fc5b384075c458b53314bdd3190a08ed994b9 Mon Sep 17 00:00:00 2001 From: realrafi Date: Wed, 8 Jul 2026 11:08:28 +0330 Subject: [PATCH 63/66] refactor: removed some functions and added toggleStatus to checklistitem --- .../controllers/check-list-item.controller.ts | 35 +++++++------------ .../providers/check-list-item.service.ts | 33 +++++------------ 2 files changed, 21 insertions(+), 47 deletions(-) diff --git a/src/modules/task-manager/controllers/check-list-item.controller.ts b/src/modules/task-manager/controllers/check-list-item.controller.ts index 2e38072..8dd0a39 100644 --- a/src/modules/task-manager/controllers/check-list-item.controller.ts +++ b/src/modules/task-manager/controllers/check-list-item.controller.ts @@ -1,12 +1,12 @@ -import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from "@nestjs/common"; -import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from "@nestjs/swagger"; +import { Controller, Post, Patch, Delete, Param, Body } from "@nestjs/common"; +import { ApiTags, ApiOperation, ApiResponse, ApiParam } from "@nestjs/swagger"; import { CheckListItemService } from "../providers/check-list-item.service"; import { CreateCheckListItemDto } from "../dto/check-list-item/create-check-list-item.dto"; import { UpdateCheckListItemDto } from "../dto/check-list-item/update-check-list-item.dto"; -import { CursorPaginationDto } from "../../../common/DTO/cursor-pagination.dto"; import { TMCheckListItem } from "../entities/check-list-item.entity"; import { AuthGuards } from "../../../common/decorators/auth-guard.decorator"; import { AdminRoute } from "../../../common/decorators/admin.decorator"; +import { ParamDto } from "../../../common/DTO/param.dto"; @AuthGuards() @AdminRoute() @@ -15,26 +15,6 @@ import { AdminRoute } from "../../../common/decorators/admin.decorator"; export class CheckListItemController { constructor(private readonly checkListItemService: CheckListItemService) {} - @Get() - @ApiOperation({ summary: "Get all check list items, optionally filtered by task" }) - @ApiQuery({ name: "taskId", required: false, description: "Filter check list items by task ID" }) - @ApiQuery({ name: "cursor", required: false, description: "Cursor for pagination (createdAt ISO string)" }) - @ApiQuery({ name: "limit", required: false, description: "Number of records to return (max 50)" }) - @ApiResponse({ status: 200, description: "Cursor-paginated list of check list items" }) - findAll(@Query() pagination: CursorPaginationDto, @Query("taskId") taskId?: string) { - if (taskId) return this.checkListItemService.findByTask(taskId, pagination); - return this.checkListItemService.findAll(pagination); - } - - @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 { - return this.checkListItemService.findOneOrFail(id); - } - @Post() @ApiOperation({ summary: "Create a new check list item" }) @ApiResponse({ status: 201, description: "CheckListItem created", type: TMCheckListItem }) @@ -60,4 +40,13 @@ export class CheckListItemController { remove(@Param("id") id: string): Promise { return this.checkListItemService.remove(id); } + + @Patch("change-status/:id") + @ApiOperation({summary: "change status of a check list item."}) + @ApiParam({name: "id", description: "CheckListItem ID"}) + @ApiResponse({status: 200, description: "successfully changed the status!"}) + @ApiResponse({ status: 404, description: "CheckListItem not found" }) + changeStatus(@Param() paramDto: ParamDto) { + return this.checkListItemService.toggleStatus(paramDto.id); + } } diff --git a/src/modules/task-manager/providers/check-list-item.service.ts b/src/modules/task-manager/providers/check-list-item.service.ts index 0a8fb8e..1259e78 100644 --- a/src/modules/task-manager/providers/check-list-item.service.ts +++ b/src/modules/task-manager/providers/check-list-item.service.ts @@ -3,9 +3,6 @@ import { CheckListItemRepository } from '../repositories/check-list-item.reposit import { TMCheckListItem } from '../entities/check-list-item.entity'; import { CreateCheckListItemDto } from '../dto/check-list-item/create-check-list-item.dto'; import { UpdateCheckListItemDto } from '../dto/check-list-item/update-check-list-item.dto'; -import { CursorPaginationDto } from '../../../common/DTO/cursor-pagination.dto'; -import { CursorPaginatedResult } from '../../../common/interfaces/cursor-paginated-result.interface'; -import { buildCursorPageFormat } from '../../../common/helpers/cursor-paginated.helper'; import { checkListItemMessage } from '../../../common/enums/message.enum'; import { TaskService } from './task.service'; @@ -16,27 +13,6 @@ export class CheckListItemService { private readonly taskService: TaskService, ) {} - async findAll(pagination: CursorPaginationDto): Promise> { - const limit = pagination.limit ?? 10; - const cursorDate = pagination.cursor ? new Date(pagination.cursor) : undefined; - - const [data] = await this.checkListItemRepository.findAllItems(limit, cursorDate); - - return buildCursorPageFormat(data, limit, cursorDate); - } - - async findByTask( - taskId: string, - pagination: CursorPaginationDto, - ): Promise> { - const limit = pagination.limit ?? 10; - const cursorDate = pagination.cursor ? new Date(pagination.cursor) : undefined; - - const [data] = await this.checkListItemRepository.findByTask(taskId, limit, cursorDate); - - return buildCursorPageFormat(data, limit, cursorDate); - } - async findOneOrFail(id: string): Promise { const checkListItem = await this.checkListItemRepository.findOneById(id); if (!checkListItem) throw new NotFoundException(checkListItemMessage.CHECKLISTITEM_NOT_FOUND); @@ -66,4 +42,13 @@ export class CheckListItemService { await this.checkListItemRepository.delete(id); return checkListItem; } + + async toggleStatus(id: string) : Promise { + const checkListItem = await this.findOneOrFail(id); + const isDone = checkListItem.isDone; + checkListItem.isDone = !isDone; + await this.checkListItemRepository.save(checkListItem); + + return checkListItem; + } } \ No newline at end of file From 83486924819bb78576caffcd1e6fa216c50339f7 Mon Sep 17 00:00:00 2001 From: realrafi Date: Wed, 8 Jul 2026 11:09:43 +0330 Subject: [PATCH 64/66] refactor: updated permissions --- src/modules/task-manager/controllers/remark.controller.ts | 3 --- src/modules/users/enums/permission.enum.ts | 1 - 2 files changed, 4 deletions(-) diff --git a/src/modules/task-manager/controllers/remark.controller.ts b/src/modules/task-manager/controllers/remark.controller.ts index 9818f58..66d539d 100644 --- a/src/modules/task-manager/controllers/remark.controller.ts +++ b/src/modules/task-manager/controllers/remark.controller.ts @@ -6,12 +6,9 @@ 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"; -import { PermissionsDec } from "../../../common/decorators/permission.decorator"; -import { PermissionEnum } from "../../users/enums/permission.enum"; @AuthGuards() @AdminRoute() -@PermissionsDec(PermissionEnum.REMARK) @ApiTags("Task Manager - Remarks") @Controller("task-manager/remarks") export class RemarkController { diff --git a/src/modules/users/enums/permission.enum.ts b/src/modules/users/enums/permission.enum.ts index 2605a6f..d59cdc8 100755 --- a/src/modules/users/enums/permission.enum.ts +++ b/src/modules/users/enums/permission.enum.ts @@ -31,5 +31,4 @@ export enum PermissionEnum { VIEW_ALL_PROJECTS="view_all_projects", TASK="task", // read, create, update, remove TASK_PHASE="task_phase", - REMARK="remark" } From 95c947a5874057fadda77ef5a7cbb8e37d75858f Mon Sep 17 00:00:00 2001 From: realrafi Date: Wed, 8 Jul 2026 11:15:20 +0330 Subject: [PATCH 65/66] refactor: removed unnecessary functions from attachment --- .../controllers/attachment.controller.ts | 22 ++----------------- .../providers/attachment.service.ts | 12 ++-------- .../repositories/attachment.repository.ts | 19 ---------------- 3 files changed, 4 insertions(+), 49 deletions(-) diff --git a/src/modules/task-manager/controllers/attachment.controller.ts b/src/modules/task-manager/controllers/attachment.controller.ts index 8a4fe1d..ec5f554 100644 --- a/src/modules/task-manager/controllers/attachment.controller.ts +++ b/src/modules/task-manager/controllers/attachment.controller.ts @@ -1,5 +1,5 @@ -import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger'; +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'; @@ -14,24 +14,6 @@ import { AdminRoute } from '../../../common/decorators/admin.decorator'; export class AttachmentController { constructor(private readonly attachmentService: AttachmentService) {} - @Get() - @ApiOperation({ summary: 'Get all attachments, optionally filtered by task' }) - @ApiQuery({ name: 'taskId', required: false, description: 'Filter attachments by task ID' }) - @ApiResponse({ status: 200, description: 'List of attachments', type: [TMAttachment] }) - findAll(@Query('taskId') taskId?: string): Promise { - if (taskId) return this.attachmentService.findByTask(taskId); - return this.attachmentService.findAll(); - } - - @Get(':id') - @ApiOperation({ summary: 'Get an attachment by ID' }) - @ApiParam({ name: 'id', description: 'Attachment ID' }) - @ApiResponse({ status: 200, description: 'Attachment found', type: TMAttachment }) - @ApiResponse({ status: 404, description: 'Attachment not found' }) - findOne(@Param('id') id: string): Promise { - return this.attachmentService.findOneOrFail(id); - } - @Post() @ApiOperation({ summary: 'Create a new attachment' }) @ApiResponse({ status: 201, description: 'Attachment created', type: TMAttachment }) diff --git a/src/modules/task-manager/providers/attachment.service.ts b/src/modules/task-manager/providers/attachment.service.ts index 6854d30..72166ab 100644 --- a/src/modules/task-manager/providers/attachment.service.ts +++ b/src/modules/task-manager/providers/attachment.service.ts @@ -4,7 +4,7 @@ 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'; +import { TaskService } from "./task.service"; @Injectable() export class AttachmentService { @@ -13,16 +13,8 @@ export class AttachmentService { private readonly taskService: TaskService, ) {} - findAll(): Promise { - return this.attachmentRepository.findAll(); - } - - findByTask(taskId: string): Promise { - return this.attachmentRepository.findByTask(taskId); - } - async findOneOrFail(id: string): Promise { - const attachment = await this.attachmentRepository.findOneById(id); + const attachment = await this.attachmentRepository.findOne({ where: { id }, relations: { task: true } }); if (!attachment) throw new NotFoundException(attachmentMessage.ATTACHMENT_NOT_FOUND); return attachment; } diff --git a/src/modules/task-manager/repositories/attachment.repository.ts b/src/modules/task-manager/repositories/attachment.repository.ts index c8b2c9d..a27d990 100644 --- a/src/modules/task-manager/repositories/attachment.repository.ts +++ b/src/modules/task-manager/repositories/attachment.repository.ts @@ -10,23 +10,4 @@ export class AttachmentRepository extends Repository{ ) { super(attachmentRepository.target, attachmentRepository.manager, attachmentRepository.queryRunner); } - - findAll() { - return this.find({ - relations: ['task'], - }); - } - - findByTask(taskId: string) { - return this.find({ - where: { taskId }, - }); - } - - findOneById(id: string) { - return this.findOne({ - where: { id }, - relations: ['task'], - }); - } } \ No newline at end of file From 4cb20f6c0d3a39b7f36187d6dec84cd666400c92 Mon Sep 17 00:00:00 2001 From: realrafi Date: Wed, 8 Jul 2026 11:16:32 +0330 Subject: [PATCH 66/66] refactor: removed unnecessary methods from checkListItem repo --- .../check-list-item.repository.ts | 36 +------------------ 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/src/modules/task-manager/repositories/check-list-item.repository.ts b/src/modules/task-manager/repositories/check-list-item.repository.ts index 6f0eae3..f64782e 100644 --- a/src/modules/task-manager/repositories/check-list-item.repository.ts +++ b/src/modules/task-manager/repositories/check-list-item.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, MoreThan } from 'typeorm'; +import { Repository } from 'typeorm'; import { TMCheckListItem } from '../entities/check-list-item.entity'; @Injectable() @@ -10,38 +10,4 @@ export class CheckListItemRepository extends Repository{ ) { super(checkListItemRepository.target, checkListItemRepository.manager, checkListItemRepository.queryRunner); } - - findAllItems(limit: number, cursor?: Date) { - return this.findAndCount({ - where: cursor ? { createdAt: MoreThan(cursor) } : {}, - order: { createdAt: 'ASC' }, - take: limit + 1, - relations: ['task'], - }); - } - - findByTask(taskId: string, limit: number, cursor?: Date) { - return this.findAndCount({ - where: cursor - ? { taskId, createdAt: MoreThan(cursor) } - : { taskId }, - order: { createdAt: 'ASC' }, - take: limit + 1, - relations: ['task'], - }); - } - - findOneById(id: string) { - return this.findOne({ - where: { id }, - relations: ['task'], - }); - } - - findOneByCreatedAt(createdAt: Date) { - return this.findOne({ - where: { createdAt }, - order: { createdAt: 'ASC' }, - }); - } } \ No newline at end of file