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

W7 Implementation: Candidate Intake & HR Screening + Assign Hiring Manager

**Status**: ✅ Complete (Backend + UI) **Version**: 1.0 **Date**: January 26, 2025

docs/W7-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.

W7 Implementation: Candidate Intake & HR Screening + Assign Hiring Manager

Status: ✅ Complete (Backend + UI)
Version: 1.0
Date: January 26, 2025


Table of Contents

  1. Overview
  2. Architecture
  3. Database Schema
  4. API Endpoints
  5. Validation Rules
  6. Frontend Components
  7. Quick Screen Questions
  8. RBAC & Security
  9. Audit Logging
  10. Manual Testing Checklist

Overview

W7 implements the HR screening workflow for candidates after resume upload (W6). The flow enables:

  1. Candidate Intake: HR edits candidate snapshot (fullName, email, phone, source, notes)
  2. HR Due Diligence: Quick screening with 8 rating questions (1-5 scale), conditional comments for low ratings, red flags flag, and summary notes
  3. HR Outcome Gate: Pass/KIV/Reject decision with conditional notes (required for KIV/REJECT)
  4. Hiring Manager Assignment: After HR screening complete, assign a manager to transition candidate to manager evaluation

Key Design Principle: Two-step status progression with completion gates

  • Step 1: Complete HR screening → Status: NEW → HR_SCREENED
  • Step 2: Assign manager → Status: HR_SCREENED → MANAGER_EVAL_PENDING

Architecture

Directory Structure

src/
├── app/
│   ├── api/
│   │   └── candidates/
│   │       ├── route.ts                              (W6: create candidate)
│   │       └── [id]/
│   │           ├── route.ts                          (W7: GET detail, PATCH snapshot)
│   │           ├── hr-screening/
│   │           │   ├── route.ts                      (W7: GET draft, PUT save draft)
│   │           │   └── complete/
│   │           │       └── route.ts                  (W7: POST complete screening)
│   │           └── assign-hiring-manager/
│   │               └── route.ts                      (W7: POST assign manager)
│   └── (app)/
│       └── candidates/
│           └── [id]/
│               ├── page.tsx                          (W7: Detail page shell + tabs)
│               └── _tabs/
│                   └── HrScreeningTab.tsx            (W7: 4 sections for HR screening)
├── lib/
│   ├── validation/
│   │   └── schemas.ts                                (W7: +3 validation schemas)
│   └── prisma.ts                                     (existing)
└── prisma/
    └── schema.prisma                                 (W7: +HrScreening model, +enums)

Data Flow

Candidate Detail Page (page.tsx)
  └─> HrScreeningTab Component
       ├─ Section A: Snapshot (editable)
       │   └─> PATCH /api/candidates/{id}
       ├─ Section B: Quick Screen (8 questions, rating + comments)
       │   └─> PUT /api/candidates/{id}/hr-screening (draft)
       │   └─> POST /api/candidates/{id}/hr-screening/complete
       ├─ Section C: Outcome Gate (Pass/KIV/Reject + notes)
       │   └─> POST /api/candidates/{id}/hr-screening/complete
       └─ Section D: Assign Manager (locked until complete)
           └─> POST /api/candidates/{id}/assign-hiring-manager

Database Schema

New Enums

enum HrScreeningMode {
  QUICK  // V1: Basic 8-question template (future: W9 will add CUSTOMIZED)
}

enum HrOutcome {
  PASS    // Proceed to manager evaluation
  KIV     // Keep in view - rejected but valuable for future
  REJECT  // Not moving forward
}

New Model: HrScreening

model HrScreening {
  id                String              @id @default(cuid())
  candidateId       String              @unique
  candidate         Candidate           @relation(fields: [candidateId], references: [id], onDelete: Cascade)
  
  // Screening content
  mode              HrScreeningMode     @default(QUICK)
  responsesJson     String              @db.Text  // JSON: [{questionId, rating, comment}]
  outcome           HrOutcome?          // NULL while draft, required at completion
  notes             String?             // Conditional: required if KIV or REJECT
  completedAt       DateTime?           // NULL while draft, set on completion
  
  // Audit
  createdByUserId   String?
  createdBy         User?               @relation("ScreenedBy", fields: [createdByUserId], references: [id], onDelete: SetNull)
  createdAt         DateTime            @default(now())
  updatedAt         DateTime            @updatedAt
  
  @@index([candidateId])
  @@index([createdByUserId])
  @@index([completedAt])
}

