# API — Action Plans Action plan module for a project: a three-level hierarchy **Activity → Task → Subtask**, each with progress entries, approvers, prerequisites, members, and a BOD (Board of Directors) escalation flow. ## General - **Base URL**: `/api/v1/projects/:projectId/action-plans` (versioned controller, v1) - **Auth**: JWT via `@Authorize({ membership: true })` — caller must be a member of `:projectId`. - **Response envelope** (`ResponseInterceptor`): responses are wrapped as `{ status, message, data }` where `data` is the payload documented below (endpoints returning raw files — PDF/XLSX — skip the envelope). - **Access tiers** (`ProjectActionPlanAccessGuard`): - `INCOMPLETE_DATA` — not accessible. - `NOT_STARTED` / `IN_PROGRESS` — full access. - `NEED_APPROVAL` / `COMPLETED` / `REJECTED` — read-only for management writes (create/update/delete), **but approval respond/re-request and BOD routes stay writable** (project closes only after approvals resolve). - **Progress input** (submit/respond/feedback) additionally requires project status `IN_PROGRESS` (`ProjectInProgressGuard`); approval routes intentionally bypass it. ## Status enums ### Derived status (item `status` field) Derived from progress rows + approval state. Display overrides applied (see BOD display): | Value | Meaning | |---|---| | `INCOMPLETE_DATA` | Leaf has no dates/weight data | | `NEED_APPROVAL` | Progress submitted, awaiting approval | | `IN_PROGRESS` | Has approved progress, not yet complete | | `NOT_STARTED` | No active progress, or paused/rolled back (display override) | | `COMPLETED` | Effective progress ≥ 100% | ### ActionPlanApprovalStatus (`approvalStatus`) `WAITING_APPROVAL` (item awaiting/queued for a new approval cycle) · `CLOSED` (approved) · `REJECTED` (rejected by VP) · `BOD_APPROVAL` (escalated, awaiting BOD board decision) ### ActionPlanBodResult (`bodResult`) — aggregate outcome of latest BOD round | Value | Meaning | |---|---| | `REJECTED_REVISE` | BOD voted REVISE → item back to VP (`approvalStatus` → `WAITING_APPROVAL`) | | `REJECTED_ROLLBACK` | BOD voted ROLLBACK → item restarting from a prerequisite | ### ActionPlanBodDisplayStatus (`bodDisplayStatus`) — chip shown to the user | Value | Rule | |---|---| | `BOD_APPROVAL` | while item is escalated (approvalStatus `BOD_APPROVAL`) | | `REJECTED_BY_BOD` | `bodResult === REJECTED_REVISE` | | `NOT_STARTED` | `bodResult === REJECTED_ROLLBACK` **or** `rollbackPaused === true` (no "Rolled Back by BOD" chip) | | `null` | no BOD state | ### ActionPlanProgressStatus (progress rows) `PENDING` · `APPROVED` · `APPROVED_WITH_NOTES` · `REJECTED` ### Other booleans - `rollbackPaused`: item halted waiting for a prerequisite to close; reads as `NOT_STARTED`. ## BOD escalation model 1. **VP** (designated approver) responds to an item's approval: `APPROVE` | `REJECT` | `ESCALATE`. `ESCALATE` requires `bodUserIds` (≥1, max 50; each must be a user in the project's program agenda) → approvalStatus `BOD_APPROVAL`, a new `ActionPlanBodRounds` round created, notification sent to each BOD. 2. **Each assigned BOD** votes on the round: `APPROVE` | `REVISE` | `ROLLBACK` (reason required, 1–500 chars). - One **rollback target lock** applies: once a BOD votes `ROLLBACK` to target T, all other BODs may only vote `ROLLBACK` to the same T (a different `rollbackId` → 400). `rollbackId` must be a **direct prerequisite** of the item. 3. **Resolution** when all assigned BODs have voted — precedence `ROLLBACK > REVISE > APPROVE`: - Any/all `ROLLBACK` votes to the same target T → ROLLBACK wins. Item → `WAITING_APPROVAL`, `bodResult = REJECTED_ROLLBACK`, `bodReason` = earliest voter's reason; T's and item's subtrees restart (leaves: last `APPROVED` progress → `PENDING`; each node → `WAITING_APPROVAL`; nodes whose prerequisites aren't met → `rollbackPaused` until the prerequisite closes). VP notified ("Action Plan Rolled Back by BOD"). - Else any `REVISE` → REVISE wins. Item → `WAITING_APPROVAL`, `bodResult = REJECTED_REVISE`, `bodReason` = earliest voter's reason; data & progress rows untouched; VP notified ("Action Plan Rejected by BOD"). - Else (all `APPROVE`) → item `approvalStatus = CLOSED` (handover); VP + admins + action plan members notified ("Action Plan Approved by BOD"). - Different `ROLLBACK` targets across voters → round stays `ACTIVE`, item stays `BOD_APPROVAL`, no writes (defensive; the single-target lock normally prevents this). 4. **Re-escalation**: VP `ESCALATE` again after a BOD REVISE carries the previous BOD list over; may *add* members, may not remove/replace. 5. **VP `REJECT`** (non-escalated flow) does not reopen closed children wholesale: it walks descendants level by level (Activity → Tasks → Subtasks). Leaves that are startable (no unmet live prerequisite) reopen → `NEED_APPROVAL`/`PENDING`; leaves blocked by an unmet prerequisite pause with `rollbackPaused = true` and resume automatically when the prerequisite reaches `CLOSED` (`.releaseRollbackPausedDependents`). 6. **Delete guard**: an item whose effective progress ≥ 100% (derived `COMPLETED`) or `approvalStatus = CLOSED` (VP- or BOD-approved) cannot be deleted — including as a descendant of a parent delete (`400` naming the offending item). --- ## 1. Activities ### `POST /` — create activity ```json { "name": "string (required)", "activityType": "string (required)", "workWeight": "number (optional)", "workVolume": "number (optional, requires workVolumeUnit)", "workVolumeUnit": "string (optional)", "startDate": "ISO date (optional)", "endDate": "ISO date (optional)", "approverId": "uuid (optional)", "prerequisiteIds": ["uuid (optional)"], "tasks": [{ "name": "string", "workWeight", "workVolume", "workVolumeUnit", "startDate", "endDate", "subtasks": [{ "name", "workWeight", "workVolume", "workVolumeUnit", "startDate", "endDate" }] }] } ``` Response includes the item fields below (§ Item payload) plus: `taskCount`, `subtaskCount`, `memberCount`, `progress`, `pendingProgress`, `weightlessLeaves`, `predecessorCount`-style derived stats, and `status`. ### `POST /bulk` — bulk create `{ "activities": […] }` → `{ created, activities: [{id,name}] }` ### `GET /` — list (paginated) Query: `page`, `limit`, `search?`, `sort_by` (`created_at` default | `start_date` | `end_date` | `start_date_end_date`), `sort_order` (`asc`|`desc`) ### `GET /tree` — hierarchical tree Returns `Activity[]` each with nested `tasks[].subtasks[]`; every level carries the item fields + `accumulatedProgress`, `pendingProgress`, `workWeight`, `workVolume`, `totalUsersAssigned`, `latestProgressStatus`. Query: `status?`, `startDate?`, `endDate?`, `search?`, `sort_by?`, `sort_order?` ### `GET /tree/gantt` — same tree with `ganttStatus` per node (`LATE` | `ON_TRACK` | `BASELINE` | …) ### `GET /:activityId` — detail (§ Item payload) ### `PATCH /:activityId` — update ```json { "name"?, "activityType"?, "workWeight"?, "workVolume"?, "workVolumeUnit"?, "startDate"?, "endDate"?, "approverId"?, "prerequisiteIds"? } ``` ### `DELETE /:activityId` — soft delete. **Blocked (400)** if item or any live descendant is complete (progress ≥ 100%) or `approvalStatus = CLOSED`. ### `POST /bulk` (DELETE) — `DELETE /bulk` bulk delete ```json { "activityIds": [], "taskIds": [], "subtaskIds": [] } ``` → `{ deleted }`. Parent ids cascade to descendants; same complete/CLOSED guard applies. --- ## 2. Approval — per item level Same three routes per level, substituting `:activityId` / `:activityId/tasks/:taskId` / `:activityId/tasks/:taskId/subtasks/:subtaskId`: ### `POST …/approval/respond` — VP (designated approver) responds Body (`ApprovalRespondDto`): ```json { "verb": "APPROVE | REJECT | ESCALATE", "reason": "string ≤500 chars (optional)", "bodUserIds": ["uuid…"] // required when verb = ESCALATE; 1–50 } ``` This is a **single 2-step form** in the UI: step 1 picks the verb (`APPROVE`/`REJECT`/`ESCALATE`); when `ESCALATE` is chosen, step 2 picks the BOD members (`bodUserIds`). No separate "escalation" endpoint exists. Effects: - `APPROVE` → `CLOSED` (item + descendants already closed). - `REJECT` → `REJECTED`; descendants reopened per the chain rule (§ BOD model point 5). - `ESCALATE` → `BOD_APPROVAL`; new BOD round; BODs notified. ### `POST …/approval/re-request` — creator re-requests approval (only when `REJECTED` / `WAITING_APPROVAL`) Body: none. ### `POST …/approval/bod` — BOD member votes **(new)** Body (`BodRespondDto`): ```json { "decision": "APPROVE | REVISE | ROLLBACK", "reason": "string, 1–500 chars (REQUIRED)", "rollbackId": "uuid (required when decision = ROLLBACK; must be a direct prerequisite of the item)" } ``` Preconditions (400/403 otherwise): item `approvalStatus = BOD_APPROVAL`; caller is an assigned BOD of the active round with no prior vote; not already resolved. ### `POST …/approval/bod/re-request` — VP re-requests BOD round **(new)** Body: none. Re-creates the escalation after a BOD REVISE (VP cycle back); carries previous BOD list (add-only on next `ESCALATE`). --- ## 3. Item payload (activity/task/subtask detail & tree nodes) BOD-related fields on every item (detail, list, tree): | Field | Type | Notes | |---|---|---| | `approvalStatus` | enum | `WAITING_APPROVAL` | `CLOSED` | `REJECTED` | `BOD_APPROVAL` | | `approvalReason` | string? | VP response reason | | `bodRoundId` | string? | latest BOD round id | | `bodResult` | enum? | `REJECTED_REVISE` | `REJECTED_ROLLBACK` | | `bodReason` | string? | aggregate round reason (earliest voter) | | `bodDisplayStatus` | enum? | `BOD_APPROVAL` | `REJECTED_BY_BOD` | `NOT_STARTED` | null | | `rollbackPaused` | boolean | paused awaiting prerequisite close | | `approverId` / `approverName` / `approverEmail` | string? | designated VP approver | | `prerequisites` / `prerequisiteIds` | [] | direct prerequisites (same-scope level) | **Detail-only additions** (`GET /:activityId`, `GET …/tasks/:taskId`, `GET …/subtasks/:subtaskId`): ```json "bodApprovals": [ { "userId": "uuid", "name": "string|null", "decision": "APPROVE | REVISE | ROLLBACK|null", "reason": "string|null", "rollbackItemId": "uuid|null", "rollbackTargetName": "string|null", "respondedAt": "ISO date" } ], "rollbackOptions": [ { "id": "uuid", "name": "string" } ] // direct prereqs; after first ROLLBACK vote, locked to that single target ``` ## 4. Tasks & Subtasks Same shape as activities under the nesting paths: - `POST :activityId/tasks` / `GET :activityId/tasks` / `GET/PATCH/DELETE :activityId/tasks/:taskId` + progress/plans/members/edit-requests/approval routes. - `POST :activityId/tasks/:taskId/subtasks` / `GET` list / `GET/PATCH/DELETE :activityId/tasks/:taskId/subtasks/:subtaskId` + same sub-routes. Member assignment (`…/members` POST/DELETE) body: `{ "userIds": ["uuid…"] }`; bulk variants under `/members/bulk` (POST assign / DELETE remove): `{ "activityIds": [], "taskIds": [], "subtaskIds": [], "userIds": [] }` → `{ assigned, skipped }` / `{ removed }`. ## 5. Progress (leaves: activity/task/subtask) - `POST :itemId/progress` (multipart): fields `progress`, `workVolume?`, `description?`, `documentSource` (`upload`|`document`), `documentRef?`, `companyId?`, `createdAt?`; files `attachments[]` (jpg/png/gif/webp/svg/pdf) and `document` (×1). → progress row payload (see below). - `GET :itemId/progress` — paginated (`page`, `limit`). - `GET :itemId/progress/:progressId` — single row. - `POST :itemId/progress/:progressId/respond` — body: ```json { "verb": "APPROVE | APPROVE_WITH_NOTES | REJECT", "progress"?, "workVolume"?, "description"? } ``` Progress row payload: `id, scopeType, scopeId, scopeName, progress, workVolume, scopeWorkVolume, scopeWorkVolumeUnit, description, status, amendsId, amendsOriginal, amendedByCount, amendedBy[], approvedBy, approvedByName, approvedAt, createdBy, createdByName, updatedBy, updatedByName, createdAt, updatedAt, attachments[{id,key,url}], document`. ### Feedback (per progress row) `POST/GET :itemId/progress/:progressId/feedbacks`, `PATCH/DELETE …/feedbacks/:feedbackId`. Body `{ "message": "string" }` (≤500). ## 6. Plans / S-curve (per level) - `GET :itemId/plans` → `{ duration: {days,startDate,endDate}, planning: [{id?,startDate,endDate,targetProgress}], sCurve: { plan: [{date,progress}], actual: [{date,progress,id}] } }` - `PUT :itemId/plans` body: `{ "plans": [{ "startDate": "ISO", "endDate": "ISO", "targetProgress": 0–100 }] }` ## 7. Edit requests (members → owner/admin) - `POST/GET :itemId/edit-requests`, project-level `GET /edit-requests`, `POST /edit-requests/:requestId/respond` body `{ "verb": "APPROVE | REJECT" }`. - Create body: `{ "field": "endDate | workWeight | workVolume | workVolumeUnit", "newValue": …, "reason": "string ≤500" }`. - List returns `requestedData`, `oldData`, `reason`, `status`, requester/responder names + timestamps. ## 8. Analytics & reports - `GET /analytics` — query: `layout`, `granularity`, `search?`, `activityName?`, `startDate?`, `endDate?`. - `GET /analytics/activity-names` → `string[]` - `GET /analytics/status-counts?status=…` - `GET /analytics/report/download?layout&granularity&startDate&endDate` — PDF (binary). - `GET /analytics/report/history` — paginated record list (`seq`, `docNumber`, `layout`, `granularity`, `windowStart`, `windowEnd`, `generatedAt`, `s3Key`). - `GET /analytics/report/history/:recordId/download` — PDF (binary). ## 9. Import & catalogs - `GET /import/template/download` — XLSX template (binary). - `POST /import` — multipart `file` (xlsx/xls/csv, ≤5 MB). Header: `Activity Name | Task Name | Subtask Name | Start Date | End Date | Work Weight | Work Volume | Work Volume Unit | Activity Type` (`-` = empty). Activity Type required; workVolume requires workVolumeUnit. → `{ imported, activities: [{id,name}] }`. - `GET/POST /work-volume-units` — POST body `{ "name"?, "symbol" }`. - `GET/POST /activity-types` — POST body `{ "name" }`. --- ## Payload changes from BOD feature (summary) | Response | Added fields | |---|---| | Activity/Task/Subtask detail | `bodRoundId`, `bodResult`, `bodReason`, `bodDisplayStatus`, `rollbackPaused`, `bodApprovals[]`, `rollbackOptions[]` | | Tree nodes (tree/gantt/list) | `bodRoundId`, `bodResult`, `bodReason`, `bodDisplayStatus`, `rollbackPaused` | | Request `…/approval/respond` | `verb` extended with `ESCALATE`; new `bodUserIds[]` | | New endpoints | `…/approval/bod` (3 levels), `…/approval/bod/re-request` (3 levels) | ## BOD notifications (email/push) - BOD requested → assigned BODs — subject `Action Plan BOD Requested`. - BOD REVISE → VP — `Action Plan Rejected by BOD` — body `{ScopeLabel} {scopeName} in {projectName} was rejected by {bodName}. You must make decision` (+ ` Note: {reason}`). - BOD ROLLBACK → VP — `Action Plan Rolled Back by BOD` — `…was rolled back by {bodName}. Its prerequisite {prereqName} was re-opened for re-approval.` (+ note). - BOD APPROVE (all) → VP + admins + members — `Action Plan Approved by BOD` — `…approved by BOD. Handover complete.` (+ note).