Files

432 lines
20 KiB
Markdown

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