Enhanced Candidate Model

model Candidate {
  // ... existing fields ...
  
  // W7 additions
  email             String?             // HR can edit
  phone             String?             // HR can edit
  source            String?             // LinkedIn, Referral, etc. (HR editable)
  notes             String?             // Internal notes (HR editable)
  hiringManagerId   String?             // Assigned after screening complete
  hiringManager     User?               @relation("CandidateHiringManager", fields: [hiringManagerId], references: [id], onDelete: SetNull)
  hrScreening       HrScreening?        // One-to-one relationship
}

Enhanced User Model

model User {
  // ... existing fields ...
  
  // W7 additions
  hiringCandidates  Candidate[]         @relation("CandidateHiringManager")
  hrScreenings      HrScreening[]       @relation("ScreenedBy")
}

New Audit Event Types

enum AuditEventType {
  // ... existing ...
  CANDIDATE_UPDATED                    // Snapshot edited
  HR_SCREENING_DRAFT_SAVED             // Draft questions/outcome saved
  HR_SCREENING_COMPLETED               // Screening marked complete
  HIRING_MANAGER_ASSIGNED              // Manager assigned & status changed
}

API Endpoints

1. GET /api/candidates/[id] - Fetch Candidate Detail

Purpose: Retrieve candidate with HR screening and hiring manager info

Method: GET

Path: /api/candidates/{candidateId}

RBAC: HR, ADMIN, MANAGER, SMO (read-only)

Response (200 OK):

{
  "id": "cm...",
  "candidateCode": "HR-001",
  "fullName": "John Doe",
  "applyingFor": "Senior Engineer",
  "email": "john@example.com",
  "phone": "+1-555-0100",
  "source": "LinkedIn",
  "notes": "Referred by Sarah",
  "status": "NEW",
  "hiringManagerId": null,
  "hiringManager": null,
  "createdByUserId": "...",
  "resumeUrl": "...",
  "createdAt": "2025-01-23T10:00:00Z"
}

Error Responses:

  • 401: Unauthorized (not logged in)
  • 403: Forbidden (insufficient role)
  • 404: Candidate not found

2. PATCH /api/candidates/[id] - Update Candidate Snapshot

Purpose: Edit candidate snapshot fields (HR only)

Method: PATCH

Path: /api/candidates/{candidateId}

RBAC: HR, ADMIN only

Request Body:

{
  "fullName": "John Doe",
  "email": "john@example.com",
  "phone": "+1-555-0100",
  "source": "LinkedIn",
  "notes": "Internal notes about candidate"
}

Validation:

  • All fields optional (partial update support)
  • Uses candidateSnapshotSchema from Zod

Response (200 OK): Updated candidate object (same as GET)

Behavior:

  • Only provided fields are updated (null values ignored)
  • Logs CANDIDATE_UPDATED event with change diff
  • Tracks: old value → new value for each changed field
  • Disabled after HR screening complete (frontend UX gate)

Error Responses:

  • 400: Validation failed
  • 401: Unauthorized
  • 403: Forbidden (not HR/ADMIN)
  • 404: Candidate not found

3. GET /api/candidates/[id]/hr-screening - Fetch Draft Screening

Purpose: Retrieve draft or completed screening

Method: GET

Path: /api/candidates/{candidateId}/hr-screening

RBAC: HR, ADMIN only

Response (200 OK):

{
  "id": "hrs-...",
  "candidateId": "cm...",
  "mode": "QUICK",
  "responses": [
    {
      "questionId": "q1",
      "rating": 4,
      "comment": null
    },
    {
      "questionId": "q2",
      "rating": 2,
      "comment": "Communication could be clearer in technical discussions"
    }
  ],
  "outcome": null,  // null while draft, set at completion
  "notes": null,
  "completedAt": null,
  "createdAt": "2025-01-23T10:15:00Z",
  "updatedAt": "2025-01-23T10:20:00Z"
}

Note: If no screening exists yet, returns null

Error Responses:

  • 401: Unauthorized
  • 403: Forbidden (not HR/ADMIN)
  • 404: Candidate not found

4. PUT /api/candidates/[id]/hr-screening - Save Draft Screening

Purpose: Save draft screening (allows partial state, no outcome validation)

Method: PUT

Path: /api/candidates/{candidateId}/hr-screening

RBAC: HR, ADMIN only

