Action Plans API
Base URL: {host}/api/v1
All responses wrapped in:
{ "statusCode": 200, "message": "...", "data": { ... }, "durationMs": 42, "_reference": [] }
Hierarchy & Member Counting
Activity
└─ Task (many)
└─ Subtask (many)
| Level | Has children? | Member count source |
|---|---|---|
| Activity | Tasks with subtasks | Union of all subtask members |
| Activity | Tasks without subtasks | Union of all task members |
| Activity | No tasks | Activity's own members |
| Task | Has subtasks | Union of all subtask members |
| Task | No subtasks | Task's own members |
| Subtask | — | Subtask's own members |
Example:
Activity1 (10 members) ← Union(Task1, Task2)
Task1 (5 members) ← Union(Subtask1, Subtask2)
Subtask1 (3 members)
Subtask2 (2 members)
Task2 (5 members) ← Task scope, no subtasks
Leaf-Only Validation
Nodes with children reject direct progress/plans/members operations.
| Operation | Activity has tasks? | Task has subtasks? |
|---|---|---|
| Progress | ❌ 400 | ❌ 400 |
| Plans | ❌ 400 | ❌ 400 |
| Members | ❌ 400 | ❌ 400 |
Subtask always valid.
Route Map
All routes under: api/projects/:projectId/action-plans
GET / list activities
POST / create activity
GET /tree tree view
GET /tree/gantt gantt
GET /analytics analytics
GET /import/template/download excel template
POST /import import
GET /:activityId get activity
PATCH /:activityId update activity
DELETE /:activityId delete activity
// Activity leaf
POST /:activityId/progress create progress
GET /:activityId/progress list progress
GET /:activityId/progress/:progressId get progress
POST /:activityId/progress/:progressId/respond respond
GET /:activityId/plans plans (S-curve)
PUT /:activityId/plans set plans
POST /:activityId/members add member
DELETE /:activityId/members/:userId remove member
GET /:activityId/members list members
POST /:activityId/feedbacks create feedback
GET /:activityId/feedbacks list feedbacks
// Task CRUD + leaf
POST /:activityId/tasks create task
GET /:activityId/tasks list tasks
GET /:activityId/tasks/:taskId get task
PATCH /:activityId/tasks/:taskId update task
DELETE /:activityId/tasks/:taskId delete task
POST /:activityId/tasks/:taskId/progress task progress
GET /:activityId/tasks/:taskId/progress list
GET /:activityId/tasks/:taskId/progress/:pid get
POST /:activityId/tasks/:taskId/progress/:pid/respond
GET /:activityId/tasks/:taskId/plans plans
PUT /:activityId/tasks/:taskId/plans
POST /:activityId/tasks/:taskId/members add member
DELETE /:activityId/tasks/:taskId/members/:uid remove member
GET /:activityId/tasks/:taskId/members list members
POST /:activityId/tasks/:taskId/feedbacks feedback
GET /:activityId/tasks/:taskId/feedbacks
// Subtask CRUD + leaf
POST /:activityId/tasks/:taskId/subtasks create subtask
GET /:activityId/tasks/:taskId/subtasks list subtasks
GET /:activityId/tasks/:taskId/subtasks/:subtaskId
PATCH /:activityId/tasks/:taskId/subtasks/:subtaskId
DELETE /:activityId/tasks/:taskId/subtasks/:subtaskId
POST /:activityId/tasks/:taskId/subtasks/:sid/members
DELETE /:activityId/tasks/:taskId/subtasks/:sid/members/:uid
GET /:activityId/tasks/:taskId/subtasks/:sid/members
GET /:activityId/tasks/:taskId/subtasks/:sid/plans
PUT /:activityId/tasks/:taskId/subtasks/:sid/plans
POST /:activityId/tasks/:taskId/subtasks/:sid/progress
GET /:activityId/tasks/:taskId/subtasks/:sid/progress
GET /:activityId/tasks/:taskId/subtasks/:sid/progress/:pid
POST /:activityId/tasks/:taskId/subtasks/:sid/progress/:pid/respond
// Feedback CRUD
POST /:aId/tasks/:tId/subtasks/:sId/progress/:pId/feedbacks
GET /.../feedbacks
PATCH /.../feedbacks/:fId
DELETE /.../feedbacks/:fId
Enums
Subtask/Task/Activity Status
INCOMPLETE_DATA | NOT_STARTED | IN_PROGRESS | NEED_APPROVAL | COMPLETED
Progress Entry Status
PENDING | APPROVED | APPROVED_WITH_NOTES | REJECTED
ActionPlanMemberScope
ACTIVITY | TASK | SUBTASK
Notifications
Fire-and-forget. Does not block operation on failure.
Member Notifications
| Trigger | Type | Recipient |
|---|---|---|
| Add to subtask | action_plan_member_added |
Added user |
| Remove from subtask | action_plan_member_removed |
Removed user |
| Add to task | action_plan_member_added |
Added user |
| Remove from task | action_plan_member_removed |
Removed user |
| Add to activity | action_plan_member_added |
Added user |
| Remove from activity | action_plan_member_removed |
Removed user |
Progress Respond Notifications
| Verb | Type | Recipient |
|---|---|---|
| APPROVE | action_plan_progress_approve |
Submitter |
| APPROVE_WITH_NOTES | action_plan_progress_approve_with_notes |
Submitter |
| REJECT | action_plan_progress_reject |
Submitter |
Request/Response Schemas
Create Activity
POST /
{
"name": "Activity name",
"tasks": [{ "name": "Task name", "workWeight": 100, "workVolume": 500, "workVolumeUnit": "m2", "startDate": "2026-01-01", "endDate": "2026-03-31",
"subtasks": [{ "name": "Subtask name", "workVolume": 250, "workVolumeUnit": "m2", "workWeight": 50, "startDate": "2026-01-01", "endDate": "2026-02-15" }]
}]
}
List Activities
GET / — Query: page, limit, search, sort_by (created_at/updated_at/name), sort_order (asc/desc)
Response: { items: [...], total, page, limit }
Get Single Activity
GET /:id
{ "id":"uuid", "projectId":"uuid", "name":"...", "taskCount":2, "subtaskCount":5, "memberCount":10, "status":"IN_PROGRESS", "progress":45.5, "workWeight":150 }
Tree / Gantt
GET /tree — Query: status filter. Each node has totalUsersAssigned, accumulatedProgress.
GET /tree/gantt — Same + derived startDate/endDate.
Progress Entry
Response shape (all scopes):
{
"id":"uuid", "scopeType":"SUBTASK|TASK|ACTIVITY", "scopeId":"uuid", "scopeName":"name",
"progress":75, "workVolume":187.5, "scopeWorkVolume":250, "scopeWorkVolumeUnit":"m2",
"description":"...", "status":"APPROVED", "amendsId":null, "amendsOriginal":null,
"amendedByCount":0, "amendedBy":[], "approvedBy":"uuid", "approvedByName":"Admin",
"approvedAt":"ISO", "createdBy":"uuid", "createdByName":"Worker",
"images":[{ "id":"uuid","key":"s3/path","originalName":"photo.jpg","url":"presigned-url" }],
"sitemap":{ "id":"uuid","key":"s3/path","originalName":"site.pdf","url":"presigned-url" }
}
Progress Create (multipart/form-data)
POST /:../progress
| Field | Type |
|---|---|
progress |
number (0-100) |
workVolume |
number, optional |
description |
string, max 5000 |
images |
file[], max 10 |
sitemap |
file, PDF |
Progress Respond
POST /:../progress/:id/respond
{ "verb":"APPROVE|APPROVE_WITH_NOTES|REJECT", "progress":80, "workVolume":200, "description":"Adjusted" }
Plans (S-Curve)
GET /:../plans
{ "duration":{ "days":90, "startDate":"...", "endDate":"..." }, "planning":[...], "sCurve":{ "plan":[...], "actual":[...] } }
PUT /:../plans — { "plans":[{ "startDate":"...", "endDate":"...", "targetProgress":50 }] }
Members
POST/DELETE/GET /:../members — { "userId":"uuid" } / response: [{ "id":"uuid","userId":"uuid","displayName":"John Doe" }]
Feedbacks
POST /:../feedbacks — { "message":"text" }
PATCH /:../feedbacks/:id — { "message":"updated" }
Analytics
GET /analytics — Query: layout (detailed/simplified), granularity (daily/weekly/monthly), search, startDate, endDate
Import
GET /import/template/download — Excel file
POST /import (multipart) — file (.xlsx/.xls/.csv, max 5MB)
Constraint Summary
| Constraint | Value |
|---|---|
| Max tasks per activity | 50 |
| Max subtasks per task | 50 |
| Max images per progress | 10 |
| Max feedback/description length | 5000 chars |
| Max import file size | 5MB |
| Progress range | 0-100 |
| Work weight/volume | Number >= 0 |
Editing Rules
- Activity with tasks → only
nameeditable - Task with subtasks → only
nameeditable - Subtask in progress →
workWeight/workVolumelocked
Soft Delete
All deletes are soft (deletedAt). Cascades: deleting activity → all tasks + subtasks. Deleting task → all subtasks.