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"
---
<objective>
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.
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`)
- 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(...)`:
- 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" — `<select>` dropdown populated from `forms` prop (required). Each option: `<option value={form.id}>{form.name}</option>`
- CTA: "Create Template" button (gold fill). On click:
-`POST /api/templates` with `{ name, formTemplateId }`
- On success (201): `router.push(\`/portal/templates/${response.id}\`)` to navigate to the new editor
- On error: show inline error message in red
- Cancel button or click overlay to close
Styling: inline styles + Tailwind utilities matching existing portal patterns. No shadcn. Background `#FAF9F7` for page, `#F9FAFB` for modal card. Use `var(--font-sans)` for all text.
</action>
<verify>
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30</automated>
- File contains `documentTemplates` and `formTemplates` imports from schema
- File contains `isNull(documentTemplates.archivedAt)` in the where clause
- File contains `'use client'` for the client component (either inline or separate)
- File contains `"/portal/templates/"` in the row click handler (navigation)
- File contains `POST` and `/api/templates` in the create handler
- File contains `"+ New Template"` button text
- File contains `"No templates yet"` empty state text
- File contains `#C9A84C` (gold accent color)
- File contains `#1B2B4B` (navy color)
-`npx tsc --noEmit` exits 0
</acceptance_criteria>
<done>Templates list page renders active templates with form name, field count, and last-updated. Create modal allows picking a form and naming a template. Navigation to editor works on row click and after creation.</done>
</task>
<task type="auto">
<name>Task 2: Create template editor page — server component, TemplatePageClient, and TemplatePanel</name>
if (!template || !template.formTemplate) notFound();
return (
<TemplatePageClient
templateId={template.id}
templateName={template.name}
formName={template.formTemplate.name}
initialFields={template.signatureFields ?? []}
/>
);
}
```
**Create `src/app/portal/(protected)/templates/[id]/_components/TemplatePageClient.tsx`** — client state owner mirroring DocumentPageClient:
```typescript
'use client';
```
Props interface:
```typescript
interface TemplatePageClientProps {
templateId: string;
templateName: string;
formName: string;
initialFields: SignatureFieldData[];
}
```
Import `SignatureFieldData`, `DocumentSigner` from `@/lib/db/schema`. Import `PdfViewerWrapper` from the documents `_components` directory (reuse the existing component — DO NOT duplicate). Use a relative path or `@/` alias: `import { PdfViewerWrapper } from '@/app/portal/(protected)/documents/[docId]/_components/PdfViewerWrapper';`
`handlePersist` — the `onPersist` callback passed to PdfViewerWrapper (per D-01). Before saving, merge `textFillData` values into `field.hint` for text fields (per Research Pitfall 6). IMPORTANT: The schema type value is `'text'` (from `SignatureFieldType`), NOT `'client-text'`. Use `f.type === 'text'` exactly:
1.**Template name** — `<input>` 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 `<input>` 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()`.
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.
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`).
</action>
<verify>
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30</automated>
- TemplatePanel.tsx contains `"Saved"` success text
- TemplatePanel.tsx contains `ConfirmDialog` import for role removal
-`npx tsc --noEmit` exits 0
</acceptance_criteria>
<done>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.</done>
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
</verification>
<success_criteria>
- 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