Request Body:

{
  "responses": [
    {
      "questionId": "q1",
      "rating": 4,
      "comment": null
    },
    {
      "questionId": "q2",
      "rating": 2,
      "comment": "Communication could be clearer"
    }
  ],
  "outcome": null,      // Optional for draft
  "notes": null         // Optional for draft
}

Validation:

  • No validation on outcome/notes (draft can be incomplete)
  • Response ratings: 1-5 only
  • Responses array format validated

Response (200 OK): Updated screening object with parsed responses

Behavior:

  • Creates HrScreening if not exists
  • Updates existing screening if exists
  • Logs HR_SCREENING_DRAFT_SAVED event
  • Stores responses as JSON in database

Error Responses:

  • 400: Validation failed
  • 401: Unauthorized
  • 403: Forbidden (not HR/ADMIN)
  • 404: Candidate not found

5. POST /api/candidates/[id]/hr-screening/complete - Complete Screening

Purpose: Mark screening complete with full validation (status transition)

Method: POST

Path: /api/candidates/{candidateId}/hr-screening/complete

RBAC: HR, ADMIN only

Request Body:

{
  "responses": [
    {
      "questionId": "q1",
      "rating": 4,
      "comment": null
    },
    {
      "questionId": "q2",
      "rating": 2,
      "comment": "Communication could be clearer"
    }
  ],
  "outcome": "PASS",      // Required: PASS, KIV, or REJECT
  "notes": null           // Conditional: required if KIV or REJECT
}

Validation (Strict - all errors block completion):

  1. Candidate Fields:

    • fullName present (error: "Full name is required")
    • applyingFor present (error: "Position is required")
  2. Outcome:

    • ✅ outcome provided (error: "Outcome is required")
    • ✅ outcome is PASS | KIV | REJECT (error on invalid value)
  3. Conditional Notes:

    • ✅ If outcome is KIV or REJECT: notes must be non-empty (error: "Notes required for this outcome")
  4. Conditional Comments:

    • ✅ For all responses with rating ≤2: comment must be non-empty
    • Error: "Please add comments for all ratings of 2 or lower"

Response (200 OK):

{
  "screening": {
    "id": "hrs-...",
    "candidateId": "cm...",
    "mode": "QUICK",
    "responses": [...],
    "outcome": "PASS",
    "notes": null,
    "completedAt": "2025-01-23T11:30:00Z"
  },
  "candidate": {
    "id": "cm...",
    "status": "HR_SCREENED",  // Transitioned from NEW
    "...": "..."
  }
}

Behavior:

  • Creates or updates HrScreening
  • Sets completedAt timestamp (audit trail)
  • Status Transition: Candidate.status NEW → HR_SCREENED
  • Logs HR_SCREENING_COMPLETED event
  • Logs CANDIDATE_STATUS_CHANGED event (dual logging)
  • JSON responses persisted to database as JSON string

Error Responses:

  • 400: Validation failed (with specific error message)
  • 401: Unauthorized
  • 403: Forbidden (not HR/ADMIN)
  • 404: Candidate not found
  • 409: Conflict (e.g., invalid status for transition)

6. POST /api/candidates/[id]/assign-hiring-manager - Assign Manager

Purpose: Assign hiring manager after screening complete (status transition)

Method: POST

Path: /api/candidates/{candidateId}/assign-hiring-manager

RBAC: HR, ADMIN only

Request Body:

{
  "hiringManagerId": "user-cuid-123"
}

Validation:

  1. Status Check: Candidate must be in HR_SCREENED status
    • Error (409): "Candidate must be in HR_SCREENED status"
  2. Manager Exists: User with hiringManagerId must exist
    • Error (404): "Manager not found"
  3. Manager Role: User must have role = MANAGER
    • Error (400): "Selected user is not a manager"

Response (200 OK):

{
  "id": "cm...",
  "candidateCode": "HR-001",
  "fullName": "John Doe",
  "status": "MANAGER_EVAL_PENDING",  // Transitioned from HR_SCREENED
  "hiringManagerId": "user-...",
  "hiringManager": {
    "id": "user-...",
    "fullName": "Alice Johnson",
    "email": "alice@company.com"
  },
  "...": "..."
}

Behavior:

  • Status Transition: Candidate.status HR_SCREENED → MANAGER_EVAL_PENDING
  • Updates Candidate.hiringManagerId
  • Logs HIRING_MANAGER_ASSIGNED event with manager name/email
  • Marks UI Section D as complete

