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

283 lines
12 KiB
Markdown

# 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_members``teams.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:
```typescript
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`
```typescript
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`
```typescript
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`
```typescript
interface IProjectTemplateTask {
assigned_team_id?: string | null; // FK to teams
}
```
### Template Wizard — `packages/projects/src/types/templateWizard.ts`
```typescript
interface TemplateTask {
assigned_team_id?: string;
}
```