docs(10-expanded-field-types-end-to-end): create phase 10 plan

Three plans covering the full field type pipeline end-to-end:
- 10-01: FieldPlacer palette extension (5 typed tokens, per-type colors, typed DragOverlay)
- 10-02: preparePdf() type-branched rendering + POST route signable filter and date stamp
- 10-03: SigningPageClient initials capture + overlay suppression + human verification

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Chandler Copeland
2026-03-21 12:45:25 -06:00
parent fadfbb3fd7
commit 2205a7bce5
4 changed files with 991 additions and 4 deletions

View File

@@ -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

View File

@@ -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"
---
<objective>
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.
</objective>
<execution_context>
@/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md
@/Users/ccopeland/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/10-expanded-field-types-end-to-end/10-RESEARCH.md
<interfaces>
<!-- Key types used by FieldPlacer. Executor must use these directly. -->
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: `<DraggableToken id="signature-token" />`
- newField dimensions hardcoded: width 144, height 36
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Parameterize DraggableToken and add four new palette tokens</name>
<files>teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx</files>
<action>
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 (
<div ref={setNodeRef} style={style} {...listeners} {...attributes}>
+ {label}
</div>
);
}
```
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 `<DraggableToken id="signature-token" />` with:
```tsx
{PALETTE_TOKENS.map((token) => (
<DraggableToken key={token.id} id={token.id} label={token.label} color={token.color} />
))}
```
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
<DragOverlay dropAnimation={null}>
{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 (
<div style={{
width: isCheckbox ? 24 : 144,
height: isCheckbox ? 24 : 36,
border: `2px solid ${color}`,
background: `${color}26`,
borderRadius: '2px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '11px',
color,
fontWeight: 600,
pointerEvents: 'none',
}}>
{!isCheckbox && label}
</div>
);
})() : null}
</DragOverlay>
```
Note: `isDraggingToken` type changes from `boolean` to `string | null`. Update `useState<boolean>(false)` to `useState<string | null>(null)`. The condition check `isDraggingToken ? ...` still works correctly since any non-null string is truthy.
</action>
<verify>TypeScript compiles without errors: `cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30`</verify>
<done>Five typed tokens visible in palette with distinct colors; TypeScript compiles clean</done>
</task>
<task type="auto">
<name>Task 2: Update handleDragEnd and renderFields for typed field creation</name>
<files>teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx</files>
<action>
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<string>(['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 `<span style={{ pointerEvents: 'none' }}>Signature</span>` 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.
</action>
<verify>TypeScript compiles without errors: `cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30`</verify>
<done>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</done>
</task>
</tasks>
<verification>
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)
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
After completion, create `.planning/phases/10-expanded-field-types-end-to-end/10-01-SUMMARY.md` following the summary template.
</output>

View File

@@ -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"
---
<objective>
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.
</objective>
<execution_context>
@/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md
@/Users/ccopeland/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/10-expanded-field-types-end-to-end/10-RESEARCH.md
<interfaces>
<!-- Key types and current implementation. Executor must use these directly. -->
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
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Type-branched field rendering in preparePdf()</name>
<files>teressa-copeland-homes/src/lib/pdf/prepare-document.ts</files>
<action>
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.
</action>
<verify>TypeScript compiles: `cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30`</verify>
<done>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</done>
</task>
<task type="auto">
<name>Task 2: Fix POST handler — signable field filter and date stamping at sign time</name>
<files>teressa-copeland-homes/src/app/api/sign/[token]/route.ts</files>
<action>
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
</action>
<verify>TypeScript compiles: `cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30`</verify>
<done>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</done>
</task>
</tasks>
<verification>
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`
</verification>
<success_criteria>
- `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
</success_criteria>
<output>
After completion, create `.planning/phases/10-expanded-field-types-end-to-end/10-02-SUMMARY.md` following the summary template.
</output>

View File

@@ -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"
---
<objective>
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.
</objective>
<execution_context>
@/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md
@/Users/ccopeland/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/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
<interfaces>
<!-- Key types and current state of files to be modified. -->
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:
<h2 style={{ color: '#1B2B4B', margin: 0, fontSize: '18px' }}>Add Signature</h2>
```
Current SignatureModal props:
```typescript
interface SignatureModalProps {
isOpen: boolean;
fieldId: string;
onConfirm: (fieldId: string, dataURL: string, save: boolean) => void;
onClose: () => void;
}
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Add title prop to SignatureModal and extend signing page for initials + overlay filtering</name>
<files>
teressa-copeland-homes/src/app/sign/[token]/_components/SignatureModal.tsx
teressa-copeland-homes/src/app/sign/[token]/_components/SigningPageClient.tsx
</files>
<action>
**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: <h2 ...>Add Signature</h2>
<h2 style={{ color: '#1B2B4B', margin: 0, fontSize: '18px' }}>{title}</h2>
```
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 `<style>` injection alongside the existing `pulse-border` keyframes for a purple variant:
```css
@keyframes pulse-border-purple {
0%, 100% { box-shadow: 0 0 0 2px #7c3aed, 0 0 8px 2px rgba(124,58,237,0.4); }
50% { box-shadow: 0 0 0 3px #7c3aed, 0 0 16px 4px rgba(124,58,237,0.6); }
}
```
Apply `animation: 'pulse-border-purple 2s infinite'` to initials field overlays and `animation: 'pulse-border 2s infinite'` to signature field overlays. Update `getFieldOverlayStyle` to accept a field type parameter, or add an `animation` property override after the call:
```typescript
const baseStyle = getFieldOverlayStyle(field, dims);
const animationStyle: React.CSSProperties = ft === 'initials'
? { animation: 'pulse-border-purple 2s infinite' }
: { animation: 'pulse-border 2s infinite' };
const fieldOverlayStyle = { ...baseStyle, ...animationStyle };
```
Update the overlay div to use `fieldOverlayStyle` and update `aria-label`:
```typescript
aria-label={ft === 'initials'
? `Initials field${isSigned ? ' (initialed)' : ' — click to initial'}`
: `Signature field${isSigned ? ' (signed)' : ' — click to sign'}`}
```
**Update SignatureModal usage** to pass the title prop:
```typescript
<SignatureModal
isOpen={modalOpen}
fieldId={activeFieldId ?? ''}
title={activeFieldType === 'initials' ? 'Add Initials' : 'Add Signature'}
onConfirm={handleModalConfirm}
onClose={() => {
setModalOpen(false);
setActiveFieldId(null);
}}
/>
```
</action>
<verify>TypeScript compiles: `cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30`</verify>
<done>TypeScript compiles clean; SignatureModal accepts optional title prop; SigningPageClient only renders interactive overlays for client-signature and initials; initials modal shows "Add Initials" title; progress bar counts both types; submit gate requires all required fields</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 2: Human verification — all four field types end-to-end</name>
<what-built>
Full Phase 10 implementation across all three plans:
- Plan 01: FieldPlacer palette extended with 5 typed tokens (Signature, Initials, Checkbox, Date, Text)
- Plan 02: preparePdf() renders each field type distinctly; POST handler filter + date stamp at sign time
- Plan 03: SigningPageClient initials capture; non-interactive overlay suppression; updated progress counting
The complete end-to-end flow for all four new field types is now implemented.
</what-built>
<how-to-verify>
Run `npm run dev` in `/Users/ccopeland/temp/red/teressa-copeland-homes` and verify:
**Step 1 — FieldPlacer palette:**
1. Open any document in the portal (e.g. /portal/documents/[docId])
2. Confirm the palette shows 5 tokens with distinct colors: Signature (blue), Initials (purple), Checkbox (green), Date (amber), Text (slate)
3. Drag each token — confirm the drag ghost shows the correct label and color
4. Drag a Checkbox token — confirm the ghost is small (square) with no text label
5. Drop all 5 token types onto the PDF — confirm each placed overlay shows the correct label and color
**Step 2 — Prepare pipeline (all 4 new types):**
1. With at least one each of Checkbox, Date, Initials, and Text fields placed, click "Prepare and Send"
2. Download the prepared PDF and open it in any PDF viewer
3. Verify: Checkbox field shows a bordered box with X diagonals
4. Verify: Date field shows an amber placeholder rectangle labeled "Date" (not the actual date yet — that stamps at sign time)
5. Verify: Initials field shows a purple bordered rectangle labeled "Initials"
6. Verify: Text field shows a light gray background rectangle
7. Verify: Signature field (if any placed) shows the blue "Sign Here" rectangle as before
**Step 3 — Signing page (initials + overlay suppression):**
1. Open the signing link (email or copy from prepare flow)
2. Confirm: Only Signature (blue pulse) and Initials (purple pulse) overlays are clickable — no overlay visible over checkbox/date/text areas
3. Click an Initials overlay — confirm the modal opens with "Add Initials" title and "Apply Initials" button
4. Draw initials and apply — confirm the initials overlay shows a preview image and turns green
5. Confirm the progress bar counts both signature and initials fields in the total
6. Complete all signature and initials fields — confirm the Submit button enables
7. Submit and confirm redirect to the confirmed page
**Step 4 — Post-signing PDF verification:**
1. Download the signed PDF from the agent portal
2. Open it and verify:
- Initials image is embedded at the initials field position
- Checkbox still shows the X mark (from prepare time)
- Date field shows the actual signing date (e.g. 03/21/2026) — NOT the placeholder
- Signature image is embedded at the signature field position
- Text field shows the light background rectangle (text fill content from textFillData if any)
</how-to-verify>
<resume-signal>Type "approved" when all 4 steps pass, or describe which verification failed</resume-signal>
<action>Run the dev server and execute the 4-step verification outlined in how-to-verify above.</action>
<verify>All 4 steps pass and human types "approved"</verify>
<done>Full end-to-end round-trip confirmed: all four field types place, prepare, and sign correctly</done>
</task>
</tasks>
<verification>
TypeScript: `cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit`
Then proceed to the human verification checkpoint above.
</verification>
<success_criteria>
- SignatureModal renders with "Add Initials" title and "Apply Initials" button when mode is initials
- SigningPageClient.tsx renders zero overlays for text/checkbox/date fields
- Initials fields open the modal and produce an embeddable dataURL
- Progress bar total = count of (client-signature + initials) fields
- Submit gate requires all (client-signature + initials) fields completed
- Full round-trip test: place all 4 new field types + prepare + sign → produces correct PDF
- Human verification approved
- TypeScript compiles without errors
</success_criteria>
<output>
After completion, create `.planning/phases/10-expanded-field-types-end-to-end/10-03-SUMMARY.md` following the summary template.
</output>