Last active 4 days ago

Revision a5ab435146c22fed899740c6ab5e5f02d35f15c8

action-plan.docs.md Raw

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

  1. VP responds via …/approval/respond with verb: APPROVE | REJECT | ESCALATE (this is one 2-step form: step 1 picks the verb, step 2 picks BOD members via bodUserIds when ESCALATE). ESCALATEBOD_APPROVAL + new BOD round; each BOD (project program agenda users) notified.
  2. 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).
  3. 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 last APPROVEDPENDING, nodes → WAITING_APPROVAL, unmet-prereq nodes → rollbackPaused until 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).
  4. Re-escalation: …/approval/bod/re-request + later ESCALATE carries previous BOD list over — add-only, cannot remove/replace.
  5. VP REJECT: walks descendants level by level; startable leaves reopen (NEED_APPROVAL, last APPROVEDPENDING), leaves blocked by unmet live prerequisite pause (rollbackPaused) and auto-resume when the prerequisite closes (releaseRollbackPausedDependents).
  6. Delete guard: progress ≥ 100% (derived COMPLETED) or approvalStatus = 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 / CreateTaskDtoname ✅; workWeight/workVolume/workVolumeUnit/startDate/endDate ✳️ (volume pair rule)
GET / list under activity
GET /:taskId detail (§ Item payload)
PATCH /:taskId UpdateTaskDtoname/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 / CreateSubtaskDtoname ✅; 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).