--- phase: 12-filled-document-preview plan: 02 type: execute wave: 2 depends_on: - 12-01 files_modified: - src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx - src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx autonomous: false requirements: - PREV-01 must_haves: truths: - "Preview button appears in PreparePanel and calls POST /api/documents/[id]/preview with current textFillData" - "PreviewModal opens after a successful preview fetch and renders the returned PDF" - "Send button is disabled until the agent has generated at least one preview (previewToken !== null)" - "Changing any text fill value resets previewToken to null and re-disables the Send button" - "Adding, moving, or deleting any field (via FieldPlacer) resets previewToken to null and re-disables Send" - "Agent can generate a new preview after making changes and the Send button re-enables" - "Human verifies the complete Preview-then-Send flow end-to-end" artifacts: - path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx" provides: "Preview button, previewToken state, Send button gating, PreviewModal render" contains: "previewToken" - path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx" provides: "onFieldsChanged callback prop fired after every persistFields call" contains: "onFieldsChanged" key_links: - from: "PreparePanel.tsx" to: "PreviewModal.tsx" via: "showPreview && previewBytes state, renders PreviewModal" pattern: "PreviewModal" - from: "PreparePanel.tsx" to: "/api/documents/[id]/preview" via: "fetch POST in handlePreview" pattern: "fetch.*preview" - from: "FieldPlacer.tsx" to: "PreparePanel.tsx" via: "onFieldsChanged prop callback" pattern: "onFieldsChanged\\?\\." --- Wire the preview flow into PreparePanel and FieldPlacer: add the Preview button, stale-token gating, and FieldPlacer change notification. Then human-verify the complete flow. Purpose: Plan 01 created the API route and modal. Plan 02 connects them to the UI and enforces PREV-01's gating requirement — Send is unavailable until at least one fresh preview is generated. Output: - PreparePanel updated with previewToken state, handlePreview fetch, PreviewModal rendering, Send button gated on token - FieldPlacer updated with onFieldsChanged prop — calls it after every persistFields() invocation - Human checkpoint verifying the full Preview-gate-Send flow @/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md @/Users/ccopeland/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/12-filled-document-preview/12-01-SUMMARY.md Current PreparePanel.tsx props interface (do not remove existing props): ```typescript interface PreparePanelProps { docId: string; defaultEmail: string; clientName: string; currentStatus: string; agentDownloadUrl?: string | null; signedAt?: Date | null; clientPropertyAddress?: string | null; } ``` Current PreparePanel.tsx Send button (line ~160-167): ```tsx ``` Current FieldPlacerProps interface (add onFieldsChanged to this): ```typescript interface FieldPlacerProps { docId: string; pageInfo: PageInfo | null; currentPage: number; children: React.ReactNode; readOnly?: boolean; // ADD: onFieldsChanged?: () => void; } ``` persistFields() call sites in FieldPlacer.tsx (must call onFieldsChanged?.() after each): - Inside handleDragEnd callback: after `persistFields(docId, next)` - Inside handleZonePointerUp / pointer move handlers: after `persistFields(docId, next)` - Delete button onClick handler: after `persistFields(docId, next)` (Search for all `persistFields(docId` occurrences and add `onFieldsChanged?.()` immediately after each) PreviewModal export from Plan 01: ```typescript // src/app/portal/(protected)/documents/[docId]/_components/PreviewModal.tsx export function PreviewModal({ pdfBytes, onClose }: { pdfBytes: ArrayBuffer; onClose: () => void }) ``` Task 1: PreparePanel — preview state, button, gating, and modal render teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx Modify PreparePanel.tsx to add preview capability. All changes are within the Draft-status branch (currentStatus === 'Draft'). Do not touch the Signed or non-Draft early returns. 1. Add three new state variables after existing state declarations: ```typescript const [previewToken, setPreviewToken] = useState(null); const [previewBytes, setPreviewBytes] = useState(null); const [showPreview, setShowPreview] = useState(false); ``` 2. Add a `handlePreview` async function (alongside `handlePrepare`): ```typescript async function handlePreview() { setLoading(true); setResult(null); try { const res = await fetch(`/api/documents/${docId}/preview`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ textFillData }), }); if (res.ok) { const bytes = await res.arrayBuffer(); setPreviewBytes(bytes); setPreviewToken(Date.now().toString()); setShowPreview(true); } else { const err = await res.json().catch(() => ({ error: 'Preview failed' })); setResult({ ok: false, message: err.message ?? err.error ?? 'Preview failed' }); } } catch (e) { setResult({ ok: false, message: String(e) }); } finally { setLoading(false); } } ``` 3. Update `setTextFillData` usage: wherever `setTextFillData` is called (currently via `TextFillForm onChange`), also reset the stale token. The TextFillForm onChange prop currently calls `setTextFillData` directly. Change it to a wrapper: ```typescript function handleTextFillChange(data: Record) { setTextFillData(data); setPreviewToken(null); } ``` Then pass `onChange={handleTextFillChange}` to ``. 4. Add a `handleFieldsChanged` callback to be passed to FieldPlacer (used in Task 2): ```typescript function handleFieldsChanged() { setPreviewToken(null); } ``` 5. Update the Send button's disabled condition to include `previewToken === null`: ```tsx disabled={loading || previewToken === null || parseEmails(recipients).length === 0} ``` 6. Add the Preview button immediately BEFORE the Send button: ```tsx ``` 7. Render PreviewModal conditionally after the Send button: ```tsx {showPreview && previewBytes && ( setShowPreview(false)} /> )} ``` 8. Add import for PreviewModal at the top: `import { PreviewModal } from './PreviewModal';` 9. Export `handleFieldsChanged` is for internal use only — it will be passed as a prop to FieldPlacer which is a sibling component rendered in the document page, not within PreparePanel itself. Check the document page file (`page.tsx` in `[docId]/`) to see how FieldPlacer and PreparePanel are composed together. If they are rendered at the same level in a server component, you may need to create a small client wrapper or pass the callback via a prop. If PreparePanel and FieldPlacer are already siblings in a client component, handle the wiring there. If they are co-located in the same client component, expose `onFieldsChanged` as a prop to PreparePanel. Use whichever composition pattern already exists — do not over-engineer. Note: If FieldPlacer is rendered inside PreparePanel or alongside it in the same client component, the simplest approach is to pass `onFieldsChanged={handleFieldsChanged}` to FieldPlacer directly from wherever FieldPlacer is currently rendered. ```bash cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | grep "PreparePanel\|PreviewModal" ``` No TypeScript errors in PreparePanel.tsx or PreviewModal.tsx. PreparePanel has previewToken state; Preview button appears and calls handlePreview; Send button disabled when previewToken is null; text fill changes reset previewToken; PreviewModal renders when showPreview is true; TypeScript compiles cleanly. Task 2: FieldPlacer — add onFieldsChanged callback prop teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx Add `onFieldsChanged?: () => void` to FieldPlacerProps and call it after every `persistFields()` invocation. 1. Update the interface: ```typescript interface FieldPlacerProps { docId: string; pageInfo: PageInfo | null; currentPage: number; children: React.ReactNode; readOnly?: boolean; onFieldsChanged?: () => void; // NEW } ``` 2. Destructure in the function signature: ```typescript export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly = false, onFieldsChanged }: FieldPlacerProps) ``` 3. Find every call to `persistFields(docId, ...)` in the file (there are multiple — in handleDragEnd, in pointer event handlers, and in the delete button onClick). After EACH one, add: ```typescript onFieldsChanged?.(); ``` 4. Do not call `onFieldsChanged` in `loadFields()` (the initial load) — only after user-initiated mutations. Then wire the prop in the document page: find where FieldPlacer is rendered (likely in the document [docId] page.tsx or a layout component). Pass `onFieldsChanged` from wherever PreparePanel's `handleFieldsChanged` can be reached. If FieldPlacer and PreparePanel are rendered at the same level in a server component and cannot share state directly, create a minimal `DocumentPageClient.tsx` wrapper component that holds the `previewToken` reset callback and passes it down to both FieldPlacer and PreparePanel — but only if necessary. Prefer the simplest wiring that makes the staleness detection work. ```bash cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | grep "FieldPlacer" ``` No TypeScript errors in FieldPlacer.tsx. Manual check: Search for all `persistFields(docId` occurrences in FieldPlacer.tsx and confirm `onFieldsChanged?.()` follows each one. ```bash grep -n "persistFields\|onFieldsChanged" /Users/ccopeland/temp/red/teressa-copeland-homes/src/app/portal/\(protected\)/documents/\[docId\]/_components/FieldPlacer.tsx ``` Every `persistFields` line should have a corresponding `onFieldsChanged` immediately after. FieldPlacerProps has onFieldsChanged?: () => void; every persistFields() call site is followed by onFieldsChanged?.(); TypeScript compiles cleanly; onFieldsChanged is wired from the document page so that field mutations reset PreparePanel's previewToken. Task 3: Human verification — Preview-gate-Send flow Complete Phase 12 filled-document-preview flow: - POST /api/documents/[id]/preview generates a versioned temp PDF and returns bytes - PreviewModal renders the PDF from ArrayBuffer with page navigation - PreparePanel has a Preview button and the Send button is gated on previewToken - Text fill changes and field changes reset the stale token 1. Start the dev server: `cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run dev` 2. Log in as the agent at http://localhost:3000/portal 3. Open any Draft document that has at least one field placed (or place a field first) 4. Verify the Send/Prepare button is DISABLED before previewing — confirm it says "Prepare and Send" and is grayed out 5. Click "Preview" — a loading state should appear briefly 6. Verify the PreviewModal opens and shows the PDF with all embedded content (text fields, field overlays, agent signature if present) 7. Verify Prev/Next navigation works across pages 8. Close the modal — verify the Send button is now ENABLED 9. Change a text fill value — verify the Send button goes back to DISABLED 10. Click Preview again — verify modal opens with updated content 11. Send button should re-enable — click "Prepare and Send" and verify the signing email is sent normally 12. Verify the uploads/ directory has no lingering _preview_*.pdf files after the preview request completes Human verification — follow the how-to-verify steps above Agent confirms all 12 verification steps pass Human types "approved" — PREV-01 is complete Type "approved" if all 12 steps pass, or describe which step failed ```bash cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20 ``` Zero TypeScript errors. ```bash grep -n "previewToken\|handlePreview\|PreviewModal" /Users/ccopeland/temp/red/teressa-copeland-homes/src/app/portal/\(protected\)/documents/\[docId\]/_components/PreparePanel.tsx ``` All three appear in PreparePanel. ```bash grep -n "onFieldsChanged" /Users/ccopeland/temp/red/teressa-copeland-homes/src/app/portal/\(protected\)/documents/\[docId\]/_components/FieldPlacer.tsx ``` Appears in interface, destructure, and at least 2 call sites. - Preview button visible in PreparePanel on Draft documents - Send button disabled until at least one preview generated - Text fill changes re-disable Send button - Field changes (drag/drop/delete) re-disable Send button - Preview modal opens with correct fully-embedded PDF content - Page navigation works in modal - No _preview_*.pdf files linger in uploads/ after preview - Human approves all 12 verification steps - PREV-01 requirement complete After completion, create `.planning/phases/12-filled-document-preview/12-02-SUMMARY.md` following the summary template.