Last active 4 days ago

Revision eedc138c5d8666af99821820af088369867d93c8

action-plan.docs.md Raw

Action Plans API

Base URL: {host}/api/v1

All responses wrapped in:

{
  "statusCode": 200,
  "message": "...",
  "data": { ... },
  "durationMs": 42,
  "_reference": []
}

Shown data inline below for brevity.


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

Enums

Subtask/Task/Activity Status

Value Description
INCOMPLETE_DATA Required fields missing
NOT_STARTED Ready but no progress
IN_PROGRESS Progress entries exist
NEED_APPROVAL Pending progress requiring response
COMPLETED 100% complete

Progress Entry Status

Value Description
PENDING Submitted, awaiting response
APPROVED Accepted
APPROVED_WITH_NOTES Accepted with amendments (new version created)
REJECTED Rejected

ActionPlanMemberScope

Value Description
ACTIVITY Member assigned to activity
TASK Member assigned to task
SUBTASK Member assigned to subtask

Routes

1. Activities

Base: /api/projects/:projectId/action-plans

POST /

Create activity with optional nested tasks & subtasks.

Body:

{
	"name": "Activity name", // string, 1-255
	"tasks": [
		// optional, max 50
		{
			"name": "Task name", // string, 1-255
			"workWeight": 100, // optional, number >= 0
			"workVolume": 500, // optional, number >= 0
			"workVolumeUnit": "m2", // optional, string max 100
			"startDate": "2026-01-01", // optional, ISO date
			"endDate": "2026-03-31", // optional, ISO date
			"subtasks": [
				// optional, max 50
				{
					"name": "Subtask name", // string, 1-255
					"workVolume": 250, // optional, number >= 0
					"workVolumeUnit": "m2", // optional, string max 100
					"workWeight": 50, // optional, number >= 0
					"startDate": "2026-01-01", // optional, ISO date
					"endDate": "2026-02-15", // optional, ISO date
				},
			],
		},
	],
}

Response: Full activity with derived stats (same shape as GET /:id).


GET /

List activities for a project. Paginated, searchable.

Query:

Param Type Default Description
page number 1 Page number
limit number 20 Items per page (max 100)
search string Filter by name
sort_by created_at | updated_at | name created_at Sort field
sort_order asc | desc desc Sort direction

Response:

{
	"items": [
		{
			"id": "uuid",
			"projectId": "uuid",
			"name": "Activity name",
			"createdBy": "uuid",
			"createdAt": "ISO date",
			"updatedAt": "ISO date",
			"deletedAt": null,
			"taskCount": 2,
			"subtaskCount": 5,
			"workWeight": 150,
		},
	],
	"total": 10,
	"page": 1,
	"limit": 20,
}

GET /tree

Full Activity > Task > Subtask tree. Each level has accumulated stats.

Query:

Param Type Default Description
status string Filter subtasks by status

Response:

[
	{
		"id": "uuid",
		"name": "Activity name",
		"status": "IN_PROGRESS",
		"totalTasks": 2,
		"totalUsersAssigned": 10, // memberCount (bottom-up)
		"accumulatedProgress": 45.5, // avg(), percent
		"createdBy": "uuid",
		"createdAt": "ISO date",
		"updatedAt": "ISO date",
		"tasks": [
			{
				"id": "uuid",
				"name": "Task name",
				"status": "IN_PROGRESS",
				"totalSubtasks": 2,
				"totalUsersAssigned": 5, // memberCount (bottom-up)
				"accumulatedProgress": 60, // avg(), percent
				"workWeight": 100,
				"workVolume": 500,
				"workVolumeUnit": "m2",
				"startDate": "ISO date",
				"endDate": "ISO date",
				"subtasks": [
					{
						"id": "uuid",
						"name": "Subtask name",
						"status": "IN_PROGRESS",
						"totalUsersAssigned": 3, // memberCount
						"progress": 75, // effective (non-rejected), percent
						"workVolume": 250,
						"workVolumeUnit": "m2",
						"workWeight": 50,
						"startDate": "ISO date",
						"endDate": "ISO date",
					},
				],
			},
		],
	},
]

GET /tree/gantt

Same tree structure but each node includes startDate/endDate derived from children. Activity startDate = min of all subtasks. Activity endDate = max of all subtasks. Task dates use own stored values. Supports status query filter (same as /tree).


GET /:id

