# Action Plans API Base URL: `{host}/api/v1` All responses wrapped in: ```json { "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 // Activity progress feedbacks POST /:activityId/progress/:pid/feedbacks create feedback GET /:activityId/progress/:pid/feedbacks list feedbacks PATCH /:activityId/progress/:pid/feedbacks/:fid edit feedback DELETE /:activityId/progress/:pid/feedbacks/:fid delete feedback // 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 // Task progress feedbacks POST /:activityId/tasks/:tid/progress/:pid/feedbacks GET /:activityId/tasks/:tid/progress/:pid/feedbacks PATCH /:activityId/tasks/:tid/progress/:pid/feedbacks/:fid DELETE /:activityId/tasks/:tid/progress/:pid/feedbacks/:fid // 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 // Subtask progress feedbacks 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` --- ## Authorization All write operations require the actor to have an active role in `workspace_member_scopes`. Validation via shared helper `validateWorkspaceRole` in `action-plans.helpers.ts`. ### Role Hierarchy Scope Authorization checks across 4 scope levels — owner/admin at any level can manage child projects: ``` AGENDA └─ PROGRAM └─ PROJECT ← projectId from request ``` A user must have an `active` row in `workspace_member_scopes` with: - `role` matching the required role set - `user_id` = actor - One of: `scope_type=PROJECT, scope_id=projectId` OR `scope_type=WORKSPACE, workspace_id=project.workspace_id` OR `scope_type=PROGRAM, scope_id=project.program_id` OR `scope_type=AGENDA, scope_id=project.program.agenda_id` ### Operation → Role Mapping | Operation | Required Role | |---|---| | Create activity/task/subtask | `OWNER` / `ADMIN` | | Update activity/task/subtask | `OWNER` / `ADMIN` | | Delete activity/task/subtask | `OWNER` / `ADMIN` | | Set plans | `OWNER` / `ADMIN` | | Add/remove member (any scope) | `OWNER` / `ADMIN` | | Respond progress (approve/reject) | `OWNER` / `ADMIN` | | Respond edit request | `OWNER` / `ADMIN` | | Submit progress | `MEMBER` / `OWNER` / `ADMIN` + `actionPlanMembers` check | | Create edit request | `MEMBER` / `OWNER` / `ADMIN` + `actionPlanMembers` check | ### Double-Check for Member Operations Member operations (`createProgress`, `createEditRequest`) have two-tier validation: 1. **Tier 1** — `validateWorkspaceRole`: user must have active membership in workspace 2. **Tier 2** — `validateScopeMember`: user must be assigned to the specific scope via `actionPlanMembers` --- ## Notifications Fire-and-forget. Does not block operation on failure. All events are **bidirectional** — both actor (pelaku) and target (korban) receive the notification for audit trail. ### Event Keys | Event Key | Category | Description | |---|---|---| | `action_plan.member.added` | Targeted (actor + target) | Member assigned to scope | | `action_plan.member.removed` | Targeted (actor + target) | Member removed from scope | | `action_plan.progress.submitted` | Broadcast (all approvers) | Member submits progress | | `action_plan.progress.approved` | Targeted (actor + target) | Admin approves progress | | `action_plan.progress.rejected` | Targeted (actor + target) | Admin rejects progress | | `action_plan.edit_request.requested` | Broadcast (all approvers) | Member requests edit | | `action_plan.edit_request.responded` | Targeted (actor + target) | Admin responds to edit request | ### Recipient Resolution **Broadcast events** (`progress.submitted`, `edit_request.requested`): Resolve all `OWNER`/`ADMIN` users from `workspace_member_scopes` across 4-level hierarchy (PROJECT → WORKSPACE → PROGRAM → AGENDA). Plus the submitter for audit trail. **Targeted events** (all others): Notify the specific actor + the specific target user only. No broadcast to hierarchy owners. ### Notification Recipients by Trigger | Trigger | Recipients | |---|---| | Submit progress | Submitter + all OWNER/ADMIN in hierarchy | | Approve/reject progress | Submitter + approving admin | | Submit edit request | All OWNER/ADMIN in hierarchy | | Respond edit request | Requester + responding admin | | Add member | Added user + admin who added | | Remove member | Removed user + admin who removed | --- ## Request/Response Schemas ### Create Activity `POST /` ```jsonc { "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` ```jsonc { "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): ```jsonc { "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` ```jsonc { "verb":"APPROVE|APPROVE_WITH_NOTES|REJECT", "progress":80, "workVolume":200, "description":"Adjusted" } ``` ### Plans (S-Curve) `GET /:../plans` ```jsonc { "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 `name` editable - Task with subtasks → only `name` editable - Subtask in progress → `workWeight`/`workVolume` locked ### Soft Delete All deletes are soft (`deletedAt`). Cascades: deleting activity → all tasks + subtasks. Deleting task → all subtasks.