Last active 4 days ago

Revision 62e03eb4209c7a067d6a083860424b317218c6d6

action-plan.docs.md Raw

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 (approvalStatusWAITING_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

{
  "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": [<CreateActivityPayload>…] }{ 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

{ "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

{ "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):

{
  "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:

  • APPROVECLOSED (item + descendants already closed).
  • REJECTREJECTED; descendants reopened per the chain rule (§ BOD model point 5).
  • ESCALATEBOD_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):

{
  "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
approvalReason string? VP response reason
bodRoundId string? latest BOD round id
bodResult enum? REJECTED_REVISE
bodReason string? aggregate round reason (earliest voter)
bodDisplayStatus enum? BOD_APPROVAL
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):

"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:
{ "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-namesstring[]
  • 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).