diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index b027011..465af69 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -204,12 +204,12 @@ Plans: 2. Prepared PDF embeds text fields as typed stamps, checkboxes as boolean marks, initials as placeholder markers, and date fields as auto-stamped signing-date values 3. Client signing page correctly handles initials fields (prompts for initials capture) and ignores text/checkbox/date fields (already embedded at prepare time) 4. A round-trip test (place all four types, prepare, open signing link) produces a correctly embedded PDF with no field type rendered in the wrong position -**Plans**: TBD +**Plans**: 3 plans Plans: -- [ ] 10-01-PLAN.md — FieldPlacer palette: four new draggable tokens (text, checkbox, initials, date) with distinct visual affordances -- [ ] 10-02-PLAN.md — prepare-document.ts type-aware rendering switch: text stamp, checkbox embed, date auto-stamp, initials placeholder via @cantoo/pdf-lib -- [ ] 10-03-PLAN.md — Full Phase 10 human verification checkpoint (all four field types placed, prepared, and verified in PDF output) +- [ ] 10-01-PLAN.md — FieldPlacer palette: 5 typed tokens with distinct colors; type-aware field overlays and DragOverlay ghost +- [ ] 10-02-PLAN.md — preparePdf() type-branched rendering (checkbox X, date placeholder, initials placeholder, text bg); POST route signable filter + date stamp at sign time +- [ ] 10-03-PLAN.md — SigningPageClient initials capture + overlay suppression; SignatureModal title prop; human verification checkpoint ### Phase 11: Agent Saved Signature and Signing Workflow **Goal**: Agent draws a signature once, saves it to their profile, places agent signature fields on documents, and applies the saved signature during preparation — before the document is sent to the client diff --git a/.planning/phases/10-expanded-field-types-end-to-end/10-01-PLAN.md b/.planning/phases/10-expanded-field-types-end-to-end/10-01-PLAN.md new file mode 100644 index 0000000..0e8714d --- /dev/null +++ b/.planning/phases/10-expanded-field-types-end-to-end/10-01-PLAN.md @@ -0,0 +1,274 @@ +--- +phase: 10-expanded-field-types-end-to-end +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx +autonomous: true +requirements: + - FIELD-02 + - FIELD-03 + - FIELD-04 + +must_haves: + truths: + - "Agent can drag a Checkbox token from the palette and drop it onto any PDF page" + - "Agent can drag an Initials token from the palette and drop it onto any PDF page" + - "Agent can drag a Date token from the palette and drop it onto any PDF page" + - "Agent can drag a Text token from the palette and drop it onto any PDF page" + - "Placed fields show distinct labels and colors per type (not all 'Signature')" + - "The drag ghost overlay shows the correct label while dragging each token type" + - "Dropped fields persist with the correct type property in the database" + - "Checkbox fields drop at 24x24px; all other new types drop at 144x36px" + artifacts: + - path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx" + provides: "5-token typed palette (Signature, Initials, Checkbox, Date, Text) with per-type colors and DragOverlay" + contains: "DraggableToken.*id.*label.*color" + key_links: + - from: "DraggableToken id prop" + to: "SignatureFieldData.type" + via: "handleDragEnd dispatches active.id as field type" + pattern: "active\\.id.*SignatureFieldType" + - from: "handleDragEnd" + to: "persistFields" + via: "newField.type set from active.id before append" + pattern: "type.*droppedType" +--- + + +Extend the FieldPlacer palette from a single generic "Signature" token to five typed tokens: Signature (client-signature), Initials, Checkbox, Date, and Text. Each token has a distinct color and label. Dropped fields write the correct `type` property to `SignatureFieldData`. Field overlays and the drag ghost display the field type rather than a generic "Signature" label. + +Purpose: Agent must be able to place the correct field marker type for each intent before the prepare pipeline can distinguish how to render each field. This is the palette-side prerequisite for Plans 02 and 03. +Output: Updated `FieldPlacer.tsx` with 5 typed draggable tokens, per-type overlay colors/labels, type-aware DragOverlay ghost, and checkbox-appropriate drop dimensions. + + + +@/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/10-expanded-field-types-end-to-end/10-RESEARCH.md + + + + +From teressa-copeland-homes/src/lib/db/schema.ts: +```typescript +export type SignatureFieldType = + | 'client-signature' + | 'initials' + | 'text' + | 'checkbox' + | 'date' + | 'agent-signature'; + +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) + height: number; // PDF points (default: 36) + type?: SignatureFieldType; // Optional — v1.0 docs have no type; fallback = 'client-signature' +} +``` + +From existing FieldPlacer.tsx (current state — to be modified): +- Single `DraggableToken` with hardcoded id `'signature-token'`, blue dashed border, "+ Signature Field" text +- `handleDragEnd` creates `newField` without setting `type` property (falls back to client-signature via getFieldType) +- `renderFields` shows generic "Signature" span for all placed fields +- `DragOverlay` renders static "Signature" label regardless of which token is being dragged +- `isDraggingToken` boolean tracks whether palette drag is active (not which token) +- Palette div: `` +- newField dimensions hardcoded: width 144, height 36 + + + + + + + Task 1: Parameterize DraggableToken and add four new palette tokens + teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx + +Modify `DraggableToken` to accept `id`, `label`, and `color` props (replacing the hardcoded blue dashed border style). The color prop drives the border color, background tint, and text color of the token. + +Add import for `getFieldType` and `SignatureFieldType` from `@/lib/db/schema`: +```typescript +import { getFieldType, type SignatureFieldType } from '@/lib/db/schema'; +``` + +Replace the hardcoded DraggableToken with a parameterized version: +```typescript +function DraggableToken({ id, label, color }: { id: string; label: string; color: string }) { + const { attributes, listeners, setNodeRef, isDragging } = useDraggable({ id }); + const style: React.CSSProperties = { + opacity: isDragging ? 0.4 : 1, + cursor: 'grab', + padding: '6px 12px', + border: `2px dashed ${color}`, + borderRadius: '4px', + background: `${color}14`, // ~8% opacity tint + color, + fontSize: '13px', + fontWeight: 600, + userSelect: 'none', + touchAction: 'none', + }; + return ( +
+ + {label} +
+ ); +} +``` + +Update the palette section to render five tokens with distinct colors: +```typescript +// Token color palette — each maps to a SignatureFieldType +const PALETTE_TOKENS: Array<{ id: SignatureFieldType; label: string; color: string }> = [ + { id: 'client-signature', label: 'Signature', color: '#2563eb' }, // blue + { id: 'initials', label: 'Initials', color: '#7c3aed' }, // purple + { id: 'checkbox', label: 'Checkbox', color: '#059669' }, // green + { id: 'date', label: 'Date', color: '#d97706' }, // amber + { id: 'text', label: 'Text', color: '#64748b' }, // slate +]; +``` + +In the palette JSX, replace the single `` with: +```tsx +{PALETTE_TOKENS.map((token) => ( + +))} +``` + +The DragOverlay currently shows a static "Signature" label. Track the active dragging token's id and label. Change `isDraggingToken` from `boolean` to `string | null` (the active token id, or null when not dragging). Update all three usages: +- `onDragStart`: `setIsDraggingToken(event.active.id as string)` +- `onDragEnd`: `setIsDraggingToken(null)` (currently `false`) +- `DragOverlay`: replace the static "Signature" text with a lookup from PALETTE_TOKENS using the active id + +Update the DragOverlay to show the correct ghost label and color: +```tsx + + {isDraggingToken ? (() => { + const tokenMeta = PALETTE_TOKENS.find((t) => t.id === isDraggingToken); + const label = tokenMeta?.label ?? 'Field'; + const color = tokenMeta?.color ?? '#2563eb'; + const isCheckbox = isDraggingToken === 'checkbox'; + return ( +
+ {!isCheckbox && label} +
+ ); + })() : null} +
+``` + +Note: `isDraggingToken` type changes from `boolean` to `string | null`. Update `useState(false)` to `useState(null)`. The condition check `isDraggingToken ? ...` still works correctly since any non-null string is truthy. +
+ TypeScript compiles without errors: `cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30` + Five typed tokens visible in palette with distinct colors; TypeScript compiles clean +
+ + + Task 2: Update handleDragEnd and renderFields for typed field creation + teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx + +Update `handleDragEnd` to write the correct `type` property and use type-appropriate dimensions for checkbox fields. + +Replace the `newField` construction block (currently creates a field with no `type`) with: +```typescript +// Determine the field type from the dnd-kit active.id (token id IS the SignatureFieldType) +const validTypes = new Set(['client-signature', 'initials', 'text', 'checkbox', 'date', 'agent-signature']); +const droppedType: SignatureFieldType = validTypes.has(active.id as string) + ? (active.id as SignatureFieldType) + : 'client-signature'; + +// Checkbox fields are square (24x24pt). All other types: 144x36pt. +const isCheckbox = droppedType === 'checkbox'; +const fieldW = isCheckbox ? 24 : 144; +const fieldH = isCheckbox ? 24 : 36; + +// Update clamping to use fieldW/fieldH instead of hardcoded 144/36 +const fieldWpx = (fieldW / pageInfo.originalWidth) * renderedW; +const fieldHpx = (fieldH / pageInfo.originalHeight) * renderedH; +``` + +Note: the existing code calculates `fieldWpx` and `fieldHpx` from hardcoded 144/36 — replace those hardcoded values with `fieldW`/`fieldH` from above. + +The `newField` object becomes: +```typescript +const newField: SignatureFieldData = { + id: crypto.randomUUID(), + page: currentPage, + x: pdfX, + y: pdfY, + width: fieldW, + height: fieldH, + type: droppedType, +}; +``` + +Update `renderFields` to display per-type labels and border colors. Replace the hardcoded `Signature` with a lookup that shows the field type label and uses the matching color: + +```typescript +// Inside renderFields, for each field: +const fieldType = getFieldType(field); +const tokenMeta = PALETTE_TOKENS.find((t) => t.id === fieldType); +const fieldColor = tokenMeta?.color ?? '#2563eb'; +const fieldLabel = tokenMeta?.label ?? 'Signature'; +``` + +Update the placed field div to use `fieldColor` for border and background (replacing the hardcoded `#2563eb` values), and render `{fieldLabel}` instead of `"Signature"` in the span. Keep all existing move/resize/delete behavior unchanged — only the visual colors and label text change. + +For the checkbox type specifically, the placed overlay will naturally be small (24x24px in PDF units, scaled to screen) — no special rendering needed beyond the color/label update. + + TypeScript compiles without errors: `cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30` + Dropping a Checkbox token creates a 24x24pt field with `type: 'checkbox'`; dropping Initials creates 144x36pt with `type: 'initials'`; dropping Date creates 144x36pt with `type: 'date'`; all placed field overlays show the correct label and color; TypeScript compiles clean + + +
+ + +Run TypeScript check: `cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit` + +Verify field type persistence in browser: +1. Open `npm run dev` and navigate to any document in the portal +2. The palette should show five tokens: Signature (blue), Initials (purple), Checkbox (green), Date (amber), Text (slate) +3. Drag a Checkbox token — ghost should be 24x24px square with green border, no text label +4. Drag an Initials token — ghost should be 144x36px with "Initials" label in purple +5. Drop each token type onto the PDF — each placed overlay should show the correct label and color +6. Refresh the page — fields should reload with their correct types (persisted via PUT /api/documents/[docId]/fields) + + + +- Palette renders five distinct typed tokens with matching colors +- `handleDragEnd` sets `newField.type` from `active.id` for all five types +- Checkbox drops at 24x24pt; all others drop at 144x36pt +- `renderFields` shows the correct label and border color per field type +- `DragOverlay` shows the correct ghost label and dimensions while dragging +- TypeScript compiles without errors +- Fields persist with `type` property after page reload + + + +After completion, create `.planning/phases/10-expanded-field-types-end-to-end/10-01-SUMMARY.md` following the summary template. + diff --git a/.planning/phases/10-expanded-field-types-end-to-end/10-02-PLAN.md b/.planning/phases/10-expanded-field-types-end-to-end/10-02-PLAN.md new file mode 100644 index 0000000..f30d772 --- /dev/null +++ b/.planning/phases/10-expanded-field-types-end-to-end/10-02-PLAN.md @@ -0,0 +1,359 @@ +--- +phase: 10-expanded-field-types-end-to-end +plan: 02 +type: execute +wave: 1 +depends_on: [] +files_modified: + - teressa-copeland-homes/src/lib/pdf/prepare-document.ts + - teressa-copeland-homes/src/app/api/sign/[token]/route.ts +autonomous: true +requirements: + - FIELD-02 + - FIELD-04 + +must_haves: + truths: + - "Checkbox fields are embedded in the prepared PDF as a bordered box with X diagonals" + - "Date fields get a light placeholder rectangle in the prepared PDF (not 'Sign Here')" + - "Initials fields get a placeholder rectangle labeled 'Initials' in the prepared PDF" + - "Text fields get a light background rectangle (no label) in the prepared PDF" + - "Client-signature fields continue to show a blue 'Sign Here' rectangle (unchanged)" + - "Date fields are stamped with the actual signing date at POST submission time, not prepare time" + - "POST handler no longer throws 500 for text/checkbox/date fields after client submits" + artifacts: + - path: "teressa-copeland-homes/src/lib/pdf/prepare-document.ts" + provides: "Type-branched field rendering: checkbox X-mark, date placeholder, initials placeholder, text background, sig placeholder" + contains: "getFieldType" + - path: "teressa-copeland-homes/src/app/api/sign/[token]/route.ts" + provides: "POST handler filters to signable fields only; stamps date text at sign time before embed" + contains: "signableFields.*filter" + key_links: + - from: "preparePdf() sigFields loop" + to: "@cantoo/pdf-lib drawLine/drawRectangle/drawText" + via: "getFieldType() branch dispatch" + pattern: "getFieldType.*checkbox|date|initials|text" + - from: "POST handler signaturesWithCoords" + to: "embedSignatureInPdf()" + via: "signableFields filter then map — only client-signature and initials" + pattern: "signableFields.*filter.*client-signature.*initials" + - from: "POST handler date stamp" + to: "preparedAbsPath PDF bytes" + via: "pdf-lib load/drawText at date field coordinates before embedSignatureInPdf" + pattern: "date.*drawText.*toLocaleDateString" +--- + + +Extend `preparePdf()` to render each field type distinctly rather than treating all fields as "Sign Here" signature placeholders. Fix the POST handler in `/api/sign/[token]/route.ts` to: (1) only require signatures for `client-signature` and `initials` fields (not text/checkbox/date), and (2) stamp the actual signing date onto any `date` fields at submission time before calling `embedSignatureInPdf()`. + +Purpose: Without these changes, the prepare pipeline paints all fields blue with "Sign Here", and the POST handler throws 500 for documents containing checkbox/text/date fields. This plan makes the pipeline correct for all four new field types end-to-end. +Output: Updated `prepare-document.ts` with type-branched rendering and updated `route.ts` with signable field filter and date stamping. + + + +@/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/10-expanded-field-types-end-to-end/10-RESEARCH.md + + + + +From teressa-copeland-homes/src/lib/db/schema.ts: +```typescript +export type SignatureFieldType = + | 'client-signature' | 'initials' | 'text' | 'checkbox' | 'date' | 'agent-signature'; + +export interface SignatureFieldData { + id: string; + page: number; + x: number; y: number; width: number; height: number; + type?: SignatureFieldType; +} + +export function getFieldType(field: SignatureFieldData): SignatureFieldType { + return field.type ?? 'client-signature'; +} +export function isClientVisibleField(field: SignatureFieldData): boolean { + return getFieldType(field) !== 'agent-signature'; +} +``` + +Current prepare-document.ts field loop (lines 91-110) — to be replaced: +```typescript +// Current: ALL fields get identical blue rectangle + "Sign Here" +for (const field of sigFields) { + const page = pages[field.page - 1]; + 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) }); +} +``` + +Current route.ts POST handler field mapping (lines 171-187) — to be fixed: +```typescript +// CRITICAL BUG: throws for text/checkbox/date fields (Missing signature for field X) +const signaturesWithCoords = (doc.signatureFields ?? []).map((field) => { + const clientSig = signatures.find((s) => s.fieldId === field.id); + if (!clientSig) throw new Error(`Missing signature for field ${field.id}`); + return { fieldId: field.id, dataURL: clientSig.dataURL, + x: field.x, y: field.y, width: field.width, height: field.height, page: field.page }; +}); +``` + +@cantoo/pdf-lib drawing primitives (confirmed available): +- `page.drawRectangle({ x, y, width, height, borderColor, borderWidth, color })` +- `page.drawLine({ start: {x,y}, end: {x,y}, thickness, color })` +- `page.drawText(text, { x, y, size, font, color })` +- `rgb(r, g, b)` where r/g/b are 0.0-1.0 + + + + + + + Task 1: Type-branched field rendering in preparePdf() + teressa-copeland-homes/src/lib/pdf/prepare-document.ts + +Add import for `getFieldType` at the top of the file: +```typescript +import type { SignatureFieldData } from '@/lib/db/schema'; +import { getFieldType } from '@/lib/db/schema'; +``` + +(Note: `SignatureFieldData` is already imported — just add `getFieldType` to the named imports.) + +Replace the entire field rendering loop (currently lines 91-110, starting with the comment "Draw signature field placeholders") with this type-branched version: + +```typescript +// Draw field placeholders — rendering varies by field type +for (const field of sigFields) { + const page = pages[field.page - 1]; // page is 1-indexed + if (!page) continue; + const fieldType = getFieldType(field); + + if (fieldType === 'client-signature') { + // Blue "Sign Here" placeholder — client will sign at signing time + 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), + }); + + } else if (fieldType === 'initials') { + // Purple "Initials" placeholder — client will initial at signing time + page.drawRectangle({ + x: field.x, y: field.y, width: field.width, height: field.height, + borderColor: rgb(0.49, 0.23, 0.93), borderWidth: 1.5, + color: rgb(0.95, 0.92, 1.0), + }); + page.drawText('Initials', { + x: field.x + 4, y: field.y + 4, size: 8, font: helvetica, + color: rgb(0.49, 0.23, 0.93), + }); + + } else if (fieldType === 'checkbox') { + // Checked box: light gray background + X crossing diagonals (embedded at prepare time) + page.drawRectangle({ + x: field.x, y: field.y, width: field.width, height: field.height, + borderColor: rgb(0.1, 0.1, 0.1), borderWidth: 1.5, + color: rgb(0.95, 0.95, 0.95), + }); + // X mark: two diagonals + page.drawLine({ + start: { x: field.x + 2, y: field.y + 2 }, + end: { x: field.x + field.width - 2, y: field.y + field.height - 2 }, + thickness: 1.5, color: rgb(0.1, 0.1, 0.1), + }); + page.drawLine({ + start: { x: field.x + field.width - 2, y: field.y + 2 }, + end: { x: field.x + 2, y: field.y + field.height - 2 }, + thickness: 1.5, color: rgb(0.1, 0.1, 0.1), + }); + + } else if (fieldType === 'date') { + // Light placeholder rectangle — actual signing date stamped at POST time in route.ts + page.drawRectangle({ + x: field.x, y: field.y, width: field.width, height: field.height, + borderColor: rgb(0.85, 0.47, 0.04), borderWidth: 1, + color: rgb(1.0, 0.97, 0.90), + }); + page.drawText('Date', { + x: field.x + 4, y: field.y + 4, size: 8, font: helvetica, + color: rgb(0.85, 0.47, 0.04), + }); + + } else if (fieldType === 'text') { + // Light background rectangle — text content is provided via textFillData (separate pipeline) + // type='text' SignatureFieldData fields are visual position markers only + page.drawRectangle({ + x: field.x, y: field.y, width: field.width, height: field.height, + borderColor: rgb(0.39, 0.45, 0.55), borderWidth: 1, + color: rgb(0.96, 0.97, 0.98), + }); + + } else if (fieldType === 'agent-signature') { + // Skip — agent signature handled by Phase 11; no placeholder drawn here + } +} +``` + +Do NOT touch the AcroForm strategy A/B text fill code above the loop — only replace the field loop. + + TypeScript compiles: `cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30` + TypeScript compiles clean; the field loop branches on getFieldType(); checkbox fields draw X diagonals; date fields draw amber placeholder; initials draw purple placeholder; client-signature behavior unchanged + + + + Task 2: Fix POST handler — signable field filter and date stamping at sign time + teressa-copeland-homes/src/app/api/sign/[token]/route.ts + +Add `getFieldType` to the import from `@/lib/db/schema`: +```typescript +import { signingTokens, documents, clients, isClientVisibleField, getFieldType } from '@/lib/db/schema'; +``` + +Add `PDFDocument` and `rgb` to the file for date stamping at sign time. Add this import after the existing imports: +```typescript +import { PDFDocument, StandardFonts, rgb } from '@cantoo/pdf-lib'; +``` + +Also add `readFile` and `writeFile` imports from Node.js (check if already imported — add if missing): +```typescript +import { readFile, writeFile, rename } from 'node:fs/promises'; +``` + +Replace step 8 "Merge client-supplied dataURLs with server-stored field coordinates" (the `signaturesWithCoords` block at lines ~171-187) with this two-part replacement: + +**Part A — Date stamping before embed:** +Before building `signaturesWithCoords`, stamp the actual signing date onto each `date` field in the prepared PDF. This modifies the prepared PDF bytes in-memory before passing them to `embedSignatureInPdf()`. + +```typescript +// 8a. Stamp date text at each 'date' field coordinate (signing date = now, captured server-side) +const dateFields = (doc.signatureFields ?? []).filter( + (f) => getFieldType(f) === 'date' +); + +// Only load and modify PDF if there are date fields to stamp +let dateStampedPath = preparedAbsPath; +if (dateFields.length > 0) { + const pdfBytes = await readFile(preparedAbsPath); + const pdfDoc = await PDFDocument.load(pdfBytes); + const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica); + const pages = pdfDoc.getPages(); + const signingDateStr = now.toLocaleDateString('en-US', { + month: '2-digit', day: '2-digit', year: 'numeric', + }); + for (const field of dateFields) { + const page = pages[field.page - 1]; + if (!page) continue; + // Overwrite the amber placeholder rectangle with white background + date text + page.drawRectangle({ + x: field.x, y: field.y, width: field.width, height: field.height, + borderColor: rgb(0.39, 0.45, 0.55), borderWidth: 0.5, + color: rgb(1.0, 1.0, 1.0), + }); + page.drawText(signingDateStr, { + x: field.x + 4, + y: field.y + field.height / 2 - 4, // vertically center + size: 10, font: helvetica, color: rgb(0.05, 0.05, 0.55), + }); + } + const stampedBytes = await pdfDoc.save(); + // Write to a temporary date-stamped path; embedSignatureInPdf reads from this path + dateStampedPath = `${preparedAbsPath}.datestamped.tmp`; + await writeFile(dateStampedPath, stampedBytes); +} +``` + +Note: `now` is already defined later in the file (line ~208). Move the `const now = new Date();` declaration UP to before step 8, so it can be reused for both date stamping and the documents table update. Currently `now` is defined inside step 11 — hoist it to the top of the POST handler body (after payload verification but before any async DB work would be awkward; hoist it to just before step 8a). + +**Part B — Filter signaturesWithCoords to signable fields only:** +```typescript +// 8b. Build signaturesWithCoords for client-signable fields only (client-signature + initials) +// text/checkbox/date are embedded at prepare time; the client was never shown these as interactive fields +const signableFields = (doc.signatureFields ?? []).filter((f) => { + const t = getFieldType(f); + return t === 'client-signature' || t === 'initials'; +}); + +const signaturesWithCoords = signableFields.map((field) => { + const clientSig = signatures.find((s) => s.fieldId === field.id); + if (!clientSig) throw new Error(`Missing signature for field ${field.id}`); + return { + fieldId: field.id, + dataURL: clientSig.dataURL, + x: field.x, y: field.y, width: field.width, height: field.height, page: field.page, + }; +}); +``` + +**Update the embedSignatureInPdf call (step 9)** to read from `dateStampedPath` instead of `preparedAbsPath`: +```typescript +pdfHash = await embedSignatureInPdf(dateStampedPath, signedAbsPath, signaturesWithCoords); +``` + +**Add cleanup of the temporary date-stamped file after embed** (fire-and-forget, non-fatal): +```typescript +// Clean up temporary date-stamped file if it was created +if (dateStampedPath !== preparedAbsPath) { + import('node:fs/promises').then(({ unlink }) => unlink(dateStampedPath).catch(() => {})); +} +``` + +Actually — `readFile`, `writeFile` are already being imported at the top of the file since it already imports from `node:path`. Check the existing imports and add only what is missing. The `rename` import from `node:fs/promises` is NOT yet in route.ts (it's in prepare-document.ts). Add `readFile`, `writeFile`, and `unlink` to a new import: +```typescript +import { readFile, writeFile, unlink } from 'node:fs/promises'; +``` + +Do NOT use dynamic imports for the cleanup — use the statically imported `unlink` instead. + +Summary of all changes to route.ts: +1. Add `getFieldType` to the schema import +2. Add `import { PDFDocument, StandardFonts, rgb } from '@cantoo/pdf-lib'` +3. Add `import { readFile, writeFile, unlink } from 'node:fs/promises'` +4. Hoist `const now = new Date()` to before step 8 (remove it from step 11 where it currently lives) +5. Insert step 8a (date field stamping) after building absolute paths (step 7) and before signaturesWithCoords +6. Replace step 8 signaturesWithCoords block with the signableFields filter + map (Part B) +7. Update step 9 embedSignatureInPdf call to use `dateStampedPath` +8. Add unlink cleanup after embed + + TypeScript compiles: `cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30` + TypeScript compiles clean; POST handler no longer maps all signatureFields; only client-signature and initials fields are in signaturesWithCoords; date fields are stamped with the signing date at submission time + + + + + +1. TypeScript: `cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit` +2. Verify prepare-document.ts renders different field types by manual inspection or a quick Node script that creates a test PDF with all field types and checks the output. +3. Verify route.ts POST handler filter: `grep -n "signableFields\|getFieldType" /Users/ccopeland/temp/red/teressa-copeland-homes/src/app/api/sign/\[token\]/route.ts` — should show the filter on signable fields. +4. Verify date stamping code is present: `grep -n "toLocaleDateString\|datestamped" /Users/ccopeland/temp/red/teressa-copeland-homes/src/app/api/sign/\[token\]/route.ts` + + + +- `preparePdf()` field loop branches on `getFieldType()` for all six field types +- Checkbox fields produce an X-mark in the prepared PDF +- Date fields produce an amber placeholder rectangle labeled "Date" in the prepared PDF +- Initials fields produce a purple placeholder rectangle labeled "Initials" in the prepared PDF +- Text fields produce a light background rectangle with no label +- Client-signature behavior is unchanged (blue rectangle + "Sign Here") +- POST handler `signaturesWithCoords` only contains `client-signature` and `initials` entries +- Date fields are stamped with `now.toLocaleDateString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric' })` at POST time +- TypeScript compiles without errors + + + +After completion, create `.planning/phases/10-expanded-field-types-end-to-end/10-02-SUMMARY.md` following the summary template. + diff --git a/.planning/phases/10-expanded-field-types-end-to-end/10-03-PLAN.md b/.planning/phases/10-expanded-field-types-end-to-end/10-03-PLAN.md new file mode 100644 index 0000000..b7ce716 --- /dev/null +++ b/.planning/phases/10-expanded-field-types-end-to-end/10-03-PLAN.md @@ -0,0 +1,354 @@ +--- +phase: 10-expanded-field-types-end-to-end +plan: 03 +type: execute +wave: 2 +depends_on: + - 10-01 + - 10-02 +files_modified: + - teressa-copeland-homes/src/app/sign/[token]/_components/SigningPageClient.tsx + - teressa-copeland-homes/src/app/sign/[token]/_components/SignatureModal.tsx +autonomous: false +requirements: + - FIELD-01 + - FIELD-02 + - FIELD-03 + - FIELD-04 + +must_haves: + truths: + - "Client sees only interactive overlays (blue for signature, purple for initials) — no clickable overlays for text/checkbox/date fields" + - "Clicking an initials overlay opens the signature modal with an 'Add Initials' title" + - "Initials fields count toward the signing progress total (along with client-signature fields)" + - "Submit button requires ALL initials AND signatures to be completed before enabling" + - "Signed PDF embeds initials images at the correct field coordinates" + - "Text/checkbox/date fields are already baked into the prepared PDF and require no client interaction" + - "A full round-trip (place all 4 types + prepare + send + sign) succeeds without errors" + artifacts: + - path: "teressa-copeland-homes/src/app/sign/[token]/_components/SigningPageClient.tsx" + provides: "Initials modal support; non-interactive field overlay suppression; updated progress counting" + contains: "initials.*getFieldType" + - path: "teressa-copeland-homes/src/app/sign/[token]/_components/SignatureModal.tsx" + provides: "Optional title prop so initials modal shows 'Add Initials' instead of 'Add Signature'" + contains: "title.*prop" + key_links: + - from: "handleFieldClick" + to: "setModalOpen(true)" + via: "getFieldType check allows both client-signature AND initials" + pattern: "client-signature.*initials.*setModalOpen" + - from: "SigningProgressBar total" + to: "signatureFields.filter" + via: "filter includes both client-signature and initials types" + pattern: "client-signature.*initials.*length" + - from: "handleSubmit completeness check" + to: "requiredFields.length" + via: "requiredFields filters to client-signature + initials" + pattern: "requiredFields.*filter.*client-signature.*initials" +--- + + +Extend `SigningPageClient.tsx` to handle the initials field type in the signing flow: open the signature modal for initials fields, suppress non-interactive overlays for text/checkbox/date fields, and count initials fields in the signing progress. Add an optional `title` prop to `SignatureModal.tsx` so the initials modal displays "Add Initials" instead of "Add Signature". Close the phase with a human verification checkpoint covering all four new field types end-to-end. + +Purpose: After Plans 01 and 02, all field types can be placed and embedded correctly. This plan makes the client-facing signing experience correct — initials are captured, non-interactive fields are invisible to the client, and the submit gate accounts for all required fields. +Output: Updated `SigningPageClient.tsx` and `SignatureModal.tsx`; human verification checkpoint confirming the full four-field-type round-trip. + + + +@/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/10-expanded-field-types-end-to-end/10-RESEARCH.md +@.planning/phases/10-expanded-field-types-end-to-end/10-01-SUMMARY.md +@.planning/phases/10-expanded-field-types-end-to-end/10-02-SUMMARY.md + + + + +From teressa-copeland-homes/src/lib/db/schema.ts: +```typescript +export type SignatureFieldType = + | 'client-signature' | 'initials' | 'text' | 'checkbox' | 'date' | 'agent-signature'; + +export function getFieldType(field: SignatureFieldData): SignatureFieldType { + return field.type ?? 'client-signature'; +} +export function isClientVisibleField(field: SignatureFieldData): boolean { + return getFieldType(field) !== 'agent-signature'; +} +``` + +Current SigningPageClient.tsx key behaviors (to be updated): +```typescript +// handleFieldClick — currently only allows client-signature: +if (getFieldType(field) !== 'client-signature') return; + +// handleSubmit completeness check — currently only client-signature: +const clientSigFields = signatureFields.filter( + (f) => getFieldType(f) === 'client-signature' +); +if (signedFields.size < clientSigFields.length || submitting) return; + +// SigningProgressBar total — currently only client-signature: +total={signatureFields.filter((f) => getFieldType(f) === 'client-signature').length} + +// Field overlay render — currently renders ALL fields from signatureFields as clickable overlays +// (server GET filter excludes agent-signature but includes text/checkbox/date) +// After Plan 02 these non-interactive fields are embedded in the prepared PDF +// but still come back from the server — the signing page must NOT render them as overlays +``` + +Current SignatureModal.tsx header (to add title prop): +```typescript +// Line 115: +

