Handover workspace

ERS, Todo, OfferReview, and Docu in one view

Imported from live server docs, code structure, and deployment notes.

Apr 3, 2026, 12:38 PM

OfferReview

W11 Candidate List Implementation

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.

W11-IMPLEMENTATION.md

Updated Feb 19, 2026, 6:59 AM

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

  1. 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:

  • Candidate model with statusUpdatedAt (added in W9/W10)
  • CandidateStatus enum with all statuses
  • User model (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

RoleCan SeeFilters
HRAll candidatesBy status, position, manager, date
ManagerOnly assigned to selfBy status, position, date
SMOTO_SMO + decided (APPROVED/REJECTED/KIV)By status, date
AdminAll candidatesBy 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:

  1. Login as HR user
  2. 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:

  1. Ensure Manager user has assigned candidates in MANAGER_EVAL_PENDING
  2. Login as Manager
  3. 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:

  1. Ensure candidates exist with status=TO_SMO
  2. Login as SMO user
  3. 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:

  1. Login as Admin user
  2. 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:

  1. Multiple candidates exist
  2. Login as any user
  3. Navigate /candidates

Actions:

  1. Type candidate name in search box
  2. Type candidate code in search box
  3. 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:

  1. Candidates exist with multiple statuses
  2. Login as HR user
  3. Navigate /candidates

Actions:

  1. Select status=NEW from filter
  2. 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:

  1. Candidates exist with different positions
  2. Login as any user

Actions:

  1. Select position from dropdown
  2. Click "Apply Filters"

Expected:

  • โœ… Table filters by applyingFor
  • โœ… URL includes applyingFor param
  • โœ… Results update

Test Case 8: Manager Filter

Setup:

  1. Candidates assigned to different managers
  2. Login as HR user

Actions:

  1. Select manager from dropdown
  2. Click "Apply Filters"

Expected:

  • โœ… Table shows only candidates assigned to selected manager
  • โœ… URL includes hiringManagerId param

Test Case 9: Date Range Filter

Setup:

  1. Candidates created at different dates
  2. Login as any user

Actions:

  1. Set "Created From" date
  2. Set "Created To" date
  3. 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:

  1. Candidates with various statuses, some older than thresholds:
    • NEW > 2 days old
    • HR_SCREENED > 2 days old
    • MANAGER_EVAL_PENDING > 3 days old

Actions:

  1. Check "Overdue Only"
  2. 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:

  1. More than 20 candidates exist
  2. Login as Admin

Actions:

  1. First page loads
  2. Click "Next"
  3. Verify page shows items 21-40
  4. 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:

  1. Candidates list displayed
  2. Login as any user

Actions:

  1. Click on a candidate row
  2. 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:

  1. Candidate created/transitioned to status 5 days ago
  2. 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:

  1. 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:

  1. 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:

  1. Login as Manager with no assigned candidates
  2. 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:

  1. Candidate assigned to Manager "John Doe" (john@example.com)

Expected:

  • โœ… "Assigned Manager" column shows:
  • โœ… Null when not assigned (shows "-")

Test Case 18: URL Parameters Persist

Setup:

  1. Apply filters: status=NEW, applyingFor="Engineer"
  2. Refresh page
  3. 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:

  1. 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:

  1. 2 managers: Manager A and Manager B
  2. Manager A has candidates assigned
  3. Manager B has different candidates assigned

Actions:

  1. Login as Manager A
  2. 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