PSA/docs/features/teams-v2.md
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

12 KiB

Teams V2

The Teams V2 feature adds comprehensive team management capabilities to Alga PSA: team member roles, team avatars, team assignment to tickets/tasks/templates, board-level default teams, organizational hierarchy with reports-to chains, and an org chart visualization. All UI is gated behind the teams-v2 PostHog feature flag.

Core Features

Team Member Roles

Each team member has a role field: 'member' or 'lead'.

  • Team managers are automatically assigned the 'lead' role
  • New members added via addUserToTeam default to 'member'
  • assignManagerToTeam ensures the manager is also a team member with 'lead' role

Team Avatars

Teams can have avatar images, reusing the existing document_associations / EntityImageService pattern.

  • Upload: uploadTeamAvatar(teamId, formData) — stores image via uploadEntityImage('team', ...)
  • Delete: deleteTeamAvatar(teamId) — removes via deleteEntityImage('team', ...)
  • Single fetch: getTeamAvatarUrlAction(teamId, tenant) — returns URL or null
  • Batch fetch: getTeamAvatarUrlsBatchAction(teamIds[], tenant) — returns Map<string, string | null>, queries document_associations where entity_type='team' and is_entity_logo=true
  • Component: TeamAvatar (packages/ui/src/components/TeamAvatar.tsx) — wraps EntityAvatar, shows colored initials as fallback
  • Hook: useTeamAvatar (packages/teams/src/hooks/useTeamAvatar.ts) — SWR-cached client-side fetching with refreshAvatar() and invalidateTeamAvatar()

Team Assignment to Tickets

When a team is assigned to a ticket:

  1. ticket.assigned_team_id is set
  2. The team lead becomes the primary assignee (assigned_to) if none exists
  3. Remaining team members are added as ticket_resources with role='team_member'
  4. Duplicate resources are skipped

Removal supports three modes via removeTeamFromTicket(ticketId, { mode, keepUserIds? }):

  • 'remove_all' — removes all team member resources
  • 'keep_all' — only clears assigned_team_id, keeps resources
  • 'selective' — removes all except specified keepUserIds

Filtering: ITicketListFilters.assignedTeamIds enables filtering ticket lists by team. The optimized ticket query joins the teams table and supports combined user + team + unassigned filtering with OR logic.

Team Assignment to Project Tasks

Same pattern as tickets: assigned_team_id column on project_tasks with a foreign key to teams. Task forms use UserAndTeamPicker for primary agent and MultiUserAndTeamPicker for additional agents. Team selection through either picker sets the assigned_team_id and populates members.

Team Assignment to Project Templates

assigned_team_id column on project_template_tasks. When a template is applied to create a project, the team assignment is optionally copied based on copyOptions.copyAssignments.

Relevant template actions:

  • createTemplateFromWizard — inserts tasks with assigned_team_id
  • updateTemplateFromEditor — preserves assigned_team_id on updates
  • saveTemplateAsNew — copies assigned_team_id from source tasks

Board Default Team

Boards can have a default_assigned_team_id. When a ticket is created on that board (via QuickAddTicket), the team is pre-selected. Configured in Settings > Boards via UserAndTeamPicker.

Organizational Hierarchy (Reports-To)

A reports_to column on the users table establishes manager relationships independent of team structure. Used for:

  • Org chart visualization
  • Time entry delegation authorization
  • Time sheet approval chains
  • Scheduling and availability

Configured per-user in User Details settings (only visible when teams-v2 flag is enabled).

Org Chart Visualization

A ReactFlow-based interactive org chart in Settings > User Management:

  • Top-down tree layout from reports_to relationships
  • Custom nodes showing avatar, name, role, and inactive badge
  • Click a node to open User Details drawer
  • Batch-fetches user avatars
  • Supports zoom and pan

Client Portal Display

The client portal displays team information in read-only mode:

  • Ticket list: Team avatar badges in the assigned-to column
  • Ticket details: Team avatar and name alongside the assignee
  • Project Kanban/list views: Team badges on task cards

Database Schema

Migrations

Migration Table Change
20260226170000_add_reports_to_to_users users Add reports_to UUID column (FK to users.user_id)
20260226170500_seed_reports_to_from_teams users Seed reports_to from team_membersteams.manager_id
20260226171000_add_role_to_team_members team_members Add role TEXT column (default 'member')
20260226171500_seed_team_member_leads team_members Set role='lead' where user_id = team.manager_id
20260226172000_add_assigned_team_id_to_tickets tickets Add assigned_team_id UUID (FK to teams.team_id). No transaction (Citus).
20260226172500_add_assigned_team_id_to_project_tasks project_tasks Add assigned_team_id UUID (FK to teams.team_id)
20260227000001_add_team_to_document_associations_entity_type document_associations Add 'team' to entity_type CHECK constraint. No transaction.
20260227100000_add_assigned_team_id_to_project_template_tasks project_template_tasks Add assigned_team_id UUID (FK to teams.team_id)
20260227200000_add_default_assigned_team_id_to_boards boards Add default_assigned_team_id UUID (FK to teams.team_id). No transaction.

All foreign keys use composite (tenant, id) pattern for Citus compatibility.

Key Relationships

teams
  ├── team_members (team_id, user_id, role)
  ├── tickets.assigned_team_id
  ├── project_tasks.assigned_team_id
  ├── project_template_tasks.assigned_team_id
  ├── boards.default_assigned_team_id
  └── document_associations (entity_type='team', is_entity_logo=true)

users
  └── reports_to → users.user_id

Server Actions API

Team CRUD — packages/teams/src/actions/team-actions/teamActions.ts

