Files

436 lines
18 KiB
Markdown
Raw Permalink Normal View History

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