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>
This commit is contained in:
@@ -255,7 +255,7 @@ Plans:
|
||||
2. The Send button is disabled until the agent has generated at least one preview of the current field state
|
||||
3. If the agent changes any fields after previewing, the Send button is re-disabled until a fresh preview is generated (staleness detection)
|
||||
4. The preview PDF uses a versioned path and does not overwrite the final prepared PDF (legal integrity of prepared document is preserved)
|
||||
**Plans**: TBD
|
||||
**Plans**: 2 plans
|
||||
|
||||
Plans:
|
||||
- [ ] 12-01-PLAN.md — POST /api/documents/[id]/preview route (reuses preparePdf in preview mode, versioned path, staleness token), PreviewModal component with react-pdf rendering + ArrayBuffer copy
|
||||
@@ -270,7 +270,7 @@ Plans:
|
||||
2. AI-placed text fields are pre-filled with client name, property address, and signing date where those values are available from the client profile
|
||||
3. AI placement produces correct field positions on a full 20-page Utah REPC — coordinates convert correctly from percentage-based AI output to PDF user-space points (Y-axis inversion verified by unit test)
|
||||
4. Agent can review, adjust, or delete any AI-placed field before proceeding to prepare — AI placement is a starting point, not a lock
|
||||
**Plans**: TBD
|
||||
**Plans**: 4 plans
|
||||
|
||||
Plans:
|
||||
- [ ] 13-01-PLAN.md — lib/ai/extract-text.ts (pdfjs-dist legacy build, server-only), lib/ai/field-placement.ts (GPT-4o-mini structured output, manual JSON schema, server-only guard), aiCoordsToPagePdfSpace() utility + unit test
|
||||
|
||||
227
.planning/phases/12-filled-document-preview/12-01-PLAN.md
Normal file
227
.planning/phases/12-filled-document-preview/12-01-PLAN.md
Normal file
@@ -0,0 +1,227 @@
|
||||
---
|
||||
phase: 12-filled-document-preview
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- src/app/api/documents/[id]/preview/route.ts
|
||||
- src/app/portal/(protected)/documents/[docId]/_components/PreviewModal.tsx
|
||||
autonomous: true
|
||||
requirements:
|
||||
- PREV-01
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "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)"
|
||||
artifacts:
|
||||
- path: "teressa-copeland-homes/src/app/api/documents/[id]/preview/route.ts"
|
||||
provides: "POST route — generates preview PDF and returns bytes"
|
||||
exports: ["POST"]
|
||||
- path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreviewModal.tsx"
|
||||
provides: "Modal component rendering PDF from ArrayBuffer"
|
||||
exports: ["PreviewModal"]
|
||||
key_links:
|
||||
- from: "src/app/api/documents/[id]/preview/route.ts"
|
||||
to: "src/lib/pdf/prepare-document.ts"
|
||||
via: "import preparePdf"
|
||||
pattern: "preparePdf\\(srcPath, previewPath"
|
||||
- from: "src/app/portal/.../PreviewModal.tsx"
|
||||
to: "react-pdf Document"
|
||||
via: "file={pdfBytes} ArrayBuffer prop"
|
||||
pattern: "file=\\{pdfBytes\\}"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/Users/ccopeland/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Key contracts the executor needs. No codebase exploration required. -->
|
||||
|
||||
From teressa-copeland-homes/src/lib/pdf/prepare-document.ts:
|
||||
```typescript
|
||||
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):
|
||||
```typescript
|
||||
// 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):
|
||||
```typescript
|
||||
// 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):
|
||||
```typescript
|
||||
// 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):
|
||||
```typescript
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
import.meta.url,
|
||||
).toString();
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: POST /api/documents/[id]/preview route</name>
|
||||
<files>teressa-copeland-homes/src/app/api/documents/[id]/preview/route.ts</files>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
```bash
|
||||
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | grep "preview/route"
|
||||
```
|
||||
No TypeScript errors in preview/route.ts.
|
||||
</verify>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: PreviewModal component</name>
|
||||
<files>teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreviewModal.tsx</files>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
```bash
|
||||
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | grep "PreviewModal"
|
||||
```
|
||||
No TypeScript errors in PreviewModal.tsx.
|
||||
</verify>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
```bash
|
||||
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30
|
||||
```
|
||||
Zero TypeScript errors project-wide.
|
||||
|
||||
```bash
|
||||
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.
|
||||
</verification>
|
||||
|
||||
<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>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/12-filled-document-preview/12-01-SUMMARY.md` following the summary template.
|
||||
</output>
|
||||
333
.planning/phases/12-filled-document-preview/12-02-PLAN.md
Normal file
333
.planning/phases/12-filled-document-preview/12-02-PLAN.md
Normal file
@@ -0,0 +1,333 @@
|
||||
---
|
||||
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\\?\\."
|
||||
---
|
||||
|
||||
<objective>
|
||||
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
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/Users/ccopeland/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/12-filled-document-preview/12-01-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Key contracts the executor needs. Read these before touching the files. -->
|
||||
|
||||
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
|
||||
<button
|
||||
onClick={handlePrepare}
|
||||
disabled={loading || parseEmails(recipients).length === 0}
|
||||
className="w-full py-2 px-4 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed text-sm font-medium"
|
||||
type="button"
|
||||
>
|
||||
{loading ? 'Preparing...' : 'Prepare and Send'}
|
||||
</button>
|
||||
```
|
||||
|
||||
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 })
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: PreparePanel — preview state, button, gating, and modal render</name>
|
||||
<files>teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx</files>
|
||||
<action>
|
||||
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<string | null>(null);
|
||||
const [previewBytes, setPreviewBytes] = useState<ArrayBuffer | null>(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<string, string>) {
|
||||
setTextFillData(data);
|
||||
setPreviewToken(null);
|
||||
}
|
||||
```
|
||||
Then pass `onChange={handleTextFillChange}` to `<TextFillForm>`.
|
||||
|
||||
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
|
||||
<button
|
||||
onClick={handlePreview}
|
||||
disabled={loading}
|
||||
className="w-full py-2 px-4 bg-gray-700 text-white rounded hover:bg-gray-800 disabled:opacity-50 disabled:cursor-not-allowed text-sm font-medium"
|
||||
type="button"
|
||||
>
|
||||
{loading ? 'Generating preview...' : previewToken ? 'Preview again' : 'Preview'}
|
||||
</button>
|
||||
```
|
||||
|
||||
7. Render PreviewModal conditionally after the Send button:
|
||||
```tsx
|
||||
{showPreview && previewBytes && (
|
||||
<PreviewModal pdfBytes={previewBytes} onClose={() => 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.
|
||||
</action>
|
||||
<verify>
|
||||
```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.
|
||||
</verify>
|
||||
<done>
|
||||
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.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: FieldPlacer — add onFieldsChanged callback prop</name>
|
||||
<files>teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx</files>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
```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.
|
||||
</verify>
|
||||
<done>
|
||||
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.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 3: Human verification — Preview-gate-Send flow</name>
|
||||
<what-built>
|
||||
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
|
||||
</what-built>
|
||||
<how-to-verify>
|
||||
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
|
||||
</how-to-verify>
|
||||
<action>Human verification — follow the how-to-verify steps above</action>
|
||||
<verify>Agent confirms all 12 verification steps pass</verify>
|
||||
<done>Human types "approved" — PREV-01 is complete</done>
|
||||
<resume-signal>Type "approved" if all 12 steps pass, or describe which step failed</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
```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.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/12-filled-document-preview/12-02-SUMMARY.md` following the summary template.
|
||||
</output>
|
||||
Reference in New Issue
Block a user