Function Parameters Returns Permission
createTeam teamData (with optional members[]) ITeam user_settings / create
updateTeam teamId, Partial<ITeam> ITeam user_settings / update
deleteTeam teamId DeletionValidationResult user_settings / delete
getTeamById teamId ITeam (with members) user_settings / read
getTeams ITeam[] (with members) user_settings / read
addUserToTeam teamId, userId ITeam user_settings / update
removeUserFromTeam teamId, userId ITeam user_settings / update
assignManagerToTeam teamId, userId ITeam user_settings / update

Avatar Actions — packages/teams/src/actions/team-actions/avatarActions.ts

Function Parameters Returns Permission
uploadTeamAvatar teamId, FormData { success, avatarUrl? } user_settings / update
deleteTeamAvatar teamId { success } user_settings / update
getTeamAvatarUrlAction teamId, tenant string | null None
getTeamAvatarUrlsBatchAction teamIds[], tenant Map<string, string | null> None

Ticket Team Assignment — packages/tickets/src/actions/teamAssignmentActions.ts

Function Parameters Returns Permission
assignTeamToTicket ticketId, teamId void ticket / update
removeTeamFromTicket ticketId, { mode, keepUserIds? } void ticket / update

UI Components

UserAndTeamPicker

File: packages/ui/src/components/UserAndTeamPicker.tsx

Single-select picker for choosing a user or a team. Shows team member count and lead name. Fetches avatars in batch when the dropdown opens.

Key props: value, onValueChange, onTeamSelect, users, teams, getUserAvatarUrlsBatch, getTeamAvatarUrlsBatch

Used in: TaskForm (primary agent), TemplateTaskForm (primary agent), TicketInfo (assignee), QuickAddTicket (assignee), BoardsSettings (default team), TemplateTasksStep (wizard)

MultiUserAndTeamPicker

File: packages/ui/src/components/MultiUserAndTeamPicker.tsx

Multi-select picker for users and teams. Supports filter mode with "Unassigned" option, compact display, and checkbox-based selection. Teams appear in a separate section.

Key props: values, onValuesChange, teams, teamValues, onTeamValuesChange, filterMode, compactDisplay

Used in: TaskForm (additional agents), TemplateTaskForm (additional agents), TicketProperties (additional agents), TicketingDashboard (assignee filter)

TeamAvatar

File: packages/ui/src/components/TeamAvatar.tsx

Display component wrapping EntityAvatar. Shows the team's uploaded avatar image or colored initials as fallback. Supports sizes: xs, sm, md, lg.

Props: teamId, teamName, avatarUrl, size, className

Used in: Task cards, ticket columns, ticket details/properties, client portal views, picker components


Integration Points

Component Package Team Functionality Feature Flag
TaskForm projects Assign team via primary/additional agent pickers Yes
TaskCard projects Display team badge Yes
TaskListView projects Team column in list Yes
KanbanBoard / StatusColumn projects Team badge on cards Yes
ProjectDetail projects Team data management, avatar batch fetch Yes
TemplateTaskForm projects Assign team in template tasks Yes
TemplateEditor projects Team name/avatar state, pass to child components Yes
TemplateTaskListView projects Team badge display Yes
TemplateTasksStep projects Team assignment in wizard Yes
TicketDetails tickets Team assignment/removal handlers Yes
TicketInfo tickets Primary assignee with team picker, team badge Yes
TicketProperties tickets Additional agents with team picker, team badge/removal dialog Yes
QuickAddTicket tickets Team assignment, board default team Yes
TicketingDashboard tickets Team filter, team avatars in columns Yes
ticket-columns tickets Team avatar in assigned-to column Yes
optimizedTicketActions tickets Team join, team filter in queries N/A (backend)
ClientKanbanBoard client-portal Read-only team badge on tasks No (display only)
ClientTaskListView client-portal Read-only team badge in list No (display only)
TicketDetails (client) client-portal Read-only team badge No (display only)
TicketList (client) client-portal Team avatar in columns No (display only)
UserDetails server/settings Reports-to dropdown Yes
OrgChart server/settings Org chart visualization Yes
BoardsSettings server/settings Default team per board Yes

Feature Flag

All teams-v2 UI is gated behind the teams-v2 PostHog feature flag:

const { enabled: teamsV2Enabled } = useFeatureFlag('teams-v2', { defaultValue: false });

When disabled:

  • Standard UserPicker / MultiUserPicker are rendered instead of team-aware pickers
  • Team badges, team assignment UI, and org chart are hidden
  • Database columns exist but remain null
  • Client portal team display is unconditional (shows data if present)

Type Definitions

Team Types — packages/types/src/interfaces/auth.interfaces.ts

interface ITeamMember extends IUserWithRoles {
  role: 'member' | 'lead';
}

interface ITeam extends TenantEntity {
  team_id: string;
  team_name: string;
  manager_id: string | null;
  members: ITeamMember[];
}

Ticket — packages/types/src/interfaces/ticket.interfaces.ts

interface ITicket {
  assigned_team_id?: string | null;  // FK to teams
}

interface ITicketListItem {
  assigned_team_name?: string | null;  // Joined from teams table
}

interface ITicketListFilters {
  assignedTeamIds?: string[];  // Filter by team IDs
}

Project Template Task — packages/types/src/interfaces/projectTemplate.interfaces.ts

interface IProjectTemplateTask {
  assigned_team_id?: string | null;  // FK to teams
}

Template Wizard — packages/projects/src/types/templateWizard.ts

interface TemplateTask {
  assigned_team_id?: string;
}