diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 7c42e97..44d63ac 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -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 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 -**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 **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.1. Agent and Client Initials (INSERTED) | 3/3 | 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 | - | diff --git a/.planning/phases/12.1-per-field-text-editing-and-quick-fill/12.1-01-PLAN.md b/.planning/phases/12.1-per-field-text-editing-and-quick-fill/12.1-01-PLAN.md new file mode 100644 index 0000000..cd1738a --- /dev/null +++ b/.planning/phases/12.1-per-field-text-editing-and-quick-fill/12.1-01-PLAN.md @@ -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\\]" +--- + + +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 + + + +@/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md +@/Users/ccopeland/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/phases/12.1-per-field-text-editing-and-quick-fill/12.1-RESEARCH.md +@.planning/phases/12-filled-document-preview/12-02-SUMMARY.md + + + + +From src/lib/pdf/prepare-document.ts (current): +```typescript +export async function preparePdf( + srcPath: string, + destPath: string, + textFields: Record, // currently keyed by label — changing to fieldId + sigFields: SignatureFieldData[], + agentSignatureData: string | null = null, + agentInitialsData: string | null = null, +): Promise +``` +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(); +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. + + + + + + + Task 1: Replace positional text fill with field-ID-keyed lookup in preparePdf + teressa-copeland-homes/src/lib/pdf/prepare-document.ts + +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` — 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. + + + cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30 + + + - `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 + + + + + Task 2: Add optional text-edit props to PdfViewerWrapper, PdfViewer, and FieldPlacer click-to-select + + 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 + + +**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; + onFieldSelect?: (fieldId: string | null) => void; + onFieldValueChange?: (fieldId: string, value: string) => void; +}) { + return ( + + ); +} +``` + +**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 `` render (line 72), add the 4 new props: + ```tsx + + ``` +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; + 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 + { + // 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; + children: React.ReactNode; + onZonePointerMove: (e: React.PointerEvent) => void; + onZonePointerUp: (e: React.PointerEvent) => void; + onClick?: (e: React.MouseEvent) => void; + }) { + // ...existing body, add onClick to the div: + return ( +
+ {children} +
+ ); + } + ``` + +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' && ( + {fieldLabel} + )} + ``` + With a conditional that handles text differently: + ```tsx + {fieldType === 'text' ? ( + isSelected ? ( + 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..." + /> + ) : ( + + {currentValue || fieldLabel} + + ) + ) : ( + fieldType !== 'checkbox' && ( + {fieldLabel} + ) + )} + ``` + +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), + ``` +
+ + cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30 + + + - 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 + +
+ +
+ + +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 + + + +- 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 + + + +After completion, create `.planning/phases/12.1-per-field-text-editing-and-quick-fill/12.1-01-SUMMARY.md` + diff --git a/.planning/phases/12.1-per-field-text-editing-and-quick-fill/12.1-02-PLAN.md b/.planning/phases/12.1-per-field-text-editing-and-quick-fill/12.1-02-PLAN.md new file mode 100644 index 0000000..389c30a --- /dev/null +++ b/.planning/phases/12.1-per-field-text-editing-and-quick-fill/12.1-02-PLAN.md @@ -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)" + pattern: "textFillData" + - from: "PreparePanel.tsx handlePrepare" + to: "/api/documents/[id]/prepare" + via: "textFillData prop (now Record)" + pattern: "textFillData" +--- + + +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 + + + +@/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md +@/Users/ccopeland/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/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 + + + + +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; + 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) +``` + + + + + + + Task 1: Extend DocumentPageClient with selectedFieldId + textFillData shared state + teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx + +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(null); + const [selectedFieldId, setSelectedFieldId] = useState(null); + const [textFillData, setTextFillData] = useState>({}); + + 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 ( +
+
+ +
+
+ +
+
+ ); +} +``` + +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. +
+ + cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30 + + + - 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 + +
+ + + Task 2: Replace TextFillForm with QuickFillPanel in PreparePanel and delete TextFillForm.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 + + +**PreparePanel.tsx** — make these changes: + +1. **Add 3 new props** to `PreparePanelProps`: + ```typescript + textFillData: Record; + selectedFieldId: string | null; + onQuickFill: (fieldId: string, value: string) => void; + ``` + +2. **Remove `textFillData` local state** — delete lines: + ```typescript + const [textFillData, setTextFillData] = useState>( + () => (clientPropertyAddress ? { propertyAddress: clientPropertyAddress } : {} as Record) + ); + ``` + +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: +
+ + +
+ ``` + +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 */} +
+ + {selectedFieldId ? ( +
+

+ Click a suggestion to fill the selected field. +

+ {clientName && ( + + )} + {clientPropertyAddress && ( + + )} + +
+ ) : ( +

+ Click a text field on the document to edit or quick-fill it. +

+ )} +
+ ``` + + 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. +
+ + 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" + + + - 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 + +
+ + + Task 3: Human verification — per-field text editing and quick-fill end-to-end + + 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 + + + 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 + + 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. + + MISSING — checkpoint requires human browser verification; no automated equivalent + + Human has typed "approved" confirming all 12 verification steps passed + Type "approved" or describe any issues found + + +
+ + +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 + + + +- 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 + + + +After completion, create `.planning/phases/12.1-per-field-text-editing-and-quick-fill/12.1-02-SUMMARY.md` +