docs(12.1): create phase plan — per-field text editing and quick-fill
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user