Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

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

  1. Each phase can optionally define its own set of task statuses
  2. Phases without custom statuses fall back to the project-level defaults (backward compatible)
  3. Tasks moving between phases with different statuses are automatically remapped
  4. Project templates support per-phase status configuration
  5. 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)

  1. Open Project Settings > Task Statuses
  2. Select a phase from the phase dropdown/tabs
  3. Toggle "Use custom statuses for this phase" (vs. project defaults)
  4. Add/remove/reorder statuses from the tenant library
  5. Save — Kanban board now shows phase-specific columns

Flow 2 — View phase-specific Kanban (Technician)

  1. Open a project, select a phase tab
  2. Kanban board columns reflect that phase's effective statuses
  3. Task status dropdown shows only that phase's statuses
  4. Moving a task to a different phase auto-remaps its status

Flow 3 — Move task between phases (Technician)

  1. Move or reassign a task from Phase A to Phase B
  2. System resolves status: same name → keep; no match + open → first open; no match + closed → first closed
  3. Task appears in the correct column in Phase B's board

Flow 4 — Create template with phase statuses (Project Manager)

  1. Create/edit a project template
  2. Define phases within the template
  3. For each phase, optionally configure custom status columns
  4. Creating a project from this template copies phase-specific statuses

Flow 5 — Client portal view (Client user)

  1. Client views project in client portal
  2. Selects a phase — sees that phase's status columns
  3. 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_id column to project_status_mappings table referencing project_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 = NULL represent project-level defaults (no data migration needed)
  • FR-1.4: Add nullable template_phase_id column to project_template_status_mappings referencing project_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 on tenant

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 optional phaseId and use effective resolution

FR-3: Phase Status Configuration

  • FR-3.1: addStatusToProject() accepts optional phaseId — 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: ProjectTaskStatusSettings shows 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: AddStatusDialog passes phaseId when adding to a phase

FR-6: Kanban Board & Task UI

  • FR-6.1: ProjectDetail fetches 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: ProjectDetailView fetches phase-effective statuses when phase selection changes
  • FR-7.3: ClientKanbanBoard renders phase-specific status columns
  • FR-7.4: ClientTaskListView groups tasks by phase-effective status groups

FR-8: Template Support

  • FR-8.1: project_template_status_mappings supports template_phase_id for per-phase template statuses
  • FR-8.2: TemplateStatusManager allows 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_mappings with correct phase_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/REORDERED include optional phaseId
  • FR-9.3: ProjectTaskStatusEditor and ProjectTaskStatusSelector (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 tenant in 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_id column; 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)

  1. Existing projects with no phase-specific statuses behave identically to before
  2. A phase can be configured with custom statuses via the settings UI
  3. Kanban board shows phase-specific status columns when viewing a customized phase
  4. Moving a task between phases with different statuses correctly remaps the status
  5. Project templates can define per-phase statuses
  6. Creating a project from a template with phase statuses sets up correct mappings
  7. Client portal displays phase-specific statuses correctly
  8. All existing tests pass; new tests cover the new functionality