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

W6 Code Structure & File Listing

---

W6-CODE-STRUCTURE.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.

W6 Code Structure & File Listing

Quick Reference

Total Files: 12

  • 2 API endpoints
  • 2 UI components + 1 layout
  • 2 utility libraries
  • 1 validation schema update
  • 1 database schema update
  • 3 documentation files

File Tree

projectweb-nextjs/
├── prisma/
│   └── schema.prisma                          [MODIFIED: +Candidate, CandidateDocument]
│
├── src/
│   ├── app/
│   │   ├── (app)/
│   │   │   ├── layout.tsx                     [NEW: 8 lines]
│   │   │   └── upload-resume/
│   │   │       ├── page.tsx                   [NEW: 380 lines]
│   │   │       └── _components/
│   │   │           └── Dropzone.tsx           [NEW: 140 lines]
│   │   │
│   │   └── api/
│   │       ├── uploads/
│   │       │   └── presign/
│   │       │       └── route.ts               [NEW: 50 lines]
│   │       │
│   │       └── candidates/
│   │           └── route.ts                   [NEW: 95 lines]
│   │
│   └── lib/
│       ├── validation/
│       │   └── schemas.ts                     [MODIFIED: +presignUploadSchema, createCandidateSchema]
│       │
│       ├── storage/
│       │   └── s3.ts                          [NEW: 35 lines]
│       │
│       └── candidates/
│           └── code.ts                        [NEW: 8 lines]
│
└── docs/
    ├── W6-UPLOAD-RESUME-IMPLEMENTATION.md    [NEW: Full spec]
    ├── W6-IMPLEMENTATION-SUMMARY.md           [NEW: Overview]
    └── W6-SETUP-GUIDE.md                      [NEW: Deployment guide]

Schema Changes (Prisma)

Enums Added

enum CandidateStatus {
  NEW
  HR_SCREENED
  MANAGER_EVAL_PENDING
  MANAGER_EVAL_COMPLETED
  TO_SMO
  DECISION_MADE
  COMPLETED
}

enum DocumentCategory {
  RESUME
  SCREENING_NOTES
  EVALUATION
  DECISION
  OFFER
}

enum DocumentStatus {
  AVAILABLE
  PENDING_SCAN
}

enum AuditEventType {
  // ... existing ...
  RESUME_UPLOADED  // [NEW]
}

Models Added

model Candidate {
  id              String @id @default(cuid())
  candidateCode   String @unique
  fullName        String?
  applyingFor     String
  status          CandidateStatus @default(NEW)
  createdByUserId String
  createdByUser   User? @relation("CandidateCreatedBy", fields: [createdByUserId], references: [id], onDelete: SetNull)
  documents       CandidateDocument[]
  createdAt       DateTime @default(now())
  updatedAt       DateTime @updatedAt
}

model CandidateDocument {
  id              String @id @default(cuid())
  candidateId     String
  candidate       Candidate @relation(fields: [candidateId], references: [id], onDelete: Cascade)
  category        DocumentCategory
  filename        String
  mimeType        String
  sizeBytes       Int
  storageKey      String
  version         Int @default(1)
  status          DocumentStatus @default(AVAILABLE)
  uploadedByUserId String
  uploadedByUser  User? @relation("DocumentUploadedBy", fields: [uploadedByUserId], references: [id], onDelete: SetNull)
  createdAt       DateTime @default(now())
  updatedAt       DateTime @updatedAt
}

User Model Update

model User {
  // ... existing fields ...
  createdCandidates   Candidate[] @relation("CandidateCreatedBy")
  uploadedDocuments   CandidateDocument[] @relation("DocumentUploadedBy")
}

API Endpoints

1. POST /api/uploads/presign

Request:

{
  "filename": "resume.pdf",
  "mimeType": "application/pdf",
  "sizeBytes": 524288
}

Response (200):

{
  "uploadUrl": "https://s3.amazonaws.com/...",
  "storageKey": "candidates/temp-abc123/resume/1704974400000.pdf",
  "expiresIn": 3600
}

Errors:

  • 400: Invalid file metadata
  • 401: Not authenticated
  • 403: Not HR/ADMIN
  • 500: Server error

2. POST /api/candidates

Request:

{
  "applyingFor": "Software Engineer",
  "fullName": "John Doe",
  "notes": "Referred by Jane Smith",
  "resume": {
    "filename": "resume.pdf",
    "mimeType": "application/pdf",
    "sizeBytes": 524288,
    "storageKey": "candidates/temp-abc123/resume/1704974400000.pdf"
  }
}

Response (201):

{
  "candidateId": "cuid123",
  "candidateCode": "CAND-2026-45678",
  "message": "Candidate created and resume uploaded successfully"
}

Errors:

  • 400: Invalid candidate data
  • 401: Not authenticated
  • 403: Not HR/ADMIN
  • 500: Database/transaction error