Get single activity with derived stats.

Response:

{
	"id": "uuid",
	"projectId": "uuid",
	"name": "Activity name",
	"createdBy": "uuid",
	"createdAt": "ISO date",
	"updatedAt": "ISO date",
	"deletedAt": null,
	"taskCount": 2,
	"subtaskCount": 5,
	"memberCount": 10, // bottom-up (see rules above)
	"status": "IN_PROGRESS",
	"progress": 45.5, // avg(), percent
	"workWeight": 150,
}

PATCH /:id

Update activity. If activity has tasks, only name is editable.

Body:

{
	"name": "New name", // string, 1-255, optional
}

DELETE /:id

Soft-delete activity and all its tasks + subtasks. No body. Returns 204 status code.


2. Tasks

Base: /api/action-plans/activities/:activityId/tasks

POST /

Create task under an activity.

Body:

{
	"name": "Task name", // string, 1-255
	"workWeight": 100, // optional, number >= 0
	"workVolume": 500, // optional, number >= 0
	"workVolumeUnit": "m2", // optional, string max 100
	"startDate": "2026-01-01", // optional, ISO date
	"endDate": "2026-03-31", // optional, ISO date
}

Response: Task object with id, name, workWeight, workVolume, workVolumeUnit, startDate, endDate, createdAt.


GET /

List all tasks under an activity.

Response:

{
	"tasks": [
		{
			"id": "uuid",
			"name": "Task name",
			"workWeight": 100,
			"workVolume": 500,
			"workVolumeUnit": "m2",
			"startDate": "ISO date",
			"endDate": "ISO date",
			"createdAt": "ISO date",
			"updatedAt": "ISO date",
			"subtaskCount": 3,
		},
	],
}

GET /:id

Get task detail with derived stats.

Response:

{
	"id": "uuid",
	"activityId": "uuid",
	"name": "Task name",
	"workWeight": 150,
	"workVolume": 500,
	"workVolumeUnit": "m2",
	"startDate": "ISO date",
	"endDate": "ISO date",
	"createdBy": "uuid",
	"createdAt": "ISO date",
	"updatedAt": "ISO date",
	"subtaskCount": 3,
	"memberCount": 5, // bottom-up (see rules above)
	"status": "IN_PROGRESS",
	"progress": 60, // avg(), percent
	"canAddSubtask": false,
}

canAddSubtask is false if the task has subtasks and any subtask is beyond INCOMPLETE_DATA status.


PATCH /:id

Update task. If task has subtasks, only name is editable.

Body:

{
	"name": "New name", // string, 1-255, optional
	"workWeight": 200, // optional (only when no subtasks)
	"workVolume": 600, // optional (only when no subtasks)
	"workVolumeUnit": "km2", // optional (only when no subtasks)
	"startDate": "2026-01-01", // optional (only when no subtasks)
	"endDate": "2026-06-30", // optional (only when no subtasks)
}

DELETE /:id

Soft-delete task and all its subtasks. No body.


3. Subtasks

Base: /api/action-plans/tasks/:taskId/subtasks

POST /

Create subtask under a task.

Body:

{
	"name": "Subtask name", // string, 1-255
	"workVolume": 250, // optional, number >= 0
	"workVolumeUnit": "m2", // optional, string max 100
	"workWeight": 50, // optional, number >= 0
	"startDate": "2026-01-01", // optional, ISO date
	"endDate": "2026-02-15", // optional, ISO date
}

Response: Subtask object (see GET /:id).


GET /

List all subtasks under a task.

Response: Array of subtask objects, each with memberCount, members[].{userId, displayName}, status, progress, progressCount.


GET /:id

Get subtask detail.

Response:

{
	"id": "uuid",
	"taskId": "uuid",
	"task": { "id": "uuid", "activityId": "uuid" },
	"name": "Subtask name",
	"workVolume": 250,
	"workVolumeUnit": "m2",
	"workWeight": 50,
	"startDate": "ISO date",
	"endDate": "ISO date",
	"createdBy": "uuid",
	"createdAt": "ISO date",
	"updatedAt": "ISO date",
	"deletedAt": null,
	"members": [
		{
			"id": "uuid",
			"userId": "uuid",
			"displayName": "John Doe",
		},
	],
	"memberCount": 3,
	"status": "IN_PROGRESS",
	"progress": 75,
	"progressCount": 2,
	"progressPlanCount": 1,
	"plans": [
		{
			"id": "uuid",
			"startDate": "ISO date",
			"endDate": "ISO date",
			"targetProgress": 50,
		},
	],
}

