334 lines
15 KiB
Markdown
334 lines
15 KiB
Markdown
|
|
---
|
||
|
|
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>
|