docs(12.1): create phase plan — per-field text editing and quick-fill

This commit is contained in:
Chandler Copeland
2026-03-21 16:15:55 -06:00
parent 0eeed0fb73
commit 62ba448460
3 changed files with 872 additions and 1 deletions

View File

@@ -272,7 +272,11 @@ Plans:
3. When a text field is selected, PreparePanel shows quick-fill suggestion buttons (Client Name, Property Address, Client Email) that insert the corresponding value into the selected field 3. When a text field is selected, PreparePanel shows quick-fill suggestion buttons (Client Name, Property Address, Client Email) that insert the corresponding value into the selected field
4. Text fill values entered per-field appear correctly embedded in the preview PDF and the final prepared PDF 4. Text fill values entered per-field appear correctly embedded in the preview PDF and the final prepared PDF
5. The staleness token (previewToken) is reset when any text field value changes 5. The staleness token (previewToken) is reset when any text field value changes
**Plans**: TBD **Plans**: 2 plans
Plans:
- [ ] 12.1-01-PLAN.md — preparePdf() field-ID-keyed text lookup + Strategy B removal; FieldPlacer click-to-select inline input; PdfViewer + PdfViewerWrapper optional prop chain
- [ ] 12.1-02-PLAN.md — DocumentPageClient selectedFieldId + textFillData shared state; PreparePanel QuickFillPanel + TextFillForm removal; human verification checkpoint
### Phase 13: AI Field Placement and Pre-fill ### Phase 13: AI Field Placement and Pre-fill
**Goal**: Agent clicks one button and AI auto-places all field types on the PDF in correct positions and pre-fills text fields with known client and property data **Goal**: Agent clicks one button and AI auto-places all field types on the PDF in correct positions and pre-fills text fields with known client and property data
@@ -311,4 +315,5 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 →
| 11. Agent Saved Signature and Signing Workflow | 3/3 | Complete | 2026-03-21 | - | | 11. Agent Saved Signature and Signing Workflow | 3/3 | Complete | 2026-03-21 | - |
| 11.1. Agent and Client Initials (INSERTED) | 3/3 | Complete | 2026-03-21 | - | | 11.1. Agent and Client Initials (INSERTED) | 3/3 | Complete | 2026-03-21 | - |
| 12. Filled Document Preview | 2/2 | Complete | 2026-03-21 | - | | 12. Filled Document Preview | 2/2 | Complete | 2026-03-21 | - |
| 12.1. Per-Field Text Editing and Quick-Fill (INSERTED) | v1.1 | 0/2 | Not started | - |
| 13. AI Field Placement and Pre-fill | v1.1 | 0/4 | Not started | - | | 13. AI Field Placement and Pre-fill | v1.1 | 0/4 | Not started | - |

View File