PATCH /:id

Update subtask. Work weight & work volume are locked once subtask is in progress.

Body:

{
	"name": "New name", // string, 1-255, optional
	"workVolume": 300, // optional (locked after progress starts)
	"workVolumeUnit": "kg", // optional (locked after progress starts)
	"workWeight": 60, // optional (locked after progress starts)
	"startDate": "2026-01-01", // optional
	"endDate": "2026-02-15", // optional
}

DELETE /:id

Soft-delete subtask. No body.


4. Progress

Three sets of progress endpoints — subtask, task, and activity scope.


Subtask Progress

Base: /api/action-plans/subtasks/:subtaskId/progress

POST / (multipart/form-data)

Submit progress entry. Supports file uploads.

Form fields:

Field Type Description
progress number (0-100) New cumulative progress %
workVolume number Optional work volume completed
description string Optional note, max 5000 chars
images file[] Image attachments, max 10
sitemap file Single PDF sitemap

Progress auto-accumulates from previous APPROVED entries. If not provided, defaults to current effective progress. When workVolume is provided with totalVolume, progress % is derived from workVolume ratio. New progress must be >= current non-REJECTED floor.

Response: Full progress entry (see GET /:id).

GET /

List progress history for a subtask. Paginated, newest first.

Query: page (default 1), limit (default 50, max 100)

Response:

{
	"items": [
		{
			"id": "uuid",
			"subtaskId": "uuid",
			"subtaskName": "Subtask name",
			"progress": 75,
			"workVolume": 187.5,
			"subtaskWorkVolume": 250,
			"subtaskWorkVolumeUnit": "m2",
			"description": "Completed foundation",
			"status": "APPROVED",
			"amendsId": null,
			"amendsOriginal": null,
			"amendedByCount": 0,
			"amendedBy": [],
			"approvedBy": "uuid",
			"approvedByName": "Admin",
			"approvedAt": "ISO date",
			"createdBy": "uuid",
			"createdByName": "Worker",
			"updatedBy": null,
			"updatedByName": null,
			"createdAt": "ISO date",
			"updatedAt": "ISO date",
			"images": [{ "id": "uuid", "key": "s3/path", "originalName": "photo.jpg", "url": "presigned-url" }],
			"sitemap": { "id": "uuid", "key": "s3/path", "originalName": "site.pdf", "url": "presigned-url" },
		},
	],
	"pagination": {
		"page": 1,
		"limit": 50,
		"total": 2,
		"pages": 1,
		"hasNext": false,
		"hasPrev": false,
	},
}
GET /:id

Get single progress entry with full detail including images and amendment chain.

Response: Same shape as one item in the list above.

POST /:id/respond

Admin responds to a PENDING progress entry.

Body:

{
	"verb": "APPROVE", // "APPROVE" | "APPROVE_WITH_NOTES" | "REJECT"
	"progress": 80, // optional, 0-100 (for APPROVE_WITH_NOTES)
	"workVolume": 200, // optional (for APPROVE_WITH_NOTES)
	"description": "Adjusted", // optional (for APPROVE_WITH_NOTES)
}

Verbs:

  • APPROVE — progress accepted as-is
  • APPROVE_WITH_NOTES — original marked APPROVED_WITH_NOTES + new amended entry created with admin values
  • REJECT — progress rejected, doesn't count toward effective progress

Task Progress

Base: /api/action-plans/tasks/:taskId

POST /progress

Create progress entry at task scope.

Body: Same as subtask progress body (minimal: { "progress": 25 }).

GET /progress

List progress entries. Query: page, limit.

GET /progress/:id

Get single progress entry.

POST /progress/:id/respond

Respond to PENDING progress entry. Body: Same respond schema.


Activity Progress

Base: /api/action-plans/activities/:activityId

POST /progress
GET /progress
GET /progress/:id
POST /progress/:id/respond

Same as task progress. Scope = ACTIVITY.


5. Progress Plans (S-Curve)

Set target progress over time. All levels (subtask/task/activity) share same shape.


Subtask Plans

Base: /api/action-plans/tasks/:taskId/subtasks/:id

GET /plans

Response:

{
	"duration": {
		"days": 90,
		"startDate": "2026-01-01",
		"endDate": "2026-03-31",
	},
	"planning": [
		{
			"id": "uuid",
			"startDate": "2026-01-01",
			"endDate": "2026-02-15",
			"targetProgress": 50,
		},
		{
			"id": "uuid",
			"startDate": "2026-02-15",
			"endDate": "2026-03-31",
			"targetProgress": 50,
		},
	],
	"sCurve": {
		"plan": [
			{ "date": 1704067200000, "progress": 0 },
			{ "date": 1709251200000, "progress": 50 },
			{ "date": 1711929600000, "progress": 100 },
		],
		"actual": [
			{ "date": 1705276800000, "progress": 25, "id": "uuid" },
			{ "date": 1708300800000, "progress": 60, "id": "uuid" },
		],
	},
}

sCurve.plan — cumulative target over time (derived from plan segments).
sCurve.actual — APPROVED entries sorted by approvedAt. Only latest version per amendment chain.

If no plans stored and subtask has dates, a default segment 0%→100% is returned.


PUT /plans

Set (replace) all plans. Validates: continuous dates, within subtask date range, sum to 100%.

Body:

{
	"plans": [
		{
			"startDate": "2026-01-01",
			"endDate": "2026-02-15",
			"targetProgress": 50,
		},
		{
			"startDate": "2026-02-15",
			"endDate": "2026-03-31",
			"targetProgress": 50,
		},
	],
}

Response: Same as GET /plans.


Task Plans

Base: /api/action-plans/tasks/:taskId

GET /plans
PUT /plans

Same shape as subtask plans. Scope = TASK.


Activity Plans

Base: /api/action-plans/activities/:activityId

GET /plans
PUT /plans

Same shape as subtask plans. Scope = ACTIVITY.


6. Members

Member CRUD across all three hierarchy levels. Uses unified actionPlanMembers table.


Subtask Members

Base: /api/action-plans/tasks/:taskId/subtasks/:id

POST /members

Add member to subtask. Also cascades to activity members (upsert).

Body:

{
	"userId": "uuid",
}

Response:

{
	"id": "uuid",
	"scopeType": "SUBTASK",
	"scopeId": "uuid",
	"userId": "uuid",
}
GET /members

List members of a subtask.

Response:

[
	{
		"id": "uuid",
		"userId": "uuid",
		"displayName": "John Doe",
	},
]
DELETE /members/:userId

Remove member from subtask. No body. Returns deleted member object.


Task Members

Base: /api/action-plans/tasks/:taskId

POST /members
GET /members
DELETE /members/:userId

Same pattern as subtask members. scopeType = TASK.


Activity Members

Base: /api/action-plans/activities/:activityId

POST /members
GET /members
DELETE /members/:userId

Same pattern as subtask members. scopeType = ACTIVITY.


7. Feedbacks

Discussion thread per progress entry. Sender can edit own message.

Base: /api/action-plans/progress/:progressId/feedbacks

POST /

Body:

{
	"message": "Please clarify the numbers", // string, 1-5000
}

GET /

List feedbacks for a progress entry. Paginated. Query: page, limit.

PATCH /:id

Edit own message. Body:

{
	"message": "Updated message", // string, 1-5000
}

DELETE /:id

Delete own message. No body.


8. Analytics

Base: /api/projects/:projectId/action-plans/analytics

GET /

Query:

Param Type Values Default
layout string detailed | simplified detailed
granularity string daily | weekly | monthly daily
search string
startDate ISO date
endDate ISO date

9. Import

Base: /api/projects/:projectId/action-plans/import

GET /template/download

Download Excel template file. Returns .xlsx binary.

POST / (multipart/form-data)

Import activities from spreadsheet.

Form field:

Field Type Description
file file .xlsx, .xls, or .csv, max 5MB

Response:

{
	"imported": 3,
	"activities": [
		{ "id": "uuid", "name": "Activity 1" },
		{ "id": "uuid", "name": "Activity 2" },
	],
}

Constraint Summary

Constraint Value
Max tasks per activity 50
Max subtasks per task 50
Max plans per scope Unlimited
Max images per progress 10
Max feedback length 5000 chars
Max 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 and workVolume locked

Soft Delete

All deletes are soft (sets deletedAt). Cascades: deleting activity deletes all tasks + subtasks. Deleting task deletes all subtasks.