Error Responses:

  • 400: Validation failed
  • 401: Unauthorized
  • 403: Forbidden (not HR/ADMIN)
  • 404: Candidate or manager not found
  • 409: Conflict (candidate not in HR_SCREENED status)

Validation Rules

Zod Schemas

candidateSnapshotSchema

{
  fullName?: string,
  email?: string,
  phone?: string,
  source?: string,
  notes?: string
}
  • All optional (partial update)
  • No regex validation (allow any format)

hrScreeningResponseSchema

{
  questionId: string (cuid),
  rating: number (1-5),
  comment?: string
}

hrScreeningSchema

{
  responses: hrScreeningResponseSchema[],
  outcome?: HrOutcome,
  notes?: string
}

Refinements (Zod .refine() checks):

  1. Notes Required for KIV/REJECT:

    .refine(
      (data) => {
        if (data.outcome === 'KIV' || data.outcome === 'REJECT') {
          return data.notes && data.notes.trim().length > 0;
        }
        return true;
      },
      { message: 'Notes are required for KIV or REJECT outcome' }
    )
    
  2. Comments Required for Low Ratings:

    .refine(
      (data) => {
        return data.responses.every((r) => {
          if (r.rating <= 2) {
            return r.comment && r.comment.trim().length > 0;
          }
          return true;
        });
      },
      { message: 'Comments required for all ratings ≤2' }
    )
    

assignHiringManagerSchema

{
  hiringManagerId: string (cuid)
}

Frontend Components

Candidate Detail Page (src/app/(app)/candidates/[id]/page.tsx)

Type: Client component ('use client')

Features:

  • Sticky header with candidate name, code, position, and status badge
  • Color-coded status badges:
    • NEW: Blue
    • HR_SCREENED: Green
    • MANAGER_EVAL_PENDING: Yellow
    • Other: Gray
  • Tab navigation (currently: HR Screening)
  • Fetches candidate on mount via GET /api/candidates/{id}
  • Passes candidate to child tabs via props + update callback

Props to HrScreeningTab:

candidate: Candidate  // Full candidate object
onCandidateUpdate: (updated: Candidate) => void

HR Screening Tab (src/app/(app)/candidates/[id]/_tabs/HrScreeningTab.tsx)

Type: Client component ('use client')

State Management:

  • snapshotData: Form state for candidate fields
  • screening: Fetched HrScreening object or null
  • responses: Map of questionId → {rating, comment}
  • outcome: Selected outcome (PASS/KIV/REJECT)
  • outcomeNotes: Notes textarea value
  • managers: List of manager options (placeholder for now)
  • expandedSections: Toggle state for 4 sections
  • saving: Loading state during API calls

Architecture: Four expandable sections

Section A: Candidate Snapshot (Editable)

Fields:

  • Full Name (required for completion)
  • Email (optional)
  • Phone (optional)
  • Source (optional, placeholder text)
  • Internal Notes (optional)

Actions:

  • Save Snapshot button → PATCH /api/candidates/{id}
  • Disabled after screening complete (UI gate only)

Validation: All optional on frontend (backend validates on completion)

Section B: HR Due Diligence Quick Screen

8 Questions (hardcoded for V1):

  1. Technical Skills Match
  2. Communication Skills
  3. Problem Solving Ability
  4. Cultural Fit
  5. Experience Level
  6. Growth Potential
  7. Team Collaboration
  8. Overall Impression

Rating Interface:

  • 5 buttons (1-5) per question
  • Selected rating highlighted in blue
  • Non-selected in gray

Conditional Comments:

  • If rating ≤2, comment textarea appears (red border, red background)
  • Label: "Comment (required for ratings ≤2)"
  • Placeholder: "Explain the low rating..."

Additional Fields:

  • Red flags checkbox
  • Summary notes textarea

Actions:

  • Save Draft button → PUT /api/candidates/{id}/hr-screening
  • Disabled after screening complete

Validation: None (draft can be incomplete)

Section C: HR Outcome & Completion Gate

Outcome Dropdown:

  • "Select outcome..." (placeholder)
  • "Pass - Proceed to manager evaluation"
  • "KIV - Keep in view for future opportunities"
  • "Reject - Not moving forward"

Conditional Notes:

  • Only shown if outcome is KIV or REJECT
  • Label: "Notes *" (red asterisk)
  • Placeholder: "Explain the decision..."
  • Red border if outcome selected

