Files
red/.planning/phases/12-filled-document-preview/12-01-PLAN.md
Chandler Copeland 10d4eb738a docs(12-filled-document-preview): create phase 12 plan
Two-plan wave structure for PREV-01: preview API route + modal (Plan 01,
Wave 1) then PreparePanel/FieldPlacer wiring + human verification (Plan 02,
Wave 2). Send button gated on previewToken staleness detection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 15:25:48 -06:00

11 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
12-filled-document-preview 01 execute 1
src/app/api/documents/[id]/preview/route.ts
src/app/portal/(protected)/documents/[docId]/_components/PreviewModal.tsx
true
PREV-01
truths artifacts key_links
POST /api/documents/[id]/preview returns PDF bytes for any document with valid field data
Preview route uses a versioned _preview_{timestamp}.pdf path — never overwrites _prepared.pdf
Preview route applies the same 422 guards as prepare route (agent-signature missing, agent-initials missing)
Preview files are deleted immediately after reading (fire-and-forget unlink)
PreviewModal renders PDF from ArrayBuffer using react-pdf Document + Page with prev/next navigation
PreviewModal configures pdfjs.GlobalWorkerOptions.workerSrc independently (not inherited from PdfViewer)
path provides exports
teressa-copeland-homes/src/app/api/documents/[id]/preview/route.ts POST route — generates preview PDF and returns bytes
POST
path provides exports
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreviewModal.tsx Modal component rendering PDF from ArrayBuffer
PreviewModal
from to via pattern
src/app/api/documents/[id]/preview/route.ts src/lib/pdf/prepare-document.ts import preparePdf preparePdf(srcPath, previewPath
from to via pattern
src/app/portal/.../PreviewModal.tsx react-pdf Document file={pdfBytes} ArrayBuffer prop file={pdfBytes}
Create the preview API route and PreviewModal component that together enable the agent to see a fully-prepared PDF in a modal before sending.

Purpose: PREV-01 requires the agent see a live filled preview (text, signatures, stamps embedded) before the Send button becomes available. Plan 01 builds the two new artifacts. Plan 02 wires them into PreparePanel and gates the Send button.

Output:

  • POST /api/documents/[id]/preview — auth-guarded route that calls preparePdf() to a versioned temp path, streams bytes back, then deletes the temp file
  • PreviewModal component — react-pdf Document/Page modal with prev/next page navigation

<execution_context> @/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md @/Users/ccopeland/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md

From teressa-copeland-homes/src/lib/pdf/prepare-document.ts:

export async function preparePdf(
  srcPath: string,          // Absolute path to source PDF
  destPath: string,         // Absolute path to write prepared PDF
  textFields: Record<string, string>,
  sigFields: SignatureFieldData[],
  agentSignatureData: string | null = null,
  agentInitialsData: string | null = null,
): Promise<void>

From teressa-copeland-homes/src/lib/db/schema.ts (existing pattern):

// getFieldType coalesces missing type field to 'client-signature'
export function getFieldType(f: SignatureFieldData): SignatureFieldType
export type SignatureFieldData = { ... }
// users table columns (relevant subset):
//   agentSignatureData: text()
//   agentInitialsData: text()

From react-pdf (installed v10.4.1):

// node_modules/react-pdf/dist/shared/types.d.ts
export type File = string | ArrayBuffer | Blob | Source | null;
// Document accepts ArrayBuffer directly as the file prop

From existing prepare route pattern (src/app/api/documents/[id]/prepare/route.ts):

// Auth guard pattern
const session = await auth();
if (!session?.user?.id) return new Response('Unauthorized', { status: 401 });

// Path construction pattern
const UPLOADS_DIR = path.join(process.cwd(), 'uploads');
const srcPath = path.join(UPLOADS_DIR, doc.filePath);
// Path traversal guard
if (!previewPath.startsWith(UPLOADS_DIR)) return new Response('Forbidden', { status: 403 });

// 422 guard pattern (mirror exactly in preview route)
const hasAgentSigFields = sigFields.some(f => getFieldType(f) === 'agent-signature');
if (hasAgentSigFields && !agentSignatureData) {
  return Response.json({ error: 'agent-signature-missing', message: 'No agent signature saved.' }, { status: 422 });
}
const hasAgentInitialsFields = sigFields.some(f => getFieldType(f) === 'agent-initials');
if (hasAgentInitialsFields && !agentInitialsData) {
  return Response.json({ error: 'agent-initials-missing', message: 'No agent initials saved.' }, { status: 422 });
}

From existing PdfViewer worker configuration (pattern to replicate in PreviewModal):

pdfjs.GlobalWorkerOptions.workerSrc = new URL(
  'pdfjs-dist/build/pdf.worker.min.mjs',
  import.meta.url,
).toString();
Task 1: POST /api/documents/[id]/preview route teressa-copeland-homes/src/app/api/documents/[id]/preview/route.ts Create the directory `src/app/api/documents/[id]/preview/` and write `route.ts` as a Next.js Route Handler.
The route must:
1. Auth guard: `const session = await auth(); if (!session?.user?.id) return new Response('Unauthorized', { status: 401 });`
2. Params: `const { id } = await params;` — params is `Promise<{ id: string }>` (Next.js 15 async params)
3. Parse body: `const body = await req.json() as { textFillData?: Record<string, string> };`
4. Fetch document from DB: `db.query.documents.findFirst({ where: eq(documents.id, id) })` — return 404 if not found, 422 if no filePath
5. Build versioned preview path: `doc.filePath.replace(/\.pdf$/, \`_preview_${Date.now()}.pdf\`)` joined with UPLOADS_DIR — NOT `_prepared.pdf`
6. Path traversal guard: `if (!previewPath.startsWith(UPLOADS_DIR)) return new Response('Forbidden', { status: 403 });`
7. Fetch agent data: single `db.query.users.findFirst({ where: eq(users.id, session.user.id), columns: { agentSignatureData: true, agentInitialsData: true } })`
8. Apply 422 guards (mirror prepare route exactly):
   - `hasAgentSigFields && !agentSignatureData` → 422 with `{ error: 'agent-signature-missing' }`
   - `hasAgentInitialsFields && !agentInitialsData` → 422 with `{ error: 'agent-initials-missing' }`
9. Wrap in try/finally: `try { await preparePdf(srcPath, previewPath, textFields, sigFields, agentSigData, agentInitialsData); const pdfBytes = await readFile(previewPath); return new Response(pdfBytes, { headers: { 'Content-Type': 'application/pdf' } }); } finally { unlink(previewPath).catch(() => {}); }`
10. The try/finally ensures the temp file is always deleted even if readFile throws.

Imports: `import { auth } from '@/lib/auth'`, `import { db } from '@/lib/db'`, `import { documents, users } from '@/lib/db/schema'`, `import { getFieldType } from '@/lib/db/schema'`, `import type { SignatureFieldData } from '@/lib/db/schema'`, `import { eq } from 'drizzle-orm'`, `import { preparePdf } from '@/lib/pdf/prepare-document'`, `import path from 'node:path'`, `import { readFile, unlink } from 'node:fs/promises'`

Do NOT import `rename` or `writeFile` — preparePdf handles its own write internally.
Do NOT update any document DB columns (status, preparedFilePath) — this is preview only.
```bash cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | grep "preview/route" ``` No TypeScript errors in preview/route.ts. File exists at src/app/api/documents/[id]/preview/route.ts; exports POST; TypeScript compiles without errors in that file; does not import rename/writeFile; uses try/finally for cleanup. Task 2: PreviewModal component teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreviewModal.tsx Create `PreviewModal.tsx` as a `'use client'` component.
Props interface:
```typescript
interface PreviewModalProps {
  pdfBytes: ArrayBuffer;
  onClose: () => void;
}
```

Implementation requirements:
1. Configure pdfjs worker at module scope (required — PreviewModal is a new module independent of PdfViewer):
   ```typescript
   pdfjs.GlobalWorkerOptions.workerSrc = new URL(
     'pdfjs-dist/build/pdf.worker.min.mjs',
     import.meta.url,
   ).toString();
   ```
2. Import CSS layers that PdfViewer uses: `import 'react-pdf/dist/Page/AnnotationLayer.css'` and `import 'react-pdf/dist/Page/TextLayer.css'`
3. State: `const [numPages, setNumPages] = useState(0)` and `const [pageNumber, setPageNumber] = useState(1)`
4. Pass `pdfBytes` (the ArrayBuffer state variable from parent) directly to `<Document file={pdfBytes}>` — NEVER wrap in `new Uint8Array()` or rebuild on each render; the parent stores it in useState so it is a stable reference
5. `onLoadSuccess`: `({ numPages }) => setNumPages(numPages)`
6. Use fixed overlay: `position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 50; display: flex; align-items: center; justify-content: center` — apply as Tailwind classes or inline style (inline style matches existing PreparePanel pattern)
7. Modal inner container: white background, max-width 900px, 90vw width, max-height 90vh, overflow-y auto, padding 16px
8. Navigation: Prev button (disabled when pageNumber <= 1), page counter `{pageNumber} / {numPages || '?'}`, Next button (disabled when pageNumber >= numPages), Close button
9. Render: `<Document file={pdfBytes} onLoadSuccess={...}><Page pageNumber={pageNumber} /></Document>`

Import: `import { Document, Page, pdfjs } from 'react-pdf'`

This component does NOT need to call any API. It receives bytes from the parent.
```bash cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | grep "PreviewModal" ``` No TypeScript errors in PreviewModal.tsx. File exists at src/app/portal/(protected)/documents/[docId]/_components/PreviewModal.tsx; exports PreviewModal with correct props; configures pdfjs worker; renders Document with ArrayBuffer file prop; TypeScript compiles cleanly. ```bash cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30 ``` Zero TypeScript errors project-wide.
ls /Users/ccopeland/temp/red/teressa-copeland-homes/src/app/api/documents/\[id\]/preview/route.ts && ls /Users/ccopeland/temp/red/teressa-copeland-homes/src/app/portal/\(protected\)/documents/\[docId\]/_components/PreviewModal.tsx

Both files exist.

<success_criteria>

  • POST /api/documents/[id]/preview route exists, auth-guarded, uses versioned path, mirrors 422 guards from prepare route, cleans up temp file in try/finally
  • PreviewModal exists, accepts ArrayBuffer prop, configures pdfjs worker independently, renders react-pdf Document/Page with prev/next navigation
  • Zero TypeScript compilation errors </success_criteria>
After completion, create `.planning/phases/12-filled-document-preview/12-01-SUMMARY.md` following the summary template.