Candidate Pool
Overview
The Candidate Pool is a recruiter-facing view that aggregates all candidates across every job into a single searchable list. Unlike the per-job pipeline view, the pool lets recruiters spot high-value candidates, track their overall status, and jump into any individual pipeline without navigating job-by-job.
Route: /dashboard/pool
Layout
Split-pane design matching Pipeline.tsx:
/dashboard/pool
┌─────────────────────────┬───────────────────────────────────┐
│ LEFT PANE (w-1/3) │ RIGHT PANE (flex-1) │
│ │ │
│ [Total | Active | Hired│ Participant header │
│ stat mini-cards] │ Avatar · Name · Email │
│ [Search input] │ Jobs count · Interviews · ... │
│ [Status | Sort by] │ │
│ │ [Tabs: Jobs | Activity] │
│ • Candidate row │ │
│ avatar, name, email │ Jobs tab: │
│ jobs count, status, │ Job card (title, status, │
│ last activity │ progress bar, last activity) │
│ ... │ → click → ResponsiveSheet │
│ [Pagination] │ → CandidateDetailPanel │
│ │ │
│ │ Activity tab: │
│ │ Chronological timeline of │
│ │ all stage events (no extra │
│ │ API call — built from already- │
│ │ fetched stageProgression) │
└─────────────────────────┴───────────────────────────────────┘Mobile: full left pane → tap a candidate → full right pane (back button restores list). Same md:hidden / md:flex breakpoint logic as Pipeline.tsx.
State Management
selectedParticipantId is plain React state in CandidatePool.tsx. No URL param changes when a candidate is selected — matches the Pipeline page pattern.
Left Pane: Candidate List
Stat Cards
Three compact cards at the top (totals for the current search scope, before the status filter):
| Card | Value |
|---|---|
| Total | All unique participants found by search |
| Active | latestStatus === 'active' count |
| Hired | latestStatus === 'hired' count |
Counts come from the summary facet in the GET /v1/pipeline/pool aggregation response — zero extra requests.
Search
400 ms debounce. Regex match on participant name and email (minimum 2 characters, same pattern as the Pipeline page). Applied before the $group stage so the grouping and counts both reflect the search.
Status Filter
Dropdown: All / Active / Shortlisted / Hired / Rejected / Withdrawn. Passes status query param. Applied inside the $facet (after grouping), so it filters the paginated data and total buckets without affecting the summary counts.
Sort Options
| Option | Sorts by |
|---|---|
| Last Activity (default) | lastActivityAt desc |
| Name A–Z | name asc |
| Most Jobs | jobCount desc |
Candidate Row
- Avatar with initials (first + last letter of name, or first 2 chars of email)
- Name (primary), email (secondary if name exists)
- Status badge (same
STATUS_COLORSmap used across pipeline components) N jobs · datefooter line
Right Pane: Candidate Profile
Loaded via GET /v1/pipeline/pool/:participantId on candidate selection.
Header
Large avatar initials, name, email, and three stat badges:
- N jobs — count of pipelines
- N interviews — from
participant.stats.totalInterviews - N stages completed — aggregated from all
stageProgressionentries withstatus === 'completed'
Tags
Unique tags collected from all pipelines ([...new Set(pipelines.flatMap(p => p.tags))]). Displayed as pills.
Jobs Tab
One card per CandidatePipeline document, showing:
- Job title + status badge
- Mini progress bar:
completed / totalstages (linear fill) - Last activity date
Clicking a job card opens a ResponsiveSheet containing <CandidateDetailPanel pipelineId={pipeline._id} /> — the same full-featured panel used everywhere else (stage timeline, AI report, unlock, notes, global status selector).
Activity Tab
A vertical timeline constructed client-side from the already-fetched stageProgression data — no extra API call. Events extracted per stage:
| Event | Source field |
|---|---|
Invited to stageName | invitedAt |
Started stageName | startedAt |
Completed stageName | completedAt |
All events across all pipelines are merged and sorted by timestamp descending. Each item shows the event label, job title, and formatted datetime.
No Schema Changes Required
CandidatePipeline.organizationId with the existing { organizationId: 1, status: 1 } compound index handles the pool list query efficiently. { participantId: 1, lastActivityAt: -1 } handles the profile query. No new indexes needed.