Mark Complete Button:

  • Label: "Mark HR Screening Complete"
  • Only shown if not completed
  • Green background
  • Triggers POST /api/candidates/{id}/hr-screening/complete

Validation (before submit):

  1. fullName present → Toast: "Full name is required"
  2. applyingFor present → Toast: "Position is required"
  3. outcome selected → Toast: "Outcome is required"
  4. If KIV/REJECT: notes non-empty → Toast: "Notes required for this outcome"
  5. For each rating ≤2: comment non-empty → Toast: "Please add comments for all ratings of 2 or lower"

Completion State:

  • After successful completion, show green info box: "Screening Locked - This screening has been completed. Section D is now available for hiring manager assignment."
  • Snapshot, screening, outcome become disabled (read-only)

Section D: Assign Hiring Manager

Lock State:

  • Header button disabled if screening not completed
  • Shows badge: "Locked until screening complete"
  • Shows badge: "Assigned" if manager already assigned

Manager Selection (only shown if not completed AND screening complete):

  • Dropdown: "Choose a manager..."
  • Options: Populated from API (currently placeholder list)

Assign Button:

  • Label: "Assign & Send for Evaluation"
  • Purple background
  • Disabled until manager selected
  • Triggers POST /api/candidates/{id}/assign-hiring-manager

Completion State:

  • After successful assignment, show blue info box with assigned manager name and email
  • Prevents re-assignment (button hidden)

UI/UX Patterns

Section Toggles:

  • Click section header to expand/collapse
  • Chevron icon rotates on toggle
  • Sections A, B default expanded; C, D default expanded

Loading States:

  • Button label changes: "Saving..." → "Completing..." → "Assigning..."
  • Button disabled during submission

Toast Notifications:

  • Success: "Snapshot saved", "Draft saved", "Screening completed", "Manager assigned"
  • Error: "Failed to save", "Failed to complete", specific validation errors
  • Uses simple alert() for now (can replace with Sonner later)

Disabled Fields:

  • After screening complete:
    • Snapshot fields: disabled, gray background, cursor: not-allowed
    • Quick screen fields: disabled
    • Outcome dropdown: disabled (value shown read-only)
    • Draft button: disabled

Visual Hierarchy:

  • Sticky header: 30px top, z-index 10
  • Sections: white bg, gray border, 6px rounded
  • Buttons: padding-4 py-2, rounded-md, clear color scheme
  • Form inputs: gray border, focus ring blue

Quick Screen Questions

V1 Template (hardcoded in HrScreeningTab):

  1. Technical Skills Match

    • Rating: 1-5
    • Purpose: Does the candidate have the required technical skills?
  2. Communication Skills

    • Rating: 1-5
    • Purpose: Can they communicate clearly and effectively?
  3. Problem Solving Ability

    • Rating: 1-5
    • Purpose: Do they demonstrate strong analytical/problem-solving skills?
  4. Cultural Fit

    • Rating: 1-5
    • Purpose: Do they align with company values and team culture?
  5. Experience Level

    • Rating: 1-5
    • Purpose: Is their experience appropriate for the role level?
  6. Growth Potential

    • Rating: 1-5
    • Purpose: Do they show potential for growth and learning?
  7. Team Collaboration

    • Rating: 1-5
    • Purpose: Can they work effectively with team members?
  8. Overall Impression

    • Rating: 1-5
    • Purpose: Overall, would you recommend this candidate?

Rating Scale Interpretation:

  • 5: Excellent / Highly recommended
  • 4: Good / Recommended
  • 3: Adequate / Acceptable
  • 2: Below average / Concerns (requires comment)
  • 1: Poor / Major concerns (requires comment)

Future (W9): Template engine will allow custom questions per role


RBAC & Security

Role Permissions

RoleGET CandidatePATCH SnapshotGET/PUT DraftPOST CompletePOST Assign Manager
HR
ADMIN
MANAGER✅ (read)
SMO✅ (read)
USER

Enforcement Points

Backend (API routes):

  • requireRole(['HR', 'ADMIN']) on all edit endpoints
  • requireRole(['HR', 'ADMIN', 'MANAGER', 'SMO']) on GET detail
  • Status validation: Manager assignment only works if status === 'HR_SCREENED'

Frontend (UI component):

  • Assumes current user is HR (no role check in component - add if needed)
  • Disables edit fields based on screening completion status
  • Section D locked until screening complete

