diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 56696f5..b587325 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -98,9 +98,13 @@ Plans: 2. Signature field coordinates are stored in PDF user space (bottom-left origin) — verified by a unit test against an actual Utah real estate form 3. Agent can fill in text fields (property address, client names, dates, prices) on the document before sending 4. Agent can assign the prepared document to a specific client and initiate a signing request -**Plans**: TBD +**Plans**: 4 plans -Plans: none yet +Plans: +- [ ] 05-01-PLAN.md — Schema extension (signatureFields, textFillData, assignedClientId, preparedFilePath JSONB columns), migration 0003, @cantoo/pdf-lib prepare utility, GET/PUT /api/documents/[id]/fields, POST /api/documents/[id]/prepare +- [ ] 05-02-PLAN.md — dnd-kit FieldPlacer component: drag-and-drop signature field placement, Y-axis coordinate conversion, blue rectangle overlay, field persistence +- [ ] 05-03-PLAN.md — TextFillForm + PreparePanel: key-value text fill form, client selector dropdown, Prepare and Send workflow +- [ ] 05-04-PLAN.md — Full Phase 5 human verification checkpoint ### Phase 6: Signing Flow **Goal**: Client receives an email link, opens the prepared PDF in any browser, draws a signature, and the signed document is stored with a complete, legally defensible audit trail @@ -142,6 +146,6 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 | 2. Marketing Site | 2/3 | In Progress| | | 3. Agent Portal Shell | 4/4 | Complete | 2026-03-19 | | 4. PDF Ingest | 4/4 | Complete | 2026-03-20 | -| 5. PDF Fill and Field Mapping | 0/? | Not started | - | +| 5. PDF Fill and Field Mapping | 0/4 | Not started | - | | 6. Signing Flow | 0/? | Not started | - | | 7. Audit Trail and Download | 0/? | Not started | - | diff --git a/.planning/phases/05-pdf-fill-and-field-mapping/05-01-PLAN.md b/.planning/phases/05-pdf-fill-and-field-mapping/05-01-PLAN.md new file mode 100644 index 0000000..a921a0b --- /dev/null +++ b/.planning/phases/05-pdf-fill-and-field-mapping/05-01-PLAN.md @@ -0,0 +1,407 @@ +--- +phase: 05-pdf-fill-and-field-mapping +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - teressa-copeland-homes/src/lib/db/schema.ts + - teressa-copeland-homes/src/lib/pdf/prepare-document.ts + - teressa-copeland-homes/src/app/api/documents/[id]/fields/route.ts + - teressa-copeland-homes/src/app/api/documents/[id]/prepare/route.ts +autonomous: true +requirements: [DOC-04, DOC-05, DOC-06] + +must_haves: + truths: + - "Schema has signatureFields (JSONB), textFillData (JSONB), assignedClientId (text), and preparedFilePath (text) columns on documents table" + - "Migration 0003 is applied — columns exist in the local PostgreSQL database" + - "GET /api/documents/[id]/fields returns stored signature fields as JSON array" + - "PUT /api/documents/[id]/fields stores a SignatureFieldData[] array to the signatureFields column" + - "POST /api/documents/[id]/prepare fills AcroForm fields (or draws text), burns signature rectangles, writes prepared PDF to uploads/clients/{clientId}/{docId}_prepared.pdf, and transitions document status to Sent" + artifacts: + - path: "teressa-copeland-homes/src/lib/db/schema.ts" + provides: "Extended documents table with 4 new nullable columns" + contains: "signatureFields" + - path: "teressa-copeland-homes/src/lib/pdf/prepare-document.ts" + provides: "Server-side PDF mutation utility using @cantoo/pdf-lib" + exports: ["preparePdf"] + - path: "teressa-copeland-homes/src/app/api/documents/[id]/fields/route.ts" + provides: "GET/PUT API for signature field coordinates" + exports: ["GET", "PUT"] + - path: "teressa-copeland-homes/src/app/api/documents/[id]/prepare/route.ts" + provides: "POST endpoint that mutates PDF and transitions status to Sent" + exports: ["POST"] + key_links: + - from: "PUT /api/documents/[id]/fields" + to: "documents.signatureFields (JSONB)" + via: "drizzle db.update().set({ signatureFields })" + pattern: "db\\.update.*signatureFields" + - from: "POST /api/documents/[id]/prepare" + to: "teressa-copeland-homes/src/lib/pdf/prepare-document.ts" + via: "import { preparePdf } from '@/lib/pdf/prepare-document'" + pattern: "preparePdf" + - from: "prepare-document.ts" + to: "uploads/clients/{clientId}/{docId}_prepared.pdf" + via: "atomic write via tmp → rename" + pattern: "rename.*tmp" +--- + + +Extend the database schema with four new columns on the documents table, generate and apply the Drizzle migration, create the server-side PDF preparation utility using @cantoo/pdf-lib, and add two new API route files for signature field storage and document preparation. + +Purpose: Establish the data contracts and server logic that the field-placer UI (Plan 02) and text-fill UI (Plan 03) will consume. Everything in Phase 5 builds on these server endpoints. + +Output: Migration 0003 applied, two new API routes live, @cantoo/pdf-lib utility with atomic write behavior. + + + +@/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/05-pdf-fill-and-field-mapping/05-RESEARCH.md + + + + +From teressa-copeland-homes/src/lib/db/schema.ts (current state — MODIFY this file): +```typescript +import { pgEnum, pgTable, text, timestamp } from "drizzle-orm/pg-core"; +import { relations } from "drizzle-orm"; + +export const documentStatusEnum = pgEnum("document_status", ["Draft", "Sent", "Viewed", "Signed"]); + +export const documents = pgTable("documents", { + id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()), + name: text("name").notNull(), + clientId: text("client_id").notNull().references(() => clients.id, { onDelete: "cascade" }), + status: documentStatusEnum("status").notNull().default("Draft"), + sentAt: timestamp("sent_at"), + createdAt: timestamp("created_at").defaultNow().notNull(), + formTemplateId: text("form_template_id").references(() => formTemplates.id), + filePath: text("file_path"), + // ADD: signatureFields, textFillData, assignedClientId, preparedFilePath +}); +``` + +From teressa-copeland-homes/src/app/api/documents/[id]/file/route.ts (auth pattern to mirror): +```typescript +import { auth } from '@/lib/auth'; +// params is a Promise in Next.js 15 — always await before destructuring +export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) { + const session = await auth(); + if (!session) return new Response('Unauthorized', { status: 401 }); + const { id } = await params; + // ... +} +``` + +File path patterns from Phase 4 decisions: +- UPLOADS_DIR = path.join(process.cwd(), 'uploads') +- DB stores relative paths: 'clients/{clientId}/{docId}.pdf' +- Absolute path at read time: path.join(UPLOADS_DIR, relPath) +- New prepared file: 'clients/{clientId}/{docId}_prepared.pdf' + + + + + + + Task 1: Extend schema + generate and apply migration 0003 + + teressa-copeland-homes/src/lib/db/schema.ts + teressa-copeland-homes/drizzle/0003_*.sql (generated) + teressa-copeland-homes/drizzle/meta/_journal.json (updated) + + +Add four nullable columns to the documents table in schema.ts. Import `jsonb` from 'drizzle-orm/pg-core'. Add these columns to the documents pgTable definition — place them after the existing `filePath` column: + +```typescript +import { jsonb } from 'drizzle-orm/pg-core'; // add to existing import + +// Add to documents table after filePath: +signatureFields: jsonb('signature_fields').$type(), +textFillData: jsonb('text_fill_data').$type>(), +assignedClientId: text('assigned_client_id'), +preparedFilePath: text('prepared_file_path'), +``` + +Also export this TypeScript interface from schema.ts (add near the top of the file, before the table definitions): + +```typescript +export interface SignatureFieldData { + id: string; + page: number; // 1-indexed + x: number; // PDF user space, bottom-left origin, points + y: number; // PDF user space, bottom-left origin, points + width: number; // PDF points (default: 144 — 2 inches) + height: number; // PDF points (default: 36 — 0.5 inches) +} +``` + +Then run in the teressa-copeland-homes directory: +```bash +cd /Users/ccopeland/temp/red/teressa-copeland-homes +npm run db:generate +npm run db:migrate +``` + +The migration should add 4 columns (ALTER TABLE documents ADD COLUMN ...) — confirm the generated SQL looks correct before applying. The migration command will prompt if needed; migration applies via drizzle-kit migrate (not push — schema is controlled via migrations in this project). + + + cd /Users/ccopeland/temp/red/teressa-copeland-homes && node -e "const { db } = require('./src/lib/db'); db.execute('SELECT signature_fields, text_fill_data, assigned_client_id, prepared_file_path FROM documents LIMIT 0').then(() => { console.log('PASS: columns exist'); process.exit(0); }).catch(e => { console.error('FAIL:', e.message); process.exit(1); });" 2>/dev/null || echo "Verify via: ls drizzle/0003_*.sql should show generated file" + + schema.ts has all 4 new nullable columns typed correctly; drizzle/0003_*.sql exists; npm run db:migrate completes without error + + + + Task 2: Create pdf prepare-document utility + API routes for fields and prepare + + teressa-copeland-homes/src/lib/pdf/prepare-document.ts + teressa-copeland-homes/src/app/api/documents/[id]/fields/route.ts + teressa-copeland-homes/src/app/api/documents/[id]/prepare/route.ts + + +**Step A — Install @cantoo/pdf-lib** (do NOT install `pdf-lib` — they conflict): +```bash +cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm install @cantoo/pdf-lib +``` + +**Step B — Create teressa-copeland-homes/src/lib/pdf/prepare-document.ts:** + +```typescript +import { PDFDocument, StandardFonts, rgb } from '@cantoo/pdf-lib'; +import { readFile, writeFile, rename } from 'node:fs/promises'; +import type { SignatureFieldData } from '@/lib/db/schema'; + +/** + * Fills AcroForm text fields and draws signature rectangles on a PDF. + * Uses atomic write: write to tmp path, verify %PDF header, rename to final path. + * + * @param srcPath - Absolute path to original PDF + * @param destPath - Absolute path to write prepared PDF (e.g. {docId}_prepared.pdf) + * @param textFields - Key/value map: { fieldNameOrLabel: value } (agent-provided) + * @param sigFields - Signature field array from SignatureFieldData[] + */ +export async function preparePdf( + srcPath: string, + destPath: string, + textFields: Record, + sigFields: SignatureFieldData[], +): Promise { + const pdfBytes = await readFile(srcPath); + const pdfDoc = await PDFDocument.load(pdfBytes); + const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica); + const pages = pdfDoc.getPages(); + + // Strategy A: fill existing AcroForm fields by name + // form.flatten() MUST happen before drawing rectangles + try { + const form = pdfDoc.getForm(); + for (const [fieldName, value] of Object.entries(textFields)) { + try { + form.getTextField(fieldName).setText(value); + } catch { + // Field name not found in AcroForm — silently skip + } + } + form.flatten(); + } catch { + // No AcroForm in this PDF — skip to rectangle drawing + } + + // Draw signature field placeholders (blue rectangle + "Sign Here" label) + for (const field of sigFields) { + const page = pages[field.page - 1]; // page is 1-indexed + if (!page) continue; + page.drawRectangle({ + x: field.x, + y: field.y, + width: field.width, + height: field.height, + borderColor: rgb(0.15, 0.39, 0.92), + borderWidth: 1.5, + color: rgb(0.90, 0.94, 0.99), + }); + page.drawText('Sign Here', { + x: field.x + 4, + y: field.y + 4, + size: 8, + font: helvetica, + color: rgb(0.15, 0.39, 0.92), + }); + } + + const modifiedBytes = await pdfDoc.save(); + + // Atomic write: tmp → rename (prevents partial writes from corrupting the only copy) + const tmpPath = `${destPath}.tmp`; + await writeFile(tmpPath, modifiedBytes); + + // Verify output is a valid PDF (check magic bytes) + const magic = modifiedBytes.slice(0, 4); + const header = Buffer.from(magic).toString('ascii'); + if (!header.startsWith('%PDF')) { + throw new Error('preparePdf: output is not a valid PDF (magic bytes check failed)'); + } + + await rename(tmpPath, destPath); +} +``` + +**Step C — Create teressa-copeland-homes/src/app/api/documents/[id]/fields/route.ts:** + +GET returns the stored signatureFields array. PUT accepts a SignatureFieldData[] body and stores it. + +```typescript +import { auth } from '@/lib/auth'; +import { db } from '@/lib/db'; +import { documents } from '@/lib/db/schema'; +import { eq } from 'drizzle-orm'; +import type { SignatureFieldData } from '@/lib/db/schema'; + +export async function GET( + _req: Request, + { params }: { params: Promise<{ id: string }> } +) { + const session = await auth(); + if (!session) return new Response('Unauthorized', { status: 401 }); + + const { id } = await params; + const doc = await db.query.documents.findFirst({ where: eq(documents.id, id) }); + if (!doc) return Response.json({ error: 'Not found' }, { status: 404 }); + + return Response.json(doc.signatureFields ?? []); +} + +export async function PUT( + req: Request, + { params }: { params: Promise<{ id: string }> } +) { + const session = await auth(); + if (!session) return new Response('Unauthorized', { status: 401 }); + + const { id } = await params; + const fields: SignatureFieldData[] = await req.json(); + + const [updated] = await db + .update(documents) + .set({ signatureFields: fields }) + .where(eq(documents.id, id)) + .returning(); + + if (!updated) return Response.json({ error: 'Not found' }, { status: 404 }); + return Response.json(updated.signatureFields ?? []); +} +``` + +**Step D — Create teressa-copeland-homes/src/app/api/documents/[id]/prepare/route.ts:** + +POST: loads the document, resolves the src PDF, calls preparePdf, stores the preparedFilePath, updates textFillData, assignedClientId, status to 'Sent', and sets sentAt. + +```typescript +import { auth } from '@/lib/auth'; +import { db } from '@/lib/db'; +import { documents } from '@/lib/db/schema'; +import { eq } from 'drizzle-orm'; +import { preparePdf } from '@/lib/pdf/prepare-document'; +import path from 'node:path'; + +const UPLOADS_DIR = path.join(process.cwd(), 'uploads'); + +export async function POST( + req: Request, + { params }: { params: Promise<{ id: string }> } +) { + const session = await auth(); + if (!session) return new Response('Unauthorized', { status: 401 }); + + const { id } = await params; + const body = await req.json() as { + textFillData?: Record; + assignedClientId?: string; + }; + + const doc = await db.query.documents.findFirst({ where: eq(documents.id, id) }); + if (!doc) return Response.json({ error: 'Not found' }, { status: 404 }); + if (!doc.filePath) return Response.json({ error: 'Document has no PDF file' }, { status: 422 }); + + const srcPath = path.join(UPLOADS_DIR, doc.filePath); + // Prepared variant stored next to original with _prepared suffix + const preparedRelPath = doc.filePath.replace(/\.pdf$/, '_prepared.pdf'); + const destPath = path.join(UPLOADS_DIR, preparedRelPath); + + // Path traversal guard + if (!destPath.startsWith(UPLOADS_DIR)) { + return new Response('Forbidden', { status: 403 }); + } + + const sigFields = (doc.signatureFields as import('@/lib/db/schema').SignatureFieldData[]) ?? []; + const textFields = body.textFillData ?? {}; + + await preparePdf(srcPath, destPath, textFields, sigFields); + + const [updated] = await db + .update(documents) + .set({ + preparedFilePath: preparedRelPath, + textFillData: body.textFillData ?? null, + assignedClientId: body.assignedClientId ?? doc.assignedClientId ?? null, + status: 'Sent', + sentAt: new Date(), + }) + .where(eq(documents.id, id)) + .returning(); + + return Response.json(updated); +} +``` + +**Verify build compiles cleanly after creating all files:** +```bash +cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run build 2>&1 | tail -20 +``` + + + cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run build 2>&1 | grep -E "(error|Error|FAILED|compiled successfully)" | head -10 + + + - @cantoo/pdf-lib in package.json dependencies + - src/lib/pdf/prepare-document.ts exports preparePdf function + - GET /api/documents/[id]/fields returns 401 unauthenticated (verifiable via curl) + - PUT /api/documents/[id]/fields returns 401 unauthenticated + - POST /api/documents/[id]/prepare returns 401 unauthenticated + - npm run build completes without TypeScript errors + + + + + + +After both tasks complete: +1. `ls /Users/ccopeland/temp/red/teressa-copeland-homes/drizzle/0003_*.sql` — migration file exists +2. `ls /Users/ccopeland/temp/red/teressa-copeland-homes/src/lib/pdf/prepare-document.ts` — utility exists +3. `ls /Users/ccopeland/temp/red/teressa-copeland-homes/src/app/api/documents/[docId]/fields/route.ts` — route exists (note: directory name may use [id] to match existing pattern) +4. `npm run build` — clean compile +5. `curl -s http://localhost:3000/api/documents/test-id/fields` — returns "Unauthorized" (server must be running) + + + +- Migration 0003 applied: documents table has 4 new nullable columns (signature_fields JSONB, text_fill_data JSONB, assigned_client_id TEXT, prepared_file_path TEXT) +- SignatureFieldData interface exported from schema.ts with id, page, x, y, width, height fields +- @cantoo/pdf-lib installed (NOT the original pdf-lib — they conflict) +- preparePdf utility uses atomic tmp→rename write pattern +- form.flatten() called BEFORE drawing signature rectangles +- GET/PUT /api/documents/[id]/fields returns 401 without auth, correct JSON with auth +- POST /api/documents/[id]/prepare returns 401 without auth; with auth loads doc, calls preparePdf, transitions status to Sent +- npm run build compiles without errors + + + +After completion, create `.planning/phases/05-pdf-fill-and-field-mapping/05-01-SUMMARY.md` + diff --git a/.planning/phases/05-pdf-fill-and-field-mapping/05-02-PLAN.md b/.planning/phases/05-pdf-fill-and-field-mapping/05-02-PLAN.md new file mode 100644 index 0000000..e530227 --- /dev/null +++ b/.planning/phases/05-pdf-fill-and-field-mapping/05-02-PLAN.md @@ -0,0 +1,394 @@ +--- +phase: 05-pdf-fill-and-field-mapping +plan: 02 +type: execute +wave: 2 +depends_on: [05-01] +files_modified: + - teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx + - teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx +autonomous: true +requirements: [DOC-04] + +must_haves: + truths: + - "Agent sees a field palette on the document detail page with a draggable Signature Field token" + - "Agent can drag the Signature Field token onto any part of any PDF page and release it to place a field" + - "Placed fields appear as blue-bordered semi-transparent rectangles overlaid on the correct position of the PDF page" + - "Stored fields persist across page reload (coordinates are saved to the server via PUT /api/documents/[id]/fields)" + - "Fields placed at the top of a page have high PDF Y values (near originalHeight); fields placed at the bottom have low PDF Y values — Y-axis flip is correct" + - "Placed fields can be deleted individually via a remove button" + artifacts: + - path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx" + provides: "dnd-kit DndContext with draggable token palette and droppable PDF page overlay" + min_lines: 80 + - path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx" + provides: "Extended to accept and render placed fields as absolute-positioned overlays; exposes pageInfo via onPageLoad callback" + key_links: + - from: "FieldPlacer.tsx onDragEnd" + to: "screenToPdfCoords() formula" + via: "inline coordinate conversion using pageContainerRef.getBoundingClientRect()" + pattern: "renderedH - screenY.*originalHeight" + - from: "FieldPlacer.tsx" + to: "PUT /api/documents/[id]/fields" + via: "fetch PUT on every field add/remove" + pattern: "fetch.*fields.*PUT" + - from: "PdfViewer.tsx Page" + to: "FieldPlacer.tsx pageInfo state" + via: "onLoadSuccess callback sets pageInfo: { originalWidth, originalHeight, width, height, scale }" + pattern: "onLoadSuccess.*originalWidth" +--- + + +Extend the document detail page with a drag-and-drop field placer. The agent drags a Signature Field token from a palette onto any PDF page. On drop, screen coordinates are converted to PDF user-space coordinates (Y-axis flip formula), stored in state and persisted via PUT /api/documents/[id]/fields. Placed fields are rendered as blue rectangle overlays on the PDF. Fields load from the server on mount. + +Purpose: Fulfills DOC-04 — agent can place signature fields on any page of a PDF and those coordinates survive for downstream PDF preparation and signing. + +Output: FieldPlacer.tsx client component + extended PdfViewer.tsx. + + + +@/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 (after Plan 01): +```typescript +export interface SignatureFieldData { + id: string; + page: number; // 1-indexed + x: number; // PDF user space, bottom-left origin, points + y: number; // PDF user space, bottom-left origin, points + width: number; // PDF points + height: number; // PDF points +} +``` + +From teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx (current — MODIFY): +```typescript +'use client'; +import { useState } from 'react'; +import { Document, Page, pdfjs } from 'react-pdf'; +import 'react-pdf/dist/Page/AnnotationLayer.css'; +import 'react-pdf/dist/Page/TextLayer.css'; + +pdfjs.GlobalWorkerOptions.workerSrc = new URL( + 'pdfjs-dist/build/pdf.worker.min.mjs', + import.meta.url, +).toString(); + +export function PdfViewer({ docId }: { docId: string }) { + const [numPages, setNumPages] = useState(0); + const [pageNumber, setPageNumber] = useState(1); + const [scale, setScale] = useState(1.0); + // ...renders Document + Page with Prev/Next, Zoom In/Out, Download +} +``` + +API contract (from Plan 01): +- GET /api/documents/[id]/fields → SignatureFieldData[] (returns [] if none) +- PUT /api/documents/[id]/fields → body: SignatureFieldData[] → returns updated array + +Coordinate conversion (CRITICAL — do not re-derive): +```typescript +// Screen (DOM) → PDF user space. Y-axis flip required. +// DOM: Y=0 at top, increases downward +// PDF: Y=0 at bottom, increases upward +function screenToPdfCoords(screenX: number, screenY: number, pageInfo: PageInfo) { + const pdfX = (screenX / pageInfo.width) * pageInfo.originalWidth; + const pdfY = ((pageInfo.height - screenY) / pageInfo.height) * pageInfo.originalHeight; + return { x: pdfX, y: pdfY }; +} + +// PDF user space → screen (for rendering stored fields) +function pdfToScreenCoords(pdfX: number, pdfY: number, pageInfo: PageInfo) { + const left = (pdfX / pageInfo.originalWidth) * pageInfo.width; + // top is measured from DOM top; pdfY is from PDF bottom — reverse flip + const top = pageInfo.height - (pdfY / pageInfo.originalHeight) * pageInfo.height; + return { left, top }; +} +``` + +Pitfall guard for originalHeight: +```typescript +// Some PDFs have non-standard mediaBox ordering — use Math.max to handle both +originalWidth: Math.max(page.view[0], page.view[2]), +originalHeight: Math.max(page.view[1], page.view[3]), +``` + +dnd-kit drop position pattern (from research — MEDIUM confidence, verify during impl): +```typescript +// The draggable token is in the palette, not on the canvas. +// Use DragOverlay for visual ghost + compute final position from +// the mouse/touch coordinates at drop time relative to the container rect. +// event.delta gives displacement from drag start position of the activator. +// For an item dragged FROM the palette onto the PDF zone: +// finalX = activatorClientX + event.delta.x - containerRect.left +// finalY = activatorClientY + event.delta.y - containerRect.top +// The activator coordinates come from event.activatorEvent (MouseEvent or TouchEvent). +``` + + + + + + + Task 1: Install dnd-kit and create FieldPlacer component + + teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx + + +**Step A — Install dnd-kit:** +```bash +cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm install @dnd-kit/core @dnd-kit/utilities +``` + +**Step B — Create FieldPlacer.tsx.** + +This is a client component. It: +1. Fetches existing fields from GET /api/documents/[id]/fields on mount +2. Renders a palette with a single draggable "Signature Field" token using `useDraggable` +3. Renders the PDF page container as a `useDroppable` zone +4. On drop, converts screen coordinates to PDF user-space using the Y-flip formula, adds the new field to state, persists via PUT /api/documents/[id]/fields +5. Renders placed fields as absolute-positioned divs over the PDF page (using pdfToScreenCoords) +6. Each placed field has an X button to delete it (removes from state, persists) + +Key implementation details: +- Accept props: `{ docId: string; pageInfo: PageInfo | null; currentPage: number; children: React.ReactNode }` where `children` is the `/` tree rendered by PdfViewer +- The droppable zone wraps the children (PDF canvas) with `position: relative` so overlays position correctly +- Default field size: 144 × 36 PDF points (2 inches × 0.5 inches at 72 DPI) +- Use `DragOverlay` to show a ghost during drag (better UX than transform-based dragging) +- The page container ref (`useRef`) is attached to the droppable wrapper div — use `getBoundingClientRect()` at drop time for current rendered dimensions (NOT stale pageInfo.width because zoom may have changed) +- Persist via async fetch (fire-and-forget with error logging — don't block UI) +- Fields state: `useState([])` loaded from server on mount + +PageInfo interface (define locally in this file): +```typescript +interface PageInfo { + originalWidth: number; + originalHeight: number; + width: number; + height: number; + scale: number; +} +``` + +Structure: +```typescript +'use client'; +import { useState, useEffect, useRef, useCallback } from 'react'; +import { + DndContext, + useDraggable, + useDroppable, + DragOverlay, + type DragEndEvent, +} from '@dnd-kit/core'; +import { CSS } from '@dnd-kit/utilities'; +import type { SignatureFieldData } from '@/lib/db/schema'; + +// ... DraggableToken sub-component using useDraggable +// ... FieldPlacer main component +``` + +Coordinate math — use EXACTLY this formula (do not re-derive): +```typescript +function screenToPdfCoords(screenX: number, screenY: number, containerRect: DOMRect, pageInfo: PageInfo) { + // Use containerRect dimensions (current rendered size) not stale pageInfo + const renderedW = containerRect.width; + const renderedH = containerRect.height; + const pdfX = (screenX / renderedW) * pageInfo.originalWidth; + const pdfY = ((renderedH - screenY) / renderedH) * pageInfo.originalHeight; + return { x: pdfX, y: pdfY }; +} + +function pdfToScreenCoords(pdfX: number, pdfY: number, containerRect: DOMRect, pageInfo: PageInfo) { + const renderedW = containerRect.width; + const renderedH = containerRect.height; + const left = (pdfX / pageInfo.originalWidth) * renderedW; + const top = renderedH - (pdfY / pageInfo.originalHeight) * renderedH; + return { left, top }; +} +``` + +Persist helper (keep outside component to avoid re-creation): +```typescript +async function persistFields(docId: string, fields: SignatureFieldData[]) { + try { + await fetch(`/api/documents/${docId}/fields`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(fields), + }); + } catch (e) { + console.error('Failed to persist fields:', e); + } +} +``` + +Field overlay rendering — subtract height because PDF Y is at bottom-left, but DOM top is at top-left: +```typescript +{fields.filter(f => f.page === currentPage).map(field => { + const containerRect = containerRef.current?.getBoundingClientRect(); + if (!containerRect || !pageInfo) return null; + const { left, top } = pdfToScreenCoords(field.x, field.y, containerRect, pageInfo); + const widthPx = (field.width / pageInfo.originalWidth) * containerRect.width; + const heightPx = (field.height / pageInfo.originalHeight) * containerRect.height; + return ( +
+ Signature + +
+ ); +})} +``` +
+ + cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run build 2>&1 | grep -E "(error TS|Error:|compiled successfully|Build)" | head -10 + + + - FieldPlacer.tsx exists and exports FieldPlacer component + - @dnd-kit/core and @dnd-kit/utilities in package.json + - Component accepts docId, pageInfo, currentPage, children props + - Uses exactly the Y-flip formula from research (no re-derivation) + - npm run build compiles without TypeScript errors + +
+ + + Task 2: Extend PdfViewer to expose pageInfo and integrate FieldPlacer + + teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx + + +Rewrite PdfViewer.tsx to: +1. Add `pageInfo` state (`useState(null)`) where PageInfo is `{ originalWidth, originalHeight, width, height, scale }` +2. Update the `` component's `onLoadSuccess` callback to set pageInfo using the `Math.max` pattern for originalWidth/Height: + ```typescript + onLoadSuccess={(page) => { + setPageInfo({ + originalWidth: Math.max(page.view[0], page.view[2]), + originalHeight: Math.max(page.view[1], page.view[3]), + width: page.width, + height: page.height, + scale: page.scale, + }); + }} + ``` +3. Import and wrap the `` tree inside ``. FieldPlacer renders children (the PDF canvas) plus the droppable zone + field overlays. +4. Add a `docId` prop to PdfViewer: `{ docId: string }` (already exists — no change needed) +5. Keep all existing controls (Prev, Next, Zoom In, Zoom Out, Download) unchanged + +Note on PdfViewerWrapper: PdfViewerWrapper.tsx (dynamic import wrapper) does NOT need to change — it already passes `docId` through to PdfViewer. + +Note on `page.view`: For react-pdf v10 (installed), the `Page.onLoadSuccess` callback receives a `page` object where `page.view` is `[x1, y1, x2, y2]`. For standard US Letter PDFs this is `[0, 0, 612, 792]`. The `Math.max` pattern handles non-standard mediaBox ordering. + +Do NOT use both `width` and `scale` props on `` — use only `scale`. Using both causes double scaling. + +The final JSX structure should be: +```tsx +
+ {/* controls toolbar */} +
+ {/* Prev, page counter, Next, Zoom In, Zoom Out, Download — keep existing */} +
+ + {/* PDF + field overlay */} + + setNumPages(numPages)} + className="shadow-lg" + > + { + setPageInfo({ + originalWidth: Math.max(page.view[0], page.view[2]), + originalHeight: Math.max(page.view[1], page.view[3]), + width: page.width, + height: page.height, + scale: page.scale, + }); + }} + /> + + +
+``` + +After modifying PdfViewer.tsx, run build to confirm no TypeScript errors: +```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 + + + - PdfViewer.tsx has pageInfo state with Math.max mediaBox pattern on onLoadSuccess + - FieldPlacer is imported and wraps the Document/Page tree + - Only scale prop used on Page (not both width + scale) + - npm run build compiles without TypeScript errors + - PdfViewerWrapper.tsx unchanged + +
+ +
+ + +After both tasks complete: +1. `npm run build` in teressa-copeland-homes — clean compile +2. Run `npm run dev` and navigate to any document detail page +3. A "Signature Field" draggable token appears in a palette area above or beside the PDF +4. Drag the token onto the PDF page — a blue rectangle appears at the drop location +5. Refresh the page — the blue rectangle is still there (persisted to DB) +6. Click the × button on a placed field — it disappears from the overlay and from DB +7. Navigate to page 2 of a multi-page document — fields placed on page 1 don't appear on page 2 + + + +- Agent can drag a Signature Field token onto any PDF page and see a blue rectangle overlay at the correct position +- Coordinates stored in PDF user space (Y-flip applied) — placing a field at visual top of page stores high pdfY value +- Fields persist across page reload (PUT /api/documents/[id]/fields called on every change) +- Fields are page-scoped (field.page === currentPage filter applied) +- npm run build is clean + + + +After completion, create `.planning/phases/05-pdf-fill-and-field-mapping/05-02-SUMMARY.md` + diff --git a/.planning/phases/05-pdf-fill-and-field-mapping/05-03-PLAN.md b/.planning/phases/05-pdf-fill-and-field-mapping/05-03-PLAN.md new file mode 100644 index 0000000..79aeb4e --- /dev/null +++ b/.planning/phases/05-pdf-fill-and-field-mapping/05-03-PLAN.md @@ -0,0 +1,439 @@ +--- +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` + diff --git a/.planning/phases/05-pdf-fill-and-field-mapping/05-04-PLAN.md b/.planning/phases/05-pdf-fill-and-field-mapping/05-04-PLAN.md new file mode 100644 index 0000000..b192941 --- /dev/null +++ b/.planning/phases/05-pdf-fill-and-field-mapping/05-04-PLAN.md @@ -0,0 +1,131 @@ +--- +phase: 05-pdf-fill-and-field-mapping +plan: 04 +type: execute +wave: 3 +depends_on: [05-02, 05-03] +files_modified: [] +autonomous: false +requirements: [DOC-04, DOC-05, DOC-06] + +must_haves: + truths: + - "Agent can drag and drop a Signature Field token onto a PDF page and the blue rectangle appears at the correct location" + - "Placed signature fields persist across page reload" + - "Agent can remove a placed signature field using the × button" + - "Agent can fill in text fields (label + value pairs) in the Prepare Document panel" + - "Agent can select a client from the dropdown and click Prepare and Send" + - "After Prepare and Send, document status shows Sent in both the document page and the dashboard" + - "The prepared PDF file exists at uploads/clients/{clientId}/{docId}_prepared.pdf and contains the blue signature rectangle" + artifacts: + - path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx" + provides: "Drag-and-drop field placement UI" + - path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx" + provides: "Text fill + client assign + Prepare and Send workflow" + - path: "teressa-copeland-homes/uploads/clients/" + provides: "Prepared PDF files on disk ({docId}_prepared.pdf)" + key_links: + - from: "human tester" + to: "PDF signature field overlay" + via: "drag-and-drop interaction in browser" + - from: "human tester" + to: "uploads/ directory" + via: "ls command to verify prepared PDF file exists" +--- + + +Human verification checkpoint for Phase 5. Agent (Teressa) tests the complete field mapping and document preparation workflow in the browser: dragging signature fields, filling text fields, assigning a client, and triggering document preparation. + +Purpose: Confirm all three Phase 5 requirements (DOC-04, DOC-05, DOC-06) are met end-to-end before Phase 6 begins. + +Output: Human sign-off or specific issues to address. + + + +@/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md +@/Users/ccopeland/.claude/get-shit-done/templates/summary.md + + + +@.planning/ROADMAP.md +@.planning/phases/05-pdf-fill-and-field-mapping/05-02-SUMMARY.md +@.planning/phases/05-pdf-fill-and-field-mapping/05-03-SUMMARY.md + + + + + + + Complete Phase 5 PDF fill and field mapping workflow: + - Drag-and-drop signature field placement on any PDF page (DOC-04) + - Text fill form with key-value pairs for AcroForm fields (DOC-05) + - Client assignment and Prepare and Send action that writes a prepared PDF and transitions status to Sent (DOC-06) + + + Run the app if not already running: + ``` + cd teressa-copeland-homes && npm run dev + ``` + + Perform these verification steps in order: + + 1. Open any document detail page (navigate to a client profile, click a document name) + Expected: Document page has two columns — PDF viewer on the left, "Prepare Document" panel on the right + + 2. Drag-and-drop test — drag the "Signature Field" token from the palette onto the PDF page + Expected: A blue-bordered semi-transparent rectangle appears where you dropped it + + 3. Reload the page + Expected: The blue signature field rectangle is still visible (persisted to DB) + + 4. Click the × button on the placed signature field + Expected: The rectangle disappears from the overlay and from the DB (reload to confirm) + + 5. Place at least one signature field — drag a new one onto the document + + 6. In the "Prepare Document" panel on the right: + - Select a client from the dropdown + - Add one text fill row: label = "test" (or any AcroForm field name from the PDF), value = "Test Value" + - Click "Prepare and Send" + Expected: Loading state ("Preparing..."), then success message ("Document prepared successfully...") + + 7. After success, check the document status + Expected: Status shows "Sent" on the document page (the heading area) and on the portal dashboard + + 8. Verify the prepared PDF was created on disk: + ``` + ls uploads/clients/ + ``` + Expected: A file named `{docId}_prepared.pdf` exists in uploads/clients/{clientId}/ + + 9. Verify the prepared PDF is readable — open it with a PDF viewer: + ``` + open uploads/clients/{clientId}/{docId}_prepared.pdf + ``` + Expected: PDF opens and shows a blue rectangle with "Sign Here" label at the position where you placed the field + + 10. Verify a document that is already "Sent" shows the locked message in PreparePanel + (Navigate to the document you just prepared) + Expected: "Document status is Sent — preparation is only available for Draft documents." + + Type "approved" if all 10 steps pass, or describe which step(s) failed and what you observed. + + + + + +Human tester confirms all 10 verification steps pass. + + + +All 10 verification steps pass as confirmed by human tester. Phase 5 declared complete. + +Requirements covered: +- DOC-04: Drag-and-drop signature field placement — confirmed in steps 2-5 +- DOC-05: Text fill fields — confirmed in step 6 +- DOC-06: Assign to client + initiate signing request (status → Sent) — confirmed in steps 6-8 + + + +After completion, create `.planning/phases/05-pdf-fill-and-field-mapping/05-04-SUMMARY.md` +