Last active 4 days ago

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.
Newer Older