Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
11 KiB
PRD — Per-Phase Task Statuses
- Slug:
per-phase-task-statuses - Date:
2026-03-18 - Status: Draft
Summary
Allow each project phase to have its own independent set of task status columns. Currently all phases share the project-level status configuration. With this change, a "Design" phase can use "To Do / In Design / Review / Done" while a "Development" phase uses "Backlog / In Progress / Code Review / QA / Done".
Problem
MSPs run projects with phases that represent fundamentally different workflows (design, development, deployment, onboarding). Forcing all phases to share the same status columns means either:
- Generic statuses that don't describe any phase's workflow well
- Cluttered boards with statuses irrelevant to the current phase
- Workarounds like separate projects per workflow type
Goals
- Each phase can optionally define its own set of task statuses
- Phases without custom statuses fall back to the project-level defaults (backward compatible)
- Tasks moving between phases with different statuses are automatically remapped
- Project templates support per-phase status configuration
- Client portal correctly displays phase-specific statuses
Non-goals
- Per-task status overrides (statuses remain at the phase/project level)
- Status workflow rules (e.g., "must go through Review before Done")
- Cross-phase status analytics or reporting
- Drag-and-drop status column customization directly on the Kanban board
Users and Primary Flows
Personas
- Project Manager (MSP admin): Configures per-phase statuses in project settings; creates templates with phase-specific statuses
- Technician: Views and interacts with phase-specific Kanban columns; moves tasks between phases
- Client user: Views project progress in client portal with correct phase-specific statuses (read-only)
Primary Flows
Flow 1 — Configure phase statuses (Project Manager)
- Open Project Settings > Task Statuses
- Select a phase from the phase dropdown/tabs
- Toggle "Use custom statuses for this phase" (vs. project defaults)
- Add/remove/reorder statuses from the tenant library
- Save — Kanban board now shows phase-specific columns
Flow 2 — View phase-specific Kanban (Technician)
- Open a project, select a phase tab
- Kanban board columns reflect that phase's effective statuses
- Task status dropdown shows only that phase's statuses
- Moving a task to a different phase auto-remaps its status
Flow 3 — Move task between phases (Technician)
- Move or reassign a task from Phase A to Phase B
- System resolves status: same name → keep; no match + open → first open; no match + closed → first closed
- Task appears in the correct column in Phase B's board
Flow 4 — Create template with phase statuses (Project Manager)
- Create/edit a project template
- Define phases within the template
- For each phase, optionally configure custom status columns
- Creating a project from this template copies phase-specific statuses
Flow 5 — Client portal view (Client user)
- Client views project in client portal
- Selects a phase — sees that phase's status columns
- Tasks grouped/displayed by phase-effective statuses
UX / UI Notes
MSP Project Settings
- Phase selector (dropdown or tabs) above the status configuration area
- Default shows "Project Defaults" — the existing project-level statuses
- Each phase shows toggle: "Use project defaults" / "Custom statuses for this phase"
- When toggling to custom: option to "Copy project defaults as starting point" or "Start empty"
- Add/remove/reorder UI remains the same, just scoped to the selected phase
Kanban Board
- Already has phase tabs — no new navigation needed
- Status columns change when switching between phases that have different statuses
- No visual indicator needed that statuses are phase-specific vs. project defaults
Task Status Select
- Shows effective statuses for the task's current phase
- No change to UX, just data source changes
Client Portal
- Kanban and list views show phase-effective statuses
- No configuration UI (read-only)
Requirements
Functional Requirements
FR-1: Database Schema
- FR-1.1: Add nullable
phase_idcolumn toproject_status_mappingstable referencingproject_phases(phase_id)with ON DELETE CASCADE - FR-1.2: Add index on
(tenant, project_id, phase_id)for efficient lookups - FR-1.3: Existing rows with
phase_id = NULLrepresent project-level defaults (no data migration needed) - FR-1.4: Add nullable
template_phase_idcolumn toproject_template_status_mappingsreferencingproject_template_phases(template_phase_id)with ON DELETE CASCADE - FR-1.5: EE/Citus companion migration — FK constraints use composite keys
(tenant, phase_id)since both tables are distributed ontenant
FR-2: Effective Status Resolution
- FR-2.1: New model function
getEffectiveStatusMappings(knex, tenant, projectId, phaseId)— returns phase-specific statuses if any exist for that phase_id, otherwise falls back to project-level statuses (phase_id IS NULL) - FR-2.2: Resolution is transparent — callers get a flat list of statuses regardless of source
- FR-2.3: All status-fetching actions (
getProjectTaskStatuses,getProjectStatusMappings,getClientProjectStatuses) accept optionalphaseIdand use effective resolution
FR-3: Phase Status Configuration
- FR-3.1:
addStatusToProject()accepts optionalphaseId— creates mapping with that phase_id - FR-3.2:
reorderProjectStatuses()scopes to phase if phaseId provided - FR-3.3: New action
copyProjectStatusesToPhase(projectId, phaseId)— copies project defaults as phase-specific mappings - FR-3.4: New action
removePhaseStatuses(phaseId)— deletes all mappings for a phase, reverting it to project defaults - FR-3.5:
deleteProjectStatusMapping()— no change needed (works by mapping ID)
FR-4: Cross-Phase Task Movement
- FR-4.1: When moving a task between phases within the same project, resolve status mapping:
- Same status name exists in target phase → use it
- No name match + source status was open → first open status by display_order
- No name match + source status was closed → first closed status by display_order
- FR-4.2: Cross-project moves continue to work as before (existing logic in
moveTaskToPhase) - FR-4.3:
updateTaskStatus()validates the status mapping belongs to the task's phase's effective statuses
FR-5: Settings UI
- FR-5.1:
ProjectTaskStatusSettingsshows a phase selector (dropdown or tabs) above the status list - FR-5.2: "Project Defaults" is the first/default option in the phase selector
- FR-5.3: Each phase shows toggle: "Use project defaults" vs "Custom statuses"
- FR-5.4: Toggling to custom offers "Copy from project defaults" action
- FR-5.5: Toggling back to defaults shows confirmation (will delete phase-specific mappings)
- FR-5.6:
AddStatusDialogpasses phaseId when adding to a phase
FR-6: Kanban Board & Task UI
- FR-6.1:
ProjectDetailfetches phase-effective statuses when selectedPhase changes - FR-6.2: KanbanBoard renders columns from phase-effective statuses (receives via props, minimal change)
- FR-6.3: TaskStatusSelect shows effective statuses for the task's phase (receives via props)
- FR-6.4: Status task counts computed from phase-effective statuses
- FR-6.5: Completed task count computed from phase-effective statuses
FR-7: Client Portal
- FR-7.1:
getClientProjectStatuses()accepts optional phaseId and uses effective resolution - FR-7.2:
ProjectDetailViewfetches phase-effective statuses when phase selection changes - FR-7.3:
ClientKanbanBoardrenders phase-specific status columns - FR-7.4:
ClientTaskListViewgroups tasks by phase-effective status groups
FR-8: Template Support
- FR-8.1:
project_template_status_mappingssupportstemplate_phase_idfor per-phase template statuses - FR-8.2:
TemplateStatusManagerallows per-phase status configuration with phase selector - FR-8.3:
TemplateStatusColumnsStep(wizard) supports phase-aware status setup - FR-8.4: Project creation from template copies phase-specific template status mappings to
project_status_mappingswith correctphase_id
FR-9: Supporting Systems
- FR-9.1: Phase task import (
IImportReferenceData.statusMappings) resolves against phase-effective statuses - FR-9.2: Event payloads for
PROJECT_STATUS_ADDED/UPDATED/DELETED/REORDEREDinclude optionalphaseId - FR-9.3:
ProjectTaskStatusEditorandProjectTaskStatusSelector(used during project creation) handle phase statuses from templates
Non-functional Requirements
- NFR-1: Backward compatible — existing projects with no phase-specific statuses work identically
- NFR-2: No data migration needed — existing rows have phase_id NULL which is the project-level default
- NFR-3: Citus compatible — all queries include tenant in WHERE/JOIN conditions; FK references use composite keys
Data / API / Integrations
Schema Changes
project_status_mappings — add column:
phase_id UUID NULL REFERENCES project_phases(phase_id) ON DELETE CASCADE
Index: (tenant, project_id, phase_id)
project_template_status_mappings — add column:
template_phase_id UUID NULL REFERENCES project_template_phases(template_phase_id) ON DELETE CASCADE
Type Changes
IProjectStatusMapping — add: phase_id?: string
ProjectStatus — add: phase_id?: string
Key Query Pattern — Effective Resolution
-- Check if phase has custom statuses
SELECT COUNT(*) FROM project_status_mappings
WHERE tenant = ? AND project_id = ? AND phase_id = ?;
-- If count > 0: use phase statuses
SELECT * FROM project_status_mappings
WHERE tenant = ? AND project_id = ? AND phase_id = ?
ORDER BY display_order;
-- If count = 0: fall back to project defaults
SELECT * FROM project_status_mappings
WHERE tenant = ? AND project_id = ? AND phase_id IS NULL
ORDER BY display_order;
Security / Permissions
- No new permissions needed — status configuration is already gated by project management permissions
- Client portal: read-only access to effective statuses, enforced by existing
getClientProjectStatuses()access checks - Tenant isolation: all queries include
tenantin WHERE clauses (existing pattern)
Rollout / Migration
- Zero-downtime deployment: nullable column addition, no data changes
- Backward compatible: all phases fall back to project defaults until customized
- No feature flag needed: the feature is additive and opt-in per phase
- Rollback: drop the
phase_idcolumn; phase-specific mappings are lost but project defaults remain
Open Questions
None — all design decisions have been agreed upon (see Key Design Decisions in SCRATCHPAD.md).
Acceptance Criteria (Definition of Done)
- Existing projects with no phase-specific statuses behave identically to before
- A phase can be configured with custom statuses via the settings UI
- Kanban board shows phase-specific status columns when viewing a customized phase
- Moving a task between phases with different statuses correctly remaps the status
- Project templates can define per-phase statuses
- Creating a project from a template with phase statuses sets up correct mappings
- Client portal displays phase-specific statuses correctly
- All existing tests pass; new tests cover the new functionality