--- phase: 19-template-editor-ui plan: "02" type: execute wave: 2 depends_on: ["19-01"] files_modified: - teressa-copeland-homes/src/app/portal/(protected)/templates/page.tsx - teressa-copeland-homes/src/app/portal/(protected)/templates/[id]/page.tsx - teressa-copeland-homes/src/app/portal/(protected)/templates/[id]/_components/TemplatePageClient.tsx - teressa-copeland-homes/src/app/portal/(protected)/templates/[id]/_components/TemplatePanel.tsx autonomous: true requirements: [TMPL-05, TMPL-06, TMPL-07, TMPL-08, TMPL-09] must_haves: truths: - "Agent can see a list of all active templates at /portal/templates" - "Agent can create a new template by selecting a form from the library" - "Agent can open a template at /portal/templates/[id] and see the PDF with fields" - "Agent can drag-drop fields onto the template PDF via FieldPlacer" - "Agent can add/remove/rename signer role labels (Buyer, Seller, custom)" - "Agent can click AI Auto-place to populate fields on the template" - "Agent can type text hints on client-text fields that are saved as field.hint" - "Agent can save the template and fields persist across page refresh" artifacts: - path: "teressa-copeland-homes/src/app/portal/(protected)/templates/page.tsx" provides: "Templates list page with create modal" contains: "TemplatesPage" - path: "teressa-copeland-homes/src/app/portal/(protected)/templates/[id]/page.tsx" provides: "Template editor server component" contains: "TemplateEditorPage" - path: "teressa-copeland-homes/src/app/portal/(protected)/templates/[id]/_components/TemplatePageClient.tsx" provides: "Template editor state owner" contains: "TemplatePageClient" - path: "teressa-copeland-homes/src/app/portal/(protected)/templates/[id]/_components/TemplatePanel.tsx" provides: "Right panel with roles, AI button, save button" contains: "TemplatePanel" key_links: - from: "TemplatePageClient.tsx" to: "/api/templates/[id]" via: "handlePersist callback passed as onPersist to PdfViewerWrapper" pattern: "PATCH.*templates.*signatureFields" - from: "TemplatePageClient.tsx" to: "PdfViewerWrapper" via: "fileUrl + fieldsUrl + onPersist props" pattern: "fileUrl.*fieldsUrl.*onPersist" - from: "TemplatePanel.tsx" to: "/api/templates/[id]/ai-prepare" via: "AI Auto-place button POST call" pattern: "ai-prepare.*POST" - from: "TemplatePanel.tsx" to: "/api/templates/[id]" via: "Save button PATCH call" pattern: "PATCH.*signatureFields" --- Build the template editor UI — list page at `/portal/templates`, editor page at `/portal/templates/[id]` with TemplatePageClient state owner and TemplatePanel right panel — enabling the agent to place fields, assign signer roles, set text hints, use AI auto-place, and save templates. Purpose: This is the core user-facing deliverable of Phase 19. The agent gains the ability to visually build reusable field templates on any PDF form. Output: Four new files (list page, editor server component, TemplatePageClient, TemplatePanel) implementing all TMPL-05 through TMPL-09 requirements. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/19-template-editor-ui/19-CONTEXT.md @.planning/phases/19-template-editor-ui/19-RESEARCH.md @.planning/phases/19-template-editor-ui/19-UI-SPEC.md @.planning/phases/19-template-editor-ui/19-01-SUMMARY.md From PdfViewerWrapper.tsx (after Plan 01): ```typescript export function PdfViewerWrapper({ docId, docStatus, onFieldsChanged, selectedFieldId, textFillData, onFieldSelect, onFieldValueChange, aiPlacementKey, signers, unassignedFieldIds, onPersist, fieldsUrl, fileUrl, // NEW from Plan 01 }: { docId: string; docStatus?: string; onFieldsChanged?: () => void; selectedFieldId?: string | null; textFillData?: Record; onFieldSelect?: (fieldId: string | null) => void; onFieldValueChange?: (fieldId: string, value: string) => void; aiPlacementKey?: number; signers?: DocumentSigner[]; unassignedFieldIds?: Set; onPersist?: (fields: SignatureFieldData[]) => Promise | void; fieldsUrl?: string; fileUrl?: string; }) ``` From schema.ts: ```typescript export interface SignatureFieldData { id: string; page: number; x: number; y: number; width: number; height: number; type?: SignatureFieldType; signerEmail?: string; hint?: string; } export interface DocumentSigner { email: string; color: string; } export const documentTemplates = pgTable("document_templates", { id: text("id"), name: text("name").notNull(), formTemplateId: text("form_template_id").notNull(), signatureFields: jsonb("signature_fields").$type(), archivedAt: timestamp("archived_at"), createdAt: timestamp("created_at"), updatedAt: timestamp("updated_at"), }); ``` From DocumentPageClient.tsx (pattern to mirror): ```typescript const SIGNER_COLORS = ['#6366f1', '#f43f5e', '#10b981', '#f59e0b']; const [aiPlacementKey, setAiPlacementKey] = useState(0); const [selectedFieldId, setSelectedFieldId] = useState(null); const [textFillData, setTextFillData] = useState>({}); ``` From clients/page.tsx (list page pattern): ```typescript export default async function ClientsPage() { const clientRows = await db.select({ ... }).from(clients)...; return ; } ``` API routes from Plan 01: - GET /api/templates/[id]/file — streams PDF - GET /api/templates/[id]/fields — returns signatureFields[] - POST /api/templates/[id]/ai-prepare — AI auto-place, returns { fields } API routes from Phase 18: - GET /api/templates — list active templates (name, formName, fieldCount, updatedAt) - POST /api/templates — create template { name, formTemplateId } - PATCH /api/templates/[id] — update { name?, signatureFields? } - DELETE /api/templates/[id] — soft-delete Forms API for form picker: - GET /api/forms (or direct DB query for formTemplates) Task 1: Create templates list page with create-template modal teressa-copeland-homes/src/app/portal/(protected)/templates/page.tsx teressa-copeland-homes/src/app/portal/(protected)/clients/page.tsx, teressa-copeland-homes/src/app/portal/_components/ClientsPageClient.tsx, teressa-copeland-homes/src/app/api/templates/route.ts, teressa-copeland-homes/src/lib/db/schema.ts, .planning/phases/19-template-editor-ui/19-UI-SPEC.md Create `src/app/portal/(protected)/templates/page.tsx` as a **server component** (per D-18) that: 1. Imports `db` from `@/lib/db` and `documentTemplates`, `formTemplates` from `@/lib/db/schema`. 2. Queries all active templates with a LEFT JOIN to formTemplates: ```typescript const templates = await db .select({ id: documentTemplates.id, name: documentTemplates.name, formName: formTemplates.name, signatureFields: documentTemplates.signatureFields, updatedAt: documentTemplates.updatedAt, }) .from(documentTemplates) .leftJoin(formTemplates, eq(documentTemplates.formTemplateId, formTemplates.id)) .where(isNull(documentTemplates.archivedAt)) .orderBy(desc(documentTemplates.updatedAt)); ``` 3. Also queries all form templates for the form picker in the create modal: ```typescript const forms = await db.select({ id: formTemplates.id, name: formTemplates.name }).from(formTemplates).orderBy(formTemplates.name); ``` 4. Renders a `TemplatesListClient` (defined as a `'use client'` component inline or in the same file using a named export — follow the pattern from ClientsPageClient if it's a separate file, OR keep it inline for simplicity). The server component passes `templates` and `forms` as props. **TemplatesListClient** (client component, can be defined in the same file or a sibling): Layout per UI-SPEC: - Page heading: "Templates" (24px/700, navy `#1B2B4B`) - Subtitle: `{templates.length} template{templates.length !== 1 ? 's' : ''}` (14px/400, gray-500 `#6B7280`) - Top-right: "+ New Template" button (gold `#C9A84C` fill, white text, 36px height) - If no templates: empty state card with "No templates yet" heading, "Create a template to reuse field placements across documents." body, and "+ Create your first template" CTA - Template list: flex column, gap 8px. Each row is a clickable div (cursor pointer) that navigates to `/portal/templates/${template.id}` using `useRouter().push(...)`: - Template name (14px/600, navy) - Form name (14px/400, gray-500) - Field count: `${(template.signatureFields ?? []).length} field${count !== 1 ? 's' : ''}` (12px/400, gray-500) - Last updated: formatted date (12px/400, gray-500) — use `new Date(template.updatedAt).toLocaleDateString()` - Row hover: `background: #F0EDE8` - Row has border-bottom `1px solid #E5E7EB` **Create Template Modal:** - State: `const [showModal, setShowModal] = useState(false);` - Modal overlay: fixed inset-0, bg black/50, z-50, flex center - Modal card: white rounded-lg, padding 24px, max-width 400px - Fields: - "Template name" text input (required) - "Select form" — `` with value `name`, onChange calls `onNameChange`, onBlur calls PATCH to save name immediately (per Research Pitfall 3): ```typescript const handleNameBlur = async () => { await fetch(`/api/templates/${templateId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }), }); }; ``` Style: 14px, border-bottom 1px solid #E5E7EB when at rest, border-bottom 1px solid #C9A84C when focused. Full width. 2. **Signers / Roles section** — heading "Signers / Roles" (12px uppercase, `#6B7280`, letterSpacing 0.08em). List of role pills: - Each pill: flex row, gap 8px, padding 8px, items-center - Color dot: 8px width/height, borderRadius 50%, background `signer.color` - Role label: 14px/400. If editing, show inline `` same font. Click label to enter edit mode. Enter/blur commits rename. Escape cancels. - Remove button: `x` character, 12px, color `#DC2626` on hover, cursor pointer. On click, count fields with `signerEmail === signer.email` (pass field count from parent, OR call `/api/templates/[id]/fields` to check). If count > 0, show ConfirmDialog (import from `@/app/portal/_components/ConfirmDialog`). Dialog text per UI-SPEC: title "Remove role?", body "Removing '{role}' will unassign {N} field(s). This cannot be undone.", confirm "Remove Role" (red), cancel "Cancel". 3. **Add role** — text input + "Add" button. Preset suggestion chips below: "Buyer", "Co-Buyer", "Seller", "Co-Seller" — clicking a chip inserts that value. Only show chips that are not already in the signers list. Input placeholder: `"Role label (e.g. Buyer)"`. 4. **AI Auto-place button** — full width, background `#1B2B4B`, color white, height 36px, border-radius 6px. Text: "AI Auto-place Fields". Loading state: "Placing..." with a simple CSS spinner (border animation). Error state: inline red error text below button. On click calls `onAiAutoPlace()`. ```typescript const [aiLoading, setAiLoading] = useState(false); const [aiError, setAiError] = useState(null); const handleAi = async () => { setAiLoading(true); setAiError(null); try { await onAiAutoPlace(); } catch (e) { setAiError(e instanceof Error ? e.message : 'AI placement failed. Check that the form PDF is accessible and try again.'); } finally { setAiLoading(false); } }; ``` 5. **Save button** — full width, background `#C9A84C`, color white, height 36px, border-radius 6px. Text: "Save Template". Loading: "Saving..." at 0.7 opacity. Success: inline "Saved" in green `#059669` below, fades after 3s via setTimeout. Error: inline red text. ```typescript const [saving, setSaving] = useState(false); const [saveStatus, setSaveStatus] = useState<'idle' | 'saved' | 'error'>('idle'); const handleSaveClick = async () => { setSaving(true); setSaveStatus('idle'); try { await onSave(); setSaveStatus('saved'); setTimeout(() => setSaveStatus('idle'), 3000); } catch { setSaveStatus('error'); } finally { setSaving(false); } }; ``` Style the entire panel: width 280px, flexShrink 0, background `#F9FAFB`, borderRadius 8px, padding 16px, display flex, flexDirection column, gap 24px. Position sticky, top 96px (64px nav + 32px padding). Import `ConfirmDialog` from `@/app/portal/_components/ConfirmDialog` for role removal confirmation. Use the existing ConfirmDialog API — read the component first to understand its props (likely: `open`, `title`, `message`, `confirmLabel`, `onConfirm`, `onCancel`). cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30 - File exists: `src/app/portal/(protected)/templates/page.tsx` - File exists: `src/app/portal/(protected)/templates/[id]/page.tsx` - File exists: `src/app/portal/(protected)/templates/[id]/_components/TemplatePageClient.tsx` - File exists: `src/app/portal/(protected)/templates/[id]/_components/TemplatePanel.tsx` - templates/page.tsx contains `export default async function` (server component) - templates/page.tsx contains `isNull(documentTemplates.archivedAt)` in the query - templates/page.tsx contains `"+ New Template"` button text - templates/page.tsx contains `POST` and `/api/templates` for template creation - templates/[id]/page.tsx contains `notFound()` for missing/archived templates - templates/[id]/page.tsx contains `with: { formTemplate: true }` in the query - TemplatePageClient.tsx contains `onPersist={handlePersist}` in the PdfViewerWrapper usage - TemplatePageClient.tsx contains `fieldsUrl={\`/api/templates/${templateId}/fields\`}` - TemplatePageClient.tsx contains `fileUrl={\`/api/templates/${templateId}/file\`}` - TemplatePageClient.tsx contains `deriveRolesFromFields` function - TemplatePageClient.tsx contains `f.hint` or `hint:` for the hint merge in handlePersist - TemplatePanel.tsx contains `"Signers / Roles"` section heading - TemplatePanel.tsx contains `"AI Auto-place Fields"` button text - TemplatePanel.tsx contains `"Save Template"` button text - TemplatePanel.tsx contains `#C9A84C` (gold) and `#1B2B4B` (navy) colors - TemplatePanel.tsx contains `"Placing..."` or `"Placing…"` loading text - TemplatePanel.tsx contains `"Saved"` success text - TemplatePanel.tsx contains `ConfirmDialog` import for role removal - `npx tsc --noEmit` exits 0 Complete template editor UI is functional: list page shows templates with create modal, editor page renders PDF with FieldPlacer in template mode (onPersist, fieldsUrl, fileUrl), TemplatePanel provides role management, AI auto-place, and save. All TMPL-05 through TMPL-09 requirements are addressed. 1. `cd teressa-copeland-homes && npx tsc --noEmit` exits 0 2. `npm run build` succeeds 3. Navigate to `/portal/templates` — list page renders (may be empty) 4. Click "+ New Template" — modal opens with form picker 5. Create a template — redirects to `/portal/templates/[id]` 6. Template editor page shows PDF on left, TemplatePanel on right 7. FieldPlacer drag-drop works (fields appear on PDF) 8. "Signers / Roles" section shows Buyer and Seller by default 9. "AI Auto-place Fields" button is clickable (requires OPENAI_API_KEY for actual placement) 10. "Save Template" button persists fields and name - All four new files exist and compile cleanly - Templates list page is accessible at /portal/templates - Template editor is accessible at /portal/templates/[id] - FieldPlacer operates in template mode (onPersist saves to /api/templates/[id], fields load from /api/templates/[id]/fields, PDF loads from /api/templates/[id]/file) - Role labels (not emails) are used in the template editor - Text hints on client-text fields are merged into field.hint before persisting - AI Auto-place triggers POST /api/templates/[id]/ai-prepare - Save persists via PATCH /api/templates/[id] After completion, create `.planning/phases/19-template-editor-ui/19-02-SUMMARY.md`