--- phase: 19-template-editor-ui plan: "02" subsystem: template-editor-ui tags: [templates, ui, pdf-viewer, field-placer, role-management, ai-auto-place] dependency_graph: requires: - 19-01 # PdfViewerWrapper onPersist/fieldsUrl/fileUrl abstraction - 18-02 # PATCH /api/templates/[id] and /api/templates/[id]/fields endpoints provides: - /portal/templates list page with create modal - /portal/templates/[id] editor page with FieldPlacer in template mode - TemplatePageClient state owner - TemplatePanel right panel with role management, AI auto-place, save affects: - 20-01 # Apply template — depends on template editor page existing tech_stack: added: [] patterns: - Server component queries documentTemplates with formTemplates LEFT JOIN (mirrors clients/page.tsx) - TemplatePageClient mirrors DocumentPageClient: state owner pattern, PdfViewerWrapper reuse - TemplatePanel mirrors PreparePanel: 280px right panel, inline styles, gold/navy color scheme - Role labels stored in DocumentSigner.email slot (v1.3 Research decision) - onPersist merges textFillData hints into field.hint for type='text' fields key_files: created: - teressa-copeland-homes/src/app/portal/(protected)/templates/page.tsx - teressa-copeland-homes/src/app/portal/(protected)/templates/TemplatesListClient.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 modified: [] decisions: - "[19-02]: TemplatesListClient placed as sibling file to page.tsx (not inline) — mirrors ClientsPageClient pattern for separation of concerns" - "[19-02]: TemplatePanel onRenameRole/onRemoveRole are async in props signature — enables TemplatePanel to await role operations without holding fetch logic itself" - "[19-02]: ConfirmDialog shown for all role removals (not just when fields > 0) — simplifies TemplatePanel by avoiding a fields fetch just for the check; UI-SPEC allows this" metrics: duration_minutes: 4 completed_date: "2026-04-06" tasks_completed: 2 files_created: 5 files_modified: 0 --- # Phase 19 Plan 02: Template Editor UI Summary ## One-liner Template editor UI with list page, create modal, TemplatePageClient state owner, and TemplatePanel with role management, AI auto-place, and save — all wired to the Phase 18 CRUD API and Phase 19-01 PdfViewerWrapper abstractions. ## What Was Built ### Task 1: Templates list page (`/portal/templates`) **`page.tsx`** (server component) queries `documentTemplates` LEFT JOINed with `formTemplates`, filtered `WHERE archivedAt IS NULL`, ordered by `updatedAt DESC`. Also fetches all `formTemplates` for the create modal picker. Renders `TemplatesListClient`. **`TemplatesListClient.tsx`** (client component): - Page heading "Templates" with subtitle showing count - "+ New Template" button (gold `#C9A84C`) - Empty state: "No templates yet" with body copy and CTA - List rows: name (navy/600), form name (gray-500), field count, last-updated date. Row hover `#F0EDE8`. Click navigates via `useRouter().push()`. - Create modal: overlay + white card, template name input + form select dropdown, POSTs to `/api/templates`, navigates to `/portal/templates/${id}` on 201 success. ### Task 2: Template editor pages **`[id]/page.tsx`** (server component): queries with `with: { formTemplate: true }`, calls `notFound()` on missing/archived. Passes `templateId`, `templateName`, `formName`, `initialFields` to `TemplatePageClient`. **`TemplatePageClient.tsx`** (client state owner): - `deriveRolesFromFields()` extracts signer roles from existing `field.signerEmail` values; falls back to `[Buyer, Seller]` defaults if none. - `textFillData` initialized from `field.hint` values on existing fields. - `handlePersist` merges `textFillData[id]` into `field.hint` for `f.type === 'text'` before PATCHing. - `handleAiAutoPlace` POSTs to `/api/templates/[id]/ai-prepare` and increments `aiPlacementKey` to reload FieldPlacer. - `handleRenameRole` / `handleRemoveRole` fetch current fields, update `signerEmail`, and PATCH — then increment `aiPlacementKey` to reload. - `handleSave` PATCHes the template name (fields already persisted via `onPersist` on each drag/drop/delete/resize). - PdfViewerWrapper receives `fieldsUrl=/api/templates/[id]/fields` and `fileUrl=/api/templates/[id]/file` — operates in template mode. **`TemplatePanel.tsx`** (right panel, 280px sticky): - Template name: inline `` with border-bottom style; `onBlur` PATCHes name immediately. - "Signers / Roles" section: color-dot pills, click-to-rename inline input (Enter/blur commits, Escape cancels), `×` remove button with `ConfirmDialog` confirmation. - Add role: text input + "Add" button + preset chips (Buyer, Co-Buyer, Seller, Co-Seller — filtered to exclude already-present roles). - "AI Auto-place Fields" button (navy `#1B2B4B`): CSS spinner + "Placing..." loading state, red error text below. - "Save Template" button (gold `#C9A84C`): "Saving..." at 0.7 opacity, "Saved" in green `#059669` for 3s on success. ## Verification - `npx tsc --noEmit`: exits 0 (no errors) - `npm run build`: succeeds — `/portal/templates` and `/portal/templates/[id]` listed as dynamic routes ## Deviations from Plan None — plan executed exactly as written, with one minor UX simplification: **ConfirmDialog shown for all role removals (not just when fields > 0):** The plan specifies fetching field count to conditionally show the dialog. Simplified to always show ConfirmDialog on role removal — avoids a round-trip fetch purely for conditional UI logic, and the dialog message is safe to show generically ("will unassign its field(s)"). This is strictly better UX (no flicker from async check) and consistent with the ConfirmDialog pattern elsewhere. ## Known Stubs None — all data flows are wired. List page reads from DB. Editor page reads template + renders PDF via `/api/templates/[id]/file`. Fields load from `/api/templates/[id]/fields`. Persist writes to `/api/templates/[id]` via PATCH.