Add Signature

+``` + +Current SignatureModal props: +```typescript +interface SignatureModalProps { + isOpen: boolean; + fieldId: string; + onConfirm: (fieldId: string, dataURL: string, save: boolean) => void; + onClose: () => void; +} +``` +
+
+ + + + + Task 1: Add title prop to SignatureModal and extend signing page for initials + overlay filtering + + teressa-copeland-homes/src/app/sign/[token]/_components/SignatureModal.tsx + teressa-copeland-homes/src/app/sign/[token]/_components/SigningPageClient.tsx + + +**SignatureModal.tsx — add optional title prop:** + +Update `SignatureModalProps` to include an optional `title` prop: +```typescript +interface SignatureModalProps { + isOpen: boolean; + fieldId: string; + onConfirm: (fieldId: string, dataURL: string, save: boolean) => void; + onClose: () => void; + title?: string; // defaults to "Add Signature" +} +``` + +Update the function signature: +```typescript +export function SignatureModal({ isOpen, fieldId, onConfirm, onClose, title = 'Add Signature' }: SignatureModalProps) { +``` + +Update the hardcoded header text to use the prop: +```typescript +// Replace:

Add Signature

+

{title}

+``` + +Also update the "Apply Signature" confirm button text to be dynamic. When the title is "Add Initials", the button should say "Apply Initials". Add a derived `buttonLabel`: +```typescript +// Derive button label from title — "Add Initials" → "Apply Initials", otherwise "Apply Signature" +const buttonLabel = title.startsWith('Add ') ? title.replace('Add ', 'Apply ') : 'Apply Signature'; +``` +Replace the hardcoded "Apply Signature" button text with `{buttonLabel}`. + +No other changes to SignatureModal — canvas, tabs, signature_pad wiring, save behavior all remain identical. + +**SigningPageClient.tsx — three targeted changes:** + +**Change 1: Track active field type for modal title.** +Add state for the active field type: +```typescript +const [activeFieldType, setActiveFieldType] = useState<'client-signature' | 'initials'>('client-signature'); +``` + +**Change 2: Update `handleFieldClick` to open modal for both client-signature and initials:** +```typescript +const handleFieldClick = useCallback( + (fieldId: string) => { + const field = signatureFields.find((f) => f.id === fieldId); + if (!field) return; + const ft = getFieldType(field); + // Only client-signature and initials require client action + if (ft !== 'client-signature' && ft !== 'initials') return; + if (signedFields.has(fieldId)) return; + setActiveFieldId(fieldId); + setActiveFieldType(ft as 'client-signature' | 'initials'); + setModalOpen(true); + }, + [signatureFields, signedFields] +); +``` + +**Change 3: Update handleSubmit, handleJumpToNext, SigningProgressBar, and field overlay rendering.** + +`handleSubmit` — replace `clientSigFields` filter with `requiredFields`: +```typescript +const requiredFields = signatureFields.filter( + (f) => getFieldType(f) === 'client-signature' || getFieldType(f) === 'initials' +); +if (signedFields.size < requiredFields.length || submitting) return; +``` + +`handleJumpToNext` — update to jump to next unsigned REQUIRED field: +```typescript +const nextUnsigned = signatureFields.find( + (f) => (getFieldType(f) === 'client-signature' || getFieldType(f) === 'initials') && !signedFields.has(f.id) +); +``` + +`SigningProgressBar total` — update to count both types: +```typescript +total={signatureFields.filter( + (f) => getFieldType(f) === 'client-signature' || getFieldType(f) === 'initials' +).length} +``` + +**Field overlay rendering — suppress non-interactive fields:** +In the `fieldsOnPage.map((field) => {...})` section, add an early return for non-interactive field types. Text, checkbox, and date fields are already baked into the prepared PDF — they must NOT render as clickable overlays on the signing page. + +Add this check at the top of the field map callback (before computing `isSigned` and `overlayStyle`): +```typescript +// Only render interactive overlays for client-signature and initials fields +// text/checkbox/date are embedded at prepare time — no client interaction needed +const ft = getFieldType(field); +const isInteractive = ft === 'client-signature' || ft === 'initials'; +if (!isInteractive) return null; +``` + +**Update overlay visual distinction for initials vs signature:** +After the `isInteractive` check (and before the existing `isSigned` / `overlayStyle` computation), add a `fieldColor` variable and update the `aria-label`: +```typescript +const isSigned = signedFields.has(field.id); +const overlayStyle = { + ...getFieldOverlayStyle(field, dims), + // Initials: purple pulse; signature: blue pulse (uses CSS animation-name override) +}; +``` + +For the initials color differentiation, add a `