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:
@@ -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
|
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)
|
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
|
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:
|
Plans:
|
||||||
- [ ] 10-01-PLAN.md — FieldPlacer palette: four new draggable tokens (text, checkbox, initials, date) with distinct visual affordances
|
- [ ] 10-01-PLAN.md — FieldPlacer palette: 5 typed tokens with distinct colors; type-aware field overlays and DragOverlay ghost
|
||||||
- [ ] 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-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 — Full Phase 10 human verification checkpoint (all four field types placed, prepared, and verified in PDF output)
|
- [ ] 10-03-PLAN.md — SigningPageClient initials capture + overlay suppression; SignatureModal title prop; human verification checkpoint
|
||||||
|
|
||||||
### Phase 11: Agent Saved Signature and Signing Workflow
|
### 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
|
**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
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user