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