Codex 5.3 Refactor Note: Canonical refactor plan: docs/CODEX-5.3-REFACTOR-PLAN.md. This document is retained for historical and implementation context during the refactor.
W11 Candidate List Implementation
โ A. Summary
W11 implements Candidate List - a role-based, filtered list view with search, pagination, and quick navigation to candidate detail. Users see saved views tailored to their role (HR, Manager, SMO, Admin), apply filters (status, position, manager, date range, overdue), and click to open candidate details.
Key Features:
- Role-specific saved views (HR: needs screening/manager; Manager: my queue/waiting for SMO; SMO: needs decision)
- Server-side RBAC filtering (Manager only sees assigned, SMO only sees TO_SMO/decided)
- Advanced filtering: status, position, manager, date range, overdue toggle
- Search: candidate name, code, or position
- Pagination (server-side)
- Computed columns: age in status (days), owner role, overdue indicator
๐ B. Routes
Pages
/candidates(authenticated)- Layout: header + sidebar (saved views) + main (filters + table)
- Query params: q, status, applyingFor, hiringManagerId, assignedToMe, createdFrom, createdTo, overdue, page, pageSize, sort
- Shows: candidate list with pagination
API Endpoints
GET /api/candidates(NEW GET handler)- Auth required
- Query params: q (search), status (comma-list), applyingFor, hiringManagerId, assignedToMe, createdFrom, createdTo, overdue, page, pageSize, sort
- Response:
{ items: CandidateRow[], page, pageSize, total, hasMore } - RBAC: Manager โ only assigned; SMO โ only TO_SMO/decided; HR/Admin โ all
- Returns computed fields: ageInStatusDays, ownerRole, isOverdue
๐พ C. Data Model Changes
Prisma Schema
No new tables required.
Already exists:
Candidatemodel withstatusUpdatedAt(added in W9/W10)CandidateStatusenum with all statusesUsermodel (for hiring managers)
No migration needed - statusUpdatedAt already exists in schema.
๐จ D. UI Components
Files Created/Updated
src/app/(app)/candidates/page.tsx (NEW - 180 lines)
- Layout: header + grid (sidebar + main)
- Fetches managers, positions for filter dropdowns
- Fetches candidates via GET /api/candidates
- Passes data to SavedViews, FiltersBar, CandidatesTable
src/app/(app)/candidates/_components/SavedViews.tsx (NEW - 95 lines)
- Role-specific saved views (HR: needs screening, needs manager, active pipeline, all)
- Manager: my queue, waiting for SMO, my reviewed, all assigned
- SMO: needs decision, recent decisions
- Admin: all candidates
- Active view highlight based on query params
src/app/(app)/candidates/_components/FiltersBar.tsx (NEW - 180 lines)
- Search: name/code/position
- Status: multi-select dropdown
- Position: dropdown
- Manager: dropdown
- Date range: createdFrom, createdTo
- Overdue checkbox
- Apply/Clear buttons
src/app/(app)/candidates/_components/CandidatesTable.tsx (NEW - 200 lines)
- Columns: Candidate (name + code), Applying For, Status (color badge + overdue โ ), Owner, Manager, Age (days), Updated, Actions
- Row click โ
/candidates/[id] - Pagination: Previous/Next
- Empty state: "No candidates found"
- Status colors: NEW (blue), HR_SCREENED (green), MANAGER_EVAL_PENDING (yellow), MANAGER_REVIEWED (purple), TO_SMO (indigo), APPROVED (green), REJECTED (red), KIV (orange)
โ๏ธ E. API Logic
File: src/app/api/candidates/route.ts - UPDATED (GET added)
GET /api/candidates
1. requireAuth() โ verify user
2. Parse query params:
- q: search string
- status: comma-separated status list
- applyingFor: position filter
- hiringManagerId: manager filter
- assignedToMe: boolean (override hiringManagerId)
- createdFrom, createdTo: date range
- overdue: boolean
- page, pageSize: pagination
- sort: field_direction (default: updatedAt_desc)
3. Build Prisma where clause:
- Search: OR fullName, candidateCode, applyingFor (case-insensitive)
- Status: IN(status) if provided
- Position: contains applyingFor
- Date range: createdAt between dates
- RBAC filtering:
* if role=MANAGER: where hiringManagerId = user.id
* if role=SMO: where status IN(TO_SMO, APPROVED, REJECTED, KIV)
* HR/ADMIN: no filter
4. Execute queries:
- findMany(where, orderBy, skip, take) โ items
- count(where) โ total
5. Transform items to CandidateRow:
- Compute ageInStatusDays = now - statusUpdatedAt
- Compute ownerRole = getOwnerRole(status)
- Compute isOverdue = isOverdue(status, ageInStatusDays)
- Include hiringManager (id, fullName, email)
6. Filter by overdue (client-side after transform)
7. Return { items, page, pageSize, total, hasMore }
Helper Functions:
src/lib/candidates/owner.ts (NEW - 60 lines)
getOwnerRole(status): OwnerRole
- NEW/HR_SCREENED โ HR
- MANAGER_EVAL_PENDING/MANAGER_REVIEWED โ Manager
- TO_SMO โ SMO
- APPROVED/REJECTED/KIV/COMPLETED โ null
getAgeInStatusDays(statusUpdatedAt): number
- Calculate days since statusUpdatedAt
- Return floor((now - statusUpdatedAt) / ms_per_day)
isOverdue(status, ageInDays): boolean
- NEW > 2 days
- HR_SCREENED > 2 days
- MANAGER_EVAL_PENDING > 3 days
- MANAGER_REVIEWED > 3 days
- TO_SMO > 2 days
- APPROVED/REJECTED/KIV/COMPLETED > 999 (never)
๐ F. RBAC Checks
Access Control
| Role | Can See | Filters |
|---|---|---|
| HR | All candidates | By status, position, manager, date |
| Manager | Only assigned to self | By status, position, date |
| SMO | TO_SMO + decided (APPROVED/REJECTED/KIV) | By status, date |
| Admin | All candidates | By any filter |
Implementation
Server-side in GET /api/candidates:
if (auth.role === 'MANAGER') {
where.hiringManagerId = auth.userId;
} else if (auth.role === 'SMO') {
where.status = { in: [TO_SMO, APPROVED, REJECTED, KIV] };
}
// HR and ADMIN: no restriction
UI (saved views only show relevant options per role)
๐ G. Audit Events
No new audit events for W11 (list view is read-only).
โ H. Test Checklist
Prerequisites
- Ensure candidates exist with various statuses (NEW, HR_SCREENED, etc.)
- Set up 3 users: HR, Manager (with some assigned candidates), SMO
- Database seeded with test data
Test Case 1: HR User - Saved Views
Setup:
- Login as HR user
- Navigate
/candidates
Expected:
- โ Sidebar shows: "Needs HR Screening", "Needs Manager Assignment", "Active Pipeline", "All Candidates"
- โ Default view shows all candidates in active pipeline
- โ Click "Needs HR Screening" filters to status=NEW
- โ Click "Needs Manager Assignment" shows NEW + HR_SCREENED (without manager)
Test Case 2: Manager User - My Queue
Setup:
- Ensure Manager user has assigned candidates in MANAGER_EVAL_PENDING
- Login as Manager
- Navigate
/candidates
Expected:
- โ Sidebar shows: "My Queue", "Waiting for SMO", "My Reviewed", "All Assigned"
- โ Click "My Queue" shows only assigned candidates with status=MANAGER_EVAL_PENDING
- โ Other users' candidates NOT visible in table
- โ Click "All Assigned" shows all assigned candidates (all statuses)
Test Case 3: SMO User - Needs Decision
Setup:
- Ensure candidates exist with status=TO_SMO
- Login as SMO user
- Navigate
/candidates
Expected:
- โ Sidebar shows: "Needs Decision", "Recent Decisions"
- โ Click "Needs Decision" shows only status=TO_SMO
- โ Click "Recent Decisions" shows APPROVED/REJECTED/KIV
- โ Candidates with other statuses NOT visible
Test Case 4: Admin User - All Candidates
Setup:
- Login as Admin user
- Navigate
/candidates
Expected:
- โ Sidebar shows: "All Candidates"
- โ Can see all candidates regardless of status
- โ Can filter by any status, position, manager, date
Test Case 5: Search
Setup:
- Multiple candidates exist
- Login as any user
- Navigate
/candidates
Actions:
- Type candidate name in search box
- Type candidate code in search box
- Type position in search box
Expected:
- โ Search filters by name (case-insensitive)
- โ Search filters by code (case-insensitive)
- โ Search filters by applyingFor (case-insensitive)
- โ Results update in table
Test Case 6: Status Filter
Setup:
- Candidates exist with multiple statuses
- Login as HR user
- Navigate
/candidates
Actions:
- Select status=NEW from filter
- Click "Apply Filters"
Expected:
- โ Table shows only NEW candidates
- โ URL includes status=NEW param
- โ Clear Filters resets to all statuses
Test Case 7: Position Filter
Setup:
- Candidates exist with different positions
- Login as any user
Actions:
- Select position from dropdown
- Click "Apply Filters"
Expected:
- โ Table filters by applyingFor
- โ URL includes applyingFor param
- โ Results update
Test Case 8: Manager Filter
Setup:
- Candidates assigned to different managers
- Login as HR user
Actions:
- Select manager from dropdown
- Click "Apply Filters"
Expected:
- โ Table shows only candidates assigned to selected manager
- โ URL includes hiringManagerId param
Test Case 9: Date Range Filter
Setup:
- Candidates created at different dates
- Login as any user
Actions:
- Set "Created From" date
- Set "Created To" date
- Click "Apply Filters"
Expected:
- โ Table shows candidates created within date range
- โ URL includes createdFrom and createdTo params
- โ Candidates outside range hidden
Test Case 10: Overdue Toggle
Setup:
- Candidates with various statuses, some older than thresholds:
- NEW > 2 days old
- HR_SCREENED > 2 days old
- MANAGER_EVAL_PENDING > 3 days old
Actions:
- Check "Overdue Only"
- Click "Apply Filters"
Expected:
- โ Table shows only overdue candidates
- โ Overdue indicator โ appears on status badge
- โ URL includes overdue=true param
- โ When unchecked, shows all candidates again
Test Case 11: Pagination
Setup:
- More than 20 candidates exist
- Login as Admin
Actions:
- First page loads
- Click "Next"
- Verify page shows items 21-40
- Click "Previous"
Expected:
- โ First page shows 1-20 items
- โ Next button navigates to page 2
- โ Previous button navigates back
- โ URL includes page param
- โ Bottom shows: "Showing X to Y of Z"
Test Case 12: Row Click โ Open Candidate Detail
Setup:
- Candidates list displayed
- Login as any user
Actions:
- Click on a candidate row
- Observe navigation
Expected:
- โ
Navigates to
/candidates/[id] - โ Candidate detail page loads (W12)
- โ All cells in row clickable except Actions button
Test Case 13: Age in Status Column
Setup:
- Candidate created/transitioned to status 5 days ago
- Login as any user
Expected:
- โ "Age in Status" column shows 5 days
- โ Calculated from statusUpdatedAt
- โ Updates correctly for each candidate
Test Case 14: Owner Role Column
Setup:
- Candidates in various statuses
Expected:
- โ NEW โ HR
- โ HR_SCREENED โ HR
- โ MANAGER_EVAL_PENDING โ Manager
- โ MANAGER_REVIEWED โ Manager
- โ TO_SMO โ SMO
- โ APPROVED/REJECTED/KIV โ (empty/null)
Test Case 15: Status Badge Colors
Setup:
- Candidates with all statuses visible
Expected:
- โ NEW: blue badge
- โ HR_SCREENED: green badge
- โ MANAGER_EVAL_PENDING: yellow badge
- โ MANAGER_REVIEWED: purple badge
- โ TO_SMO: indigo badge
- โ APPROVED: green badge
- โ REJECTED: red badge
- โ KIV: orange badge
Test Case 16: Empty State
Setup:
- Login as Manager with no assigned candidates
- Click "My Queue" (status=MANAGER_EVAL_PENDING)
Expected:
- โ Shows message: "No candidates found"
- โ Clear Filters button visible
- โ Click clear โ navigates to base candidates list
Test Case 17: Hiring Manager Display
Setup:
- Candidate assigned to Manager "John Doe" (john@example.com)
Expected:
- โ
"Assigned Manager" column shows:
- Name: "John Doe"
- Email: "john@example.com" (smaller text)
- โ Null when not assigned (shows "-")
Test Case 18: URL Parameters Persist
Setup:
- Apply filters: status=NEW, applyingFor="Engineer"
- Refresh page
- Navigate to candidate detail โ back
Expected:
- โ Filters persist after page refresh
- โ Same filters active when returning from detail page
- โ URL reflects all active filters
Test Case 19: Multiple Filters Combined
Setup:
- Apply multiple filters:
- Status: MANAGER_EVAL_PENDING
- Position: "Senior Engineer"
- Manager: "Jane Smith"
- Overdue: checked
Expected:
- โ All filters apply together (AND logic)
- โ URL includes all params
- โ Results show only candidates matching ALL filters
- โ Clear filters resets all
Test Case 20: RBAC Enforcement - Manager Cannot See Other Candidates
Setup:
- 2 managers: Manager A and Manager B
- Manager A has candidates assigned
- Manager B has different candidates assigned
Actions:
- Login as Manager A
- Navigate
/candidates
Expected:
- โ Only Manager A's assigned candidates visible
- โ Cannot see Manager B's candidates
- โ API returns 403 or filters server-side
๐ Additional Notes
- Default sort: updatedAt DESC
- Page size: 20 (configurable up to 100)
- Search is case-insensitive
- Overdue calculation is client-side after fetch (based on computed ageInStatusDays)
- Saved views are pre-defined; users cannot create custom views in W11
- No analytics or heavy aggregations; simple list only