@@ -0,0 +1,435 @@
---
phase: 12.1-per-field-text-editing-and-quick-fill
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- teressa-copeland-homes/src/lib/pdf/prepare-document.ts
- teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx
- teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx
- teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewerWrapper.tsx
autonomous: true
requirements:
- TXTF-01
- TXTF-03
must_haves:
truths:
- "preparePdf draws text at the correct field-box position using the UUID field ID as the lookup key — not positional index"
- "Strategy B top-of-page fallback stamping is removed — no UUID strings appear at the top of page 1"
- "FieldPlacer accepts selectedFieldId, textFillData, onFieldSelect, onFieldValueChange as optional props without TypeScript errors"
- "PdfViewer and PdfViewerWrapper compile cleanly with the new optional props threaded through"
- "Clicking a text field box on the PDF selects it and renders an inline input"
- "The inline input does not trigger the move/drag handler (data-no-move + stopPropagation)"
- "Clicking a non-text field or the PDF background deselects the current field"
artifacts:
- path: "teressa-copeland-homes/src/lib/pdf/prepare-document.ts"
provides: "Field-ID-keyed text drawing; Strategy B and positional loop removed"
contains: "textFields[field.id]"
- path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx"
provides: "Per-field click-to-select + inline input overlay for text fields"
exports: ["FieldPlacer"]
- path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx"
provides: "Optional prop forwarding to FieldPlacer"
- path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewerWrapper.tsx"
provides: "Optional prop forwarding to PdfViewer"
key_links:
- from: "DocumentPageClient.tsx (Plan 02)"
to: "PdfViewerWrapper.tsx"
via: "selectedFieldId, textFillData, onFieldSelect, onFieldValueChange optional props"
pattern: "selectedFieldId\\?"
- from: "FieldPlacer.tsx renderFields()"
to: "text field box inline input"
via: "onClick calls onFieldSelect?(field.id); input onChange calls onFieldValueChange?(field.id, value)"
pattern: "onFieldSelect\\?\\."
- from: "prepare-document.ts"
to: "field-ID lookup"
via: "textFields[field.id] direct lookup"
pattern: "textFields\\[field\\.id\\]"
---
<objective>
Replace the broken positional text fill pipeline with a field-ID-keyed approach, and add the per-field click-to-select + inline input interaction to FieldPlacer.
Purpose: The current preparePdf() assigns textFillData values positionally (sort by page/y, match by index), which breaks when field order or count doesn't align with the textFillData entries. Strategy B then stamps UUID keys as visible text at the top of page 1. This plan fixes both issues at the infrastructure and interaction layers.
Output:
- prepare-document.ts: direct field-ID lookup replaces positional loop; Strategy B removed
- FieldPlacer.tsx: text fields render inline input when selected; DroppableZone deselects on background click
- PdfViewer.tsx + PdfViewerWrapper.tsx: optional prop chain threaded through so Plan 02 can wire them
</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/phases/12.1-per-field-text-editing-and-quick-fill/12.1-RESEARCH.md
@.planning/phases/12-filled-document-preview/12-02-SUMMARY.md
<interfaces>
<!-- Current signatures — executor should not need to re-read files -->
From src/lib/pdf/prepare-document.ts (current):
```typescript
export async function preparePdf(
srcPath: string,
destPath: string,
textFields: Record<string, string>, // currently keyed by label — changing to fieldId
sigFields: SignatureFieldData[],
agentSignatureData: string | null = null,
agentInitialsData: string | null = null,
): Promise<void>
```
Current broken positional block (lines 77-112) to REMOVE:
```typescript
const remainingEntries: Array<[string, string]> = Object.entries(textFields).filter(...)
const textFields_sorted = sigFields.filter(f => getFieldType(f) === 'text').sort(...)
const fieldConsumedKeys = new Set<string>();
textFields_sorted.forEach((field, idx) => { const entry = remainingEntries[idx]; ... });
```
Current Strategy B block (lines 118-147) to REMOVE:
```typescript
const unstampedEntries = remainingEntries.filter(([key]) => !fieldConsumedKeys.has(key));
if (unstampedEntries.length > 0) { ... firstPage.drawText(`${key}: ${value}`, ...) }
```
From src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx (current):
```typescript
interface FieldPlacerProps {
docId: string;
pageInfo: PageInfo | null;
currentPage: number;
children: React.ReactNode;
readOnly?: boolean;
onFieldsChanged?: () => void;
}
```
DroppableZone currently at line 726 — needs onClick handler added.
renderFields() per-field div at line 612 — text fields need click-to-select rendering.
From src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx (current):
```typescript
export function PdfViewer({ docId, docStatus, onFieldsChanged }: { docId: string; docStatus?: string; onFieldsChanged?: () => void })
```
FieldPlacer render at line 72 — needs new optional props forwarded.
From src/app/portal/(protected)/documents/[docId]/_components/PdfViewerWrapper.tsx (current):
```typescript
export function PdfViewerWrapper({ docId, docStatus, onFieldsChanged }: { docId: string; docStatus?: string; onFieldsChanged?: () => void })
```
Wraps PdfViewer via Next.js dynamic() — needs 4 new optional props added and forwarded.
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Replace positional text fill with field-ID-keyed lookup in preparePdf</name>
<files>teressa-copeland-homes/src/lib/pdf/prepare-document.ts</files>
<action>
In `teressa-copeland-homes/src/lib/pdf/prepare-document.ts`:
1. **Remove the positional block** (lines ~47-112): Delete the entire section that includes:
- `acroFilledKeys` Set declaration and population
- `remainingEntries` array construction
- `textFields_sorted` sort
- `fieldConsumedKeys` Set
- `textFields_sorted.forEach(...)` loop that draws text at field positions
2. **Remove Strategy A** (AcroForm filling, lines ~52-69): Remove the try/catch block that calls `pdfDoc.getForm()`, fills AcroForm fields, and calls `form.flatten()`. Also remove `hasAcroForm` variable. The AcroForm strategy is no longer needed — text values are always drawn at field box positions.
3. **Remove Strategy B** (lines ~118-147): Delete the `unstampedEntries` block that stamps key/value pairs at the top of page 1.
4. **Add the new field-ID-keyed drawing loop** in their place, before the existing `for (const field of sigFields)` loop that handles non-text field types. Insert:
```typescript
// Phase 12.1: Draw text values at placed text field boxes — keyed by field ID (UUID)
for (const field of sigFields) {
if (getFieldType(field) !== 'text') continue;
const value = textFields[field.id];
if (!value) continue;
const page = pages[field.page - 1];
if (!page) continue;
const fontSize = Math.max(6, Math.min(11, field.height - 4));
page.drawText(value, {
x: field.x + 4,
y: field.y + 4,
size: fontSize,
font: helvetica,
color: rgb(0.05, 0.05, 0.05),
});
}
```
5. **In the field-type dispatch loop** (the existing `for (const field of sigFields)` block that renders client-signature, initials, checkbox, date, agent-signature, agent-initials), the `text` case currently says "Text value drawn above". Keep that comment as-is (or update to "Drawn in field-ID loop above") — no additional drawing needed there.
6. **Update the JSDoc comment** at the top of preparePdf to remove the reference to AcroForm Strategy A and Strategy B. Replace with:
```
* Text fill: textFields is keyed by SignatureFieldData.id (UUID). Each text-type
* placed field box has its value looked up directly by field ID and drawn at
* the field's coordinates.
```
7. Remove `acroFilledKeys`, `hasAcroForm`, `remainingEntries`, `fieldConsumedKeys`, `textFields_sorted` variables since none exist in the new code.
Note: The `textFields` parameter type is still `Record<string, string>` — no signature change needed. The calling routes pass `body.textFillData` directly, which will now be `{ [fieldId]: value }` once Plan 02 wires the UI. The function signature remains backward-compatible.
</action>
<verify>
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30</automated>
</verify>
<done>
- `textFields[field.id]` lookup exists in prepare-document.ts
- No `remainingEntries`, `fieldConsumedKeys`, `textFields_sorted`, or `unstampedEntries` variables remain
- No AcroForm `getForm()`/`flatten()` calls remain
- No Strategy B `drawText` at top of page 1 remains
- `npx tsc --noEmit` passes with zero errors
</done>
</task>
<task type="auto">
<name>Task 2: Add optional text-edit props to PdfViewerWrapper, PdfViewer, and FieldPlacer click-to-select</name>
<files>
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewerWrapper.tsx
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx
</files>
<action>
**PdfViewerWrapper.tsx** — Add 4 optional props and forward to PdfViewer:
```typescript
export function PdfViewerWrapper({
docId,
docStatus,
onFieldsChanged,
selectedFieldId,
textFillData,
onFieldSelect,
onFieldValueChange,
}: {
docId: string;
docStatus?: string;
onFieldsChanged?: () => void;
selectedFieldId?: string | null;
textFillData?: Record<string, string>;
onFieldSelect?: (fieldId: string | null) => void;
onFieldValueChange?: (fieldId: string, value: string) => void;
}) {
return (
<PdfViewer
docId={docId}
docStatus={docStatus}
onFieldsChanged={onFieldsChanged}
selectedFieldId={selectedFieldId}
textFillData={textFillData}
onFieldSelect={onFieldSelect}
onFieldValueChange={onFieldValueChange}
/>
);
}
```
**PdfViewer.tsx** — Same 4 optional props; forward to FieldPlacer; clear selectedFieldId on page change:
1. Add the same 4 optional props to the PdfViewer function signature.
2. In the `<FieldPlacer>` render (line 72), add the 4 new props:
```tsx
<FieldPlacer
docId={docId}
pageInfo={pageInfo}
currentPage={pageNumber}
readOnly={readOnly}
onFieldsChanged={onFieldsChanged}
selectedFieldId={selectedFieldId}
textFillData={textFillData}
onFieldSelect={onFieldSelect}
onFieldValueChange={onFieldValueChange}
>
```
3. In the Prev/Next page buttons' onClick handlers, also call `onFieldSelect?.(null)` to deselect when the agent navigates pages. The next button becomes:
```tsx
onClick={() => { setPageNumber(p => Math.min(numPages, p + 1)); onFieldSelect?.(null); }}
```
And prev button:
```tsx
onClick={() => { setPageNumber(p => Math.max(1, p - 1)); onFieldSelect?.(null); }}
```
**FieldPlacer.tsx** — Add 4 optional props; wire click-to-select on text fields; deselect on background click:
1. Extend `FieldPlacerProps` interface:
```typescript
interface FieldPlacerProps {
docId: string;
pageInfo: PageInfo | null;
currentPage: number;
children: React.ReactNode;
readOnly?: boolean;
onFieldsChanged?: () => void;
selectedFieldId?: string | null;
textFillData?: Record<string, string>;
onFieldSelect?: (fieldId: string | null) => void;
onFieldValueChange?: (fieldId: string, value: string) => void;
}
```
2. Destructure the 4 new props in the function signature alongside the existing props.
3. In `DroppableZone`, add an `onClick` prop that deselects when the agent clicks the PDF background (not a field):
```tsx
<DroppableZone
id="pdf-drop-zone"
containerRef={containerRef}
onZonePointerMove={handleZonePointerMove}
onZonePointerUp={handleZonePointerUp}
onClick={(e) => {
// Deselect if clicking the zone background (not a field box)
if (!(e.target as HTMLElement).closest('[data-field-id]')) {
onFieldSelect?.(null);
}
}}
>
```
Also update the `DroppableZone` component definition (lines 120-150) to accept and spread an `onClick` prop:
```typescript
function DroppableZone({
id, containerRef, children, onZonePointerMove, onZonePointerUp, onClick,
}: {
id: string;
containerRef: React.RefObject<HTMLDivElement | null>;
children: React.ReactNode;
onZonePointerMove: (e: React.PointerEvent<HTMLDivElement>) => void;
onZonePointerUp: (e: React.PointerEvent<HTMLDivElement>) => void;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
}) {
// ...existing body, add onClick to the div:
return (
<div
ref={...}
style={{ position: 'relative' }}
onPointerMove={onZonePointerMove}
onPointerUp={onZonePointerUp}
onClick={onClick}
>
{children}
</div>
);
}
```
4. In `renderFields()`, change the text field rendering. Currently text fields use the same div as other field types. Replace the entire per-field div for `text` type fields with this pattern (inside the `.map()` callback in `renderFields()`):
Add these variables at the top of the `.map()` callback (alongside existing `isMoving`, `fieldType`, etc.):
```typescript
const isSelected = selectedFieldId === field.id;
const currentValue = textFillData?.[field.id] ?? '';
```
Then, inside the field box div, replace the current content:
```tsx
// Current (for all types):
{fieldType !== 'checkbox' && (
<span style={{ pointerEvents: 'none' }}>{fieldLabel}</span>
)}
```
With a conditional that handles text differently:
```tsx
{fieldType === 'text' ? (
isSelected ? (
<input
data-no-move
autoFocus
value={currentValue}
onChange={(e) => onFieldValueChange?.(field.id, e.target.value)}
onPointerDown={(e) => e.stopPropagation()}
style={{
flex: 1,
background: 'transparent',
border: 'none',
outline: 'none',
fontSize: '10px',
color: fieldColor,
width: '100%',
cursor: 'text',
padding: 0,
}}
placeholder="Type value..."
/>
) : (
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', pointerEvents: 'none' }}>
{currentValue || fieldLabel}
</span>
)
) : (
fieldType !== 'checkbox' && (
<span style={{ pointerEvents: 'none' }}>{fieldLabel}</span>
)
)}
```
5. Add an `onClick` handler to the per-field div (alongside `onPointerDown`) that fires for text field selection:
```tsx
onClick={(e) => {
if (readOnly) return;
if (getFieldType(field) === 'text') {
e.stopPropagation(); // prevent DroppableZone's deselect handler from firing
onFieldSelect?.(field.id);
} else {
// Non-text field click: deselect any selected text field
onFieldSelect?.(null);
}
}}
```
Important: The existing `onPointerDown` handler is NOT removed — move/resize still uses it. `onClick` fires after pointerdown+pointerup with no movement (< 5px — MouseSensor `{ distance: 5 }` threshold ensures drags don't trigger onClick).
6. Update the field box div's `cursor` style to reflect selection state for text fields:
```typescript
cursor: readOnly ? 'default' : (isMoving ? 'grabbing' : (fieldType === 'text' ? 'text' : 'grab')),
```
7. Update the border style to highlight selected text fields:
```typescript
border: `2px solid ${isSelected && fieldType === 'text' ? fieldColor : fieldColor}`,
// Also thicken border when selected:
border: isSelected && fieldType === 'text' ? `2px solid ${fieldColor}` : `2px solid ${fieldColor}`,
// Add a box shadow for selected state:
boxShadow: isSelected && fieldType === 'text'
? `0 0 0 2px ${fieldColor}66, ${isMoving ? `0 4px 12px ${fieldColor}59` : ''}`
: (isMoving ? `0 4px 12px ${fieldColor}59` : undefined),
```
</action>
<verify>
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30</automated>
</verify>
<done>
- PdfViewerWrapper, PdfViewer, FieldPlacer all compile with zero TypeScript errors
- FieldPlacerProps has selectedFieldId?, textFillData?, onFieldSelect?, onFieldValueChange? optional props
- DroppableZone has onClick prop accepted and applied
- Text field boxes in renderFields() show inline input when isSelected === true
- Non-text field onClick calls onFieldSelect?.(null)
- npx tsc --noEmit passes with zero errors
</done>
</task>
</tasks>
<verification>
After both tasks complete:
- `npx tsc --noEmit` in teressa-copeland-homes passes with zero errors
- `prepare-document.ts` contains `textFields[field.id]` and does NOT contain `remainingEntries`, `unstampedEntries`, `fieldConsumedKeys`, `getForm()`, or `form.flatten()`
- `FieldPlacer.tsx` exports `FieldPlacer` with the 4 new optional props in its interface
- `PdfViewerWrapper.tsx` and `PdfViewer.tsx` accept and forward the 4 new optional props
</verification>
<success_criteria>
- preparePdf draws text at field-ID-keyed positions — no positional assignment, no Strategy B stamps
- FieldPlacer text fields show inline input on click (isSelected state); show value or label otherwise
- Prop chain complete from PdfViewerWrapper through PdfViewer to FieldPlacer — Plan 02 can wire state from DocumentPageClient without any additional changes to these files
- TypeScript: zero errors
</success_criteria>
<output>
After completion, create `.planning/phases/12.1-per-field-text-editing-and-quick-fill/12.1-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,431 @@
---
phase: 12.1-per-field-text-editing-and-quick-fill
plan: 02
type: execute
wave: 2
depends_on:
- 12.1-01
files_modified:
- teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx
- teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx
- teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/TextFillForm.tsx
autonomous: false
requirements:
- TXTF-01
- TXTF-02
- TXTF-03
must_haves:
truths:
- "Agent can click a placed text field box and type a value — the value is stored under the field's UUID, not a label"
- "Changing any text field value resets previewToken to null, re-disabling the Send button"
- "When a text field is selected, PreparePanel shows Client Name / Property Address / Client Email quick-fill buttons"
- "Quick-fill buttons insert the client profile value into the currently selected field and reset previewToken"
- "textFillData is no longer local state in PreparePanel — it lives in DocumentPageClient and is passed as a prop"
- "TextFillForm is removed from PreparePanel and the file is deleted"
- "PreparePanel's handlePreview and handlePrepare use textFillData prop (not local state)"
- "Agent can preview and send a document with per-field text fill values correctly embedded"
artifacts:
- path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx"
provides: "selectedFieldId + textFillData shared state; handleFieldValueChange + handleQuickFill callbacks"
contains: "selectedFieldId"
- path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx"
provides: "QuickFillPanel when field selected; no TextFillForm; textFillData as prop"
contains: "onQuickFill"
- path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/TextFillForm.tsx"
provides: "Deleted — no longer used"
key_links:
- from: "DocumentPageClient.tsx handleFieldValueChange"
to: "FieldPlacer.tsx onFieldValueChange prop"
via: "PdfViewerWrapper onFieldValueChange prop -> PdfViewer -> FieldPlacer"
pattern: "onFieldValueChange"
- from: "DocumentPageClient.tsx handleQuickFill"
to: "PreparePanel.tsx onQuickFill prop"
via: "direct prop passing"
pattern: "onQuickFill"
- from: "PreparePanel.tsx handlePreview"
to: "/api/documents/[id]/preview"
via: "textFillData prop (now Record<fieldId, string>)"
pattern: "textFillData"
- from: "PreparePanel.tsx handlePrepare"
to: "/api/documents/[id]/prepare"
via: "textFillData prop (now Record<fieldId, string>)"
pattern: "textFillData"
---
<objective>
Wire the per-field text editing state bridge in DocumentPageClient, replace TextFillForm in PreparePanel with the QuickFillPanel, and delete TextFillForm.tsx.
Purpose: Plan 01 created the prop chain and field interaction UI. This plan creates the shared state that drives it — selectedFieldId and textFillData lifted to DocumentPageClient so FieldPlacer (left column) and PreparePanel (right sidebar) stay in sync.
Output:
- DocumentPageClient.tsx: selectedFieldId + textFillData state + two new callbacks; props threaded to PdfViewerWrapper and PreparePanel
- PreparePanel.tsx: TextFillForm removed; QuickFillPanel added; textFillData + selectedFieldId + onQuickFill props added; textFillData moved from local state to prop
- TextFillForm.tsx: deleted
</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/phases/12.1-per-field-text-editing-and-quick-fill/12.1-RESEARCH.md
@.planning/phases/12.1-per-field-text-editing-and-quick-fill/12.1-01-SUMMARY.md
<interfaces>
<!-- Current file signatures from the codebase — executor should not re-read files -->
From DocumentPageClient.tsx (current — to be extended):
```typescript
'use client';
import { useState, useCallback } from 'react';
import { PdfViewerWrapper } from './PdfViewerWrapper';
import { PreparePanel } from './PreparePanel';
interface DocumentPageClientProps {
docId: string;
docStatus: string;
defaultEmail: string;
clientName: string;
agentDownloadUrl?: string | null;
signedAt?: Date | null;
clientPropertyAddress?: string | null;
}
// Currently has: [previewToken, setPreviewToken], handleFieldsChanged
// Passes to PdfViewerWrapper: { docId, docStatus, onFieldsChanged }
// Passes to PreparePanel: { docId, defaultEmail, clientName, currentStatus, agentDownloadUrl, signedAt, clientPropertyAddress, previewToken, onPreviewTokenChange }
```
From PreparePanel.tsx (current — to be overhauled):
```typescript
interface PreparePanelProps {
docId: string;
defaultEmail: string;
clientName: string;
currentStatus: string;
agentDownloadUrl?: string | null;
signedAt?: Date | null;
clientPropertyAddress?: string | null;
previewToken: string | null;
onPreviewTokenChange: (token: string | null) => void;
}
// Currently has:
// - textFillData local state (seeded from clientPropertyAddress — REMOVE)
// - TextFillForm component (REMOVE)
// - handleTextFillChange (REMOVE — textFillData is now a prop)
// - handlePreview: uses local textFillData (update to use prop)
// - handlePrepare: uses local textFillData (update to use prop)
```
From PdfViewerWrapper.tsx (after Plan 01):
```typescript
export function PdfViewerWrapper({
docId, docStatus, onFieldsChanged,
selectedFieldId, textFillData, onFieldSelect, onFieldValueChange,
}: {
docId: string; docStatus?: string; onFieldsChanged?: () => void;
selectedFieldId?: string | null;
textFillData?: Record<string, string>;
onFieldSelect?: (fieldId: string | null) => void;
onFieldValueChange?: (fieldId: string, value: string) => void;
})
```
TextFillForm.tsx (to be deleted — the entire component is replaced):
```typescript
// Generic label/value row form — no longer used after this plan
export function TextFillForm({ onChange, initialData }: TextFillFormProps)
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Extend DocumentPageClient with selectedFieldId + textFillData shared state</name>
<files>teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx</files>
<action>
Rewrite `DocumentPageClient.tsx` in full (it is a small file — 51 lines currently). The new version:
```typescript
'use client';
import { useState, useCallback } from 'react';
import { PdfViewerWrapper } from './PdfViewerWrapper';
import { PreparePanel } from './PreparePanel';
interface DocumentPageClientProps {
docId: string;
docStatus: string;
defaultEmail: string;
clientName: string;
agentDownloadUrl?: string | null;
signedAt?: Date | null;
clientPropertyAddress?: string | null;
}
export function DocumentPageClient({
docId,
docStatus,
defaultEmail,
clientName,
agentDownloadUrl,
signedAt,
clientPropertyAddress,
}: DocumentPageClientProps) {
const [previewToken, setPreviewToken] = useState<string | null>(null);
const [selectedFieldId, setSelectedFieldId] = useState<string | null>(null);
const [textFillData, setTextFillData] = useState<Record<string, string>>({});
const handleFieldsChanged = useCallback(() => {
setPreviewToken(null);
}, []);
const handleFieldValueChange = useCallback((fieldId: string, value: string) => {
setTextFillData(prev => ({ ...prev, [fieldId]: value }));
setPreviewToken(null); // TXTF-03: reset staleness on any text value change
}, []);
const handleQuickFill = useCallback((fieldId: string, value: string) => {
setTextFillData(prev => ({ ...prev, [fieldId]: value }));
setPreviewToken(null); // TXTF-03: reset staleness on quick fill
}, []);
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<PdfViewerWrapper
docId={docId}
docStatus={docStatus}
onFieldsChanged={handleFieldsChanged}
selectedFieldId={selectedFieldId}
textFillData={textFillData}
onFieldSelect={setSelectedFieldId}
onFieldValueChange={handleFieldValueChange}
/>
</div>
<div className="lg:col-span-1 lg:sticky lg:top-6 lg:self-start lg:max-h-[calc(100vh-6rem)] lg:overflow-y-auto">
<PreparePanel
docId={docId}
defaultEmail={defaultEmail}
clientName={clientName}
currentStatus={docStatus}
agentDownloadUrl={agentDownloadUrl}
signedAt={signedAt}
clientPropertyAddress={clientPropertyAddress}
previewToken={previewToken}
onPreviewTokenChange={setPreviewToken}
textFillData={textFillData}
selectedFieldId={selectedFieldId}
onQuickFill={handleQuickFill}
/>
</div>
</div>
);
}
```
Key notes:
- `textFillData` starts as `{}` — do NOT seed from `clientPropertyAddress`. The old seeding (`{ propertyAddress: clientPropertyAddress }`) mapped to a label key, which is the broken pattern being replaced. Quick-fill makes it trivial to insert the value once a field is selected.
- Both `handleFieldValueChange` and `handleQuickFill` call `setPreviewToken(null)` to satisfy TXTF-03 (staleness reset).
- `setSelectedFieldId` is passed directly as `onFieldSelect` — no wrapper needed.
</action>
<verify>
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30</automated>
</verify>
<done>
- DocumentPageClient.tsx has selectedFieldId, textFillData state variables
- handleFieldValueChange and handleQuickFill both call setPreviewToken(null)
- PdfViewerWrapper receives selectedFieldId, textFillData, onFieldSelect, onFieldValueChange
- PreparePanel receives textFillData, selectedFieldId, onQuickFill (in addition to existing props)
- textFillData starts as {} (no clientPropertyAddress seeding)
- npx tsc --noEmit passes with zero errors
</done>
</task>
<task type="auto">
<name>Task 2: Replace TextFillForm with QuickFillPanel in PreparePanel and delete TextFillForm.tsx</name>
<files>
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/TextFillForm.tsx
</files>
<action>
**PreparePanel.tsx** — make these changes:
1. **Add 3 new props** to `PreparePanelProps`:
```typescript
textFillData: Record<string, string>;
selectedFieldId: string | null;
onQuickFill: (fieldId: string, value: string) => void;
```
2. **Remove `textFillData` local state** — delete lines:
```typescript
const [textFillData, setTextFillData] = useState<Record<string, string>>(
() => (clientPropertyAddress ? { propertyAddress: clientPropertyAddress } : {} as Record<string, string>)
);
```
3. **Remove `handleTextFillChange`** function — it was the wrapper that called `setTextFillData` and `onPreviewTokenChange(null)`. This is no longer needed; the parent handles it.
4. **Remove the `TextFillForm` import** at the top of the file.
5. **Remove the TextFillForm JSX block** in the return:
```tsx
// REMOVE THIS:
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Text fill fields</label>
<TextFillForm
onChange={handleTextFillChange}
initialData={clientPropertyAddress ? { propertyAddress: clientPropertyAddress } : undefined}
/>
</div>
```
6. **Replace it with the QuickFillPanel**. Insert this in place of the TextFillForm block, before the Preview button:
```tsx
{/* Quick-fill panel — only shown when a text field is selected */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Text field fill</label>
{selectedFieldId ? (
<div className="space-y-1.5">
<p className="text-xs text-gray-400">
Click a suggestion to fill the selected field.
</p>
{clientName && (
<button
type="button"
onClick={() => onQuickFill(selectedFieldId, clientName)}
className="w-full text-left px-3 py-2 text-sm border rounded bg-white hover:bg-blue-50 hover:border-blue-300 transition-colors"
>
<span className="text-xs text-gray-400 block">Client Name</span>
<span className="truncate block">{clientName}</span>
</button>
)}
{clientPropertyAddress && (
<button
type="button"
onClick={() => onQuickFill(selectedFieldId, clientPropertyAddress)}
className="w-full text-left px-3 py-2 text-sm border rounded bg-white hover:bg-blue-50 hover:border-blue-300 transition-colors"
>
<span className="text-xs text-gray-400 block">Property Address</span>
<span className="truncate block">{clientPropertyAddress}</span>
</button>
)}
<button
type="button"
onClick={() => onQuickFill(selectedFieldId, defaultEmail)}
className="w-full text-left px-3 py-2 text-sm border rounded bg-white hover:bg-blue-50 hover:border-blue-300 transition-colors"
>
<span className="text-xs text-gray-400 block">Client Email</span>
<span className="truncate block">{defaultEmail}</span>
</button>
</div>
) : (
<p className="text-sm text-gray-400 italic">
Click a text field on the document to edit or quick-fill it.
</p>
)}
</div>
```
Note: `defaultEmail` is already a prop on PreparePanel (used for the recipients textarea pre-fill). It IS the client email — reuse it for the "Client Email" quick-fill button. No new prop needed (per research Pitfall 3).
7. **Update `handlePreview`** — `textFillData` is now a prop, not local state. The body is already correct (`body: JSON.stringify({ textFillData })`) but verify the prop is used, not a stale reference to the old local state.
8. **Update `handlePrepare`** — same: `textFillData` prop is used in `body: JSON.stringify({ textFillData, emailAddresses })`. Verify.
9. **Destructure the 3 new props** in the function signature:
```typescript
export function PreparePanel({
docId, defaultEmail, clientName, currentStatus,
agentDownloadUrl, signedAt, clientPropertyAddress,
previewToken, onPreviewTokenChange,
textFillData, selectedFieldId, onQuickFill,
}: PreparePanelProps)
```
**TextFillForm.tsx** — delete the file:
Use the Bash tool or Write tool to delete the file. Since Write overwrites, you cannot delete with it. Use:
```bash
rm teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/TextFillForm.tsx
```
(Run from the project root `/Users/ccopeland/temp/red`)
If for any reason the delete fails, leave the file in place but ensure it is no longer imported anywhere — TypeScript will tree-shake it. The important thing is PreparePanel no longer imports or uses it.
</action>
<verify>
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30 && echo "TSC_CLEAN" && grep -r "TextFillForm" src/ --include="*.tsx" --include="*.ts" | grep -v "\.tsx:.*import" | head -5 || echo "NO_REMAINING_TEXTFILLFORM_IMPORTS"</automated>
</verify>
<done>
- PreparePanel.tsx compiles with zero TypeScript errors
- PreparePanel has no import of TextFillForm
- PreparePanel has no local textFillData state
- PreparePanel has selectedFieldId, textFillData, onQuickFill in its props interface
- QuickFillPanel renders when selectedFieldId is non-null
- TextFillForm.tsx is deleted (or at minimum has no remaining imports in the codebase)
- handlePreview and handlePrepare use textFillData from props
- npx tsc --noEmit passes with zero errors
</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 3: Human verification — per-field text editing and quick-fill end-to-end</name>
<what-built>
Full per-field text editing flow:
- Placed text field boxes on PDF are clickable — click selects the field and shows an inline input
- Typing in the inline input stores the value under the field's UUID in textFillData
- PreparePanel sidebar shows quick-fill buttons for Client Name, Property Address, Client Email when a text field is selected
- Clicking a quick-fill button inserts the value into the selected field
- Every text value change resets previewToken (Send button re-disabled)
- Preview PDF embeds text values at the correct field-box positions (not at page top)
- TextFillForm is gone from PreparePanel
- Full Preview-gate-Send flow works with the new per-field data model
</what-built>
<how-to-verify>
1. Open a document in Draft status that has at least 2 placed text field boxes
2. Click one text field box — verify it highlights and shows an inline cursor/input
3. Verify the PreparePanel sidebar now shows "Text field fill" section with quick-fill buttons
4. Click "Client Name" quick-fill button — verify the field shows the client's name
5. Click the second text field box — verify it is now selected and the first field shows the filled value as a label
6. Type a custom value in the second field — verify it persists in the box
7. Click "Preview" — verify the preview PDF shows both text values at their field box positions (not at the top of page 1)
8. Verify the Send button is now enabled (previewToken set)
9. Type in the first field again — verify the Send button becomes disabled again (previewToken reset)
10. Click "Preview" again — verify Send becomes enabled
11. Click "Prepare and Send" — verify the final PDF embeds both text values at their field positions
12. Verify there is NO generic label/value "Text fill fields" form anywhere in PreparePanel
</how-to-verify>
<action>Human verification of the complete per-field text editing and quick-fill flow. No code changes — all automated work is complete in Tasks 1 and 2. Follow the how-to-verify steps above.</action>
<verify>
<automated>MISSING — checkpoint requires human browser verification; no automated equivalent</automated>
</verify>
<done>Human has typed "approved" confirming all 12 verification steps passed</done>
<resume-signal>Type "approved" or describe any issues found</resume-signal>
</task>
</tasks>
<verification>
After all tasks complete:
- `npx tsc --noEmit` in teressa-copeland-homes passes with zero errors
- No import of `TextFillForm` anywhere in the codebase
- PreparePanel has `textFillData`, `selectedFieldId`, `onQuickFill` in its props interface
- DocumentPageClient has `selectedFieldId` and `textFillData` state variables
- Both `handleFieldValueChange` and `handleQuickFill` in DocumentPageClient call `setPreviewToken(null)`
- Human verification checkpoint approved
</verification>
<success_criteria>
- TXTF-01: Agent clicks a text field box, types a value; the value is stored by UUID field ID
- TXTF-02: PreparePanel quick-fill panel appears when a text field is selected; Client Name / Property Address / Client Email buttons work
- TXTF-03: Text values appear at field positions in preview and final PDF; every value change resets the staleness token
- TextFillForm is removed; no label-keyed or positional text fill remains anywhere in the codebase
- Human has approved the full flow end-to-end
</success_criteria>
<output>
After completion, create `.planning/phases/12.1-per-field-text-editing-and-quick-fill/12.1-02-SUMMARY.md`
</output>