Key Functions

Storage Helper: src/lib/storage/s3.ts

async function generatePresignedUrl(
  storageKey: string,
  mimeType: string
): Promise<string>

Returns: S3 PUT presigned URL (expires in 1 hour)

function generateStorageKey(
  candidateCode: string,
  filename: string
): string

Returns: candidates/{candidateCode}/resume/{timestamp}.{ext}


Candidate Code Generator: src/lib/candidates/code.ts

function generateCandidateCode(): string

Returns: CAND-YYYY-##### (e.g., CAND-2026-45678)


Validation Schemas: src/lib/validation/schemas.ts

const presignUploadSchema = z.object({
  filename: z.string().min(1).max(255),
  mimeType: z.enum([...]),
  sizeBytes: z.number().positive().max(10485760),
})

const createCandidateSchema = z.object({
  applyingFor: z.string().min(1).max(255),
  fullName: z.string().max(255).optional().or(z.literal('')),
  notes: z.string().max(2000).optional().or(z.literal('')),
  resume: z.object({...}),
})

UI Components

Main Page: src/app/(app)/upload-resume/page.tsx

Component: UploadResumePage (Client)

Features:

  • Form with 4 fields (position, name, resume, notes)
  • Dropzone integration
  • 3-stage submission flow
  • Progress indicator
  • Error/success states
  • Auto-redirect on success

State:

type UploadState = 'idle' | 'uploading' | 'success' | 'error'

Dropzone: src/app/(app)/upload-resume/_components/Dropzone.tsx

Component: Dropzone (Client)

Props:

interface DropzoneProps {
  onFilesSelected: (files: File[]) => void
  disabled?: boolean
}

Validation:

  • Types: PDF, DOC, DOCX only
  • Size: Max 10MB
  • Returns first file only

Layout: src/app/(app)/layout.tsx

Component: AppLayout (Server)

Content: Minimal wrapper (passes children)


RBAC Implementation

All endpoints use:

const payload = await requireRole([Role.HR, Role.ADMIN])

Returns 403 Forbidden if user is MANAGER or SMO.


Audit Events

CANDIDATE_CREATED

{
  "eventType": "CANDIDATE_CREATED",
  "userId": "hr-user-id",
  "details": {
    "entity_type": "Candidate",
    "entity_id": "cand-id",
    "candidateCode": "CAND-2026-45678",
    "status": "NEW",
    "applyingFor": "Software Engineer",
    "fullName": "John Doe"
  }
}

RESUME_UPLOADED

{
  "eventType": "RESUME_UPLOADED",
  "userId": "hr-user-id",
  "details": {
    "entity_type": "CandidateDocument",
    "entity_id": "doc-id",
    "candidateId": "cand-id",
    "candidateCode": "CAND-2026-45678",
    "filename": "resume.pdf",
    "sizeBytes": 524288,
    "storageKey": "candidates/temp-abc/resume/1704974400000.pdf"
  }
}

Configuration

Environment Variables

DATABASE_URL=postgresql://postgres:postgres@localhost:5432/projectweb
JWT_SECRET=dev-secret-key
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
S3_BUCKET_NAME=offers-review
S3_ENDPOINT=https://s3.amazonaws.com  # Optional

Dependencies

  • @prisma/client – ORM
  • zod – Validation
  • @aws-sdk/client-s3 – S3 client
  • @aws-sdk/s3-request-presigner – Presigned URLs
  • jsonwebtoken – JWT verification
  • next – Framework

Testing Checklist

  • Database migration applied
  • AWS credentials configured
  • Dev server running without errors
  • HR user can access /upload-resume
  • MANAGER/SMO cannot access /upload-resume (403)
  • Can upload PDF/DOC/DOCX successfully
  • Cannot upload invalid type (error message)
  • Cannot upload >10MB (error message)
  • Candidate record created in DB
  • Resume document record created in DB
  • Audit logs written
  • File uploaded to S3
  • Auto-redirect to /candidates/{id} works
  • Cancel button redirects to /dashboard

Deployment Steps

  1. Apply migration:

    npx prisma migrate dev --name add_candidates_and_documents
    
  2. Install dependencies:

    npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
    
  3. Configure environment:

    # Update .env.local with AWS credentials
    
  4. Start dev server:

    npm run dev
    
  5. Test end-to-end:

    • Login as HR
    • Navigate to /upload-resume
    • Upload resume
    • Verify redirect and database records

File Statistics

FileTypeLines
schema.prismaSchema+60
s3.tsUtility35
code.tsUtility8
schemas.tsValidation+50
presign/route.tsAPI50
candidates/route.tsAPI95
upload-resume/page.tsxUI380
Dropzone.tsxComponent140
(app)/layout.tsxLayout8
TotalCode~1100

Plus 3 documentation files (~1500 lines).


W6 Complete & Ready for Testing