API — Action Plans
Action plan module: Activity → Task → Subtask hierarchy with progress entries, approvers, prerequisites, members, and a BOD (Board of Directors) escalation flow.
General
- Base:
/api/v1/projects/:projectId/action-plans— all paths below are relative to this base. - Auth: JWT,
@Authorize({ membership: true })— caller must be a project member. - Envelope: responses wrapped
{ status, message, data }; binary endpoints (PDF/XLSX) bypass the envelope. - Access tiers (
ProjectActionPlanAccessGuard):INCOMPLETE_DATA— not accessible.NOT_STARTED/IN_PROGRESS— full access.NEED_APPROVAL/COMPLETED/REJECTED— read-only for management writes; approval & BOD routes stay writable.
- Progress input (submit/respond/feedback) requires project
IN_PROGRESS(ProjectInProgressGuard).
Status enums
Derived item status
INCOMPLETE_DATA · NEED_APPROVAL · IN_PROGRESS · NOT_STARTED (also display override for paused/rolled back) · COMPLETED
approvalStatus
WAITING_APPROVAL · CLOSED · REJECTED · BOD_APPROVAL (escalated)
bodResult
REJECTED_REVISE · REJECTED_ROLLBACK
bodDisplayStatus
| Value | Rule |
|---|---|
BOD_APPROVAL |
while escalated |
REJECTED_BY_BOD |
bodResult === REJECTED_REVISE |
NOT_STARTED |
bodResult === REJECTED_ROLLBACK or rollbackPaused === true |
null |
no BOD state |
Progress row status
PENDING · APPROVED · APPROVED_WITH_NOTES · REJECTED
BOD escalation flow
- VP responds via
…/approval/respondwithverb:APPROVE|REJECT|ESCALATE(this is one 2-step form: step 1 picks the verb, step 2 picks BOD members viabodUserIdswhenESCALATE).ESCALATE→BOD_APPROVAL+ new BOD round; each BOD (project program agenda users) notified. - BOD votes via
…/approval/bod:APPROVE|REVISE|ROLLBACK(reason required;rollbackId= direct prerequisite when ROLLBACK). One-target lock: after the first ROLLBACK vote to target T, all later BODs may only ROLLBACK to T (else 400). - Resolution once all assigned BODs voted — precedence
ROLLBACK > REVISE > APPROVE:- ROLLBACK (same single target T) → item
WAITING_APPROVAL+bodResult = REJECTED_ROLLBACK; T's & item's subtrees restart (leaf lastAPPROVED→PENDING, nodes →WAITING_APPROVAL, unmet-prereq nodes →rollbackPauseduntil prerequisite closes); VP notified Action Plan Rolled Back by BOD. - else REVISE → item
WAITING_APPROVAL+bodResult = REJECTED_REVISE; data/progress untouched; VP notified Action Plan Rejected by BOD. - else all APPROVE →
CLOSED(handover); VP + admins + members notified Action Plan Approved by BOD. - Different ROLLBACK targets → no resolution, round stays
ACTIVE(defensive; normally prevented by the lock).
- ROLLBACK (same single target T) → item
- Re-escalation:
…/approval/bod/re-request+ laterESCALATEcarries previous BOD list over — add-only, cannot remove/replace. - VP REJECT: walks descendants level by level; startable leaves reopen (
NEED_APPROVAL, lastAPPROVED→PENDING), leaves blocked by unmet live prerequisite pause (rollbackPaused) and auto-resume when the prerequisite closes (releaseRollbackPausedDependents). - Delete guard: progress ≥ 100% (derived
COMPLETED) orapprovalStatus = CLOSED→ delete blocked 400 (also blocks cascade delete of a complete/closed descendant).
Endpoint catalog
Path legend: A = :activityId, T = :taskId, S = :subtaskId. Full paths:
- Activity level:
BASE/A/… - Task level:
BASE/A/tasks/T/… - Subtask level:
BASE/A/tasks/T/subtasks/S/…
1. Activities
POST / (base)
Body (CreateActivityDto):
| Field | Req | Notes |
|---|---|---|
name |
✅ | 1–255 chars |
activityType |
✅ | 1–100 chars |
workWeight |
✳️ | number ≥0 |
workVolume |
✳️ | number ≥0 — requires workVolumeUnit |
workVolumeUnit |
✳️ | ≤100 chars |
startDate |
✳️ | ISO date, floored to day |
endDate |
✳️ | ISO date |
tasks[] |
✳️ | ≤50, each: name ✅ (1–255), workWeight/workVolume/workVolumeUnit/startDate/endDate ✳️, nested subtasks[] ≤50 (same shape, name ✅) |
POST /bulk
Body: CreateActivityDto[] — 1–100 items. → { created, activities: [{id,name}] }
GET /
Query:
| Param | Req | Options / default |
|---|---|---|
page |
✳️ | default 1 |
limit |
✳️ | default 20, max 100 |
search |
✳️ | string |
sort_by |
✳️ | created_at (default) · updated_at · name |
sort_order |
✳️ | asc · desc (default) |
GET /tree
Query:
| Param | Req | Options / default |
|---|---|---|
status |
✳️ | derived status string filter |
startDate / endDate |
✳️ | bound effective date overlap range (one-sided allowed) |
search |
✳️ | string |
sort_by |
✳️ | created_at (default) · start_date · end_date · start_date_end_date |
sort_order |
✳️ | asc · desc (default asc) |
GET /tree/gantt
Same query as /tree; nodes carry ganttStatus.
GET /:activityId
Detail — item payload (§ Item payload).
PATCH /:activityId
Body (UpdateActivityDto) — all ✳️: name, activityType, approverId, workWeight, workVolume, workVolumeUnit (requires workVolume pair rule), startDate, endDate, prerequisiteIds[].
DELETE /:activityId
No body. 400 if item or live descendant complete (≥100%) or CLOSED.
DELETE /bulk
Body (BulkActionPlanDeleteDto):
| Field | Req | Notes |
|---|---|---|
activityIds[] |
✳️ | ≤100, default [] |
taskIds[] |
✳️ | ≤100, default [] |
subtaskIds[] |
✳️ | ≤100, default [] |
At least one id array non-empty. Parents cascade to descendants. → { deleted }. Same complete/CLOSED guard.
2. Approval (3 levels; same routes)
Patterns (per level): …/approval/respond, …/approval/re-request, …/approval/bod, …/approval/bod/re-request.
POST …/approval/respond — VP (designated approver)
Body (ApprovalRespondDto):
| Field | Req | Notes |
|---|---|---|
verb |
✅ | APPROVE · REJECT · ESCALATE |
reason |
✳️ | ≤500 chars |
bodUserIds[] |
✳️ | 1–50, each a valid agenda user — required when verb=ESCALATE |
POST …/approval/re-request — creator re-requests (only from REJECTED/WAITING_APPROVAL)
No body.
POST …/approval/bod — BOD member votes
Body (BodRespondDto):
| Field | Req | Notes |
|---|---|---|
decision |
✅ | APPROVE · REVISE · ROLLBACK |
reason |
✅ | 1–500 chars |
rollbackId |
✳️ | — required when decision=ROLLBACK; must be a direct prerequisite |
Guards: item BOD_APPROVAL, caller is assigned BOD with no vote yet, round not resolved.
POST …/approval/bod/re-request — VP resumes BOD cycle after REVISE
No body.
3. Item payload (detail/list/tree)
BOD-related fields on every item: approvalStatus, approvalReason, bodRoundId, bodResult, bodReason, bodDisplayStatus, rollbackPaused, plus approverId/approverName/approverEmail, prerequisites[], prerequisiteIds[].
Detail-only (GET /A, GET A/tasks/T, GET A/tasks/T/subtasks/S):
"bodApprovals": [{ "userId", "name", "decision", "reason", "rollbackItemId", "rollbackTargetName", "respondedAt" }],
"rollbackOptions": [{ "id", "name" }] // direct prereqs; locked to single target after first ROLLBACK vote
4. Tasks — BASE/A/tasks
| Method+Path | Body / notes |
|---|---|
POST / |
CreateTaskDto — name ✅; workWeight/workVolume/workVolumeUnit/startDate/endDate ✳️ (volume pair rule) |
GET / |
list under activity |
GET /:taskId |
detail (§ Item payload) |
PATCH /:taskId |
UpdateTaskDto — name/approverId/workWeight/workVolume/workVolumeUnit/startDate/endDate/prerequisiteIds[] all ✳️ |
DELETE /:taskId |
complete/CLOSED guard 400 |
Plus per task: progress (POST/GET /:taskId/progress, GET /:taskId/progress/:progressId, POST …/respond), feedbacks, GET/PUT /:taskId/plans, members (POST/DELETE/GET /:taskId/members), edit-requests (POST/GET /:taskId/edit-requests), approval routes (§ 2).
5. Subtasks — BASE/A/tasks/T/subtasks
| Method+Path | Body / notes |
|---|---|
POST / |
CreateSubtaskDto — name ✅; workVolume/workVolumeUnit/workWeight/startDate/endDate ✳️ (volume pair rule) |
GET / |
list under task |
GET /:subtaskId |
detail |
PATCH /:subtaskId |
UpdateSubtaskDto — all ✳️ incl approverId, prerequisiteIds[] |
DELETE /:subtaskId |
complete/CLOSED guard 400 |
Plus: members (POST/DELETE/GET /:subtaskId/members), plans (GET/PUT /:subtaskId/plans), edit-requests (POST/GET /:subtaskId/edit-requests), progress (POST/GET /:subtaskId/progress, GET /:subtaskId/progress/:progressId, POST …/respond), feedbacks (POST/GET …/feedbacks, PATCH/DELETE …/feedbacks/:feedbackId), approval routes (§ 2).
6. Members
| Method+Path | Body |
|---|---|
POST /A/members · DELETE /A/members (×task/subtask) |
{ "userIds": ["uuid…"] } — 1–100, required |
POST /members/bulk |
BulkActionPlanMembersDto: activityIds[] (≤100, default []), taskIds[] (≤100, default []), subtaskIds[] (≤100, default []), userIds[] (1–100 ✅) — ≥1 id array non-empty; parent scopes cascade to leaves → { assigned, skipped } |
DELETE /members/bulk |
same body → { removed } |
GET /A/members · GET A/tasks/T/members · GET …/subtasks/S/members |
list w/ display names + leaf scopes |
7. Progress (multipart) — 3 levels
POST …/progress — submit
Multipart form fields (CreateProgressDto):
| Field | Req | Notes |
|---|---|---|
progress |
✳️ | coerced number 0–100; omit = carry current floor |
workVolume |
✳️ | number ≥0 |
description |
✳️ | ≤5000 chars |
createdAt |
✳️ | ISO date (backdated work) |
documentSource |
✳️ | upload · document |
documentRef |
✳️ | JSON string {"documentId","versionId","revisionId"} (when source=document) |
companyId |
✳️ | guards cross-company document access |
| Files | attachments[] (jpg/png/gif/webp/svg/pdf), document (×1) |
GET …/progress
Query: page (default 1) · limit (default 50, max 100).
GET …/progress/:progressId
Single row: id, scopeType, scopeId, scopeName, progress, workVolume, scopeWorkVolume, scopeWorkVolumeUnit, description, status, amendsId, amendsOriginal, amendedByCount, amendedBy[], approvedBy, approvedByName, approvedAt, createdBy, createdByName, updatedBy, updatedByName, createdAt, updatedAt, attachments[], document.
POST …/progress/:progressId/respond
Body (RespondProgressDto): verb ✅ (APPROVE·APPROVE_WITH_NOTES·REJECT); progress ✳️ (0–100), workVolume ✳️, description ✳️ (≤5000) — overridable on APPROVE_WITH_NOTES.
8. Feedback — …/progress/:progressId/feedbacks
| Method+Path | Body |
|---|---|
POST …/feedbacks |
{ "message": "string" } — 1–5000 chars ✅ |
GET …/feedbacks |
query: page (1) · limit (50, max 100) |
PATCH …/feedbacks/:feedbackId |
{ "message" } same validation |
DELETE …/feedbacks/:feedbackId |
— |
9. Plans / S-curve — …/plans (3 levels)
GET …/plans → { duration: {days,startDate,endDate}, planning: [{id?,startDate,endDate,targetProgress}], sCurve: { plan:[{date,progress}], actual:[{date,progress,id}] } }
PUT …/plans body (SetProgressPlansDto): plans[] (≥0): startDate ✅ (date), endDate ✅ (date), targetProgress ✅ (0–100).
10. Edit requests — member → owner/admin
| Method+Path | Body |
|---|---|
POST …/edit-requests (3 levels + none at project) |
CreateEditRequestDto: endDate ✳️, workWeight ✳️, workVolume ✳️, workVolumeUnit ✳️, activityType ✳️ — one or more changed fields; reason ✅ (1–500 chars) |
GET …/edit-requests · GET /edit-requests |
query: page (1) · limit (50, max 100) · status ✳️ (PENDING·APPROVED·REJECTED) |
POST /edit-requests/:requestId/respond |
{ "verb": "APPROVE" | "REJECT" } ✅ |
List rows include requestedData, oldData, reason, status, requester/responder names, timestamps.
11. Analytics & reports
| Method+Path | Query |
|---|---|
GET /analytics |
layout ✳️ (detailed default · simplified); granularity ✳️ (daily · weekly default · monthly); search ✳️ (≤200); activityName ✳️ (≤200, case-insensitive contains); startDate/endDate ✳️ |
GET /analytics/activity-names |
— → string[] |
GET /analytics/status-counts |
status ✳️ (default IN_PROGRESS): INCOMPLETE_DATA·NEED_APPROVAL·IN_PROGRESS·NOT_STARTED·COMPLETED |
GET /analytics/report/download |
granularity ✳️ (weekly default · monthly · yearly); search, activityName, startDate, endDate ✳️ → PDF binary |
GET /analytics/report/history |
page (1) · limit (20, max 100) · startDate/endDate ✳️ → records {seq, docNumber, layout, granularity, windowStart, windowEnd, generatedAt, s3Key} |
GET /analytics/report/history/:recordId/download |
— → PDF binary |
12. Import & catalogs
| Method+Path | Body / notes |
|---|---|
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 /work-volume-units |
— |
POST /work-volume-units |
{ "name" } ✳️ (≤100), "symbol" ✅ (1–50) |
GET /activity-types |
— |
POST /activity-types |
{ "name" } ✅ (1–100) |
Payload changes from the BOD feature
| Context | Added |
|---|---|
| Item detail/list/tree | bodRoundId, bodResult, bodReason, bodDisplayStatus, rollbackPaused |
| Item detail only | bodApprovals[], rollbackOptions[] |
…/approval/respond |
verb extended with ESCALATE; new bodUserIds[] |
| New routes | …/approval/bod ×3 levels, …/approval/bod/re-request ×3 levels |
Notification copy (BOD)
- BOD requested → each assigned BOD: subject Action Plan BOD Requested.
- REVISE → VP: Action Plan Rejected by BOD —
{ScopeLabel} {scopeName} in {projectName} was rejected by {bodName}. You must make decision(+Note: {reason}). - ROLLBACK → VP: Action Plan Rolled Back by BOD —
…was rolled back by {bodName}. Its prerequisite {prereqName} was re-opened for re-approval.(+ note). - All-APPROVE → VP + admins + members: Action Plan Approved by BOD —
…was approved by BOD. Handover complete.(+ note).