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