Data Isolation

  • HR can only view/edit their own candidates (currently not enforced - add if needed)
  • Managers can view assigned candidates' screening results (future feature)

Audit Logging

Event Types

1. CANDIDATE_UPDATED

  • Trigger: PATCH /api/candidates/{id} on snapshot change
  • Metadata:
    {
      "candidateId": "...",
      "changes": {
        "fullName": { "old": "Jane", "new": "Jane Doe" },
        "email": { "old": null, "new": "jane@example.com" },
        "notes": { "old": "Initial notes", "new": "Updated notes" }
      }
    }
    
  • Actor: HR/ADMIN user performing edit
  • Timestamp: Automatic

2. HR_SCREENING_DRAFT_SAVED

  • Trigger: PUT /api/candidates/{id}/hr-screening on draft save
  • Metadata:
    {
      "candidateId": "...",
      "responses": 5,  // number of questions answered
      "outcome": null,  // null if draft
      "completedAt": null
    }
    
  • Actor: HR/ADMIN user
  • Timestamp: Automatic

3. HR_SCREENING_COMPLETED

  • Trigger: POST /api/candidates/{id}/hr-screening/complete on completion
  • Metadata:
    {
      "candidateId": "...",
      "outcome": "PASS",
      "responses": 8,
      "completedAt": "2025-01-23T11:30:00Z"
    }
    
  • Actor: HR/ADMIN user
  • Timestamp: Automatic

4. CANDIDATE_STATUS_CHANGED

  • Trigger: Auto-logged by POST /api/candidates/{id}/hr-screening/complete
  • Metadata:
    {
      "candidateId": "...",
      "oldStatus": "NEW",
      "newStatus": "HR_SCREENED",
      "trigger": "screening_completed"
    }
    
  • Actor: HR/ADMIN user
  • Timestamp: Automatic

5. HIRING_MANAGER_ASSIGNED

  • Trigger: POST /api/candidates/{id}/assign-hiring-manager on assignment
  • Metadata:
    {
      "candidateId": "...",
      "managerId": "...",
      "managerName": "Alice Johnson",
      "managerEmail": "alice@company.com"
    }
    
  • Actor: HR/ADMIN user
  • Timestamp: Automatic

Audit Trail Query Example

// Find all edits to a candidate
const trail = await prisma.auditEvent.findMany({
  where: {
    metadata: {
      path: ['candidateId'],
      equals: 'cm-...'
    }
  },
  orderBy: { createdAt: 'desc' }
});

// Result includes: CANDIDATE_UPDATED, HR_SCREENING_DRAFT_SAVED, 
// HR_SCREENING_COMPLETED, CANDIDATE_STATUS_CHANGED, HIRING_MANAGER_ASSIGNED

Manual Testing Checklist

Pre-flight

  • Database connection available (Postgres running)
  • Prisma migrations applied: npx prisma migrate dev
  • API server running: npm run dev
  • Logged in as HR user
  • Have at least 1 candidate from W6 (resume upload)
  • Have at least 2 MANAGER role users created

Test Case 1: Edit Candidate Snapshot

Setup: Navigate to /candidates/{candidateId}

Steps:

  1. Verify sticky header shows candidate name, code, position, and status badge (blue "NEW")
  2. Click Section A to expand "Candidate Snapshot"
  3. Update Full Name field
  4. Update Email and Phone fields
  5. Select Source: "LinkedIn"
  6. Add Internal Notes: "Strong technical background"
  7. Click "Save Snapshot"
  8. Verify toast: "Snapshot saved"
  9. Refresh page, verify changes persisted

Expected:

  • Form fields editable, not disabled
  • Save button active
  • Audit log shows CANDIDATE_UPDATED with all field changes

Test Case 2: Save HR Screening Draft

Setup: Continue from Test Case 1

Steps:

  1. Click Section B to expand "HR Due Diligence Quick Screen"
  2. Rate "Technical Skills Match" as 5
  3. Rate "Communication Skills" as 2
  4. Verify comment textarea appears for rating 2
  5. Add comment: "Needs improvement in presentations"
  6. Rate remaining questions (3-5, choose various ratings)
  7. Check "Red flags identified"
  8. Add Summary Notes: "Overall strong candidate with communication coaching needed"
  9. Click "Save Draft"
  10. Verify toast: "Draft saved"
  11. Refresh page, verify all responses + red flags + summary still present

