135dika revised this gist 1 month ago. Go to revision
1 file changed, 13 insertions, 5 deletions
action-plan.docs.md
| @@ -77,8 +77,12 @@ PUT /:activityId/plans set plans | |||
| 77 | 77 | POST /:activityId/members add member | |
| 78 | 78 | DELETE /:activityId/members/:userId remove member | |
| 79 | 79 | GET /:activityId/members list members | |
| 80 | - | POST /:activityId/feedbacks create feedback | |
| 81 | - | GET /:activityId/feedbacks list feedbacks | |
| 80 | + | ||
| 81 | + | // Activity progress feedbacks | |
| 82 | + | POST /:activityId/progress/:pid/feedbacks create feedback | |
| 83 | + | GET /:activityId/progress/:pid/feedbacks list feedbacks | |
| 84 | + | PATCH /:activityId/progress/:pid/feedbacks/:fid edit feedback | |
| 85 | + | DELETE /:activityId/progress/:pid/feedbacks/:fid delete feedback | |
| 82 | 86 | ||
| 83 | 87 | // Task CRUD + leaf | |
| 84 | 88 | POST /:activityId/tasks create task | |
| @@ -95,8 +99,12 @@ PUT /:activityId/tasks/:taskId/plans | |||
| 95 | 99 | POST /:activityId/tasks/:taskId/members add member | |
| 96 | 100 | DELETE /:activityId/tasks/:taskId/members/:uid remove member | |
| 97 | 101 | GET /:activityId/tasks/:taskId/members list members | |
| 98 | - | POST /:activityId/tasks/:taskId/feedbacks feedback | |
| 99 | - | GET /:activityId/tasks/:taskId/feedbacks | |
| 102 | + | ||
| 103 | + | // Task progress feedbacks | |
| 104 | + | POST /:activityId/tasks/:tid/progress/:pid/feedbacks | |
| 105 | + | GET /:activityId/tasks/:tid/progress/:pid/feedbacks | |
| 106 | + | PATCH /:activityId/tasks/:tid/progress/:pid/feedbacks/:fid | |
| 107 | + | DELETE /:activityId/tasks/:tid/progress/:pid/feedbacks/:fid | |
| 100 | 108 | ||
| 101 | 109 | // Subtask CRUD + leaf | |
| 102 | 110 | POST /:activityId/tasks/:taskId/subtasks create subtask | |
| @@ -114,7 +122,7 @@ GET /:activityId/tasks/:taskId/subtasks/:sid/progress | |||
| 114 | 122 | GET /:activityId/tasks/:taskId/subtasks/:sid/progress/:pid | |
| 115 | 123 | POST /:activityId/tasks/:taskId/subtasks/:sid/progress/:pid/respond | |
| 116 | 124 | ||
| 117 | - | // Feedback CRUD | |
| 125 | + | // Subtask progress feedbacks | |
| 118 | 126 | POST /:aId/tasks/:tId/subtasks/:sId/progress/:pId/feedbacks | |
| 119 | 127 | GET /.../feedbacks | |
| 120 | 128 | PATCH /.../feedbacks/:fId | |
135dika revised this gist 1 month ago. Go to revision
1 file changed, 170 insertions, 808 deletions
action-plan.docs.md
| @@ -3,19 +3,10 @@ | |||
| 3 | 3 | Base URL: `{host}/api/v1` | |
| 4 | 4 | ||
| 5 | 5 | All responses wrapped in: | |
| 6 | - | ||
| 7 | 6 | ```json | |
| 8 | - | { | |
| 9 | - | "statusCode": 200, | |
| 10 | - | "message": "...", | |
| 11 | - | "data": { ... }, | |
| 12 | - | "durationMs": 42, | |
| 13 | - | "_reference": [] | |
| 14 | - | } | |
| 7 | + | { "statusCode": 200, "message": "...", "data": { ... }, "durationMs": 42, "_reference": [] } | |
| 15 | 8 | ``` | |
| 16 | 9 | ||
| 17 | - | Shown `data` inline below for brevity. | |
| 18 | - | ||
| 19 | 10 | --- | |
| 20 | 11 | ||
| 21 | 12 | ## Hierarchy & Member Counting | |
| @@ -26,873 +17,244 @@ Activity | |||
| 26 | 17 | └─ Subtask (many) | |
| 27 | 18 | ``` | |
| 28 | 19 | ||
| 29 | - | | Level | Has children? | Member count source | | |
| 30 | - | | -------- | ---------------------- | ---------------------------- | | |
| 31 | - | | Activity | Tasks with subtasks | Union of all subtask members | | |
| 32 | - | | Activity | Tasks without subtasks | Union of all task members | | |
| 33 | - | | Activity | No tasks | Activity's own members | | |
| 34 | - | | Task | Has subtasks | Union of all subtask members | | |
| 35 | - | | Task | No subtasks | Task's own members | | |
| 36 | - | | Subtask | — | Subtask's own members | | |
| 20 | + | | Level | Has children? | Member count source | | |
| 21 | + | |---|---|---| | |
| 22 | + | | Activity | Tasks with subtasks | Union of all subtask members | | |
| 23 | + | | Activity | Tasks without subtasks | Union of all task members | | |
| 24 | + | | Activity | No tasks | Activity's own members | | |
| 25 | + | | Task | Has subtasks | Union of all subtask members | | |
| 26 | + | | Task | No subtasks | Task's own members | | |
| 27 | + | | Subtask | — | Subtask's own members | | |
| 37 | 28 | ||
| 38 | 29 | Example: | |
| 39 | - | ||
| 40 | 30 | ``` | |
| 41 | - | Activity1 (10 members) ← Union(Task1, Task2) | |
| 42 | - | Task1 (5 members) ← Union(Subtask1, Subtask2) | |
| 31 | + | Activity1 (10 members) ← Union(Task1, Task2) | |
| 32 | + | Task1 (5 members) ← Union(Subtask1, Subtask2) | |
| 43 | 33 | Subtask1 (3 members) | |
| 44 | 34 | Subtask2 (2 members) | |
| 45 | - | Task2 (5 members) ← Task scope, no subtasks | |
| 35 | + | Task2 (5 members) ← Task scope, no subtasks | |
| 46 | 36 | ``` | |
| 47 | 37 | ||
| 48 | 38 | --- | |
| 49 | 39 | ||
| 50 | 40 | ## Leaf-Only Validation | |
| 51 | 41 | ||
| 52 | - | Nodes with children reject direct operations. Must use child level instead. | |
| 53 | - | ||
| 54 | - | | Operation | Activity has tasks? | Task has subtasks? | | |
| 55 | - | | ---------------- | ------------------- | ------------------ | | |
| 56 | - | | Progress | ❌ 400 "use task/subtask level" | ❌ 400 "use subtask level" | | |
| 57 | - | | Progress plans | ❌ same | ❌ same | | |
| 58 | - | | Members (CRUD) | ❌ same | ❌ same | | |
| 59 | - | ||
| 60 | - | Subtask scope always valid — no check needed. | |
| 61 | - | ||
| 62 | - | --- | |
| 63 | - | ||
| 64 | - | ## Enums | |
| 65 | - | ||
| 66 | - | ### Subtask/Task/Activity Status | |
| 67 | - | | Value | Description | | |
| 68 | - | | ----------------- | ----------------------------------- | | |
| 69 | - | | `INCOMPLETE_DATA` | Required fields missing | | |
| 70 | - | | `NOT_STARTED` | Ready but no progress | | |
| 71 | - | | `IN_PROGRESS` | Progress entries exist | | |
| 72 | - | | `NEED_APPROVAL` | Pending progress requiring response | | |
| 73 | - | | `COMPLETED` | 100% complete | | |
| 74 | - | ||
| 75 | - | ### Progress Entry Status | |
| 76 | - | | Value | Description | | |
| 77 | - | | --------------------- | ---------------------------------------------- | | |
| 78 | - | | `PENDING` | Submitted, awaiting response | | |
| 79 | - | | `APPROVED` | Accepted | | |
| 80 | - | | `APPROVED_WITH_NOTES` | Accepted with amendments (new version created) | | |
| 81 | - | | `REJECTED` | Rejected | | |
| 82 | - | ||
| 83 | - | ### ActionPlanMemberScope | |
| 84 | - | | Value | Description | | |
| 85 | - | | ---------- | --------------------------- | | |
| 86 | - | | `ACTIVITY` | Member assigned to activity | | |
| 87 | - | | `TASK` | Member assigned to task | | |
| 88 | - | | `SUBTASK` | Member assigned to subtask | | |
| 89 | - | ||
| 90 | - | --- | |
| 91 | - | ||
| 92 | - | ## Notifications | |
| 93 | - | ||
| 94 | - | All notifications fire asynchronously (fire-and-forget). Member operation success is not blocked by notification service availability. | |
| 95 | - | ||
| 96 | - | ### Member Notifications | |
| 97 | - | ||
| 98 | - | | Trigger | Type | Recipient | Message | | |
| 99 | - | | ----------------- | ------------------------------- | --------- | --------------------------------------------- | | |
| 100 | - | | Add to subtask | `action_plan_member_added` | Added user | "You have been added to `[subtask name]`" | | |
| 101 | - | | Remove from subtask | `action_plan_member_removed` | Removed user | "You have been removed from `[subtask name]`" | | |
| 102 | - | | Add to task | `action_plan_member_added` | Added user | "You have been added to `[task name]`" | | |
| 103 | - | | Remove from task | `action_plan_member_removed` | Removed user | "You have been removed from `[task name]`" | | |
| 104 | - | | Add to activity | `action_plan_member_added` | Added user | "You have been added to `[activity name]`" | | |
| 105 | - | | Remove from activity | `action_plan_member_removed` | Removed user | "You have been removed from `[activity name]`" | | |
| 106 | - | ||
| 107 | - | Each includes `projectId` and `data` with relevant scope IDs. | |
| 108 | - | ||
| 109 | - | ### Progress Respond Notifications | |
| 110 | - | ||
| 111 | - | | Response | Type | Recipient | Message | | |
| 112 | - | | ----------------- | --------------------------------------- | --------- | -------------------------------------------- | | |
| 113 | - | | APPROVE | `action_plan_progress_approve` | Submitter | "Your progress on `[name]` was approved" | | |
| 114 | - | | APPROVE_WITH_NOTES | `action_plan_progress_approve_with_notes` | Submitter | "Your progress on `[name]` was approved with notes" | | |
| 115 | - | | REJECT | `action_plan_progress_reject` | Submitter | "Your progress on `[name]` was rejected" | | |
| 116 | - | ||
| 117 | - | Name resolves from whichever scope the progress belongs to (subtask/task/activity). | |
| 118 | - | ||
| 119 | - | --- | |
| 120 | - | ||
| 121 | - | ## Routes | |
| 42 | + | Nodes with children reject direct progress/plans/members operations. | |
| 122 | 43 | ||
| 123 | - | ### 1. Activities | |
| 124 | - | ||
| 125 | - | Base: `/api/projects/:projectId/action-plans` | |
| 126 | - | ||
| 127 | - | #### `POST /` | |
| 128 | - | Create activity with optional nested tasks & subtasks. | |
| 129 | - | ||
| 130 | - | **Body:** | |
| 131 | - | ```jsonc | |
| 132 | - | { | |
| 133 | - | "name": "Activity name", // string, 1-255 | |
| 134 | - | "tasks": [ // optional, max 50 | |
| 135 | - | { | |
| 136 | - | "name": "Task name", // string, 1-255 | |
| 137 | - | "workWeight": 100, // optional, number >= 0 | |
| 138 | - | "workVolume": 500, // optional, number >= 0 | |
| 139 | - | "workVolumeUnit": "m2", // optional, string max 100 | |
| 140 | - | "startDate": "2026-01-01", // optional, ISO date | |
| 141 | - | "endDate": "2026-03-31", // optional, ISO date | |
| 142 | - | "subtasks": [ // optional, max 50 | |
| 143 | - | { | |
| 144 | - | "name": "Subtask name", // string, 1-255 | |
| 145 | - | "workVolume": 250, // optional, number >= 0 | |
| 146 | - | "workVolumeUnit": "m2", // optional, string max 100 | |
| 147 | - | "workWeight": 50, // optional, number >= 0 | |
| 148 | - | "startDate": "2026-01-01", // optional, ISO date | |
| 149 | - | "endDate": "2026-02-15" // optional, ISO date | |
| 150 | - | } | |
| 151 | - | ] | |
| 152 | - | } | |
| 153 | - | ] | |
| 154 | - | } | |
| 155 | - | ``` | |
| 156 | - | ||
| 157 | - | **Response:** Full activity with derived stats (same shape as GET /:id). | |
| 158 | - | ||
| 159 | - | --- | |
| 160 | - | ||
| 161 | - | #### `GET /` | |
| 162 | - | List activities for a project. Paginated, searchable. | |
| 163 | - | ||
| 164 | - | **Query:** | |
| 165 | - | | Param | Type | Default | Description | | |
| 166 | - | |---|---|---|---| | |
| 167 | - | | `page` | number | 1 | Page number | | |
| 168 | - | | `limit` | number | 20 | Items per page (max 100) | | |
| 169 | - | | `search` | string | — | Filter by name | | |
| 170 | - | | `sort_by` | `created_at` \| `updated_at` \| `name` | `created_at` | Sort field | | |
| 171 | - | | `sort_order` | `asc` \| `desc` | `desc` | Sort direction | | |
| 172 | - | ||
| 173 | - | **Response:** | |
| 174 | - | ```jsonc | |
| 175 | - | { | |
| 176 | - | "items": [ | |
| 177 | - | { | |
| 178 | - | "id": "uuid", | |
| 179 | - | "projectId": "uuid", | |
| 180 | - | "name": "Activity name", | |
| 181 | - | "createdBy": "uuid", | |
| 182 | - | "createdAt": "ISO date", | |
| 183 | - | "updatedAt": "ISO date", | |
| 184 | - | "deletedAt": null, | |
| 185 | - | "taskCount": 2, | |
| 186 | - | "subtaskCount": 5, | |
| 187 | - | "workWeight": 150 | |
| 188 | - | } | |
| 189 | - | ], | |
| 190 | - | "total": 10, | |
| 191 | - | "page": 1, | |
| 192 | - | "limit": 20 | |
| 193 | - | } | |
| 194 | - | ``` | |
| 195 | - | ||
| 196 | - | --- | |
| 197 | - | ||
| 198 | - | #### `GET /tree` | |
| 199 | - | Full Activity > Task > Subtask tree. Each level has accumulated stats. | |
| 200 | - | ||
| 201 | - | **Query:** | |
| 202 | - | | Param | Type | Default | Description | | |
| 203 | - | |---|---|---|---| | |
| 204 | - | | `status` | string | — | Filter subtasks by status | | |
| 205 | - | ||
| 206 | - | **Response:** | |
| 207 | - | ```jsonc | |
| 208 | - | [ | |
| 209 | - | { | |
| 210 | - | "id": "uuid", | |
| 211 | - | "name": "Activity name", | |
| 212 | - | "status": "IN_PROGRESS", | |
| 213 | - | "totalTasks": 2, | |
| 214 | - | "totalUsersAssigned": 10, // memberCount (bottom-up) | |
| 215 | - | "accumulatedProgress": 45.5, // avg(), percent | |
| 216 | - | "createdBy": "uuid", | |
| 217 | - | "createdAt": "ISO date", | |
| 218 | - | "updatedAt": "ISO date", | |
| 219 | - | "tasks": [ | |
| 220 | - | { | |
| 221 | - | "id": "uuid", | |
| 222 | - | "name": "Task name", | |
| 223 | - | "status": "IN_PROGRESS", | |
| 224 | - | "totalSubtasks": 2, | |
| 225 | - | "totalUsersAssigned": 5, // memberCount (bottom-up) | |
| 226 | - | "accumulatedProgress": 60, // avg(), percent | |
| 227 | - | "workWeight": 100, | |
| 228 | - | "workVolume": 500, | |
| 229 | - | "workVolumeUnit": "m2", | |
| 230 | - | "startDate": "ISO date", | |
| 231 | - | "endDate": "ISO date", | |
| 232 | - | "subtasks": [ | |
| 233 | - | { | |
| 234 | - | "id": "uuid", | |
| 235 | - | "name": "Subtask name", | |
| 236 | - | "status": "IN_PROGRESS", | |
| 237 | - | "totalUsersAssigned": 3, // memberCount | |
| 238 | - | "progress": 75, // effective (non-rejected), percent | |
| 239 | - | "workVolume": 250, | |
| 240 | - | "workVolumeUnit": "m2", | |
| 241 | - | "workWeight": 50, | |
| 242 | - | "startDate": "ISO date", | |
| 243 | - | "endDate": "ISO date" | |
| 244 | - | } | |
| 245 | - | ] | |
| 246 | - | } | |
| 247 | - | ] | |
| 248 | - | } | |
| 249 | - | ] | |
| 250 | - | ``` | |
| 251 | - | ||
| 252 | - | --- | |
| 253 | - | ||
| 254 | - | #### `GET /tree/gantt` | |
| 255 | - | Same tree structure but each node includes `startDate`/`endDate` derived from children. | |
| 256 | - | Activity `startDate` = min of all subtasks. Activity `endDate` = max of all subtasks. | |
| 257 | - | Task dates use own stored values. Supports `status` query filter (same as `/tree`). | |
| 258 | - | ||
| 259 | - | --- | |
| 260 | - | ||
| 261 | - | #### `GET /:id` | |
| 262 | - | Get single activity with derived stats. | |
| 263 | - | ||
| 264 | - | **Response:** | |
| 265 | - | ```jsonc | |
| 266 | - | { | |
| 267 | - | "id": "uuid", | |
| 268 | - | "projectId": "uuid", | |
| 269 | - | "name": "Activity name", | |
| 270 | - | "createdBy": "uuid", | |
| 271 | - | "createdAt": "ISO date", | |
| 272 | - | "updatedAt": "ISO date", | |
| 273 | - | "deletedAt": null, | |
| 274 | - | "taskCount": 2, | |
| 275 | - | "subtaskCount": 5, | |
| 276 | - | "memberCount": 10, // bottom-up (see rules above) | |
| 277 | - | "status": "IN_PROGRESS", | |
| 278 | - | "progress": 45.5, // avg(), percent | |
| 279 | - | "workWeight": 150 | |
| 280 | - | } | |
| 281 | - | ``` | |
| 282 | - | ||
| 283 | - | --- | |
| 284 | - | ||
| 285 | - | #### `PATCH /:id` | |
| 286 | - | Update activity. If activity has tasks, only `name` is editable. | |
| 287 | - | ||
| 288 | - | **Body:** | |
| 289 | - | ```jsonc | |
| 290 | - | { | |
| 291 | - | "name": "New name" // string, 1-255, optional | |
| 292 | - | } | |
| 293 | - | ``` | |
| 294 | - | ||
| 295 | - | --- | |
| 296 | - | ||
| 297 | - | #### `DELETE /:id` | |
| 298 | - | Soft-delete activity and all its tasks + subtasks. No body. Returns `204` status code. | |
| 299 | - | ||
| 300 | - | --- | |
| 301 | - | ||
| 302 | - | ### 2. Tasks | |
| 303 | - | ||
| 304 | - | Base: `/api/action-plans/activities/:activityId/tasks` | |
| 305 | - | ||
| 306 | - | #### `POST /` | |
| 307 | - | Create task under an activity. | |
| 308 | - | ||
| 309 | - | **Body:** | |
| 310 | - | ```jsonc | |
| 311 | - | { | |
| 312 | - | "name": "Task name", // string, 1-255 | |
| 313 | - | "workWeight": 100, // optional, number >= 0 | |
| 314 | - | "workVolume": 500, // optional, number >= 0 | |
| 315 | - | "workVolumeUnit": "m2", // optional, string max 100 | |
| 316 | - | "startDate": "2026-01-01", // optional, ISO date | |
| 317 | - | "endDate": "2026-03-31" // optional, ISO date | |
| 318 | - | } | |
| 319 | - | ``` | |
| 320 | - | ||
| 321 | - | **Response:** Task object with `id`, `name`, `workWeight`, `workVolume`, `workVolumeUnit`, `startDate`, `endDate`, `createdAt`. | |
| 322 | - | ||
| 323 | - | --- | |
| 324 | - | ||
| 325 | - | #### `GET /` | |
| 326 | - | List all tasks under an activity. | |
| 44 | + | | Operation | Activity has tasks? | Task has subtasks? | | |
| 45 | + | |---|---|---| | |
| 46 | + | | Progress | ❌ 400 | ❌ 400 | | |
| 47 | + | | Plans | ❌ 400 | ❌ 400 | | |
| 48 | + | | Members | ❌ 400 | ❌ 400 | | |
| 327 | 49 | ||
| 328 | - | **Response:** | |
| 329 | - | ```jsonc | |
| 330 | - | { | |
| 331 | - | "tasks": [ | |
| 332 | - | { | |
| 333 | - | "id": "uuid", | |
| 334 | - | "name": "Task name", | |
| 335 | - | "workWeight": 100, | |
| 336 | - | "workVolume": 500, | |
| 337 | - | "workVolumeUnit": "m2", | |
| 338 | - | "startDate": "ISO date", | |
| 339 | - | "endDate": "ISO date", | |
| 340 | - | "createdAt": "ISO date", | |
| 341 | - | "updatedAt": "ISO date", | |
| 342 | - | "subtaskCount": 3 | |
| 343 | - | } | |
| 344 | - | ] | |
| 345 | - | } | |
| 346 | - | ``` | |
| 50 | + | Subtask always valid. | |
| 347 | 51 | ||
| 348 | 52 | --- | |
| 349 | 53 | ||
| 350 | - | #### `GET /:id` | |
| 351 | - | Get task detail with derived stats. | |
| 54 | + | ## Route Map | |
| 352 | 55 | ||
| 353 | - | **Response:** | |
| 354 | - | ```jsonc | |
| 355 | - | { | |
| 356 | - | "id": "uuid", | |
| 357 | - | "activityId": "uuid", | |
| 358 | - | "name": "Task name", | |
| 359 | - | "workWeight": 150, | |
| 360 | - | "workVolume": 500, | |
| 361 | - | "workVolumeUnit": "m2", | |
| 362 | - | "startDate": "ISO date", | |
| 363 | - | "endDate": "ISO date", | |
| 364 | - | "createdBy": "uuid", | |
| 365 | - | "createdAt": "ISO date", | |
| 366 | - | "updatedAt": "ISO date", | |
| 367 | - | "subtaskCount": 3, | |
| 368 | - | "memberCount": 5, // bottom-up (see rules above) | |
| 369 | - | "status": "IN_PROGRESS", | |
| 370 | - | "progress": 60, // avg(), percent | |
| 371 | - | "canAddSubtask": false | |
| 372 | - | } | |
| 373 | - | ``` | |
| 374 | - | ||
| 375 | - | `canAddSubtask` is `false` if the task has subtasks and any subtask is beyond `INCOMPLETE_DATA` status. | |
| 56 | + | All routes under: `api/projects/:projectId/action-plans` | |
| 376 | 57 | ||
| 377 | - | --- | |
| 378 | - | ||
| 379 | - | #### `PATCH /:id` | |
| 380 | - | Update task. If task has subtasks, only `name` is editable. | |
| 381 | - | ||
| 382 | - | **Body:** | |
| 383 | - | ```jsonc | |
| 384 | - | { | |
| 385 | - | "name": "New name", // string, 1-255, optional | |
| 386 | - | "workWeight": 200, // optional (only when no subtasks) | |
| 387 | - | "workVolume": 600, // optional (only when no subtasks) | |
| 388 | - | "workVolumeUnit": "km2", // optional (only when no subtasks) | |
| 389 | - | "startDate": "2026-01-01", // optional (only when no subtasks) | |
| 390 | - | "endDate": "2026-06-30" // optional (only when no subtasks) | |
| 391 | - | } | |
| 392 | 58 | ``` | |
| 393 | - | ||
| 394 | - | --- | |
| 395 | - | ||
| 396 | - | #### `DELETE /:id` | |
| 397 | - | Soft-delete task and all its subtasks. No body. | |
| 398 | - | ||
| 399 | - | --- | |
| 400 | - | ||
| 401 | - | ### 3. Subtasks | |
| 402 | - | ||
| 403 | - | Base: `/api/action-plans/tasks/:taskId/subtasks` | |
| 404 | - | ||
| 405 | - | #### `POST /` | |
| 406 | - | Create subtask under a task. | |
| 407 | - | ||
| 408 | - | **Body:** | |
| 409 | - | ```jsonc | |
| 410 | - | { | |
| 411 | - | "name": "Subtask name", // string, 1-255 | |
| 412 | - | "workVolume": 250, // optional, number >= 0 | |
| 413 | - | "workVolumeUnit": "m2", // optional, string max 100 | |
| 414 | - | "workWeight": 50, // optional, number >= 0 | |
| 415 | - | "startDate": "2026-01-01", // optional, ISO date | |
| 416 | - | "endDate": "2026-02-15" // optional, ISO date | |
| 417 | - | } | |
| 59 | + | GET / list activities | |
| 60 | + | POST / create activity | |
| 61 | + | GET /tree tree view | |
| 62 | + | GET /tree/gantt gantt | |
| 63 | + | GET /analytics analytics | |
| 64 | + | GET /import/template/download excel template | |
| 65 | + | POST /import import | |
| 66 | + | GET /:activityId get activity | |
| 67 | + | PATCH /:activityId update activity | |
| 68 | + | DELETE /:activityId delete activity | |
| 69 | + | ||
| 70 | + | // Activity leaf | |
| 71 | + | POST /:activityId/progress create progress | |
| 72 | + | GET /:activityId/progress list progress | |
| 73 | + | GET /:activityId/progress/:progressId get progress | |
| 74 | + | POST /:activityId/progress/:progressId/respond respond | |
| 75 | + | GET /:activityId/plans plans (S-curve) | |
| 76 | + | PUT /:activityId/plans set plans | |
| 77 | + | POST /:activityId/members add member | |
| 78 | + | DELETE /:activityId/members/:userId remove member | |
| 79 | + | GET /:activityId/members list members | |
| 80 | + | POST /:activityId/feedbacks create feedback | |
| 81 | + | GET /:activityId/feedbacks list feedbacks | |
| 82 | + | ||
| 83 | + | // Task CRUD + leaf | |
| 84 | + | POST /:activityId/tasks create task | |
| 85 | + | GET /:activityId/tasks list tasks | |
| 86 | + | GET /:activityId/tasks/:taskId get task | |
| 87 | + | PATCH /:activityId/tasks/:taskId update task | |
| 88 | + | DELETE /:activityId/tasks/:taskId delete task | |
| 89 | + | POST /:activityId/tasks/:taskId/progress task progress | |
| 90 | + | GET /:activityId/tasks/:taskId/progress list | |
| 91 | + | GET /:activityId/tasks/:taskId/progress/:pid get | |
| 92 | + | POST /:activityId/tasks/:taskId/progress/:pid/respond | |
| 93 | + | GET /:activityId/tasks/:taskId/plans plans | |
| 94 | + | PUT /:activityId/tasks/:taskId/plans | |
| 95 | + | POST /:activityId/tasks/:taskId/members add member | |
| 96 | + | DELETE /:activityId/tasks/:taskId/members/:uid remove member | |
| 97 | + | GET /:activityId/tasks/:taskId/members list members | |
| 98 | + | POST /:activityId/tasks/:taskId/feedbacks feedback | |
| 99 | + | GET /:activityId/tasks/:taskId/feedbacks | |
| 100 | + | ||
| 101 | + | // Subtask CRUD + leaf | |
| 102 | + | POST /:activityId/tasks/:taskId/subtasks create subtask | |
| 103 | + | GET /:activityId/tasks/:taskId/subtasks list subtasks | |
| 104 | + | GET /:activityId/tasks/:taskId/subtasks/:subtaskId | |
| 105 | + | PATCH /:activityId/tasks/:taskId/subtasks/:subtaskId | |
| 106 | + | DELETE /:activityId/tasks/:taskId/subtasks/:subtaskId | |
| 107 | + | POST /:activityId/tasks/:taskId/subtasks/:sid/members | |
| 108 | + | DELETE /:activityId/tasks/:taskId/subtasks/:sid/members/:uid | |
| 109 | + | GET /:activityId/tasks/:taskId/subtasks/:sid/members | |
| 110 | + | GET /:activityId/tasks/:taskId/subtasks/:sid/plans | |
| 111 | + | PUT /:activityId/tasks/:taskId/subtasks/:sid/plans | |
| 112 | + | POST /:activityId/tasks/:taskId/subtasks/:sid/progress | |
| 113 | + | GET /:activityId/tasks/:taskId/subtasks/:sid/progress | |
| 114 | + | GET /:activityId/tasks/:taskId/subtasks/:sid/progress/:pid | |
| 115 | + | POST /:activityId/tasks/:taskId/subtasks/:sid/progress/:pid/respond | |
| 116 | + | ||
| 117 | + | // Feedback CRUD | |
| 118 | + | POST /:aId/tasks/:tId/subtasks/:sId/progress/:pId/feedbacks | |
| 119 | + | GET /.../feedbacks | |
| 120 | + | PATCH /.../feedbacks/:fId | |
| 121 | + | DELETE /.../feedbacks/:fId | |
| 418 | 122 | ``` | |
| 419 | 123 | ||
| 420 | - | **Response:** Subtask object (see GET /:id). | |
| 421 | - | ||
| 422 | - | --- | |
| 423 | - | ||
| 424 | - | #### `GET /` | |
| 425 | - | List all subtasks under a task. | |
| 426 | - | ||
| 427 | - | **Response:** Array of subtask objects, each with `memberCount`, `members[].{userId, displayName}`, `status`, `progress`, `progressCount`. | |
| 428 | - | ||
| 429 | 124 | --- | |
| 430 | 125 | ||
| 431 | - | #### `GET /:id` | |
| 432 | - | Get subtask detail. | |
| 433 | - | ||
| 434 | - | **Response:** | |
| 435 | - | ```jsonc | |
| 436 | - | { | |
| 437 | - | "id": "uuid", | |
| 438 | - | "taskId": "uuid", | |
| 439 | - | "task": { "id": "uuid", "activityId": "uuid" }, | |
| 440 | - | "name": "Subtask name", | |
| 441 | - | "workVolume": 250, | |
| 442 | - | "workVolumeUnit": "m2", | |
| 443 | - | "workWeight": 50, | |
| 444 | - | "startDate": "ISO date", | |
| 445 | - | "endDate": "ISO date", | |
| 446 | - | "createdBy": "uuid", | |
| 447 | - | "createdAt": "ISO date", | |
| 448 | - | "updatedAt": "ISO date", | |
| 449 | - | "deletedAt": null, | |
| 450 | - | "members": [ | |
| 451 | - | { | |
| 452 | - | "id": "uuid", | |
| 453 | - | "userId": "uuid", | |
| 454 | - | "displayName": "John Doe" | |
| 455 | - | } | |
| 456 | - | ], | |
| 457 | - | "memberCount": 3, | |
| 458 | - | "status": "IN_PROGRESS", | |
| 459 | - | "progress": 75, | |
| 460 | - | "progressCount": 2, | |
| 461 | - | "progressPlanCount": 1, | |
| 462 | - | "plans": [ | |
| 463 | - | { | |
| 464 | - | "id": "uuid", | |
| 465 | - | "startDate": "ISO date", | |
| 466 | - | "endDate": "ISO date", | |
| 467 | - | "targetProgress": 50 | |
| 468 | - | } | |
| 469 | - | ] | |
| 470 | - | } | |
| 471 | - | ``` | |
| 472 | - | ||
| 473 | - | --- | |
| 474 | - | ||
| 475 | - | #### `PATCH /:id` | |
| 476 | - | Update subtask. Work weight & work volume are locked once subtask is in progress. | |
| 477 | - | ||
| 478 | - | **Body:** | |
| 479 | - | ```jsonc | |
| 480 | - | { | |
| 481 | - | "name": "New name", // string, 1-255, optional | |
| 482 | - | "workVolume": 300, // optional (locked after progress starts) | |
| 483 | - | "workVolumeUnit": "kg", // optional (locked after progress starts) | |
| 484 | - | "workWeight": 60, // optional (locked after progress starts) | |
| 485 | - | "startDate": "2026-01-01", // optional | |
| 486 | - | "endDate": "2026-02-15" // optional | |
| 487 | - | } | |
| 488 | - | ``` | |
| 489 | - | ||
| 490 | - | --- | |
| 491 | - | ||
| 492 | - | #### `DELETE /:id` | |
| 493 | - | Soft-delete subtask. No body. | |
| 126 | + | ## Enums | |
| 494 | 127 | ||
| 495 | - | --- | |
| 128 | + | ### Subtask/Task/Activity Status | |
| 129 | + | `INCOMPLETE_DATA` | `NOT_STARTED` | `IN_PROGRESS` | `NEED_APPROVAL` | `COMPLETED` | |
| 496 | 130 | ||
| 497 | - | ### 4. Progress | |
| 131 | + | ### Progress Entry Status | |
| 132 | + | `PENDING` | `APPROVED` | `APPROVED_WITH_NOTES` | `REJECTED` | |
| 498 | 133 | ||
| 499 | - | Three scopes: subtask, task, activity. | |
| 134 | + | ### ActionPlanMemberScope | |
| 135 | + | `ACTIVITY` | `TASK` | `SUBTASK` | |
| 500 | 136 | ||
| 501 | 137 | --- | |
| 502 | 138 | ||
| 503 | - | #### Subtask Progress | |
| 504 | - | ||
| 505 | - | Base: `/api/action-plans/subtasks/:subtaskId/progress` | |
| 139 | + | ## Notifications | |
| 506 | 140 | ||
| 507 | - | ##### `POST /` (multipart/form-data) | |
| 508 | - | Submit progress entry. Supports file uploads. | |
| 141 | + | Fire-and-forget. Does not block operation on failure. | |
| 509 | 142 | ||
| 510 | - | **Form fields:** | |
| 511 | - | | Field | Type | Description | | |
| 143 | + | ### Member Notifications | |
| 144 | + | | Trigger | Type | Recipient | | |
| 512 | 145 | |---|---|---| | |
| 513 | - | | `progress` | number (0-100) | New cumulative progress % | | |
| 514 | - | | `workVolume` | number | Optional work volume completed | | |
| 515 | - | | `description` | string | Optional note, max 5000 chars | | |
| 516 | - | | `images` | file[] | Image attachments, max 10 | | |
| 517 | - | | `sitemap` | file | Single PDF sitemap | | |
| 518 | - | ||
| 519 | - | Progress auto-accumulates from previous APPROVED entries. If not provided, defaults to current effective progress. | |
| 520 | - | When `workVolume` is provided with `totalVolume`, progress % is derived from workVolume ratio. | |
| 521 | - | New progress must be >= current non-REJECTED floor. | |
| 522 | - | ||
| 523 | - | **Response:** Full progress entry (see GET /:id). | |
| 524 | - | ||
| 525 | - | ##### `GET /` | |
| 526 | - | List progress history for a subtask. Paginated, newest first. | |
| 527 | - | ||
| 528 | - | **Query:** `page` (default 1), `limit` (default 50, max 100) | |
| 529 | - | ||
| 530 | - | **Response:** | |
| 531 | - | ```jsonc | |
| 532 | - | { | |
| 533 | - | "items": [ | |
| 534 | - | { | |
| 535 | - | "id": "uuid", | |
| 536 | - | "subtaskId": "uuid", | |
| 537 | - | "subtaskName": "Subtask name", | |
| 538 | - | "progress": 75, | |
| 539 | - | "workVolume": 187.5, | |
| 540 | - | "subtaskWorkVolume": 250, | |
| 541 | - | "subtaskWorkVolumeUnit": "m2", | |
| 542 | - | "description": "Completed foundation", | |
| 543 | - | "status": "APPROVED", | |
| 544 | - | "amendsId": null, | |
| 545 | - | "amendsOriginal": null, | |
| 546 | - | "amendedByCount": 0, | |
| 547 | - | "amendedBy": [], | |
| 548 | - | "approvedBy": "uuid", | |
| 549 | - | "approvedByName": "Admin", | |
| 550 | - | "approvedAt": "ISO date", | |
| 551 | - | "createdBy": "uuid", | |
| 552 | - | "createdByName": "Worker", | |
| 553 | - | "updatedBy": null, | |
| 554 | - | "updatedByName": null, | |
| 555 | - | "createdAt": "ISO date", | |
| 556 | - | "updatedAt": "ISO date", | |
| 557 | - | "images": [{ "id": "uuid", "key": "s3/path", "originalName": "photo.jpg", "url": "presigned-url" }], | |
| 558 | - | "sitemap": { "id": "uuid", "key": "s3/path", "originalName": "site.pdf", "url": "presigned-url" } | |
| 559 | - | } | |
| 560 | - | ], | |
| 561 | - | "pagination": { | |
| 562 | - | "page": 1, | |
| 563 | - | "limit": 50, | |
| 564 | - | "total": 2, | |
| 565 | - | "pages": 1, | |
| 566 | - | "hasNext": false, | |
| 567 | - | "hasPrev": false | |
| 568 | - | } | |
| 569 | - | } | |
| 570 | - | ``` | |
| 571 | - | ||
| 572 | - | ##### `GET /:id` | |
| 573 | - | Get single progress entry with full detail including images and amendment chain. | |
| 574 | - | ||
| 575 | - | **Response:** Same shape as one item in the list above. | |
| 576 | - | ||
| 577 | - | ##### `POST /:id/respond` | |
| 578 | - | Admin responds to a PENDING progress entry. Sends notification to submitter. | |
| 579 | - | ||
| 580 | - | **Body:** | |
| 581 | - | ```jsonc | |
| 582 | - | { | |
| 583 | - | "verb": "APPROVE", // "APPROVE" | "APPROVE_WITH_NOTES" | "REJECT" | |
| 584 | - | "progress": 80, // optional, 0-100 (for APPROVE_WITH_NOTES) | |
| 585 | - | "workVolume": 200, // optional (for APPROVE_WITH_NOTES) | |
| 586 | - | "description": "Adjusted" // optional (for APPROVE_WITH_NOTES) | |
| 587 | - | } | |
| 588 | - | ``` | |
| 589 | - | ||
| 590 | - | **Verbs:** | |
| 591 | - | - `APPROVE` — accepted as-is. Notifies submitter: `action_plan_progress_approve`. | |
| 592 | - | - `APPROVE_WITH_NOTES` — original marked APPROVED_WITH_NOTES + new amended entry created with admin values. Notifies submitter: `action_plan_progress_approve_with_notes`. | |
| 593 | - | - `REJECT` — rejected, doesn't count toward effective progress. Notifies submitter: `action_plan_progress_reject`. | |
| 594 | - | ||
| 595 | - | --- | |
| 596 | - | ||
| 597 | - | #### Task Progress | |
| 598 | - | ||
| 599 | - | Base: `/api/action-plans/tasks/:taskId` | |
| 600 | - | ||
| 601 | - | ##### `POST /progress` | |
| 602 | - | ##### `GET /progress` | |
| 603 | - | ##### `GET /progress/:id` | |
| 604 | - | ##### `POST /progress/:id/respond` | |
| 146 | + | | Add to subtask | `action_plan_member_added` | Added user | | |
| 147 | + | | Remove from subtask | `action_plan_member_removed` | Removed user | | |
| 148 | + | | Add to task | `action_plan_member_added` | Added user | | |
| 149 | + | | Remove from task | `action_plan_member_removed` | Removed user | | |
| 150 | + | | Add to activity | `action_plan_member_added` | Added user | | |
| 151 | + | | Remove from activity | `action_plan_member_removed` | Removed user | | |
| 605 | 152 | ||
| 606 | - | Same as subtask. Scope = TASK. Leaf-only: blocked if task has subtasks. | |
| 607 | - | ||
| 608 | - | --- | |
| 609 | - | ||
| 610 | - | #### Activity Progress | |
| 611 | - | ||
| 612 | - | Base: `/api/action-plans/activities/:activityId` | |
| 613 | - | ||
| 614 | - | ##### `POST /progress` | |
| 615 | - | ##### `GET /progress` | |
| 616 | - | ##### `GET /progress/:id` | |
| 617 | - | ##### `POST /progress/:id/respond` | |
| 618 | - | ||
| 619 | - | Same as subtask. Scope = ACTIVITY. Leaf-only: blocked if activity has tasks. | |
| 620 | - | ||
| 621 | - | --- | |
| 622 | - | ||
| 623 | - | ### 5. Progress Plans (S-Curve) | |
| 624 | - | ||
| 625 | - | Set target progress over time. All levels (subtask/task/activity) share same shape. | |
| 153 | + | ### Progress Respond Notifications | |
| 154 | + | | Verb | Type | Recipient | | |
| 155 | + | |---|---|---| | |
| 156 | + | | APPROVE | `action_plan_progress_approve` | Submitter | | |
| 157 | + | | APPROVE_WITH_NOTES | `action_plan_progress_approve_with_notes` | Submitter | | |
| 158 | + | | REJECT | `action_plan_progress_reject` | Submitter | | |
| 626 | 159 | ||
| 627 | 160 | --- | |
| 628 | 161 | ||
| 629 | - | #### Subtask Plans | |
| 162 | + | ## Request/Response Schemas | |
| 630 | 163 | ||
| 631 | - | Base: `/api/action-plans/tasks/:taskId/subtasks/:id` | |
| 632 | - | ||
| 633 | - | ##### `GET /plans` | |
| 634 | - | ||
| 635 | - | **Response:** | |
| 164 | + | ### Create Activity | |
| 165 | + | `POST /` | |
| 636 | 166 | ```jsonc | |
| 637 | 167 | { | |
| 638 | - | "duration": { | |
| 639 | - | "days": 90, | |
| 640 | - | "startDate": "2026-01-01", | |
| 641 | - | "endDate": "2026-03-31" | |
| 642 | - | }, | |
| 643 | - | "planning": [ | |
| 644 | - | { | |
| 645 | - | "id": "uuid", | |
| 646 | - | "startDate": "2026-01-01", | |
| 647 | - | "endDate": "2026-02-15", | |
| 648 | - | "targetProgress": 50 | |
| 649 | - | }, | |
| 650 | - | { | |
| 651 | - | "id": "uuid", | |
| 652 | - | "startDate": "2026-02-15", | |
| 653 | - | "endDate": "2026-03-31", | |
| 654 | - | "targetProgress": 50 | |
| 655 | - | } | |
| 656 | - | ], | |
| 657 | - | "sCurve": { | |
| 658 | - | "plan": [ | |
| 659 | - | { "date": 1704067200000, "progress": 0 }, | |
| 660 | - | { "date": 1709251200000, "progress": 50 }, | |
| 661 | - | { "date": 1711929600000, "progress": 100 } | |
| 662 | - | ], | |
| 663 | - | "actual": [ | |
| 664 | - | { "date": 1705276800000, "progress": 25, "id": "uuid" }, | |
| 665 | - | { "date": 1708300800000, "progress": 60, "id": "uuid" } | |
| 666 | - | ] | |
| 667 | - | } | |
| 168 | + | "name": "Activity name", | |
| 169 | + | "tasks": [{ "name": "Task name", "workWeight": 100, "workVolume": 500, "workVolumeUnit": "m2", "startDate": "2026-01-01", "endDate": "2026-03-31", | |
| 170 | + | "subtasks": [{ "name": "Subtask name", "workVolume": 250, "workVolumeUnit": "m2", "workWeight": 50, "startDate": "2026-01-01", "endDate": "2026-02-15" }] | |
| 171 | + | }] | |
| 668 | 172 | } | |
| 669 | 173 | ``` | |
| 670 | 174 | ||
| 671 | - | `sCurve.plan` — cumulative target over time (derived from plan segments). | |
| 672 | - | `sCurve.actual` — APPROVED entries sorted by approvedAt. Latest version per amendment chain. | |
| 673 | - | ||
| 674 | - | If no plans stored and subtask has dates, a default segment 0%→100% is returned. | |
| 175 | + | ### List Activities | |
| 176 | + | `GET /` — Query: `page`, `limit`, `search`, `sort_by` (`created_at`/`updated_at`/`name`), `sort_order` (`asc`/`desc`) | |
| 177 | + | Response: `{ items: [...], total, page, limit }` | |
| 675 | 178 | ||
| 676 | - | --- | |
| 677 | - | ||
| 678 | - | ##### `PUT /plans` | |
| 679 | - | Set (replace) all plans. Validates: continuous dates, within subtask date range, sum to 100%. | |
| 680 | - | ||
| 681 | - | **Body:** | |
| 179 | + | ### Get Single Activity | |
| 180 | + | `GET /:id` | |
| 682 | 181 | ```jsonc | |
| 683 | - | { | |
| 684 | - | "plans": [ | |
| 685 | - | { | |
| 686 | - | "startDate": "2026-01-01", | |
| 687 | - | "endDate": "2026-02-15", | |
| 688 | - | "targetProgress": 50 | |
| 689 | - | }, | |
| 690 | - | { | |
| 691 | - | "startDate": "2026-02-15", | |
| 692 | - | "endDate": "2026-03-31", | |
| 693 | - | "targetProgress": 50 | |
| 694 | - | } | |
| 695 | - | ] | |
| 696 | - | } | |
| 182 | + | { "id":"uuid", "projectId":"uuid", "name":"...", "taskCount":2, "subtaskCount":5, "memberCount":10, "status":"IN_PROGRESS", "progress":45.5, "workWeight":150 } | |
| 697 | 183 | ``` | |
| 698 | 184 | ||
| 699 | - | **Response:** Same as GET /plans. | |
| 700 | - | ||
| 701 | - | --- | |
| 702 | - | ||
| 703 | - | #### Task Plans | |
| 185 | + | ### Tree / Gantt | |
| 186 | + | `GET /tree` — Query: `status` filter. Each node has `totalUsersAssigned`, `accumulatedProgress`. | |
| 187 | + | `GET /tree/gantt` — Same + derived `startDate`/`endDate`. | |
| 704 | 188 | ||
| 705 | - | Base: `/api/action-plans/tasks/:taskId` | |
| 706 | - | ||
| 707 | - | ##### `GET /plans` | |
| 708 | - | ##### `PUT /plans` | |
| 709 | - | ||
| 710 | - | Same shape as subtask plans. Scope = TASK. Leaf-only: blocked if task has subtasks. | |
| 711 | - | ||
| 712 | - | --- | |
| 713 | - | ||
| 714 | - | #### Activity Plans | |
| 715 | - | ||
| 716 | - | Base: `/api/action-plans/activities/:activityId` | |
| 717 | - | ||
| 718 | - | ##### `GET /plans` | |
| 719 | - | ##### `PUT /plans` | |
| 720 | - | ||
| 721 | - | Same shape as subtask plans. Scope = ACTIVITY. Leaf-only: blocked if activity has tasks. | |
| 722 | - | ||
| 723 | - | --- | |
| 724 | - | ||
| 725 | - | ### 6. Members | |
| 726 | - | ||
| 727 | - | Member CRUD across all three hierarchy levels. Sends notification on add/remove. | |
| 728 | - | ||
| 729 | - | --- | |
| 730 | - | ||
| 731 | - | #### Subtask Members | |
| 732 | - | ||
| 733 | - | Base: `/api/action-plans/tasks/:taskId/subtasks/:id` | |
| 734 | - | ||
| 735 | - | ##### `POST /members` | |
| 736 | - | Add member to subtask. Sends `action_plan_member_added` notification. | |
| 737 | - | ||
| 738 | - | **Body:** | |
| 739 | - | ```jsonc | |
| 740 | - | { | |
| 741 | - | "userId": "uuid" | |
| 742 | - | } | |
| 743 | - | ``` | |
| 744 | - | ||
| 745 | - | **Response:** | |
| 189 | + | ### Progress Entry | |
| 190 | + | Response shape (all scopes): | |
| 746 | 191 | ```jsonc | |
| 747 | 192 | { | |
| 748 | - | "id": "uuid", | |
| 749 | - | "scopeType": "SUBTASK", | |
| 750 | - | "scopeId": "uuid", | |
| 751 | - | "userId": "uuid" | |
| 193 | + | "id":"uuid", "scopeType":"SUBTASK|TASK|ACTIVITY", "scopeId":"uuid", "scopeName":"name", | |
| 194 | + | "progress":75, "workVolume":187.5, "scopeWorkVolume":250, "scopeWorkVolumeUnit":"m2", | |
| 195 | + | "description":"...", "status":"APPROVED", "amendsId":null, "amendsOriginal":null, | |
| 196 | + | "amendedByCount":0, "amendedBy":[], "approvedBy":"uuid", "approvedByName":"Admin", | |
| 197 | + | "approvedAt":"ISO", "createdBy":"uuid", "createdByName":"Worker", | |
| 198 | + | "images":[{ "id":"uuid","key":"s3/path","originalName":"photo.jpg","url":"presigned-url" }], | |
| 199 | + | "sitemap":{ "id":"uuid","key":"s3/path","originalName":"site.pdf","url":"presigned-url" } | |
| 752 | 200 | } | |
| 753 | 201 | ``` | |
| 754 | 202 | ||
| 755 | - | ##### `GET /members` | |
| 756 | - | List members of a subtask. | |
| 757 | - | ||
| 758 | - | **Response:** | |
| 759 | - | ```jsonc | |
| 760 | - | [ | |
| 761 | - | { | |
| 762 | - | "id": "uuid", | |
| 763 | - | "userId": "uuid", | |
| 764 | - | "displayName": "John Doe" | |
| 765 | - | } | |
| 766 | - | ] | |
| 767 | - | ``` | |
| 768 | - | ||
| 769 | - | ##### `DELETE /members/:userId` | |
| 770 | - | Remove member from subtask. Sends `action_plan_member_removed` notification. No body. Returns deleted member object. | |
| 771 | - | ||
| 772 | - | --- | |
| 773 | - | ||
| 774 | - | #### Task Members | |
| 775 | - | ||
| 776 | - | Base: `/api/action-plans/tasks/:taskId` | |
| 777 | - | ||
| 778 | - | ##### `POST /members` | |
| 779 | - | ##### `GET /members` | |
| 780 | - | ##### `DELETE /members/:userId` | |
| 781 | - | ||
| 782 | - | Same pattern as subtask members. `scopeType = TASK`. Leaf-only: blocked if task has subtasks. | |
| 783 | - | ||
| 784 | - | --- | |
| 785 | - | ||
| 786 | - | #### Activity Members | |
| 787 | - | ||
| 788 | - | Base: `/api/action-plans/activities/:activityId` | |
| 789 | - | ||
| 790 | - | ##### `POST /members` | |
| 791 | - | ##### `GET /members` | |
| 792 | - | ##### `DELETE /members/:userId` | |
| 793 | - | ||
| 794 | - | Same pattern as subtask members. `scopeType = ACTIVITY`. Leaf-only: blocked if activity has tasks. | |
| 795 | - | ||
| 796 | - | --- | |
| 797 | - | ||
| 798 | - | ### 7. Feedbacks | |
| 799 | - | ||
| 800 | - | Discussion thread per progress entry. Sender can edit own message. | |
| 801 | - | ||
| 802 | - | Base: `/api/action-plans/progress/:progressId/feedbacks` | |
| 803 | - | ||
| 804 | - | #### `POST /` | |
| 805 | - | **Body:** | |
| 203 | + | ### Progress Create (multipart/form-data) | |
| 204 | + | `POST /:../progress` | |
| 205 | + | | Field | Type | | |
| 206 | + | |---|---| | |
| 207 | + | | `progress` | number (0-100) | | |
| 208 | + | | `workVolume` | number, optional | | |
| 209 | + | | `description` | string, max 5000 | | |
| 210 | + | | `images` | file[], max 10 | | |
| 211 | + | | `sitemap` | file, PDF | | |
| 212 | + | ||
| 213 | + | ### Progress Respond | |
| 214 | + | `POST /:../progress/:id/respond` | |
| 806 | 215 | ```jsonc | |
| 807 | - | { | |
| 808 | - | "message": "Please clarify the numbers" // string, 1-5000 | |
| 809 | - | } | |
| 216 | + | { "verb":"APPROVE|APPROVE_WITH_NOTES|REJECT", "progress":80, "workVolume":200, "description":"Adjusted" } | |
| 810 | 217 | ``` | |
| 811 | 218 | ||
| 812 | - | #### `GET /` | |
| 813 | - | List feedbacks. Paginated. **Query:** `page`, `limit`. | |
| 814 | - | ||
| 815 | - | #### `PATCH /:id` | |
| 816 | - | Edit own message. | |
| 817 | - | **Body:** | |
| 219 | + | ### Plans (S-Curve) | |
| 220 | + | `GET /:../plans` | |
| 818 | 221 | ```jsonc | |
| 819 | - | { | |
| 820 | - | "message": "Updated message" // string, 1-5000 | |
| 821 | - | } | |
| 222 | + | { "duration":{ "days":90, "startDate":"...", "endDate":"..." }, "planning":[...], "sCurve":{ "plan":[...], "actual":[...] } } | |
| 822 | 223 | ``` | |
| 224 | + | `PUT /:../plans` — `{ "plans":[{ "startDate":"...", "endDate":"...", "targetProgress":50 }] }` | |
| 823 | 225 | ||
| 824 | - | #### `DELETE /:id` | |
| 825 | - | Delete own message. No body. | |
| 226 | + | ### Members | |
| 227 | + | `POST/DELETE/GET /:../members` — `{ "userId":"uuid" }` / response: `[{ "id":"uuid","userId":"uuid","displayName":"John Doe" }]` | |
| 826 | 228 | ||
| 827 | - | --- | |
| 828 | - | ||
| 829 | - | ### 8. Analytics | |
| 830 | - | ||
| 831 | - | Base: `/api/projects/:projectId/action-plans/analytics` | |
| 832 | - | ||
| 833 | - | #### `GET /` | |
| 834 | - | **Query:** | |
| 835 | - | | Param | Type | Values | Default | | |
| 836 | - | |---|---|---|---| | |
| 837 | - | | `layout` | string | `detailed` \| `simplified` | `detailed` | | |
| 838 | - | | `granularity` | string | `daily` \| `weekly` \| `monthly` | `daily` | | |
| 839 | - | | `search` | string | — | — | | |
| 840 | - | | `startDate` | ISO date | — | — | | |
| 841 | - | | `endDate` | ISO date | — | — | | |
| 842 | - | ||
| 843 | - | --- | |
| 844 | - | ||
| 845 | - | ### 9. Import | |
| 846 | - | ||
| 847 | - | Base: `/api/projects/:projectId/action-plans/import` | |
| 229 | + | ### Feedbacks | |
| 230 | + | `POST /:../feedbacks` — `{ "message":"text" }` | |
| 231 | + | `PATCH /:../feedbacks/:id` — `{ "message":"updated" }` | |
| 848 | 232 | ||
| 849 | - | #### `GET /template/download` | |
| 850 | - | Download Excel template file. Returns `.xlsx` binary. | |
| 233 | + | ### Analytics | |
| 234 | + | `GET /analytics` — Query: `layout` (`detailed`/`simplified`), `granularity` (`daily`/`weekly`/`monthly`), `search`, `startDate`, `endDate` | |
| 851 | 235 | ||
| 852 | - | #### `POST /` (multipart/form-data) | |
| 853 | - | Import activities from spreadsheet. | |
| 854 | - | ||
| 855 | - | **Form field:** | |
| 856 | - | | Field | Type | Description | | |
| 857 | - | |---|---|---| | |
| 858 | - | | `file` | file | `.xlsx`, `.xls`, or `.csv`, max 5MB | | |
| 859 | - | ||
| 860 | - | **Response:** | |
| 861 | - | ```jsonc | |
| 862 | - | { | |
| 863 | - | "imported": 3, | |
| 864 | - | "activities": [ | |
| 865 | - | { "id": "uuid", "name": "Activity 1" }, | |
| 866 | - | { "id": "uuid", "name": "Activity 2" } | |
| 867 | - | ] | |
| 868 | - | } | |
| 869 | - | ``` | |
| 236 | + | ### Import | |
| 237 | + | `GET /import/template/download` — Excel file | |
| 238 | + | `POST /import` (multipart) — `file` (.xlsx/.xls/.csv, max 5MB) | |
| 870 | 239 | ||
| 871 | 240 | --- | |
| 872 | 241 | ||
| 873 | 242 | ## Constraint Summary | |
| 874 | 243 | ||
| 875 | - | | Constraint | Value | | |
| 876 | - | | ----------------------- | ----------- | | |
| 877 | - | | Max tasks per activity | 50 | | |
| 878 | - | | Max subtasks per task | 50 | | |
| 879 | - | | Max plans per scope | Unlimited | | |
| 880 | - | | Max images per progress | 10 | | |
| 881 | - | | Max feedback length | 5000 chars | | |
| 882 | - | | Max description length | 5000 chars | | |
| 883 | - | | Max import file size | 5MB | | |
| 884 | - | | Progress range | 0–100 | | |
| 885 | - | | Work weight/volume | Number >= 0 | | |
| 244 | + | | Constraint | Value | | |
| 245 | + | |---|---| | |
| 246 | + | | Max tasks per activity | 50 | | |
| 247 | + | | Max subtasks per task | 50 | | |
| 248 | + | | Max images per progress | 10 | | |
| 249 | + | | Max feedback/description length | 5000 chars | | |
| 250 | + | | Max import file size | 5MB | | |
| 251 | + | | Progress range | 0-100 | | |
| 252 | + | | Work weight/volume | Number >= 0 | | |
| 886 | 253 | ||
| 887 | 254 | ### Editing Rules | |
| 888 | 255 | - Activity with tasks → only `name` editable | |
| 889 | 256 | - Task with subtasks → only `name` editable | |
| 890 | - | - Subtask in progress → `workWeight` and `workVolume` locked | |
| 891 | - | ||
| 892 | - | ### Leaf-Only Blocks | |
| 893 | - | - Activity with tasks → progress, plans, members rejected (400) | |
| 894 | - | - Task with subtasks → progress, plans, members rejected (400) | |
| 895 | - | - Subtask always allowed | |
| 257 | + | - Subtask in progress → `workWeight`/`workVolume` locked | |
| 896 | 258 | ||
| 897 | 259 | ### Soft Delete | |
| 898 | - | All deletes are soft (sets `deletedAt`). Cascades: deleting activity deletes all tasks + subtasks. Deleting task deletes all subtasks. | |
| 260 | + | All deletes are soft (`deletedAt`). Cascades: deleting activity → all tasks + subtasks. Deleting task → all subtasks. | |
135dika revised this gist 1 month ago. Go to revision
1 file changed, 343 insertions, 379 deletions
action-plan.docs.md
| @@ -47,10 +47,23 @@ Activity1 (10 members) ← Union(Task1, Task2) | |||
| 47 | 47 | ||
| 48 | 48 | --- | |
| 49 | 49 | ||
| 50 | + | ## Leaf-Only Validation | |
| 51 | + | ||
| 52 | + | Nodes with children reject direct operations. Must use child level instead. | |
| 53 | + | ||
| 54 | + | | Operation | Activity has tasks? | Task has subtasks? | | |
| 55 | + | | ---------------- | ------------------- | ------------------ | | |
| 56 | + | | Progress | ❌ 400 "use task/subtask level" | ❌ 400 "use subtask level" | | |
| 57 | + | | Progress plans | ❌ same | ❌ same | | |
| 58 | + | | Members (CRUD) | ❌ same | ❌ same | | |
| 59 | + | ||
| 60 | + | Subtask scope always valid — no check needed. | |
| 61 | + | ||
| 62 | + | --- | |
| 63 | + | ||
| 50 | 64 | ## Enums | |
| 51 | 65 | ||
| 52 | 66 | ### Subtask/Task/Activity Status | |
| 53 | - | ||
| 54 | 67 | | Value | Description | | |
| 55 | 68 | | ----------------- | ----------------------------------- | | |
| 56 | 69 | | `INCOMPLETE_DATA` | Required fields missing | | |
| @@ -60,7 +73,6 @@ Activity1 (10 members) ← Union(Task1, Task2) | |||
| 60 | 73 | | `COMPLETED` | 100% complete | | |
| 61 | 74 | ||
| 62 | 75 | ### Progress Entry Status | |
| 63 | - | ||
| 64 | 76 | | Value | Description | | |
| 65 | 77 | | --------------------- | ---------------------------------------------- | | |
| 66 | 78 | | `PENDING` | Submitted, awaiting response | | |
| @@ -69,7 +81,6 @@ Activity1 (10 members) ← Union(Task1, Task2) | |||
| 69 | 81 | | `REJECTED` | Rejected | | |
| 70 | 82 | ||
| 71 | 83 | ### ActionPlanMemberScope | |
| 72 | - | ||
| 73 | 84 | | Value | Description | | |
| 74 | 85 | | ---------- | --------------------------- | | |
| 75 | 86 | | `ACTIVITY` | Member assigned to activity | | |
| @@ -78,6 +89,35 @@ Activity1 (10 members) ← Union(Task1, Task2) | |||
| 78 | 89 | ||
| 79 | 90 | --- | |
| 80 | 91 | ||
| 92 | + | ## Notifications | |
| 93 | + | ||
| 94 | + | All notifications fire asynchronously (fire-and-forget). Member operation success is not blocked by notification service availability. | |
| 95 | + | ||
| 96 | + | ### Member Notifications | |
| 97 | + | ||
| 98 | + | | Trigger | Type | Recipient | Message | | |
| 99 | + | | ----------------- | ------------------------------- | --------- | --------------------------------------------- | | |
| 100 | + | | Add to subtask | `action_plan_member_added` | Added user | "You have been added to `[subtask name]`" | | |
| 101 | + | | Remove from subtask | `action_plan_member_removed` | Removed user | "You have been removed from `[subtask name]`" | | |
| 102 | + | | Add to task | `action_plan_member_added` | Added user | "You have been added to `[task name]`" | | |
| 103 | + | | Remove from task | `action_plan_member_removed` | Removed user | "You have been removed from `[task name]`" | | |
| 104 | + | | Add to activity | `action_plan_member_added` | Added user | "You have been added to `[activity name]`" | | |
| 105 | + | | Remove from activity | `action_plan_member_removed` | Removed user | "You have been removed from `[activity name]`" | | |
| 106 | + | ||
| 107 | + | Each includes `projectId` and `data` with relevant scope IDs. | |
| 108 | + | ||
| 109 | + | ### Progress Respond Notifications | |
| 110 | + | ||
| 111 | + | | Response | Type | Recipient | Message | | |
| 112 | + | | ----------------- | --------------------------------------- | --------- | -------------------------------------------- | | |
| 113 | + | | APPROVE | `action_plan_progress_approve` | Submitter | "Your progress on `[name]` was approved" | | |
| 114 | + | | APPROVE_WITH_NOTES | `action_plan_progress_approve_with_notes` | Submitter | "Your progress on `[name]` was approved with notes" | | |
| 115 | + | | REJECT | `action_plan_progress_reject` | Submitter | "Your progress on `[name]` was rejected" | | |
| 116 | + | ||
| 117 | + | Name resolves from whichever scope the progress belongs to (subtask/task/activity). | |
| 118 | + | ||
| 119 | + | --- | |
| 120 | + | ||
| 81 | 121 | ## Routes | |
| 82 | 122 | ||
| 83 | 123 | ### 1. Activities | |
| @@ -85,36 +125,32 @@ Activity1 (10 members) ← Union(Task1, Task2) | |||
| 85 | 125 | Base: `/api/projects/:projectId/action-plans` | |
| 86 | 126 | ||
| 87 | 127 | #### `POST /` | |
| 88 | - | ||
| 89 | 128 | Create activity with optional nested tasks & subtasks. | |
| 90 | 129 | ||
| 91 | 130 | **Body:** | |
| 92 | - | ||
| 93 | 131 | ```jsonc | |
| 94 | 132 | { | |
| 95 | - | "name": "Activity name", // string, 1-255 | |
| 96 | - | "tasks": [ | |
| 97 | - | // optional, max 50 | |
| 98 | - | { | |
| 99 | - | "name": "Task name", // string, 1-255 | |
| 100 | - | "workWeight": 100, // optional, number >= 0 | |
| 101 | - | "workVolume": 500, // optional, number >= 0 | |
| 102 | - | "workVolumeUnit": "m2", // optional, string max 100 | |
| 103 | - | "startDate": "2026-01-01", // optional, ISO date | |
| 104 | - | "endDate": "2026-03-31", // optional, ISO date | |
| 105 | - | "subtasks": [ | |
| 106 | - | // optional, max 50 | |
| 107 | - | { | |
| 108 | - | "name": "Subtask name", // string, 1-255 | |
| 109 | - | "workVolume": 250, // optional, number >= 0 | |
| 110 | - | "workVolumeUnit": "m2", // optional, string max 100 | |
| 111 | - | "workWeight": 50, // optional, number >= 0 | |
| 112 | - | "startDate": "2026-01-01", // optional, ISO date | |
| 113 | - | "endDate": "2026-02-15", // optional, ISO date | |
| 114 | - | }, | |
| 115 | - | ], | |
| 116 | - | }, | |
| 117 | - | ], | |
| 133 | + | "name": "Activity name", // string, 1-255 | |
| 134 | + | "tasks": [ // optional, max 50 | |
| 135 | + | { | |
| 136 | + | "name": "Task name", // string, 1-255 | |
| 137 | + | "workWeight": 100, // optional, number >= 0 | |
| 138 | + | "workVolume": 500, // optional, number >= 0 | |
| 139 | + | "workVolumeUnit": "m2", // optional, string max 100 | |
| 140 | + | "startDate": "2026-01-01", // optional, ISO date | |
| 141 | + | "endDate": "2026-03-31", // optional, ISO date | |
| 142 | + | "subtasks": [ // optional, max 50 | |
| 143 | + | { | |
| 144 | + | "name": "Subtask name", // string, 1-255 | |
| 145 | + | "workVolume": 250, // optional, number >= 0 | |
| 146 | + | "workVolumeUnit": "m2", // optional, string max 100 | |
| 147 | + | "workWeight": 50, // optional, number >= 0 | |
| 148 | + | "startDate": "2026-01-01", // optional, ISO date | |
| 149 | + | "endDate": "2026-02-15" // optional, ISO date | |
| 150 | + | } | |
| 151 | + | ] | |
| 152 | + | } | |
| 153 | + | ] | |
| 118 | 154 | } | |
| 119 | 155 | ``` | |
| 120 | 156 | ||
| @@ -123,7 +159,6 @@ Create activity with optional nested tasks & subtasks. | |||
| 123 | 159 | --- | |
| 124 | 160 | ||
| 125 | 161 | #### `GET /` | |
| 126 | - | ||
| 127 | 162 | List activities for a project. Paginated, searchable. | |
| 128 | 163 | ||
| 129 | 164 | **Query:** | |
| @@ -136,33 +171,31 @@ List activities for a project. Paginated, searchable. | |||
| 136 | 171 | | `sort_order` | `asc` \| `desc` | `desc` | Sort direction | | |
| 137 | 172 | ||
| 138 | 173 | **Response:** | |
| 139 | - | ||
| 140 | 174 | ```jsonc | |
| 141 | 175 | { | |
| 142 | - | "items": [ | |
| 143 | - | { | |
| 144 | - | "id": "uuid", | |
| 145 | - | "projectId": "uuid", | |
| 146 | - | "name": "Activity name", | |
| 147 | - | "createdBy": "uuid", | |
| 148 | - | "createdAt": "ISO date", | |
| 149 | - | "updatedAt": "ISO date", | |
| 150 | - | "deletedAt": null, | |
| 151 | - | "taskCount": 2, | |
| 152 | - | "subtaskCount": 5, | |
| 153 | - | "workWeight": 150, | |
| 154 | - | }, | |
| 155 | - | ], | |
| 156 | - | "total": 10, | |
| 157 | - | "page": 1, | |
| 158 | - | "limit": 20, | |
| 176 | + | "items": [ | |
| 177 | + | { | |
| 178 | + | "id": "uuid", | |
| 179 | + | "projectId": "uuid", | |
| 180 | + | "name": "Activity name", | |
| 181 | + | "createdBy": "uuid", | |
| 182 | + | "createdAt": "ISO date", | |
| 183 | + | "updatedAt": "ISO date", | |
| 184 | + | "deletedAt": null, | |
| 185 | + | "taskCount": 2, | |
| 186 | + | "subtaskCount": 5, | |
| 187 | + | "workWeight": 150 | |
| 188 | + | } | |
| 189 | + | ], | |
| 190 | + | "total": 10, | |
| 191 | + | "page": 1, | |
| 192 | + | "limit": 20 | |
| 159 | 193 | } | |
| 160 | 194 | ``` | |
| 161 | 195 | ||
| 162 | 196 | --- | |
| 163 | 197 | ||
| 164 | 198 | #### `GET /tree` | |
| 165 | - | ||
| 166 | 199 | Full Activity > Task > Subtask tree. Each level has accumulated stats. | |
| 167 | 200 | ||
| 168 | 201 | **Query:** | |
| @@ -171,105 +204,97 @@ Full Activity > Task > Subtask tree. Each level has accumulated stats. | |||
| 171 | 204 | | `status` | string | — | Filter subtasks by status | | |
| 172 | 205 | ||
| 173 | 206 | **Response:** | |
| 174 | - | ||
| 175 | 207 | ```jsonc | |
| 176 | 208 | [ | |
| 177 | - | { | |
| 178 | - | "id": "uuid", | |
| 179 | - | "name": "Activity name", | |
| 180 | - | "status": "IN_PROGRESS", | |
| 181 | - | "totalTasks": 2, | |
| 182 | - | "totalUsersAssigned": 10, // memberCount (bottom-up) | |
| 183 | - | "accumulatedProgress": 45.5, // avg(), percent | |
| 184 | - | "createdBy": "uuid", | |
| 185 | - | "createdAt": "ISO date", | |
| 186 | - | "updatedAt": "ISO date", | |
| 187 | - | "tasks": [ | |
| 188 | - | { | |
| 189 | - | "id": "uuid", | |
| 190 | - | "name": "Task name", | |
| 191 | - | "status": "IN_PROGRESS", | |
| 192 | - | "totalSubtasks": 2, | |
| 193 | - | "totalUsersAssigned": 5, // memberCount (bottom-up) | |
| 194 | - | "accumulatedProgress": 60, // avg(), percent | |
| 195 | - | "workWeight": 100, | |
| 196 | - | "workVolume": 500, | |
| 197 | - | "workVolumeUnit": "m2", | |
| 198 | - | "startDate": "ISO date", | |
| 199 | - | "endDate": "ISO date", | |
| 200 | - | "subtasks": [ | |
| 201 | - | { | |
| 202 | - | "id": "uuid", | |
| 203 | - | "name": "Subtask name", | |
| 204 | - | "status": "IN_PROGRESS", | |
| 205 | - | "totalUsersAssigned": 3, // memberCount | |
| 206 | - | "progress": 75, // effective (non-rejected), percent | |
| 207 | - | "workVolume": 250, | |
| 208 | - | "workVolumeUnit": "m2", | |
| 209 | - | "workWeight": 50, | |
| 210 | - | "startDate": "ISO date", | |
| 211 | - | "endDate": "ISO date", | |
| 212 | - | }, | |
| 213 | - | ], | |
| 214 | - | }, | |
| 215 | - | ], | |
| 216 | - | }, | |
| 209 | + | { | |
| 210 | + | "id": "uuid", | |
| 211 | + | "name": "Activity name", | |
| 212 | + | "status": "IN_PROGRESS", | |
| 213 | + | "totalTasks": 2, | |
| 214 | + | "totalUsersAssigned": 10, // memberCount (bottom-up) | |
| 215 | + | "accumulatedProgress": 45.5, // avg(), percent | |
| 216 | + | "createdBy": "uuid", | |
| 217 | + | "createdAt": "ISO date", | |
| 218 | + | "updatedAt": "ISO date", | |
| 219 | + | "tasks": [ | |
| 220 | + | { | |
| 221 | + | "id": "uuid", | |
| 222 | + | "name": "Task name", | |
| 223 | + | "status": "IN_PROGRESS", | |
| 224 | + | "totalSubtasks": 2, | |
| 225 | + | "totalUsersAssigned": 5, // memberCount (bottom-up) | |
| 226 | + | "accumulatedProgress": 60, // avg(), percent | |
| 227 | + | "workWeight": 100, | |
| 228 | + | "workVolume": 500, | |
| 229 | + | "workVolumeUnit": "m2", | |
| 230 | + | "startDate": "ISO date", | |
| 231 | + | "endDate": "ISO date", | |
| 232 | + | "subtasks": [ | |
| 233 | + | { | |
| 234 | + | "id": "uuid", | |
| 235 | + | "name": "Subtask name", | |
| 236 | + | "status": "IN_PROGRESS", | |
| 237 | + | "totalUsersAssigned": 3, // memberCount | |
| 238 | + | "progress": 75, // effective (non-rejected), percent | |
| 239 | + | "workVolume": 250, | |
| 240 | + | "workVolumeUnit": "m2", | |
| 241 | + | "workWeight": 50, | |
| 242 | + | "startDate": "ISO date", | |
| 243 | + | "endDate": "ISO date" | |
| 244 | + | } | |
| 245 | + | ] | |
| 246 | + | } | |
| 247 | + | ] | |
| 248 | + | } | |
| 217 | 249 | ] | |
| 218 | 250 | ``` | |
| 219 | 251 | ||
| 220 | 252 | --- | |
| 221 | 253 | ||
| 222 | 254 | #### `GET /tree/gantt` | |
| 223 | - | ||
| 224 | 255 | Same tree structure but each node includes `startDate`/`endDate` derived from children. | |
| 225 | 256 | Activity `startDate` = min of all subtasks. Activity `endDate` = max of all subtasks. | |
| 226 | - | Task dates use own stored values. | |
| 227 | - | Supports `status` query filter (same as `/tree`). | |
| 257 | + | Task dates use own stored values. Supports `status` query filter (same as `/tree`). | |
| 228 | 258 | ||
| 229 | 259 | --- | |
| 230 | 260 | ||
| 231 | 261 | #### `GET /:id` | |
| 232 | - | ||
| 233 | 262 | Get single activity with derived stats. | |
| 234 | 263 | ||
| 235 | 264 | **Response:** | |
| 236 | - | ||
| 237 | 265 | ```jsonc | |
| 238 | 266 | { | |
| 239 | - | "id": "uuid", | |
| 240 | - | "projectId": "uuid", | |
| 241 | - | "name": "Activity name", | |
| 242 | - | "createdBy": "uuid", | |
| 243 | - | "createdAt": "ISO date", | |
| 244 | - | "updatedAt": "ISO date", | |
| 245 | - | "deletedAt": null, | |
| 246 | - | "taskCount": 2, | |
| 247 | - | "subtaskCount": 5, | |
| 248 | - | "memberCount": 10, // bottom-up (see rules above) | |
| 249 | - | "status": "IN_PROGRESS", | |
| 250 | - | "progress": 45.5, // avg(), percent | |
| 251 | - | "workWeight": 150, | |
| 267 | + | "id": "uuid", | |
| 268 | + | "projectId": "uuid", | |
| 269 | + | "name": "Activity name", | |
| 270 | + | "createdBy": "uuid", | |
| 271 | + | "createdAt": "ISO date", | |
| 272 | + | "updatedAt": "ISO date", | |
| 273 | + | "deletedAt": null, | |
| 274 | + | "taskCount": 2, | |
| 275 | + | "subtaskCount": 5, | |
| 276 | + | "memberCount": 10, // bottom-up (see rules above) | |
| 277 | + | "status": "IN_PROGRESS", | |
| 278 | + | "progress": 45.5, // avg(), percent | |
| 279 | + | "workWeight": 150 | |
| 252 | 280 | } | |
| 253 | 281 | ``` | |
| 254 | 282 | ||
| 255 | 283 | --- | |
| 256 | 284 | ||
| 257 | 285 | #### `PATCH /:id` | |
| 258 | - | ||
| 259 | 286 | Update activity. If activity has tasks, only `name` is editable. | |
| 260 | 287 | ||
| 261 | 288 | **Body:** | |
| 262 | - | ||
| 263 | 289 | ```jsonc | |
| 264 | 290 | { | |
| 265 | - | "name": "New name", // string, 1-255, optional | |
| 291 | + | "name": "New name" // string, 1-255, optional | |
| 266 | 292 | } | |
| 267 | 293 | ``` | |
| 268 | 294 | ||
| 269 | 295 | --- | |
| 270 | 296 | ||
| 271 | 297 | #### `DELETE /:id` | |
| 272 | - | ||
| 273 | 298 | Soft-delete activity and all its tasks + subtasks. No body. Returns `204` status code. | |
| 274 | 299 | ||
| 275 | 300 | --- | |
| @@ -279,19 +304,17 @@ Soft-delete activity and all its tasks + subtasks. No body. Returns `204` status | |||
| 279 | 304 | Base: `/api/action-plans/activities/:activityId/tasks` | |
| 280 | 305 | ||
| 281 | 306 | #### `POST /` | |
| 282 | - | ||
| 283 | 307 | Create task under an activity. | |
| 284 | 308 | ||
| 285 | 309 | **Body:** | |
| 286 | - | ||
| 287 | 310 | ```jsonc | |
| 288 | 311 | { | |
| 289 | - | "name": "Task name", // string, 1-255 | |
| 290 | - | "workWeight": 100, // optional, number >= 0 | |
| 291 | - | "workVolume": 500, // optional, number >= 0 | |
| 292 | - | "workVolumeUnit": "m2", // optional, string max 100 | |
| 293 | - | "startDate": "2026-01-01", // optional, ISO date | |
| 294 | - | "endDate": "2026-03-31", // optional, ISO date | |
| 312 | + | "name": "Task name", // string, 1-255 | |
| 313 | + | "workWeight": 100, // optional, number >= 0 | |
| 314 | + | "workVolume": 500, // optional, number >= 0 | |
| 315 | + | "workVolumeUnit": "m2", // optional, string max 100 | |
| 316 | + | "startDate": "2026-01-01", // optional, ISO date | |
| 317 | + | "endDate": "2026-03-31" // optional, ISO date | |
| 295 | 318 | } | |
| 296 | 319 | ``` | |
| 297 | 320 | ||
| @@ -300,56 +323,52 @@ Create task under an activity. | |||
| 300 | 323 | --- | |
| 301 | 324 | ||
| 302 | 325 | #### `GET /` | |
| 303 | - | ||
| 304 | 326 | List all tasks under an activity. | |
| 305 | 327 | ||
| 306 | 328 | **Response:** | |
| 307 | - | ||
| 308 | 329 | ```jsonc | |
| 309 | 330 | { | |
| 310 | - | "tasks": [ | |
| 311 | - | { | |
| 312 | - | "id": "uuid", | |
| 313 | - | "name": "Task name", | |
| 314 | - | "workWeight": 100, | |
| 315 | - | "workVolume": 500, | |
| 316 | - | "workVolumeUnit": "m2", | |
| 317 | - | "startDate": "ISO date", | |
| 318 | - | "endDate": "ISO date", | |
| 319 | - | "createdAt": "ISO date", | |
| 320 | - | "updatedAt": "ISO date", | |
| 321 | - | "subtaskCount": 3, | |
| 322 | - | }, | |
| 323 | - | ], | |
| 331 | + | "tasks": [ | |
| 332 | + | { | |
| 333 | + | "id": "uuid", | |
| 334 | + | "name": "Task name", | |
| 335 | + | "workWeight": 100, | |
| 336 | + | "workVolume": 500, | |
| 337 | + | "workVolumeUnit": "m2", | |
| 338 | + | "startDate": "ISO date", | |
| 339 | + | "endDate": "ISO date", | |
| 340 | + | "createdAt": "ISO date", | |
| 341 | + | "updatedAt": "ISO date", | |
| 342 | + | "subtaskCount": 3 | |
| 343 | + | } | |
| 344 | + | ] | |
| 324 | 345 | } | |
| 325 | 346 | ``` | |
| 326 | 347 | ||
| 327 | 348 | --- | |
| 328 | 349 | ||
| 329 | 350 | #### `GET /:id` | |
| 330 | - | ||
| 331 | 351 | Get task detail with derived stats. | |
| 332 | 352 | ||
| 333 | 353 | **Response:** | |
| 334 | - | ||
| 335 | 354 | ```jsonc | |
| 336 | 355 | { | |
| 337 | - | "id": "uuid", | |
| 338 | - | "activityId": "uuid", | |
| 339 | - | "name": "Task name", | |
| 340 | - | "workWeight": 150, | |
| 341 | - | "workVolume": 500, | |
| 342 | - | "workVolumeUnit": "m2", | |
| 343 | - | "startDate": "ISO date", | |
| 344 | - | "endDate": "ISO date", | |
| 345 | - | "createdBy": "uuid", | |
| 346 | - | "createdAt": "ISO date", | |
| 347 | - | "updatedAt": "ISO date", | |
| 348 | - | "subtaskCount": 3, | |
| 349 | - | "memberCount": 5, // bottom-up (see rules above) | |
| 350 | - | "status": "IN_PROGRESS", | |
| 351 | - | "progress": 60, // avg(), percent | |
| 352 | - | "canAddSubtask": false, | |
| 356 | + | "id": "uuid", | |
| 357 | + | "activityId": "uuid", | |
| 358 | + | "name": "Task name", | |
| 359 | + | "workWeight": 150, | |
| 360 | + | "workVolume": 500, | |
| 361 | + | "workVolumeUnit": "m2", | |
| 362 | + | "startDate": "ISO date", | |
| 363 | + | "endDate": "ISO date", | |
| 364 | + | "createdBy": "uuid", | |
| 365 | + | "createdAt": "ISO date", | |
| 366 | + | "updatedAt": "ISO date", | |
| 367 | + | "subtaskCount": 3, | |
| 368 | + | "memberCount": 5, // bottom-up (see rules above) | |
| 369 | + | "status": "IN_PROGRESS", | |
| 370 | + | "progress": 60, // avg(), percent | |
| 371 | + | "canAddSubtask": false | |
| 353 | 372 | } | |
| 354 | 373 | ``` | |
| 355 | 374 | ||
| @@ -358,26 +377,23 @@ Get task detail with derived stats. | |||
| 358 | 377 | --- | |
| 359 | 378 | ||
| 360 | 379 | #### `PATCH /:id` | |
| 361 | - | ||
| 362 | 380 | Update task. If task has subtasks, only `name` is editable. | |
| 363 | 381 | ||
| 364 | 382 | **Body:** | |
| 365 | - | ||
| 366 | 383 | ```jsonc | |
| 367 | 384 | { | |
| 368 | - | "name": "New name", // string, 1-255, optional | |
| 369 | - | "workWeight": 200, // optional (only when no subtasks) | |
| 370 | - | "workVolume": 600, // optional (only when no subtasks) | |
| 371 | - | "workVolumeUnit": "km2", // optional (only when no subtasks) | |
| 372 | - | "startDate": "2026-01-01", // optional (only when no subtasks) | |
| 373 | - | "endDate": "2026-06-30", // optional (only when no subtasks) | |
| 385 | + | "name": "New name", // string, 1-255, optional | |
| 386 | + | "workWeight": 200, // optional (only when no subtasks) | |
| 387 | + | "workVolume": 600, // optional (only when no subtasks) | |
| 388 | + | "workVolumeUnit": "km2", // optional (only when no subtasks) | |
| 389 | + | "startDate": "2026-01-01", // optional (only when no subtasks) | |
| 390 | + | "endDate": "2026-06-30" // optional (only when no subtasks) | |
| 374 | 391 | } | |
| 375 | 392 | ``` | |
| 376 | 393 | ||
| 377 | 394 | --- | |
| 378 | 395 | ||
| 379 | 396 | #### `DELETE /:id` | |
| 380 | - | ||
| 381 | 397 | Soft-delete task and all its subtasks. No body. | |
| 382 | 398 | ||
| 383 | 399 | --- | |
| @@ -387,19 +403,17 @@ Soft-delete task and all its subtasks. No body. | |||
| 387 | 403 | Base: `/api/action-plans/tasks/:taskId/subtasks` | |
| 388 | 404 | ||
| 389 | 405 | #### `POST /` | |
| 390 | - | ||
| 391 | 406 | Create subtask under a task. | |
| 392 | 407 | ||
| 393 | 408 | **Body:** | |
| 394 | - | ||
| 395 | 409 | ```jsonc | |
| 396 | 410 | { | |
| 397 | - | "name": "Subtask name", // string, 1-255 | |
| 398 | - | "workVolume": 250, // optional, number >= 0 | |
| 399 | - | "workVolumeUnit": "m2", // optional, string max 100 | |
| 400 | - | "workWeight": 50, // optional, number >= 0 | |
| 401 | - | "startDate": "2026-01-01", // optional, ISO date | |
| 402 | - | "endDate": "2026-02-15", // optional, ISO date | |
| 411 | + | "name": "Subtask name", // string, 1-255 | |
| 412 | + | "workVolume": 250, // optional, number >= 0 | |
| 413 | + | "workVolumeUnit": "m2", // optional, string max 100 | |
| 414 | + | "workWeight": 50, // optional, number >= 0 | |
| 415 | + | "startDate": "2026-01-01", // optional, ISO date | |
| 416 | + | "endDate": "2026-02-15" // optional, ISO date | |
| 403 | 417 | } | |
| 404 | 418 | ``` | |
| 405 | 419 | ||
| @@ -408,7 +422,6 @@ Create subtask under a task. | |||
| 408 | 422 | --- | |
| 409 | 423 | ||
| 410 | 424 | #### `GET /` | |
| 411 | - | ||
| 412 | 425 | List all subtasks under a task. | |
| 413 | 426 | ||
| 414 | 427 | **Response:** Array of subtask objects, each with `memberCount`, `members[].{userId, displayName}`, `status`, `progress`, `progressCount`. | |
| @@ -416,79 +429,74 @@ List all subtasks under a task. | |||
| 416 | 429 | --- | |
| 417 | 430 | ||
| 418 | 431 | #### `GET /:id` | |
| 419 | - | ||
| 420 | 432 | Get subtask detail. | |
| 421 | 433 | ||
| 422 | 434 | **Response:** | |
| 423 | - | ||
| 424 | 435 | ```jsonc | |
| 425 | 436 | { | |
| 426 | - | "id": "uuid", | |
| 427 | - | "taskId": "uuid", | |
| 428 | - | "task": { "id": "uuid", "activityId": "uuid" }, | |
| 429 | - | "name": "Subtask name", | |
| 430 | - | "workVolume": 250, | |
| 431 | - | "workVolumeUnit": "m2", | |
| 432 | - | "workWeight": 50, | |
| 433 | - | "startDate": "ISO date", | |
| 434 | - | "endDate": "ISO date", | |
| 435 | - | "createdBy": "uuid", | |
| 436 | - | "createdAt": "ISO date", | |
| 437 | - | "updatedAt": "ISO date", | |
| 438 | - | "deletedAt": null, | |
| 439 | - | "members": [ | |
| 440 | - | { | |
| 441 | - | "id": "uuid", | |
| 442 | - | "userId": "uuid", | |
| 443 | - | "displayName": "John Doe", | |
| 444 | - | }, | |
| 445 | - | ], | |
| 446 | - | "memberCount": 3, | |
| 447 | - | "status": "IN_PROGRESS", | |
| 448 | - | "progress": 75, | |
| 449 | - | "progressCount": 2, | |
| 450 | - | "progressPlanCount": 1, | |
| 451 | - | "plans": [ | |
| 452 | - | { | |
| 453 | - | "id": "uuid", | |
| 454 | - | "startDate": "ISO date", | |
| 455 | - | "endDate": "ISO date", | |
| 456 | - | "targetProgress": 50, | |
| 457 | - | }, | |
| 458 | - | ], | |
| 437 | + | "id": "uuid", | |
| 438 | + | "taskId": "uuid", | |
| 439 | + | "task": { "id": "uuid", "activityId": "uuid" }, | |
| 440 | + | "name": "Subtask name", | |
| 441 | + | "workVolume": 250, | |
| 442 | + | "workVolumeUnit": "m2", | |
| 443 | + | "workWeight": 50, | |
| 444 | + | "startDate": "ISO date", | |
| 445 | + | "endDate": "ISO date", | |
| 446 | + | "createdBy": "uuid", | |
| 447 | + | "createdAt": "ISO date", | |
| 448 | + | "updatedAt": "ISO date", | |
| 449 | + | "deletedAt": null, | |
| 450 | + | "members": [ | |
| 451 | + | { | |
| 452 | + | "id": "uuid", | |
| 453 | + | "userId": "uuid", | |
| 454 | + | "displayName": "John Doe" | |
| 455 | + | } | |
| 456 | + | ], | |
| 457 | + | "memberCount": 3, | |
| 458 | + | "status": "IN_PROGRESS", | |
| 459 | + | "progress": 75, | |
| 460 | + | "progressCount": 2, | |
| 461 | + | "progressPlanCount": 1, | |
| 462 | + | "plans": [ | |
| 463 | + | { | |
| 464 | + | "id": "uuid", | |
| 465 | + | "startDate": "ISO date", | |
| 466 | + | "endDate": "ISO date", | |
| 467 | + | "targetProgress": 50 | |
| 468 | + | } | |
| 469 | + | ] | |
| 459 | 470 | } | |
| 460 | 471 | ``` | |
| 461 | 472 | ||
| 462 | 473 | --- | |
| 463 | 474 | ||
| 464 | 475 | #### `PATCH /:id` | |
| 465 | - | ||
| 466 | 476 | Update subtask. Work weight & work volume are locked once subtask is in progress. | |
| 467 | 477 | ||
| 468 | 478 | **Body:** | |
| 469 | - | ||
| 470 | 479 | ```jsonc | |
| 471 | 480 | { | |
| 472 | - | "name": "New name", // string, 1-255, optional | |
| 473 | - | "workVolume": 300, // optional (locked after progress starts) | |
| 474 | - | "workVolumeUnit": "kg", // optional (locked after progress starts) | |
| 475 | - | "workWeight": 60, // optional (locked after progress starts) | |
| 476 | - | "startDate": "2026-01-01", // optional | |
| 477 | - | "endDate": "2026-02-15", // optional | |
| 481 | + | "name": "New name", // string, 1-255, optional | |
| 482 | + | "workVolume": 300, // optional (locked after progress starts) | |
| 483 | + | "workVolumeUnit": "kg", // optional (locked after progress starts) | |
| 484 | + | "workWeight": 60, // optional (locked after progress starts) | |
| 485 | + | "startDate": "2026-01-01", // optional | |
| 486 | + | "endDate": "2026-02-15" // optional | |
| 478 | 487 | } | |
| 479 | 488 | ``` | |
| 480 | 489 | ||
| 481 | 490 | --- | |
| 482 | 491 | ||
| 483 | 492 | #### `DELETE /:id` | |
| 484 | - | ||
| 485 | 493 | Soft-delete subtask. No body. | |
| 486 | 494 | ||
| 487 | 495 | --- | |
| 488 | 496 | ||
| 489 | 497 | ### 4. Progress | |
| 490 | 498 | ||
| 491 | - | Three sets of progress endpoints — subtask, task, and activity scope. | |
| 499 | + | Three scopes: subtask, task, activity. | |
| 492 | 500 | ||
| 493 | 501 | --- | |
| 494 | 502 | ||
| @@ -497,7 +505,6 @@ Three sets of progress endpoints — subtask, task, and activity scope. | |||
| 497 | 505 | Base: `/api/action-plans/subtasks/:subtaskId/progress` | |
| 498 | 506 | ||
| 499 | 507 | ##### `POST /` (multipart/form-data) | |
| 500 | - | ||
| 501 | 508 | Submit progress entry. Supports file uploads. | |
| 502 | 509 | ||
| 503 | 510 | **Form fields:** | |
| @@ -516,80 +523,74 @@ New progress must be >= current non-REJECTED floor. | |||
| 516 | 523 | **Response:** Full progress entry (see GET /:id). | |
| 517 | 524 | ||
| 518 | 525 | ##### `GET /` | |
| 519 | - | ||
| 520 | 526 | List progress history for a subtask. Paginated, newest first. | |
| 521 | 527 | ||
| 522 | 528 | **Query:** `page` (default 1), `limit` (default 50, max 100) | |
| 523 | 529 | ||
| 524 | 530 | **Response:** | |
| 525 | - | ||
| 526 | 531 | ```jsonc | |
| 527 | 532 | { | |
| 528 | - | "items": [ | |
| 529 | - | { | |
| 530 | - | "id": "uuid", | |
| 531 | - | "subtaskId": "uuid", | |
| 532 | - | "subtaskName": "Subtask name", | |
| 533 | - | "progress": 75, | |
| 534 | - | "workVolume": 187.5, | |
| 535 | - | "subtaskWorkVolume": 250, | |
| 536 | - | "subtaskWorkVolumeUnit": "m2", | |
| 537 | - | "description": "Completed foundation", | |
| 538 | - | "status": "APPROVED", | |
| 539 | - | "amendsId": null, | |
| 540 | - | "amendsOriginal": null, | |
| 541 | - | "amendedByCount": 0, | |
| 542 | - | "amendedBy": [], | |
| 543 | - | "approvedBy": "uuid", | |
| 544 | - | "approvedByName": "Admin", | |
| 545 | - | "approvedAt": "ISO date", | |
| 546 | - | "createdBy": "uuid", | |
| 547 | - | "createdByName": "Worker", | |
| 548 | - | "updatedBy": null, | |
| 549 | - | "updatedByName": null, | |
| 550 | - | "createdAt": "ISO date", | |
| 551 | - | "updatedAt": "ISO date", | |
| 552 | - | "images": [{ "id": "uuid", "key": "s3/path", "originalName": "photo.jpg", "url": "presigned-url" }], | |
| 553 | - | "sitemap": { "id": "uuid", "key": "s3/path", "originalName": "site.pdf", "url": "presigned-url" }, | |
| 554 | - | }, | |
| 555 | - | ], | |
| 556 | - | "pagination": { | |
| 557 | - | "page": 1, | |
| 558 | - | "limit": 50, | |
| 559 | - | "total": 2, | |
| 560 | - | "pages": 1, | |
| 561 | - | "hasNext": false, | |
| 562 | - | "hasPrev": false, | |
| 563 | - | }, | |
| 533 | + | "items": [ | |
| 534 | + | { | |
| 535 | + | "id": "uuid", | |
| 536 | + | "subtaskId": "uuid", | |
| 537 | + | "subtaskName": "Subtask name", | |
| 538 | + | "progress": 75, | |
| 539 | + | "workVolume": 187.5, | |
| 540 | + | "subtaskWorkVolume": 250, | |
| 541 | + | "subtaskWorkVolumeUnit": "m2", | |
| 542 | + | "description": "Completed foundation", | |
| 543 | + | "status": "APPROVED", | |
| 544 | + | "amendsId": null, | |
| 545 | + | "amendsOriginal": null, | |
| 546 | + | "amendedByCount": 0, | |
| 547 | + | "amendedBy": [], | |
| 548 | + | "approvedBy": "uuid", | |
| 549 | + | "approvedByName": "Admin", | |
| 550 | + | "approvedAt": "ISO date", | |
| 551 | + | "createdBy": "uuid", | |
| 552 | + | "createdByName": "Worker", | |
| 553 | + | "updatedBy": null, | |
| 554 | + | "updatedByName": null, | |
| 555 | + | "createdAt": "ISO date", | |
| 556 | + | "updatedAt": "ISO date", | |
| 557 | + | "images": [{ "id": "uuid", "key": "s3/path", "originalName": "photo.jpg", "url": "presigned-url" }], | |
| 558 | + | "sitemap": { "id": "uuid", "key": "s3/path", "originalName": "site.pdf", "url": "presigned-url" } | |
| 559 | + | } | |
| 560 | + | ], | |
| 561 | + | "pagination": { | |
| 562 | + | "page": 1, | |
| 563 | + | "limit": 50, | |
| 564 | + | "total": 2, | |
| 565 | + | "pages": 1, | |
| 566 | + | "hasNext": false, | |
| 567 | + | "hasPrev": false | |
| 568 | + | } | |
| 564 | 569 | } | |
| 565 | 570 | ``` | |
| 566 | 571 | ||
| 567 | 572 | ##### `GET /:id` | |
| 568 | - | ||
| 569 | 573 | Get single progress entry with full detail including images and amendment chain. | |
| 570 | 574 | ||
| 571 | 575 | **Response:** Same shape as one item in the list above. | |
| 572 | 576 | ||
| 573 | 577 | ##### `POST /:id/respond` | |
| 574 | - | ||
| 575 | - | Admin responds to a PENDING progress entry. | |
| 578 | + | Admin responds to a PENDING progress entry. Sends notification to submitter. | |
| 576 | 579 | ||
| 577 | 580 | **Body:** | |
| 578 | - | ||
| 579 | 581 | ```jsonc | |
| 580 | 582 | { | |
| 581 | - | "verb": "APPROVE", // "APPROVE" | "APPROVE_WITH_NOTES" | "REJECT" | |
| 582 | - | "progress": 80, // optional, 0-100 (for APPROVE_WITH_NOTES) | |
| 583 | - | "workVolume": 200, // optional (for APPROVE_WITH_NOTES) | |
| 584 | - | "description": "Adjusted", // optional (for APPROVE_WITH_NOTES) | |
| 583 | + | "verb": "APPROVE", // "APPROVE" | "APPROVE_WITH_NOTES" | "REJECT" | |
| 584 | + | "progress": 80, // optional, 0-100 (for APPROVE_WITH_NOTES) | |
| 585 | + | "workVolume": 200, // optional (for APPROVE_WITH_NOTES) | |
| 586 | + | "description": "Adjusted" // optional (for APPROVE_WITH_NOTES) | |
| 585 | 587 | } | |
| 586 | 588 | ``` | |
| 587 | 589 | ||
| 588 | 590 | **Verbs:** | |
| 589 | - | ||
| 590 | - | - `APPROVE` — progress accepted as-is | |
| 591 | - | - `APPROVE_WITH_NOTES` — original marked APPROVED_WITH_NOTES + new amended entry created with admin values | |
| 592 | - | - `REJECT` — progress rejected, doesn't count toward effective progress | |
| 591 | + | - `APPROVE` — accepted as-is. Notifies submitter: `action_plan_progress_approve`. | |
| 592 | + | - `APPROVE_WITH_NOTES` — original marked APPROVED_WITH_NOTES + new amended entry created with admin values. Notifies submitter: `action_plan_progress_approve_with_notes`. | |
| 593 | + | - `REJECT` — rejected, doesn't count toward effective progress. Notifies submitter: `action_plan_progress_reject`. | |
| 593 | 594 | ||
| 594 | 595 | --- | |
| 595 | 596 | ||
| @@ -598,22 +599,11 @@ Admin responds to a PENDING progress entry. | |||
| 598 | 599 | Base: `/api/action-plans/tasks/:taskId` | |
| 599 | 600 | ||
| 600 | 601 | ##### `POST /progress` | |
| 601 | - | ||
| 602 | - | Create progress entry at task scope. | |
| 603 | - | ||
| 604 | - | **Body:** Same as subtask progress body (minimal: `{ "progress": 25 }`). | |
| 605 | - | ||
| 606 | 602 | ##### `GET /progress` | |
| 607 | - | ||
| 608 | - | List progress entries. **Query:** `page`, `limit`. | |
| 609 | - | ||
| 610 | 603 | ##### `GET /progress/:id` | |
| 611 | - | ||
| 612 | - | Get single progress entry. | |
| 613 | - | ||
| 614 | 604 | ##### `POST /progress/:id/respond` | |
| 615 | 605 | ||
| 616 | - | Respond to PENDING progress entry. **Body:** Same respond schema. | |
| 606 | + | Same as subtask. Scope = TASK. Leaf-only: blocked if task has subtasks. | |
| 617 | 607 | ||
| 618 | 608 | --- | |
| 619 | 609 | ||
| @@ -622,14 +612,11 @@ Respond to PENDING progress entry. **Body:** Same respond schema. | |||
| 622 | 612 | Base: `/api/action-plans/activities/:activityId` | |
| 623 | 613 | ||
| 624 | 614 | ##### `POST /progress` | |
| 625 | - | ||
| 626 | 615 | ##### `GET /progress` | |
| 627 | - | ||
| 628 | 616 | ##### `GET /progress/:id` | |
| 629 | - | ||
| 630 | 617 | ##### `POST /progress/:id/respond` | |
| 631 | 618 | ||
| 632 | - | Same as task progress. Scope = ACTIVITY. | |
| 619 | + | Same as subtask. Scope = ACTIVITY. Leaf-only: blocked if activity has tasks. | |
| 633 | 620 | ||
| 634 | 621 | --- | |
| 635 | 622 | ||
| @@ -646,69 +633,66 @@ Base: `/api/action-plans/tasks/:taskId/subtasks/:id` | |||
| 646 | 633 | ##### `GET /plans` | |
| 647 | 634 | ||
| 648 | 635 | **Response:** | |
| 649 | - | ||
| 650 | 636 | ```jsonc | |
| 651 | 637 | { | |
| 652 | - | "duration": { | |
| 653 | - | "days": 90, | |
| 654 | - | "startDate": "2026-01-01", | |
| 655 | - | "endDate": "2026-03-31", | |
| 656 | - | }, | |
| 657 | - | "planning": [ | |
| 658 | - | { | |
| 659 | - | "id": "uuid", | |
| 660 | - | "startDate": "2026-01-01", | |
| 661 | - | "endDate": "2026-02-15", | |
| 662 | - | "targetProgress": 50, | |
| 663 | - | }, | |
| 664 | - | { | |
| 665 | - | "id": "uuid", | |
| 666 | - | "startDate": "2026-02-15", | |
| 667 | - | "endDate": "2026-03-31", | |
| 668 | - | "targetProgress": 50, | |
| 669 | - | }, | |
| 670 | - | ], | |
| 671 | - | "sCurve": { | |
| 672 | - | "plan": [ | |
| 673 | - | { "date": 1704067200000, "progress": 0 }, | |
| 674 | - | { "date": 1709251200000, "progress": 50 }, | |
| 675 | - | { "date": 1711929600000, "progress": 100 }, | |
| 676 | - | ], | |
| 677 | - | "actual": [ | |
| 678 | - | { "date": 1705276800000, "progress": 25, "id": "uuid" }, | |
| 679 | - | { "date": 1708300800000, "progress": 60, "id": "uuid" }, | |
| 680 | - | ], | |
| 681 | - | }, | |
| 638 | + | "duration": { | |
| 639 | + | "days": 90, | |
| 640 | + | "startDate": "2026-01-01", | |
| 641 | + | "endDate": "2026-03-31" | |
| 642 | + | }, | |
| 643 | + | "planning": [ | |
| 644 | + | { | |
| 645 | + | "id": "uuid", | |
| 646 | + | "startDate": "2026-01-01", | |
| 647 | + | "endDate": "2026-02-15", | |
| 648 | + | "targetProgress": 50 | |
| 649 | + | }, | |
| 650 | + | { | |
| 651 | + | "id": "uuid", | |
| 652 | + | "startDate": "2026-02-15", | |
| 653 | + | "endDate": "2026-03-31", | |
| 654 | + | "targetProgress": 50 | |
| 655 | + | } | |
| 656 | + | ], | |
| 657 | + | "sCurve": { | |
| 658 | + | "plan": [ | |
| 659 | + | { "date": 1704067200000, "progress": 0 }, | |
| 660 | + | { "date": 1709251200000, "progress": 50 }, | |
| 661 | + | { "date": 1711929600000, "progress": 100 } | |
| 662 | + | ], | |
| 663 | + | "actual": [ | |
| 664 | + | { "date": 1705276800000, "progress": 25, "id": "uuid" }, | |
| 665 | + | { "date": 1708300800000, "progress": 60, "id": "uuid" } | |
| 666 | + | ] | |
| 667 | + | } | |
| 682 | 668 | } | |
| 683 | 669 | ``` | |
| 684 | 670 | ||
| 685 | 671 | `sCurve.plan` — cumulative target over time (derived from plan segments). | |
| 686 | - | `sCurve.actual` — APPROVED entries sorted by `approvedAt`. Only latest version per amendment chain. | |
| 672 | + | `sCurve.actual` — APPROVED entries sorted by approvedAt. Latest version per amendment chain. | |
| 687 | 673 | ||
| 688 | 674 | If no plans stored and subtask has dates, a default segment 0%→100% is returned. | |
| 689 | 675 | ||
| 690 | 676 | --- | |
| 691 | 677 | ||
| 692 | 678 | ##### `PUT /plans` | |
| 693 | - | ||
| 694 | 679 | Set (replace) all plans. Validates: continuous dates, within subtask date range, sum to 100%. | |
| 695 | 680 | ||
| 696 | 681 | **Body:** | |
| 697 | - | ||
| 698 | 682 | ```jsonc | |
| 699 | 683 | { | |
| 700 | - | "plans": [ | |
| 701 | - | { | |
| 702 | - | "startDate": "2026-01-01", | |
| 703 | - | "endDate": "2026-02-15", | |
| 704 | - | "targetProgress": 50, | |
| 705 | - | }, | |
| 706 | - | { | |
| 707 | - | "startDate": "2026-02-15", | |
| 708 | - | "endDate": "2026-03-31", | |
| 709 | - | "targetProgress": 50, | |
| 710 | - | }, | |
| 711 | - | ], | |
| 684 | + | "plans": [ | |
| 685 | + | { | |
| 686 | + | "startDate": "2026-01-01", | |
| 687 | + | "endDate": "2026-02-15", | |
| 688 | + | "targetProgress": 50 | |
| 689 | + | }, | |
| 690 | + | { | |
| 691 | + | "startDate": "2026-02-15", | |
| 692 | + | "endDate": "2026-03-31", | |
| 693 | + | "targetProgress": 50 | |
| 694 | + | } | |
| 695 | + | ] | |
| 712 | 696 | } | |
| 713 | 697 | ``` | |
| 714 | 698 | ||
| @@ -721,10 +705,9 @@ Set (replace) all plans. Validates: continuous dates, within subtask date range, | |||
| 721 | 705 | Base: `/api/action-plans/tasks/:taskId` | |
| 722 | 706 | ||
| 723 | 707 | ##### `GET /plans` | |
| 724 | - | ||
| 725 | 708 | ##### `PUT /plans` | |
| 726 | 709 | ||
| 727 | - | Same shape as subtask plans. Scope = TASK. | |
| 710 | + | Same shape as subtask plans. Scope = TASK. Leaf-only: blocked if task has subtasks. | |
| 728 | 711 | ||
| 729 | 712 | --- | |
| 730 | 713 | ||
| @@ -733,16 +716,15 @@ Same shape as subtask plans. Scope = TASK. | |||
| 733 | 716 | Base: `/api/action-plans/activities/:activityId` | |
| 734 | 717 | ||
| 735 | 718 | ##### `GET /plans` | |
| 736 | - | ||
| 737 | 719 | ##### `PUT /plans` | |
| 738 | 720 | ||
| 739 | - | Same shape as subtask plans. Scope = ACTIVITY. | |
| 721 | + | Same shape as subtask plans. Scope = ACTIVITY. Leaf-only: blocked if activity has tasks. | |
| 740 | 722 | ||
| 741 | 723 | --- | |
| 742 | 724 | ||
| 743 | 725 | ### 6. Members | |
| 744 | 726 | ||
| 745 | - | Member CRUD across all three hierarchy levels. Uses unified `actionPlanMembers` table. | |
| 727 | + | Member CRUD across all three hierarchy levels. Sends notification on add/remove. | |
| 746 | 728 | ||
| 747 | 729 | --- | |
| 748 | 730 | ||
| @@ -751,47 +733,41 @@ Member CRUD across all three hierarchy levels. Uses unified `actionPlanMembers` | |||
| 751 | 733 | Base: `/api/action-plans/tasks/:taskId/subtasks/:id` | |
| 752 | 734 | ||
| 753 | 735 | ##### `POST /members` | |
| 754 | - | ||
| 755 | - | Add member to subtask. Also cascades to activity members (upsert). | |
| 736 | + | Add member to subtask. Sends `action_plan_member_added` notification. | |
| 756 | 737 | ||
| 757 | 738 | **Body:** | |
| 758 | - | ||
| 759 | 739 | ```jsonc | |
| 760 | 740 | { | |
| 761 | - | "userId": "uuid", | |
| 741 | + | "userId": "uuid" | |
| 762 | 742 | } | |
| 763 | 743 | ``` | |
| 764 | 744 | ||
| 765 | 745 | **Response:** | |
| 766 | - | ||
| 767 | 746 | ```jsonc | |
| 768 | 747 | { | |
| 769 | - | "id": "uuid", | |
| 770 | - | "scopeType": "SUBTASK", | |
| 771 | - | "scopeId": "uuid", | |
| 772 | - | "userId": "uuid", | |
| 748 | + | "id": "uuid", | |
| 749 | + | "scopeType": "SUBTASK", | |
| 750 | + | "scopeId": "uuid", | |
| 751 | + | "userId": "uuid" | |
| 773 | 752 | } | |
| 774 | 753 | ``` | |
| 775 | 754 | ||
| 776 | 755 | ##### `GET /members` | |
| 777 | - | ||
| 778 | 756 | List members of a subtask. | |
| 779 | 757 | ||
| 780 | 758 | **Response:** | |
| 781 | - | ||
| 782 | 759 | ```jsonc | |
| 783 | 760 | [ | |
| 784 | - | { | |
| 785 | - | "id": "uuid", | |
| 786 | - | "userId": "uuid", | |
| 787 | - | "displayName": "John Doe", | |
| 788 | - | }, | |
| 761 | + | { | |
| 762 | + | "id": "uuid", | |
| 763 | + | "userId": "uuid", | |
| 764 | + | "displayName": "John Doe" | |
| 765 | + | } | |
| 789 | 766 | ] | |
| 790 | 767 | ``` | |
| 791 | 768 | ||
| 792 | 769 | ##### `DELETE /members/:userId` | |
| 793 | - | ||
| 794 | - | Remove member from subtask. No body. Returns deleted member object. | |
| 770 | + | Remove member from subtask. Sends `action_plan_member_removed` notification. No body. Returns deleted member object. | |
| 795 | 771 | ||
| 796 | 772 | --- | |
| 797 | 773 | ||
| @@ -800,12 +776,10 @@ Remove member from subtask. No body. Returns deleted member object. | |||
| 800 | 776 | Base: `/api/action-plans/tasks/:taskId` | |
| 801 | 777 | ||
| 802 | 778 | ##### `POST /members` | |
| 803 | - | ||
| 804 | 779 | ##### `GET /members` | |
| 805 | - | ||
| 806 | 780 | ##### `DELETE /members/:userId` | |
| 807 | 781 | ||
| 808 | - | Same pattern as subtask members. `scopeType = TASK`. | |
| 782 | + | Same pattern as subtask members. `scopeType = TASK`. Leaf-only: blocked if task has subtasks. | |
| 809 | 783 | ||
| 810 | 784 | --- | |
| 811 | 785 | ||
| @@ -814,12 +788,10 @@ Same pattern as subtask members. `scopeType = TASK`. | |||
| 814 | 788 | Base: `/api/action-plans/activities/:activityId` | |
| 815 | 789 | ||
| 816 | 790 | ##### `POST /members` | |
| 817 | - | ||
| 818 | 791 | ##### `GET /members` | |
| 819 | - | ||
| 820 | 792 | ##### `DELETE /members/:userId` | |
| 821 | 793 | ||
| 822 | - | Same pattern as subtask members. `scopeType = ACTIVITY`. | |
| 794 | + | Same pattern as subtask members. `scopeType = ACTIVITY`. Leaf-only: blocked if activity has tasks. | |
| 823 | 795 | ||
| 824 | 796 | --- | |
| 825 | 797 | ||
| @@ -830,33 +802,26 @@ Discussion thread per progress entry. Sender can edit own message. | |||
| 830 | 802 | Base: `/api/action-plans/progress/:progressId/feedbacks` | |
| 831 | 803 | ||
| 832 | 804 | #### `POST /` | |
| 833 | - | ||
| 834 | 805 | **Body:** | |
| 835 | - | ||
| 836 | 806 | ```jsonc | |
| 837 | 807 | { | |
| 838 | - | "message": "Please clarify the numbers", // string, 1-5000 | |
| 808 | + | "message": "Please clarify the numbers" // string, 1-5000 | |
| 839 | 809 | } | |
| 840 | 810 | ``` | |
| 841 | 811 | ||
| 842 | 812 | #### `GET /` | |
| 843 | - | ||
| 844 | - | List feedbacks for a progress entry. Paginated. | |
| 845 | - | **Query:** `page`, `limit`. | |
| 813 | + | List feedbacks. Paginated. **Query:** `page`, `limit`. | |
| 846 | 814 | ||
| 847 | 815 | #### `PATCH /:id` | |
| 848 | - | ||
| 849 | 816 | Edit own message. | |
| 850 | 817 | **Body:** | |
| 851 | - | ||
| 852 | 818 | ```jsonc | |
| 853 | 819 | { | |
| 854 | - | "message": "Updated message", // string, 1-5000 | |
| 820 | + | "message": "Updated message" // string, 1-5000 | |
| 855 | 821 | } | |
| 856 | 822 | ``` | |
| 857 | 823 | ||
| 858 | 824 | #### `DELETE /:id` | |
| 859 | - | ||
| 860 | 825 | Delete own message. No body. | |
| 861 | 826 | ||
| 862 | 827 | --- | |
| @@ -866,7 +831,6 @@ Delete own message. No body. | |||
| 866 | 831 | Base: `/api/projects/:projectId/action-plans/analytics` | |
| 867 | 832 | ||
| 868 | 833 | #### `GET /` | |
| 869 | - | ||
| 870 | 834 | **Query:** | |
| 871 | 835 | | Param | Type | Values | Default | | |
| 872 | 836 | |---|---|---|---| | |
| @@ -883,11 +847,9 @@ Base: `/api/projects/:projectId/action-plans/analytics` | |||
| 883 | 847 | Base: `/api/projects/:projectId/action-plans/import` | |
| 884 | 848 | ||
| 885 | 849 | #### `GET /template/download` | |
| 886 | - | ||
| 887 | 850 | Download Excel template file. Returns `.xlsx` binary. | |
| 888 | 851 | ||
| 889 | 852 | #### `POST /` (multipart/form-data) | |
| 890 | - | ||
| 891 | 853 | Import activities from spreadsheet. | |
| 892 | 854 | ||
| 893 | 855 | **Form field:** | |
| @@ -896,14 +858,13 @@ Import activities from spreadsheet. | |||
| 896 | 858 | | `file` | file | `.xlsx`, `.xls`, or `.csv`, max 5MB | | |
| 897 | 859 | ||
| 898 | 860 | **Response:** | |
| 899 | - | ||
| 900 | 861 | ```jsonc | |
| 901 | 862 | { | |
| 902 | - | "imported": 3, | |
| 903 | - | "activities": [ | |
| 904 | - | { "id": "uuid", "name": "Activity 1" }, | |
| 905 | - | { "id": "uuid", "name": "Activity 2" }, | |
| 906 | - | ], | |
| 863 | + | "imported": 3, | |
| 864 | + | "activities": [ | |
| 865 | + | { "id": "uuid", "name": "Activity 1" }, | |
| 866 | + | { "id": "uuid", "name": "Activity 2" } | |
| 867 | + | ] | |
| 907 | 868 | } | |
| 908 | 869 | ``` | |
| 909 | 870 | ||
| @@ -924,11 +885,14 @@ Import activities from spreadsheet. | |||
| 924 | 885 | | Work weight/volume | Number >= 0 | | |
| 925 | 886 | ||
| 926 | 887 | ### Editing Rules | |
| 927 | - | ||
| 928 | 888 | - Activity with tasks → only `name` editable | |
| 929 | 889 | - Task with subtasks → only `name` editable | |
| 930 | 890 | - Subtask in progress → `workWeight` and `workVolume` locked | |
| 931 | 891 | ||
| 932 | - | ### Soft Delete | |
| 892 | + | ### Leaf-Only Blocks | |
| 893 | + | - Activity with tasks → progress, plans, members rejected (400) | |
| 894 | + | - Task with subtasks → progress, plans, members rejected (400) | |
| 895 | + | - Subtask always allowed | |
| 933 | 896 | ||
| 897 | + | ### Soft Delete | |
| 934 | 898 | All deletes are soft (sets `deletedAt`). Cascades: deleting activity deletes all tasks + subtasks. Deleting task deletes all subtasks. | |
135dika revised this gist 1 month ago. Go to revision
1 file changed, 934 insertions
action-plan.docs.md(file created)
| @@ -0,0 +1,934 @@ | |||
| 1 | + | # Action Plans API | |
| 2 | + | ||
| 3 | + | Base URL: `{host}/api/v1` | |
| 4 | + | ||
| 5 | + | All responses wrapped in: | |
| 6 | + | ||
| 7 | + | ```json | |
| 8 | + | { | |
| 9 | + | "statusCode": 200, | |
| 10 | + | "message": "...", | |
| 11 | + | "data": { ... }, | |
| 12 | + | "durationMs": 42, | |
| 13 | + | "_reference": [] | |
| 14 | + | } | |
| 15 | + | ``` | |
| 16 | + | ||
| 17 | + | Shown `data` inline below for brevity. | |
| 18 | + | ||
| 19 | + | --- | |
| 20 | + | ||
| 21 | + | ## Hierarchy & Member Counting | |
| 22 | + | ||
| 23 | + | ``` | |
| 24 | + | Activity | |
| 25 | + | └─ Task (many) | |
| 26 | + | └─ Subtask (many) | |
| 27 | + | ``` | |
| 28 | + | ||
| 29 | + | | Level | Has children? | Member count source | | |
| 30 | + | | -------- | ---------------------- | ---------------------------- | | |
| 31 | + | | Activity | Tasks with subtasks | Union of all subtask members | | |
| 32 | + | | Activity | Tasks without subtasks | Union of all task members | | |
| 33 | + | | Activity | No tasks | Activity's own members | | |
| 34 | + | | Task | Has subtasks | Union of all subtask members | | |
| 35 | + | | Task | No subtasks | Task's own members | | |
| 36 | + | | Subtask | — | Subtask's own members | | |
| 37 | + | ||
| 38 | + | Example: | |
| 39 | + | ||
| 40 | + | ``` | |
| 41 | + | Activity1 (10 members) ← Union(Task1, Task2) | |
| 42 | + | Task1 (5 members) ← Union(Subtask1, Subtask2) | |
| 43 | + | Subtask1 (3 members) | |
| 44 | + | Subtask2 (2 members) | |
| 45 | + | Task2 (5 members) ← Task scope, no subtasks | |
| 46 | + | ``` | |
| 47 | + | ||
| 48 | + | --- | |
| 49 | + | ||
| 50 | + | ## Enums | |
| 51 | + | ||
| 52 | + | ### Subtask/Task/Activity Status | |
| 53 | + | ||
| 54 | + | | Value | Description | | |
| 55 | + | | ----------------- | ----------------------------------- | | |
| 56 | + | | `INCOMPLETE_DATA` | Required fields missing | | |
| 57 | + | | `NOT_STARTED` | Ready but no progress | | |
| 58 | + | | `IN_PROGRESS` | Progress entries exist | | |
| 59 | + | | `NEED_APPROVAL` | Pending progress requiring response | | |
| 60 | + | | `COMPLETED` | 100% complete | | |
| 61 | + | ||
| 62 | + | ### Progress Entry Status | |
| 63 | + | ||
| 64 | + | | Value | Description | | |
| 65 | + | | --------------------- | ---------------------------------------------- | | |
| 66 | + | | `PENDING` | Submitted, awaiting response | | |
| 67 | + | | `APPROVED` | Accepted | | |
| 68 | + | | `APPROVED_WITH_NOTES` | Accepted with amendments (new version created) | | |
| 69 | + | | `REJECTED` | Rejected | | |
| 70 | + | ||
| 71 | + | ### ActionPlanMemberScope | |
| 72 | + | ||
| 73 | + | | Value | Description | | |
| 74 | + | | ---------- | --------------------------- | | |
| 75 | + | | `ACTIVITY` | Member assigned to activity | | |
| 76 | + | | `TASK` | Member assigned to task | | |
| 77 | + | | `SUBTASK` | Member assigned to subtask | | |
| 78 | + | ||
| 79 | + | --- | |
| 80 | + | ||
| 81 | + | ## Routes | |
| 82 | + | ||
| 83 | + | ### 1. Activities | |
| 84 | + | ||
| 85 | + | Base: `/api/projects/:projectId/action-plans` | |
| 86 | + | ||
| 87 | + | #### `POST /` | |
| 88 | + | ||
| 89 | + | Create activity with optional nested tasks & subtasks. | |
| 90 | + | ||
| 91 | + | **Body:** | |
| 92 | + | ||
| 93 | + | ```jsonc | |
| 94 | + | { | |
| 95 | + | "name": "Activity name", // string, 1-255 | |
| 96 | + | "tasks": [ | |
| 97 | + | // optional, max 50 | |
| 98 | + | { | |
| 99 | + | "name": "Task name", // string, 1-255 | |
| 100 | + | "workWeight": 100, // optional, number >= 0 | |
| 101 | + | "workVolume": 500, // optional, number >= 0 | |
| 102 | + | "workVolumeUnit": "m2", // optional, string max 100 | |
| 103 | + | "startDate": "2026-01-01", // optional, ISO date | |
| 104 | + | "endDate": "2026-03-31", // optional, ISO date | |
| 105 | + | "subtasks": [ | |
| 106 | + | // optional, max 50 | |
| 107 | + | { | |
| 108 | + | "name": "Subtask name", // string, 1-255 | |
| 109 | + | "workVolume": 250, // optional, number >= 0 | |
| 110 | + | "workVolumeUnit": "m2", // optional, string max 100 | |
| 111 | + | "workWeight": 50, // optional, number >= 0 | |
| 112 | + | "startDate": "2026-01-01", // optional, ISO date | |
| 113 | + | "endDate": "2026-02-15", // optional, ISO date | |
| 114 | + | }, | |
| 115 | + | ], | |
| 116 | + | }, | |
| 117 | + | ], | |
| 118 | + | } | |
| 119 | + | ``` | |
| 120 | + | ||
| 121 | + | **Response:** Full activity with derived stats (same shape as GET /:id). | |
| 122 | + | ||
| 123 | + | --- | |
| 124 | + | ||
| 125 | + | #### `GET /` | |
| 126 | + | ||
| 127 | + | List activities for a project. Paginated, searchable. | |
| 128 | + | ||
| 129 | + | **Query:** | |
| 130 | + | | Param | Type | Default | Description | | |
| 131 | + | |---|---|---|---| | |
| 132 | + | | `page` | number | 1 | Page number | | |
| 133 | + | | `limit` | number | 20 | Items per page (max 100) | | |
| 134 | + | | `search` | string | — | Filter by name | | |
| 135 | + | | `sort_by` | `created_at` \| `updated_at` \| `name` | `created_at` | Sort field | | |
| 136 | + | | `sort_order` | `asc` \| `desc` | `desc` | Sort direction | | |
| 137 | + | ||
| 138 | + | **Response:** | |
| 139 | + | ||
| 140 | + | ```jsonc | |
| 141 | + | { | |
| 142 | + | "items": [ | |
| 143 | + | { | |
| 144 | + | "id": "uuid", | |
| 145 | + | "projectId": "uuid", | |
| 146 | + | "name": "Activity name", | |
| 147 | + | "createdBy": "uuid", | |
| 148 | + | "createdAt": "ISO date", | |
| 149 | + | "updatedAt": "ISO date", | |
| 150 | + | "deletedAt": null, | |
| 151 | + | "taskCount": 2, | |
| 152 | + | "subtaskCount": 5, | |
| 153 | + | "workWeight": 150, | |
| 154 | + | }, | |
| 155 | + | ], | |
| 156 | + | "total": 10, | |
| 157 | + | "page": 1, | |
| 158 | + | "limit": 20, | |
| 159 | + | } | |
| 160 | + | ``` | |
| 161 | + | ||
| 162 | + | --- | |
| 163 | + | ||
| 164 | + | #### `GET /tree` | |
| 165 | + | ||
| 166 | + | Full Activity > Task > Subtask tree. Each level has accumulated stats. | |
| 167 | + | ||
| 168 | + | **Query:** | |
| 169 | + | | Param | Type | Default | Description | | |
| 170 | + | |---|---|---|---| | |
| 171 | + | | `status` | string | — | Filter subtasks by status | | |
| 172 | + | ||
| 173 | + | **Response:** | |
| 174 | + | ||
| 175 | + | ```jsonc | |
| 176 | + | [ | |
| 177 | + | { | |
| 178 | + | "id": "uuid", | |
| 179 | + | "name": "Activity name", | |
| 180 | + | "status": "IN_PROGRESS", | |
| 181 | + | "totalTasks": 2, | |
| 182 | + | "totalUsersAssigned": 10, // memberCount (bottom-up) | |
| 183 | + | "accumulatedProgress": 45.5, // avg(), percent | |
| 184 | + | "createdBy": "uuid", | |
| 185 | + | "createdAt": "ISO date", | |
| 186 | + | "updatedAt": "ISO date", | |
| 187 | + | "tasks": [ | |
| 188 | + | { | |
| 189 | + | "id": "uuid", | |
| 190 | + | "name": "Task name", | |
| 191 | + | "status": "IN_PROGRESS", | |
| 192 | + | "totalSubtasks": 2, | |
| 193 | + | "totalUsersAssigned": 5, // memberCount (bottom-up) | |
| 194 | + | "accumulatedProgress": 60, // avg(), percent | |
| 195 | + | "workWeight": 100, | |
| 196 | + | "workVolume": 500, | |
| 197 | + | "workVolumeUnit": "m2", | |
| 198 | + | "startDate": "ISO date", | |
| 199 | + | "endDate": "ISO date", | |
| 200 | + | "subtasks": [ | |
| 201 | + | { | |
| 202 | + | "id": "uuid", | |
| 203 | + | "name": "Subtask name", | |
| 204 | + | "status": "IN_PROGRESS", | |
| 205 | + | "totalUsersAssigned": 3, // memberCount | |
| 206 | + | "progress": 75, // effective (non-rejected), percent | |
| 207 | + | "workVolume": 250, | |
| 208 | + | "workVolumeUnit": "m2", | |
| 209 | + | "workWeight": 50, | |
| 210 | + | "startDate": "ISO date", | |
| 211 | + | "endDate": "ISO date", | |
| 212 | + | }, | |
| 213 | + | ], | |
| 214 | + | }, | |
| 215 | + | ], | |
| 216 | + | }, | |
| 217 | + | ] | |
| 218 | + | ``` | |
| 219 | + | ||
| 220 | + | --- | |
| 221 | + | ||
| 222 | + | #### `GET /tree/gantt` | |
| 223 | + | ||
| 224 | + | Same tree structure but each node includes `startDate`/`endDate` derived from children. | |
| 225 | + | Activity `startDate` = min of all subtasks. Activity `endDate` = max of all subtasks. | |
| 226 | + | Task dates use own stored values. | |
| 227 | + | Supports `status` query filter (same as `/tree`). | |
| 228 | + | ||
| 229 | + | --- | |
| 230 | + | ||
| 231 | + | #### `GET /:id` | |
| 232 | + | ||
| 233 | + | Get single activity with derived stats. | |
| 234 | + | ||
| 235 | + | **Response:** | |
| 236 | + | ||
| 237 | + | ```jsonc | |
| 238 | + | { | |
| 239 | + | "id": "uuid", | |
| 240 | + | "projectId": "uuid", | |
| 241 | + | "name": "Activity name", | |
| 242 | + | "createdBy": "uuid", | |
| 243 | + | "createdAt": "ISO date", | |
| 244 | + | "updatedAt": "ISO date", | |
| 245 | + | "deletedAt": null, | |
| 246 | + | "taskCount": 2, | |
| 247 | + | "subtaskCount": 5, | |
| 248 | + | "memberCount": 10, // bottom-up (see rules above) | |
| 249 | + | "status": "IN_PROGRESS", | |
| 250 | + | "progress": 45.5, // avg(), percent | |
| 251 | + | "workWeight": 150, | |
| 252 | + | } | |
| 253 | + | ``` | |
| 254 | + | ||
| 255 | + | --- | |
| 256 | + | ||
| 257 | + | #### `PATCH /:id` | |
| 258 | + | ||
| 259 | + | Update activity. If activity has tasks, only `name` is editable. | |
| 260 | + | ||
| 261 | + | **Body:** | |
| 262 | + | ||
| 263 | + | ```jsonc | |
| 264 | + | { | |
| 265 | + | "name": "New name", // string, 1-255, optional | |
| 266 | + | } | |
| 267 | + | ``` | |
| 268 | + | ||
| 269 | + | --- | |
| 270 | + | ||
| 271 | + | #### `DELETE /:id` | |
| 272 | + | ||
| 273 | + | Soft-delete activity and all its tasks + subtasks. No body. Returns `204` status code. | |
| 274 | + | ||
| 275 | + | --- | |
| 276 | + | ||
| 277 | + | ### 2. Tasks | |
| 278 | + | ||
| 279 | + | Base: `/api/action-plans/activities/:activityId/tasks` | |
| 280 | + | ||
| 281 | + | #### `POST /` | |
| 282 | + | ||
| 283 | + | Create task under an activity. | |
| 284 | + | ||
| 285 | + | **Body:** | |
| 286 | + | ||
| 287 | + | ```jsonc | |
| 288 | + | { | |
| 289 | + | "name": "Task name", // string, 1-255 | |
| 290 | + | "workWeight": 100, // optional, number >= 0 | |
| 291 | + | "workVolume": 500, // optional, number >= 0 | |
| 292 | + | "workVolumeUnit": "m2", // optional, string max 100 | |
| 293 | + | "startDate": "2026-01-01", // optional, ISO date | |
| 294 | + | "endDate": "2026-03-31", // optional, ISO date | |
| 295 | + | } | |
| 296 | + | ``` | |
| 297 | + | ||
| 298 | + | **Response:** Task object with `id`, `name`, `workWeight`, `workVolume`, `workVolumeUnit`, `startDate`, `endDate`, `createdAt`. | |
| 299 | + | ||
| 300 | + | --- | |
| 301 | + | ||
| 302 | + | #### `GET /` | |
| 303 | + | ||
| 304 | + | List all tasks under an activity. | |
| 305 | + | ||
| 306 | + | **Response:** | |
| 307 | + | ||
| 308 | + | ```jsonc | |
| 309 | + | { | |
| 310 | + | "tasks": [ | |
| 311 | + | { | |
| 312 | + | "id": "uuid", | |
| 313 | + | "name": "Task name", | |
| 314 | + | "workWeight": 100, | |
| 315 | + | "workVolume": 500, | |
| 316 | + | "workVolumeUnit": "m2", | |
| 317 | + | "startDate": "ISO date", | |
| 318 | + | "endDate": "ISO date", | |
| 319 | + | "createdAt": "ISO date", | |
| 320 | + | "updatedAt": "ISO date", | |
| 321 | + | "subtaskCount": 3, | |
| 322 | + | }, | |
| 323 | + | ], | |
| 324 | + | } | |
| 325 | + | ``` | |
| 326 | + | ||
| 327 | + | --- | |
| 328 | + | ||
| 329 | + | #### `GET /:id` | |
| 330 | + | ||
| 331 | + | Get task detail with derived stats. | |
| 332 | + | ||
| 333 | + | **Response:** | |
| 334 | + | ||
| 335 | + | ```jsonc | |
| 336 | + | { | |
| 337 | + | "id": "uuid", | |
| 338 | + | "activityId": "uuid", | |
| 339 | + | "name": "Task name", | |
| 340 | + | "workWeight": 150, | |
| 341 | + | "workVolume": 500, | |
| 342 | + | "workVolumeUnit": "m2", | |
| 343 | + | "startDate": "ISO date", | |
| 344 | + | "endDate": "ISO date", | |
| 345 | + | "createdBy": "uuid", | |
| 346 | + | "createdAt": "ISO date", | |
| 347 | + | "updatedAt": "ISO date", | |
| 348 | + | "subtaskCount": 3, | |
| 349 | + | "memberCount": 5, // bottom-up (see rules above) | |
| 350 | + | "status": "IN_PROGRESS", | |
| 351 | + | "progress": 60, // avg(), percent | |
| 352 | + | "canAddSubtask": false, | |
| 353 | + | } | |
| 354 | + | ``` | |
| 355 | + | ||
| 356 | + | `canAddSubtask` is `false` if the task has subtasks and any subtask is beyond `INCOMPLETE_DATA` status. | |
| 357 | + | ||
| 358 | + | --- | |
| 359 | + | ||
| 360 | + | #### `PATCH /:id` | |
| 361 | + | ||
| 362 | + | Update task. If task has subtasks, only `name` is editable. | |
| 363 | + | ||
| 364 | + | **Body:** | |
| 365 | + | ||
| 366 | + | ```jsonc | |
| 367 | + | { | |
| 368 | + | "name": "New name", // string, 1-255, optional | |
| 369 | + | "workWeight": 200, // optional (only when no subtasks) | |
| 370 | + | "workVolume": 600, // optional (only when no subtasks) | |
| 371 | + | "workVolumeUnit": "km2", // optional (only when no subtasks) | |
| 372 | + | "startDate": "2026-01-01", // optional (only when no subtasks) | |
| 373 | + | "endDate": "2026-06-30", // optional (only when no subtasks) | |
| 374 | + | } | |
| 375 | + | ``` | |
| 376 | + | ||
| 377 | + | --- | |
| 378 | + | ||
| 379 | + | #### `DELETE /:id` | |
| 380 | + | ||
| 381 | + | Soft-delete task and all its subtasks. No body. | |
| 382 | + | ||
| 383 | + | --- | |
| 384 | + | ||
| 385 | + | ### 3. Subtasks | |
| 386 | + | ||
| 387 | + | Base: `/api/action-plans/tasks/:taskId/subtasks` | |
| 388 | + | ||
| 389 | + | #### `POST /` | |
| 390 | + | ||
| 391 | + | Create subtask under a task. | |
| 392 | + | ||
| 393 | + | **Body:** | |
| 394 | + | ||
| 395 | + | ```jsonc | |
| 396 | + | { | |
| 397 | + | "name": "Subtask name", // string, 1-255 | |
| 398 | + | "workVolume": 250, // optional, number >= 0 | |
| 399 | + | "workVolumeUnit": "m2", // optional, string max 100 | |
| 400 | + | "workWeight": 50, // optional, number >= 0 | |
| 401 | + | "startDate": "2026-01-01", // optional, ISO date | |
| 402 | + | "endDate": "2026-02-15", // optional, ISO date | |
| 403 | + | } | |
| 404 | + | ``` | |
| 405 | + | ||
| 406 | + | **Response:** Subtask object (see GET /:id). | |
| 407 | + | ||
| 408 | + | --- | |
| 409 | + | ||
| 410 | + | #### `GET /` | |
| 411 | + | ||
| 412 | + | List all subtasks under a task. | |
| 413 | + | ||
| 414 | + | **Response:** Array of subtask objects, each with `memberCount`, `members[].{userId, displayName}`, `status`, `progress`, `progressCount`. | |
| 415 | + | ||
| 416 | + | --- | |
| 417 | + | ||
| 418 | + | #### `GET /:id` | |
| 419 | + | ||
| 420 | + | Get subtask detail. | |
| 421 | + | ||
| 422 | + | **Response:** | |
| 423 | + | ||
| 424 | + | ```jsonc | |
| 425 | + | { | |
| 426 | + | "id": "uuid", | |
| 427 | + | "taskId": "uuid", | |
| 428 | + | "task": { "id": "uuid", "activityId": "uuid" }, | |
| 429 | + | "name": "Subtask name", | |
| 430 | + | "workVolume": 250, | |
| 431 | + | "workVolumeUnit": "m2", | |
| 432 | + | "workWeight": 50, | |
| 433 | + | "startDate": "ISO date", | |
| 434 | + | "endDate": "ISO date", | |
| 435 | + | "createdBy": "uuid", | |
| 436 | + | "createdAt": "ISO date", | |
| 437 | + | "updatedAt": "ISO date", | |
| 438 | + | "deletedAt": null, | |
| 439 | + | "members": [ | |
| 440 | + | { | |
| 441 | + | "id": "uuid", | |
| 442 | + | "userId": "uuid", | |
| 443 | + | "displayName": "John Doe", | |
| 444 | + | }, | |
| 445 | + | ], | |
| 446 | + | "memberCount": 3, | |
| 447 | + | "status": "IN_PROGRESS", | |
| 448 | + | "progress": 75, | |
| 449 | + | "progressCount": 2, | |
| 450 | + | "progressPlanCount": 1, | |
| 451 | + | "plans": [ | |
| 452 | + | { | |
| 453 | + | "id": "uuid", | |
| 454 | + | "startDate": "ISO date", | |
| 455 | + | "endDate": "ISO date", | |
| 456 | + | "targetProgress": 50, | |
| 457 | + | }, | |
| 458 | + | ], | |
| 459 | + | } | |
| 460 | + | ``` | |
| 461 | + | ||
| 462 | + | --- | |
| 463 | + | ||
| 464 | + | #### `PATCH /:id` | |
| 465 | + | ||
| 466 | + | Update subtask. Work weight & work volume are locked once subtask is in progress. | |
| 467 | + | ||
| 468 | + | **Body:** | |
| 469 | + | ||
| 470 | + | ```jsonc | |
| 471 | + | { | |
| 472 | + | "name": "New name", // string, 1-255, optional | |
| 473 | + | "workVolume": 300, // optional (locked after progress starts) | |
| 474 | + | "workVolumeUnit": "kg", // optional (locked after progress starts) | |
| 475 | + | "workWeight": 60, // optional (locked after progress starts) | |
| 476 | + | "startDate": "2026-01-01", // optional | |
| 477 | + | "endDate": "2026-02-15", // optional | |
| 478 | + | } | |
| 479 | + | ``` | |
| 480 | + | ||
| 481 | + | --- | |
| 482 | + | ||
| 483 | + | #### `DELETE /:id` | |
| 484 | + | ||
| 485 | + | Soft-delete subtask. No body. | |
| 486 | + | ||
| 487 | + | --- | |
| 488 | + | ||
| 489 | + | ### 4. Progress | |
| 490 | + | ||
| 491 | + | Three sets of progress endpoints — subtask, task, and activity scope. | |
| 492 | + | ||
| 493 | + | --- | |
| 494 | + | ||
| 495 | + | #### Subtask Progress | |
| 496 | + | ||
| 497 | + | Base: `/api/action-plans/subtasks/:subtaskId/progress` | |
| 498 | + | ||
| 499 | + | ##### `POST /` (multipart/form-data) | |
| 500 | + | ||
| 501 | + | Submit progress entry. Supports file uploads. | |
| 502 | + | ||
| 503 | + | **Form fields:** | |
| 504 | + | | Field | Type | Description | | |
| 505 | + | |---|---|---| | |
| 506 | + | | `progress` | number (0-100) | New cumulative progress % | | |
| 507 | + | | `workVolume` | number | Optional work volume completed | | |
| 508 | + | | `description` | string | Optional note, max 5000 chars | | |
| 509 | + | | `images` | file[] | Image attachments, max 10 | | |
| 510 | + | | `sitemap` | file | Single PDF sitemap | | |
| 511 | + | ||
| 512 | + | Progress auto-accumulates from previous APPROVED entries. If not provided, defaults to current effective progress. | |
| 513 | + | When `workVolume` is provided with `totalVolume`, progress % is derived from workVolume ratio. | |
| 514 | + | New progress must be >= current non-REJECTED floor. | |
| 515 | + | ||
| 516 | + | **Response:** Full progress entry (see GET /:id). | |
| 517 | + | ||
| 518 | + | ##### `GET /` | |
| 519 | + | ||
| 520 | + | List progress history for a subtask. Paginated, newest first. | |
| 521 | + | ||
| 522 | + | **Query:** `page` (default 1), `limit` (default 50, max 100) | |
| 523 | + | ||
| 524 | + | **Response:** | |
| 525 | + | ||
| 526 | + | ```jsonc | |
| 527 | + | { | |
| 528 | + | "items": [ | |
| 529 | + | { | |
| 530 | + | "id": "uuid", | |
| 531 | + | "subtaskId": "uuid", | |
| 532 | + | "subtaskName": "Subtask name", | |
| 533 | + | "progress": 75, | |
| 534 | + | "workVolume": 187.5, | |
| 535 | + | "subtaskWorkVolume": 250, | |
| 536 | + | "subtaskWorkVolumeUnit": "m2", | |
| 537 | + | "description": "Completed foundation", | |
| 538 | + | "status": "APPROVED", | |
| 539 | + | "amendsId": null, | |
| 540 | + | "amendsOriginal": null, | |
| 541 | + | "amendedByCount": 0, | |
| 542 | + | "amendedBy": [], | |
| 543 | + | "approvedBy": "uuid", | |
| 544 | + | "approvedByName": "Admin", | |
| 545 | + | "approvedAt": "ISO date", | |
| 546 | + | "createdBy": "uuid", | |
| 547 | + | "createdByName": "Worker", | |
| 548 | + | "updatedBy": null, | |
| 549 | + | "updatedByName": null, | |
| 550 | + | "createdAt": "ISO date", | |
| 551 | + | "updatedAt": "ISO date", | |
| 552 | + | "images": [{ "id": "uuid", "key": "s3/path", "originalName": "photo.jpg", "url": "presigned-url" }], | |
| 553 | + | "sitemap": { "id": "uuid", "key": "s3/path", "originalName": "site.pdf", "url": "presigned-url" }, | |
| 554 | + | }, | |
| 555 | + | ], | |
| 556 | + | "pagination": { | |
| 557 | + | "page": 1, | |
| 558 | + | "limit": 50, | |
| 559 | + | "total": 2, | |
| 560 | + | "pages": 1, | |
| 561 | + | "hasNext": false, | |
| 562 | + | "hasPrev": false, | |
| 563 | + | }, | |
| 564 | + | } | |
| 565 | + | ``` | |
| 566 | + | ||
| 567 | + | ##### `GET /:id` | |
| 568 | + | ||
| 569 | + | Get single progress entry with full detail including images and amendment chain. | |
| 570 | + | ||
| 571 | + | **Response:** Same shape as one item in the list above. | |
| 572 | + | ||
| 573 | + | ##### `POST /:id/respond` | |
| 574 | + | ||
| 575 | + | Admin responds to a PENDING progress entry. | |
| 576 | + | ||
| 577 | + | **Body:** | |
| 578 | + | ||
| 579 | + | ```jsonc | |
| 580 | + | { | |
| 581 | + | "verb": "APPROVE", // "APPROVE" | "APPROVE_WITH_NOTES" | "REJECT" | |
| 582 | + | "progress": 80, // optional, 0-100 (for APPROVE_WITH_NOTES) | |
| 583 | + | "workVolume": 200, // optional (for APPROVE_WITH_NOTES) | |
| 584 | + | "description": "Adjusted", // optional (for APPROVE_WITH_NOTES) | |
| 585 | + | } | |
| 586 | + | ``` | |
| 587 | + | ||
| 588 | + | **Verbs:** | |
| 589 | + | ||
| 590 | + | - `APPROVE` — progress accepted as-is | |
| 591 | + | - `APPROVE_WITH_NOTES` — original marked APPROVED_WITH_NOTES + new amended entry created with admin values | |
| 592 | + | - `REJECT` — progress rejected, doesn't count toward effective progress | |
| 593 | + | ||
| 594 | + | --- | |
| 595 | + | ||
| 596 | + | #### Task Progress | |
| 597 | + | ||
| 598 | + | Base: `/api/action-plans/tasks/:taskId` | |
| 599 | + | ||
| 600 | + | ##### `POST /progress` | |
| 601 | + | ||
| 602 | + | Create progress entry at task scope. | |
| 603 | + | ||
| 604 | + | **Body:** Same as subtask progress body (minimal: `{ "progress": 25 }`). | |
| 605 | + | ||
| 606 | + | ##### `GET /progress` | |
| 607 | + | ||
| 608 | + | List progress entries. **Query:** `page`, `limit`. | |
| 609 | + | ||
| 610 | + | ##### `GET /progress/:id` | |
| 611 | + | ||
| 612 | + | Get single progress entry. | |
| 613 | + | ||
| 614 | + | ##### `POST /progress/:id/respond` | |
| 615 | + | ||
| 616 | + | Respond to PENDING progress entry. **Body:** Same respond schema. | |
| 617 | + | ||
| 618 | + | --- | |
| 619 | + | ||
| 620 | + | #### Activity Progress | |
| 621 | + | ||
| 622 | + | Base: `/api/action-plans/activities/:activityId` | |
| 623 | + | ||
| 624 | + | ##### `POST /progress` | |
| 625 | + | ||
| 626 | + | ##### `GET /progress` | |
| 627 | + | ||
| 628 | + | ##### `GET /progress/:id` | |
| 629 | + | ||
| 630 | + | ##### `POST /progress/:id/respond` | |
| 631 | + | ||
| 632 | + | Same as task progress. Scope = ACTIVITY. | |
| 633 | + | ||
| 634 | + | --- | |
| 635 | + | ||
| 636 | + | ### 5. Progress Plans (S-Curve) | |
| 637 | + | ||
| 638 | + | Set target progress over time. All levels (subtask/task/activity) share same shape. | |
| 639 | + | ||
| 640 | + | --- | |
| 641 | + | ||
| 642 | + | #### Subtask Plans | |
| 643 | + | ||
| 644 | + | Base: `/api/action-plans/tasks/:taskId/subtasks/:id` | |
| 645 | + | ||
| 646 | + | ##### `GET /plans` | |
| 647 | + | ||
| 648 | + | **Response:** | |
| 649 | + | ||
| 650 | + | ```jsonc | |
| 651 | + | { | |
| 652 | + | "duration": { | |
| 653 | + | "days": 90, | |
| 654 | + | "startDate": "2026-01-01", | |
| 655 | + | "endDate": "2026-03-31", | |
| 656 | + | }, | |
| 657 | + | "planning": [ | |
| 658 | + | { | |
| 659 | + | "id": "uuid", | |
| 660 | + | "startDate": "2026-01-01", | |
| 661 | + | "endDate": "2026-02-15", | |
| 662 | + | "targetProgress": 50, | |
| 663 | + | }, | |
| 664 | + | { | |
| 665 | + | "id": "uuid", | |
| 666 | + | "startDate": "2026-02-15", | |
| 667 | + | "endDate": "2026-03-31", | |
| 668 | + | "targetProgress": 50, | |
| 669 | + | }, | |
| 670 | + | ], | |
| 671 | + | "sCurve": { | |
| 672 | + | "plan": [ | |
| 673 | + | { "date": 1704067200000, "progress": 0 }, | |
| 674 | + | { "date": 1709251200000, "progress": 50 }, | |
| 675 | + | { "date": 1711929600000, "progress": 100 }, | |
| 676 | + | ], | |
| 677 | + | "actual": [ | |
| 678 | + | { "date": 1705276800000, "progress": 25, "id": "uuid" }, | |
| 679 | + | { "date": 1708300800000, "progress": 60, "id": "uuid" }, | |
| 680 | + | ], | |
| 681 | + | }, | |
| 682 | + | } | |
| 683 | + | ``` | |
| 684 | + | ||
| 685 | + | `sCurve.plan` — cumulative target over time (derived from plan segments). | |
| 686 | + | `sCurve.actual` — APPROVED entries sorted by `approvedAt`. Only latest version per amendment chain. | |
| 687 | + | ||
| 688 | + | If no plans stored and subtask has dates, a default segment 0%→100% is returned. | |
| 689 | + | ||
| 690 | + | --- | |
| 691 | + | ||
| 692 | + | ##### `PUT /plans` | |
| 693 | + | ||
| 694 | + | Set (replace) all plans. Validates: continuous dates, within subtask date range, sum to 100%. | |
| 695 | + | ||
| 696 | + | **Body:** | |
| 697 | + | ||
| 698 | + | ```jsonc | |
| 699 | + | { | |
| 700 | + | "plans": [ | |
| 701 | + | { | |
| 702 | + | "startDate": "2026-01-01", | |
| 703 | + | "endDate": "2026-02-15", | |
| 704 | + | "targetProgress": 50, | |
| 705 | + | }, | |
| 706 | + | { | |
| 707 | + | "startDate": "2026-02-15", | |
| 708 | + | "endDate": "2026-03-31", | |
| 709 | + | "targetProgress": 50, | |
| 710 | + | }, | |
| 711 | + | ], | |
| 712 | + | } | |
| 713 | + | ``` | |
| 714 | + | ||
| 715 | + | **Response:** Same as GET /plans. | |
| 716 | + | ||
| 717 | + | --- | |
| 718 | + | ||
| 719 | + | #### Task Plans | |
| 720 | + | ||
| 721 | + | Base: `/api/action-plans/tasks/:taskId` | |
| 722 | + | ||
| 723 | + | ##### `GET /plans` | |
| 724 | + | ||
| 725 | + | ##### `PUT /plans` | |
| 726 | + | ||
| 727 | + | Same shape as subtask plans. Scope = TASK. | |
| 728 | + | ||
| 729 | + | --- | |
| 730 | + | ||
| 731 | + | #### Activity Plans | |
| 732 | + | ||
| 733 | + | Base: `/api/action-plans/activities/:activityId` | |
| 734 | + | ||
| 735 | + | ##### `GET /plans` | |
| 736 | + | ||
| 737 | + | ##### `PUT /plans` | |
| 738 | + | ||
| 739 | + | Same shape as subtask plans. Scope = ACTIVITY. | |
| 740 | + | ||
| 741 | + | --- | |
| 742 | + | ||
| 743 | + | ### 6. Members | |
| 744 | + | ||
| 745 | + | Member CRUD across all three hierarchy levels. Uses unified `actionPlanMembers` table. | |
| 746 | + | ||
| 747 | + | --- | |
| 748 | + | ||
| 749 | + | #### Subtask Members | |
| 750 | + | ||
| 751 | + | Base: `/api/action-plans/tasks/:taskId/subtasks/:id` | |
| 752 | + | ||
| 753 | + | ##### `POST /members` | |
| 754 | + | ||
| 755 | + | Add member to subtask. Also cascades to activity members (upsert). | |
| 756 | + | ||
| 757 | + | **Body:** | |
| 758 | + | ||
| 759 | + | ```jsonc | |
| 760 | + | { | |
| 761 | + | "userId": "uuid", | |
| 762 | + | } | |
| 763 | + | ``` | |
| 764 | + | ||
| 765 | + | **Response:** | |
| 766 | + | ||
| 767 | + | ```jsonc | |
| 768 | + | { | |
| 769 | + | "id": "uuid", | |
| 770 | + | "scopeType": "SUBTASK", | |
| 771 | + | "scopeId": "uuid", | |
| 772 | + | "userId": "uuid", | |
| 773 | + | } | |
| 774 | + | ``` | |
| 775 | + | ||
| 776 | + | ##### `GET /members` | |
| 777 | + | ||
| 778 | + | List members of a subtask. | |
| 779 | + | ||
| 780 | + | **Response:** | |
| 781 | + | ||
| 782 | + | ```jsonc | |
| 783 | + | [ | |
| 784 | + | { | |
| 785 | + | "id": "uuid", | |
| 786 | + | "userId": "uuid", | |
| 787 | + | "displayName": "John Doe", | |
| 788 | + | }, | |
| 789 | + | ] | |
| 790 | + | ``` | |
| 791 | + | ||
| 792 | + | ##### `DELETE /members/:userId` | |
| 793 | + | ||
| 794 | + | Remove member from subtask. No body. Returns deleted member object. | |
| 795 | + | ||
| 796 | + | --- | |
| 797 | + | ||
| 798 | + | #### Task Members | |
| 799 | + | ||
| 800 | + | Base: `/api/action-plans/tasks/:taskId` | |
| 801 | + | ||
| 802 | + | ##### `POST /members` | |
| 803 | + | ||
| 804 | + | ##### `GET /members` | |
| 805 | + | ||
| 806 | + | ##### `DELETE /members/:userId` | |
| 807 | + | ||
| 808 | + | Same pattern as subtask members. `scopeType = TASK`. | |
| 809 | + | ||
| 810 | + | --- | |
| 811 | + | ||
| 812 | + | #### Activity Members | |
| 813 | + | ||
| 814 | + | Base: `/api/action-plans/activities/:activityId` | |
| 815 | + | ||
| 816 | + | ##### `POST /members` | |
| 817 | + | ||
| 818 | + | ##### `GET /members` | |
| 819 | + | ||
| 820 | + | ##### `DELETE /members/:userId` | |
| 821 | + | ||
| 822 | + | Same pattern as subtask members. `scopeType = ACTIVITY`. | |
| 823 | + | ||
| 824 | + | --- | |
| 825 | + | ||
| 826 | + | ### 7. Feedbacks | |
| 827 | + | ||
| 828 | + | Discussion thread per progress entry. Sender can edit own message. | |
| 829 | + | ||
| 830 | + | Base: `/api/action-plans/progress/:progressId/feedbacks` | |
| 831 | + | ||
| 832 | + | #### `POST /` | |
| 833 | + | ||
| 834 | + | **Body:** | |
| 835 | + | ||
| 836 | + | ```jsonc | |
| 837 | + | { | |
| 838 | + | "message": "Please clarify the numbers", // string, 1-5000 | |
| 839 | + | } | |
| 840 | + | ``` | |
| 841 | + | ||
| 842 | + | #### `GET /` | |
| 843 | + | ||
| 844 | + | List feedbacks for a progress entry. Paginated. | |
| 845 | + | **Query:** `page`, `limit`. | |
| 846 | + | ||
| 847 | + | #### `PATCH /:id` | |
| 848 | + | ||
| 849 | + | Edit own message. | |
| 850 | + | **Body:** | |
| 851 | + | ||
| 852 | + | ```jsonc | |
| 853 | + | { | |
| 854 | + | "message": "Updated message", // string, 1-5000 | |
| 855 | + | } | |
| 856 | + | ``` | |
| 857 | + | ||
| 858 | + | #### `DELETE /:id` | |
| 859 | + | ||
| 860 | + | Delete own message. No body. | |
| 861 | + | ||
| 862 | + | --- | |
| 863 | + | ||
| 864 | + | ### 8. Analytics | |
| 865 | + | ||
| 866 | + | Base: `/api/projects/:projectId/action-plans/analytics` | |
| 867 | + | ||
| 868 | + | #### `GET /` | |
| 869 | + | ||
| 870 | + | **Query:** | |
| 871 | + | | Param | Type | Values | Default | | |
| 872 | + | |---|---|---|---| | |
| 873 | + | | `layout` | string | `detailed` \| `simplified` | `detailed` | | |
| 874 | + | | `granularity` | string | `daily` \| `weekly` \| `monthly` | `daily` | | |
| 875 | + | | `search` | string | — | — | | |
| 876 | + | | `startDate` | ISO date | — | — | | |
| 877 | + | | `endDate` | ISO date | — | — | | |
| 878 | + | ||
| 879 | + | --- | |
| 880 | + | ||
| 881 | + | ### 9. Import | |
| 882 | + | ||
| 883 | + | Base: `/api/projects/:projectId/action-plans/import` | |
| 884 | + | ||
| 885 | + | #### `GET /template/download` | |
| 886 | + | ||
| 887 | + | Download Excel template file. Returns `.xlsx` binary. | |
| 888 | + | ||
| 889 | + | #### `POST /` (multipart/form-data) | |
| 890 | + | ||
| 891 | + | Import activities from spreadsheet. | |
| 892 | + | ||
| 893 | + | **Form field:** | |
| 894 | + | | Field | Type | Description | | |
| 895 | + | |---|---|---| | |
| 896 | + | | `file` | file | `.xlsx`, `.xls`, or `.csv`, max 5MB | | |
| 897 | + | ||
| 898 | + | **Response:** | |
| 899 | + | ||
| 900 | + | ```jsonc | |
| 901 | + | { | |
| 902 | + | "imported": 3, | |
| 903 | + | "activities": [ | |
| 904 | + | { "id": "uuid", "name": "Activity 1" }, | |
| 905 | + | { "id": "uuid", "name": "Activity 2" }, | |
| 906 | + | ], | |
| 907 | + | } | |
| 908 | + | ``` | |
| 909 | + | ||
| 910 | + | --- | |
| 911 | + | ||
| 912 | + | ## Constraint Summary | |
| 913 | + | ||
| 914 | + | | Constraint | Value | | |
| 915 | + | | ----------------------- | ----------- | | |
| 916 | + | | Max tasks per activity | 50 | | |
| 917 | + | | Max subtasks per task | 50 | | |
| 918 | + | | Max plans per scope | Unlimited | | |
| 919 | + | | Max images per progress | 10 | | |
| 920 | + | | Max feedback length | 5000 chars | | |
| 921 | + | | Max description length | 5000 chars | | |
| 922 | + | | Max import file size | 5MB | | |
| 923 | + | | Progress range | 0–100 | | |
| 924 | + | | Work weight/volume | Number >= 0 | | |
| 925 | + | ||
| 926 | + | ### Editing Rules | |
| 927 | + | ||
| 928 | + | - Activity with tasks → only `name` editable | |
| 929 | + | - Task with subtasks → only `name` editable | |
| 930 | + | - Subtask in progress → `workWeight` and `workVolume` locked | |
| 931 | + | ||
| 932 | + | ### Soft Delete | |
| 933 | + | ||
| 934 | + | All deletes are soft (sets `deletedAt`). Cascades: deleting activity deletes all tasks + subtasks. Deleting task deletes all subtasks. | |