--- phase: 05-pdf-fill-and-field-mapping plan: 03 type: execute wave: 2 depends_on: [05-01] files_modified: - teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx - teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/TextFillForm.tsx - teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx autonomous: true requirements: [DOC-05, DOC-06] must_haves: truths: - "Agent sees a text fill form below (or beside) the PDF viewer where they can add key-value pairs (label + value)" - "Agent can add up to 10 key-value text fill rows and remove individual rows" - "Agent sees a client selector dropdown pre-populated with the current document's assigned client (or all clients if unassigned)" - "Agent clicks Prepare and Send and receives feedback (loading state then success or error message)" - "After Prepare and Send succeeds, the document status badge on the dashboard shows Sent" - "The prepared PDF file exists on disk at uploads/clients/{clientId}/{docId}_prepared.pdf" artifacts: - path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/TextFillForm.tsx" provides: "Key-value form for agent text field data" min_lines: 50 - path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx" provides: "Combined panel: client selector + text fill form + Prepare and Send button" min_lines: 60 - path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx" provides: "Extended document detail page: fetches clients list, passes to PreparePanel" key_links: - from: "PreparePanel.tsx" to: "POST /api/documents/[id]/prepare" via: "fetch POST with { textFillData, assignedClientId }" pattern: "fetch.*prepare.*POST" - from: "page.tsx" to: "PreparePanel.tsx" via: "server component fetches clients list, passes as prop" pattern: "db.*clients.*PreparePanel" - from: "POST /api/documents/[id]/prepare response" to: "document status Sent" via: "router.refresh() after successful prepare" pattern: "router\\.refresh" --- Add the text fill form and Prepare and Send workflow to the document detail page. Agent can add labeled text values (property address, client names, dates), select the assigned client, then trigger document preparation. The server fills AcroForm fields (or draws text), burns signature rectangles, writes the prepared PDF, and transitions document status to Sent. Purpose: Fulfills DOC-05 (text fill) and DOC-06 (assign to client + initiate signing request). Completes the agent-facing preparation workflow before Phase 6 sends the actual email. Output: TextFillForm, PreparePanel components + extended document page. @/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md @/Users/ccopeland/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/STATE.md @.planning/phases/05-pdf-fill-and-field-mapping/05-RESEARCH.md @.planning/phases/05-pdf-fill-and-field-mapping/05-01-SUMMARY.md From teressa-copeland-homes/src/lib/db/schema.ts (clients table — used for client selector): ```typescript export const clients = pgTable("clients", { id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()), name: text("name").notNull(), email: text("email").notNull(), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }); ``` From teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx (current — MODIFY): ```typescript import { auth } from '@/lib/auth'; import { redirect } from 'next/navigation'; import { db } from '@/lib/db'; import { documents, clients } from '@/lib/db/schema'; import { eq } from 'drizzle-orm'; import Link from 'next/link'; import { PdfViewerWrapper } from './_components/PdfViewerWrapper'; export default async function DocumentPage({ params, }: { params: Promise<{ docId: string }>; }) { const session = await auth(); if (!session) redirect('/login'); const { docId } = await params; const doc = await db.query.documents.findFirst({ where: eq(documents.id, docId), with: { client: true }, }); if (!doc) redirect('/portal/dashboard'); return (
{/* header with back link, title */}
); } ``` API contract (from Plan 01): - POST /api/documents/[id]/prepare - body: { textFillData?: Record; assignedClientId?: string } - returns: updated document row (with status: 'Sent', sentAt, preparedFilePath) - 422 if document has no filePath Project patterns (from STATE.md): - useActionState imported from 'react' not 'react-dom' (React 19) - Client sub-components extracted to _components/ (e.g. ClientProfileClient, DashboardFilters) - 'use client' at file top (cannot inline in server component file) - Router refresh for post-action UI update: useRouter().refresh() from 'next/navigation' - StatusBadge already exists in _components — use it for displaying doc status
Task 1: Create TextFillForm and PreparePanel client components teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/TextFillForm.tsx teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx **TextFillForm.tsx** — a simple key-value pair builder for text fill data: ```typescript 'use client'; import { useState } from 'react'; interface TextRow { label: string; value: string; } interface TextFillFormProps { onChange: (data: Record) => void; } export function TextFillForm({ onChange }: TextFillFormProps) { const [rows, setRows] = useState([{ label: '', value: '' }]); function updateRow(index: number, field: 'label' | 'value', val: string) { const next = rows.map((r, i) => i === index ? { ...r, [field]: val } : r); setRows(next); onChange(Object.fromEntries(next.filter(r => r.label).map(r => [r.label, r.value]))); } function addRow() { if (rows.length >= 10) return; setRows([...rows, { label: '', value: '' }]); } function removeRow(index: number) { const next = rows.filter((_, i) => i !== index); setRows(next.length ? next : [{ label: '', value: '' }]); onChange(Object.fromEntries(next.filter(r => r.label).map(r => [r.label, r.value]))); } return (

Field label = AcroForm field name in the PDF (e.g. "PropertyAddress"). Leave blank to skip.

{rows.map((row, i) => (
updateRow(i, 'label', e.target.value)} className="flex-1 border rounded px-2 py-1 text-sm" /> updateRow(i, 'value', e.target.value)} className="flex-1 border rounded px-2 py-1 text-sm" />
))} {rows.length < 10 && ( )}
); } ``` **PreparePanel.tsx** — combines client selector, text fill form, and Prepare & Send button: ```typescript 'use client'; import { useState } from 'react'; import { useRouter } from 'next/navigation'; import { TextFillForm } from './TextFillForm'; interface Client { id: string; name: string; email: string; } interface PreparePanelProps { docId: string; clients: Client[]; currentClientId: string; currentStatus: string; } export function PreparePanel({ docId, clients, currentClientId, currentStatus }: PreparePanelProps) { const router = useRouter(); const [assignedClientId, setAssignedClientId] = useState(currentClientId); const [textFillData, setTextFillData] = useState>({}); const [loading, setLoading] = useState(false); const [result, setResult] = useState<{ ok: boolean; message: string } | null>(null); // Don't show the panel if already sent/signed const canPrepare = currentStatus === 'Draft'; async function handlePrepare() { setLoading(true); setResult(null); try { const res = await fetch(`/api/documents/${docId}/prepare`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ textFillData, assignedClientId }), }); if (!res.ok) { const err = await res.json().catch(() => ({ error: 'Unknown error' })); setResult({ ok: false, message: err.error ?? 'Prepare failed' }); } else { setResult({ ok: true, message: 'Document prepared successfully. Status updated to Sent.' }); router.refresh(); // Update the page to reflect new status } } catch (e) { setResult({ ok: false, message: String(e) }); } finally { setLoading(false); } } if (!canPrepare) { return (
Document status is {currentStatus} — preparation is only available for Draft documents.
); } return (

Prepare Document

{result && (

{result.message}

)}
); } ```
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run build 2>&1 | grep -E "(error TS|Error:|compiled successfully|Build)" | head -10 - TextFillForm.tsx exported from _components with onChange prop - PreparePanel.tsx exported from _components with docId, clients, currentClientId, currentStatus props - PreparePanel.tsx calls POST /api/documents/[id]/prepare on button click - PreparePanel calls router.refresh() on success - npm run build compiles without TypeScript errors
Task 2: Extend document detail page to render PreparePanel teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx Modify the DocumentPage server component to: 1. Import and query ALL clients (for the client selector dropdown): `await db.select().from(clients).orderBy(clients.name)` 2. Import PreparePanel and render it below the PdfViewerWrapper 3. Pass the document's current clientId as `currentClientId`, the clients array as `clients`, doc.status as `currentStatus`, and docId Updated page.tsx: ```typescript import { auth } from '@/lib/auth'; import { redirect } from 'next/navigation'; import { db } from '@/lib/db'; import { documents, clients } from '@/lib/db/schema'; import { eq, asc } from 'drizzle-orm'; import Link from 'next/link'; import { PdfViewerWrapper } from './_components/PdfViewerWrapper'; import { PreparePanel } from './_components/PreparePanel'; export default async function DocumentPage({ params, }: { params: Promise<{ docId: string }>; }) { const session = await auth(); if (!session) redirect('/login'); const { docId } = await params; const [doc, allClients] = await Promise.all([ db.query.documents.findFirst({ where: eq(documents.id, docId), with: { client: true }, }), db.select().from(clients).orderBy(asc(clients.name)), ]); if (!doc) redirect('/portal/dashboard'); return (
← Back to {doc.client?.name ?? 'Client'}

{doc.name}

{doc.client?.name} · {doc.status}

); } ``` Note: The layout changes to a 2-column grid on large screens — PDF takes 2/3, PreparePanel takes 1/3. This is a standard portal pattern consistent with the existing split-panel design in the marketing site. After updating, run build to verify: ```bash cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run build 2>&1 | tail -15 ```
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run build 2>&1 | grep -E "(error TS|Error:|compiled successfully|Build)" | head -10 - page.tsx fetches allClients in parallel with doc via Promise.all - PreparePanel rendered in right column of 2/3 + 1/3 grid - currentClientId defaults to doc.assignedClientId ?? doc.clientId - npm run build compiles without TypeScript errors
After both tasks complete: 1. `npm run build` — clean compile 2. Run `npm run dev`, navigate to any document detail page 3. Right side shows "Prepare Document" panel with: - Client dropdown pre-selected to the document's current client - Text fill form with one empty row and "+ Add field" link - "Prepare and Send" button (disabled if no client selected) 4. Add a row: label "PropertyAddress", value "123 Main St" — click Prepare and Send 5. Success message appears; page refreshes showing status "Sent" 6. Dashboard shows document with status "Sent" 7. `ls uploads/clients/{clientId}/{docId}_prepared.pdf` — prepared file exists on disk - Agent can add labeled key-value text fill rows (up to 10, individually removable) - Agent can select the client from a dropdown - Clicking Prepare and Send calls POST /api/documents/[id]/prepare and shows loading/result feedback - On success: document status transitions to Sent, router.refresh() updates the page - PreparePanel shows read-only message if document status is not Draft - npm run build is clean After completion, create `.planning/phases/05-pdf-fill-and-field-mapping/05-03-SUMMARY.md`