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