Expected:

  • Save Draft button active (screening not complete yet)
  • Comments required and shown only for ratings ≤2
  • Audit log shows HR_SCREENING_DRAFT_SAVED event

Test Case 3: Complete HR Screening (Happy Path - PASS)

Setup: Continue from Test Case 2

Steps:

  1. Verify Section C "HR Outcome & Completion Gate" is visible
  2. Select Outcome: "Pass - Proceed to manager evaluation"
  3. Verify notes field NOT shown (PASS doesn't require notes)
  4. Click "Mark HR Screening Complete"
  5. Verify toast: "Screening completed successfully"
  6. Verify status badge changed to green "HR_SCREENED"
  7. Verify Section A, B, C fields now disabled (gray background, no input)
  8. Verify green info box: "Screening Locked..."
  9. Verify Section D now unlocked

Expected:

  • Section D lock removed
  • Status badge updated to HR_SCREENED
  • Audit log shows both HR_SCREENING_COMPLETED and CANDIDATE_STATUS_CHANGED events
  • Snapshot and screening fields read-only

Test Case 4: Complete HR Screening (KIV with Notes)

Setup: New candidate, repeat Test Cases 1-2 but with different ratings

Steps:

  1. In Section B, rate questions lower (3-4 range)
  2. Save Draft
  3. In Section C, select Outcome: "KIV - Keep in view..."
  4. Verify notes field NOW SHOWN (red label with asterisk)
  5. Leave notes empty and click "Mark Complete"
  6. Verify toast error: "Notes are required for this outcome"
  7. Add notes: "Strong potential but needs more experience with distributed systems"
  8. Click "Mark Complete"
  9. Verify toast: "Screening completed successfully"
  10. Verify status changed to HR_SCREENED

Expected:

  • Notes required conditional validation works
  • Cannot complete without notes
  • Status transitions to HR_SCREENED
  • Audit log shows correct outcome in metadata

Test Case 5: Low Rating Comments Validation

Setup: New candidate

Steps:

  1. In Section B, rate "Technical Skills" as 1
  2. Verify comment textarea appears immediately
  3. Don't add comment, save draft
  4. Try to complete without comment
  5. Verify toast error: "Please add comments for all ratings of 2 or lower"
  6. Add comment: "Lacks foundational knowledge in required technologies"
  7. Try to complete again
  8. Verify completion succeeds with comment present

Expected:

  • Comments required for rating ≤2 enforced
  • Cannot complete without comments
  • Comments accepted with ratings 3-5 (ignored if present)

Test Case 6: Assign Hiring Manager (Happy Path)

Setup: Candidate with HR_SCREENED status (from Test Case 3)

Steps:

  1. Verify Section D locked state removed
  2. Click Section D to expand "Assign Hiring Manager"
  3. Verify manager dropdown with options
  4. Select first manager from dropdown
  5. Click "Assign & Send for Evaluation"
  6. Verify toast: "Manager assigned successfully"
  7. Verify status badge changed to yellow "MANAGER_EVAL_PENDING"
  8. Verify blue info box shows: "Manager Assigned: [Name] ([Email])"
  9. Verify button hidden (assignment locked)

Expected:

  • Status transitions to MANAGER_EVAL_PENDING
  • Manager info persisted in database
  • Audit log shows HIRING_MANAGER_ASSIGNED event
  • Cannot re-assign (button disabled)

Test Case 7: Assign Manager Without Screening (Error)

Setup: Candidate with NEW status

Steps:

  1. Try to navigate to candidate detail for NEW status candidate
  2. Verify Section D shows "Locked until screening complete" badge
  3. Click Section D button (should be disabled)
  4. Manually call API: POST /api/candidates/{id}/assign-hiring-manager
  5. Verify error response 409: "Candidate must be in HR_SCREENED status"

Expected:

  • Assignment gated by status = HR_SCREENED
  • API enforces, not just UI
  • Clear error message

Test Case 8: Assign Invalid Manager (Error)

Setup: Candidate with HR_SCREENED status, created USER (not MANAGER)

Steps:

  1. Manually call API: POST /api/candidates/{id}/assign-hiring-manager with user ID
  2. Verify error 400: "Selected user is not a manager"
  3. Verify candidate status unchanged
  4. Verify no audit event created

Expected:

  • Role validation enforced
  • Only MANAGER role users can be assigned
  • Clear error message

Test Case 9: Audit Trail Verification

Setup: Complete Test Cases 1-6

Steps:

  1. Query audit logs for candidate: GET /api/admin/audit?candidateId={id}
  2. Verify events in order:
    • CANDIDATE_UPDATED (snapshot edits)
    • HR_SCREENING_DRAFT_SAVED (at least 1)
    • HR_SCREENING_COMPLETED
    • CANDIDATE_STATUS_CHANGED (NEW → HR_SCREENED)
    • HIRING_MANAGER_ASSIGNED
  3. Verify metadata contains expected details for each event
  4. Verify actor user ID matches logged-in HR user
  5. Verify timestamps in ascending order

Expected:

  • Complete audit trail
  • Metadata matches what was changed
  • Non-blocking logging (didn't affect any operations)

Test Case 10: RBAC - Manager Reads Only

Setup: Candidate assigned to Manager A, logged in as Manager A

Steps:

  1. Navigate to /candidates/{id}
  2. Verify page loads (read permission granted)
  3. Verify Section A snapshot fields disabled (read-only)
  4. Verify "Save Snapshot" button hidden
  5. Verify Section B fields disabled
  6. Verify Section C fields disabled
  7. Verify Section D button disabled
  8. Manually call PATCH API
  9. Verify error 403: "Forbidden"

Expected:

  • Managers can view candidate details
  • Managers cannot edit any fields
  • Managers cannot complete screening
  • Managers cannot assign managers
  • Clear permission enforcement

Test Case 11: Filtering & Display

Setup: Multiple candidates at different stages (NEW, HR_SCREENED, MANAGER_EVAL_PENDING)

Steps:

  1. Navigate to each candidate
  2. Verify status badge color correct:
    • NEW: Blue
    • HR_SCREENED: Green
    • MANAGER_EVAL_PENDING: Yellow
  3. Verify locking/unlocking of sections matches status
  4. Verify disabled/enabled states correct per status

Expected:

  • Clear visual status indicators
  • Correct locking behavior per status
  • Intuitive workflow progression

Test Case 12: Error Handling

Setup: Various error scenarios

Steps:

  1. Try to complete screening without fullName: Verify error toast
  2. Try to complete screening without applyingFor: Verify error toast
  3. Try to complete screening without outcome: Verify error toast
  4. Try to complete KIV without notes: Verify error toast
  5. Try to save with invalid manager ID: Verify error response

Expected:

  • Clear, actionable error messages
  • User can correct and retry
  • No silent failures

Post-Flight Checks

  • All status transitions working (NEW → HR_SCREENED → MANAGER_EVAL_PENDING)
  • Audit logs complete and accurate
  • Database constraints enforced (unique HrScreening per Candidate)
  • No console errors
  • No TypeScript compilation errors
  • Performance acceptable (< 1s for API calls)

Deployment Notes

Pre-Deployment Checklist

  1. Database Migration:

    npx prisma migrate deploy --skip-generate
    # Or for development:
    npx prisma migrate dev --name add_hr_screening
    
  2. Prisma Client Generation:

    npx prisma generate
    
  3. Environment Variables:

    • Verify DATABASE_URL set correctly
    • No hardcoded credentials in code
  4. Type Checking:

    npx tsc --noEmit
    
  5. Build:

    npm run build
    

Known Limitations (V1)

  • Quick screen questions hardcoded (future: W9 template engine)
  • Manager list populated from placeholder (future: API endpoint)
  • No real-time notifications (future: WebSocket)
  • No bulk operations (future: batch screening)
  • No conditional questions (future: skip logic)

Future Enhancements (W8+)

  • W8: Manager evaluation workflow + scoring
  • W9: Custom screening templates per role
  • W10: Bulk candidate operations
  • W11: Integration with offer creation
  • W12: Reporting & analytics dashboards

Summary

W7 successfully implements the HR screening workflow with:

Complete Backend: 4 API endpoints with validation, RBAC, audit logging
Complete Frontend: 2 React components (detail page + tab) with 4-section UI
Database Schema: HrScreening model + enums + relationships
Validation: 3 Zod schemas with conditional refinements
Status Progression: NEW → HR_SCREENED → MANAGER_EVAL_PENDING
Audit Trail: 4 new event types with detailed metadata
Security: RBAC enforcement on all edit endpoints
Documentation: Complete spec with test checklist

Status: Production ready pending Postgres database availability