From af5beaf5cb2d9fed7e620be16623afa3c84c1d38 Mon Sep 17 00:00:00 2001 From: Chandler Copeland Date: Mon, 6 Apr 2026 14:10:38 -0600 Subject: [PATCH] =?UTF-8?q?docs(phase-20):=20create=20phase=20plan=20?= =?UTF-8?q?=E2=80=94=20apply=20template=20and=20portal=20nav?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .planning/ROADMAP.md | 6 +- .../20-01-PLAN.md | 422 ++++++++++++++++++ .../20-02-PLAN.md | 258 +++++++++++ 3 files changed, 685 insertions(+), 1 deletion(-) create mode 100644 .planning/phases/20-apply-template-and-portal-nav/20-01-PLAN.md create mode 100644 .planning/phases/20-apply-template-and-portal-nav/20-02-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 88b0d78..26ae1c3 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -433,7 +433,11 @@ Plans: 3. Template signer roles are automatically pre-mapped to the client's contacts (first role to primary contact, second role to co-buyer if present); agent can override any mapping before sending 4. Text hints from the template appear as quick-fill suggestion buttons in the new document's PreparePanel 5. "Templates" appears in the portal top nav and `/portal/templates` lists all active templates with form name, field count, and last-updated date -**Plans**: TBD +**Plans**: 2 plans + +Plans: +- [ ] 20-01-PLAN.md — Extend POST /api/documents with template branch + Add My Templates tab to AddDocumentModal +- [ ] 20-02-PLAN.md — Template hint quick-fill chips in PreparePanel + human E2E verification **UI hint**: yes ## Progress diff --git a/.planning/phases/20-apply-template-and-portal-nav/20-01-PLAN.md b/.planning/phases/20-apply-template-and-portal-nav/20-01-PLAN.md new file mode 100644 index 0000000..7206df6 --- /dev/null +++ b/.planning/phases/20-apply-template-and-portal-nav/20-01-PLAN.md @@ -0,0 +1,422 @@ +--- +phase: 20-apply-template-and-portal-nav +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - teressa-copeland-homes/src/app/portal/_components/AddDocumentModal.tsx + - teressa-copeland-homes/src/app/api/documents/route.ts +autonomous: true +requirements: [TMPL-10, TMPL-11, TMPL-12, TMPL-14] + +must_haves: + truths: + - "Agent sees a 'My Templates' tab in the Add Document modal alongside the existing Forms Library" + - "Agent can pick a saved template and click Add Document to create a document with all template fields pre-loaded at their saved positions" + - "Every field copied from a template has a fresh UUID — no template field ID appears in the new document" + - "Template signer roles are auto-mapped to client contacts (first role to client email, second role to co-buyer email)" + - "Editing a template afterward does not change the document's fields (snapshot independence)" + artifacts: + - path: "teressa-copeland-homes/src/app/portal/_components/AddDocumentModal.tsx" + provides: "Two-tab modal with Forms Library + My Templates" + contains: "activeTab" + - path: "teressa-copeland-homes/src/app/api/documents/route.ts" + provides: "Template apply branch in POST handler" + contains: "documentTemplateId" + key_links: + - from: "AddDocumentModal.tsx" + to: "POST /api/documents" + via: "fetch with documentTemplateId in JSON body" + pattern: "documentTemplateId.*selectedDocTemplate" + - from: "POST /api/documents" + to: "documentTemplates table" + via: "db.query.documentTemplates.findFirst" + pattern: "documentTemplates" + - from: "POST /api/documents" + to: "clients table" + via: "db.query.clients.findFirst for role-to-email mapping" + pattern: "clients\\.id.*clientId" +--- + + +Add "My Templates" tab to AddDocumentModal and extend POST /api/documents to apply a template — copying fields with fresh UUIDs and auto-mapping signer roles to client contacts. + +Purpose: Lets the agent start a new document from a saved template instead of a blank form, eliminating repetitive field placement on commonly-used PDFs. +Output: Modified AddDocumentModal.tsx with two tabs, extended POST /api/documents route with documentTemplateId branch. + + + +@$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/20-apply-template-and-portal-nav/20-CONTEXT.md +@.planning/phases/20-apply-template-and-portal-nav/20-RESEARCH.md + + + + +From src/lib/db/schema.ts: +```typescript +export interface SignatureFieldData { + id: string; + page: number; // 1-indexed + x: number; // PDF user space, bottom-left origin, points + y: number; + width: number; + height: number; + type?: SignatureFieldType; + signerEmail?: string; // In templates: carries role labels like "Buyer", "Seller" + hint?: string; // Optional label for client-text fields +} + +export interface ClientContact { + name: string; + email: string; +} + +export interface DocumentSigner { + email: string; + color: string; +} + +export const documentTemplates = pgTable("document_templates", { + id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()), + name: text("name").notNull(), + formTemplateId: text("form_template_id").notNull().references(() => formTemplates.id), + signatureFields: jsonb("signature_fields").$type(), + archivedAt: timestamp("archived_at"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + +export const documentTemplatesRelations = relations(documentTemplates, ({ one }) => ({ + formTemplate: one(formTemplates, { fields: [documentTemplates.formTemplateId], references: [formTemplates.id] }), +})); +``` + +From src/app/api/templates/route.ts GET response shape: +```typescript +// Each row in the JSON array: +{ + id: string; + name: string; + formTemplateId: string; + formName: string | null; + fieldCount: number; + createdAt: Date; + updatedAt: Date; +} +``` + +From src/app/api/documents/route.ts — existing POST handler structure: +```typescript +// Content-type branching: +// - multipart/form-data → custom PDF upload (reads file from FormData) +// - else (JSON) → form-library copy (reads formTemplateId, copies PDF from seeds/) +// Both paths: create destDir, copy/write PDF, INSERT into documents table +// The JSON branch is what we extend with documentTemplateId support +``` + +From src/app/portal/_components/AddDocumentModal.tsx — existing state: +```typescript +type FormTemplate = { id: string; name: string; filename: string }; +// Props: { clientId: string; onClose: () => void } +// State: templates (FormTemplate[]), selectedTemplate, customFile, docName, query +// Submit: customFile → FormData POST, else → JSON POST with formTemplateId +``` + + + + + + + Task 1: Extend POST /api/documents with documentTemplateId branch + teressa-copeland-homes/src/app/api/documents/route.ts + + teressa-copeland-homes/src/app/api/documents/route.ts + teressa-copeland-homes/src/lib/db/schema.ts + teressa-copeland-homes/src/app/api/templates/route.ts + + +Extend the JSON body parsing in the POST handler to also extract `documentTemplateId: string | undefined` from `body`. + +After the existing `if (!clientId || !name)` guard, add a new branch BEFORE the existing `formTemplateId` branch. The logic: + +``` +if (documentTemplateId) { + // 1. Fetch document template with its formTemplate relation + const docTemplate = await db.query.documentTemplates.findFirst({ + where: eq(documentTemplates.id, documentTemplateId), + with: { formTemplate: true }, + }); + if (!docTemplate) return Response.json({ error: 'Document template not found' }, { status: 404 }); + + // 2. Copy PDF from seeds dir using the form template's filename + const srcPath = path.join(SEEDS_DIR, docTemplate.formTemplate.filename); + await copyFile(srcPath, destPath); + + // 3. Copy fields with fresh UUIDs (D-07) — hints preserved verbatim (D-09) + const rawFields: SignatureFieldData[] = (docTemplate.signatureFields as SignatureFieldData[] | null) ?? []; + const copiedFields: SignatureFieldData[] = rawFields.map(f => ({ + ...f, + id: crypto.randomUUID(), + })); + + // 4. Role-to-email mapping (D-06) + // Collect unique role labels in order of first appearance + const seenRoles = new Set(); + const uniqueRoles: string[] = []; + for (const f of copiedFields) { + if (f.signerEmail && !seenRoles.has(f.signerEmail)) { + seenRoles.add(f.signerEmail); + uniqueRoles.push(f.signerEmail); + } + } + + // Fetch client for email + contacts + const client = await db.query.clients.findFirst({ + where: eq(clients.id, clientId), + }); + const clientEmails = [ + client?.email, + ...((client?.contacts as ClientContact[] | null) ?? []).map(c => c.email), + ].filter(Boolean) as string[]; + + const SIGNER_COLORS = ['#6366f1', '#f43f5e', '#10b981', '#f59e0b']; + const mappedSigners: DocumentSigner[] = uniqueRoles.map((role, i) => ({ + email: clientEmails[i] ?? role, + color: SIGNER_COLORS[i % SIGNER_COLORS.length], + })); + + // 5. Override formTemplateId for the DB insert + formTemplateId = docTemplate.formTemplateId; + + // 6. Insert with fields and signers + const [doc] = await db.insert(documents).values({ + id: docId, + clientId, + name, + formTemplateId: formTemplateId ?? null, + filePath: relPath, + status: 'Draft', + signatureFields: copiedFields, + signers: mappedSigners.length > 0 ? mappedSigners : null, + }).returning(); + + return Response.json(doc, { status: 201 }); +} +``` + +The existing `else` path (formTemplateId from form library) and file upload path remain completely unchanged. + +Add imports at top: `import { documentTemplates, clients } from '@/lib/db/schema';` and `import type { SignatureFieldData, ClientContact, DocumentSigner } from '@/lib/db/schema';`. The `clients` import is new; `documents` and `formTemplates` are already imported. + +Also parse `documentTemplateId` from body: add `let documentTemplateId: string | undefined;` alongside existing declarations, and in the JSON else block add `documentTemplateId = body.documentTemplateId;`. + +IMPORTANT: The template branch has its OWN db.insert + return, so the existing insert at the bottom of the function only runs for the non-template paths. Structure it so the template branch returns early. + + + cd teressa-copeland-homes && npx tsc --noEmit + + + - grep "documentTemplateId" src/app/api/documents/route.ts returns at least 3 matches (declaration, parse, condition) + - grep "crypto.randomUUID" src/app/api/documents/route.ts returns at least 2 matches (docId + field copy) + - grep "signatureFields: copiedFields" src/app/api/documents/route.ts returns 1 match + - grep "SIGNER_COLORS" src/app/api/documents/route.ts returns at least 1 match + - grep "clientEmails" src/app/api/documents/route.ts returns at least 1 match + - grep "import.*clients" src/app/api/documents/route.ts returns 1 match (clients table import) + - npx tsc --noEmit exits 0 + + POST /api/documents accepts documentTemplateId, copies fields with fresh UUIDs, maps roles to client contacts, inserts document with signatureFields and signers + + + + Task 2: Add My Templates tab to AddDocumentModal + teressa-copeland-homes/src/app/portal/_components/AddDocumentModal.tsx + + teressa-copeland-homes/src/app/portal/_components/AddDocumentModal.tsx + + +Add state variables for the template tab: + +```typescript +type DocumentTemplateRow = { + id: string; + name: string; + formName: string | null; + fieldCount: number; + updatedAt: string; +}; + +const [activeTab, setActiveTab] = useState<'forms' | 'templates'>('forms'); +const [docTemplates, setDocTemplates] = useState([]); +const [docTemplatesLoaded, setDocTemplatesLoaded] = useState(false); +const [selectedDocTemplate, setSelectedDocTemplate] = useState(null); +``` + +Add lazy fetch function — fetches templates only on first click of the My Templates tab: + +```typescript +function handleSwitchToTemplates() { + setActiveTab('templates'); + if (!docTemplatesLoaded) { + fetch('/api/templates') + .then(r => r.json()) + .then((data: DocumentTemplateRow[]) => { setDocTemplates(data); setDocTemplatesLoaded(true); }) + .catch(console.error); + } +} +``` + +Add mutual exclusivity to selection handlers: +- In `handleSelectTemplate` (existing form-library handler): add `setSelectedDocTemplate(null);` at top +- Add new handler for document template selection: +```typescript +const handleSelectDocTemplate = (t: DocumentTemplateRow) => { + setSelectedDocTemplate(t); + setSelectedTemplate(null); + setCustomFile(null); + setDocName(t.name); +}; +``` + +Extend `handleSubmit`: +- Add a NEW branch at the top of the try block, before the existing `if (customFile)`: +```typescript +if (selectedDocTemplate) { + await fetch('/api/documents', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + clientId, + name: docName.trim(), + documentTemplateId: selectedDocTemplate.id, + }), + }); +} else if (customFile) { + // ... existing custom file path unchanged +} else { + // ... existing form library path unchanged +} +``` + +Update the early-return guard in handleSubmit: +```typescript +if (!docName.trim() || (!selectedTemplate && !customFile && !selectedDocTemplate)) return; +``` + +Update the submit button disabled condition: +```typescript +disabled={saving || (!selectedTemplate && !customFile && !selectedDocTemplate) || !docName.trim()} +``` + +Render two tab buttons between the h2 heading and the search input. Use underline-style tabs matching the project's plain Tailwind approach: + +```tsx +
+ + +
+``` + +Conditionally render tab content: +- When `activeTab === 'forms'`: show the existing search input + form list + custom file upload section (ALL existing markup unchanged) +- When `activeTab === 'templates'`: show the templates list: + +```tsx +{activeTab === 'templates' && ( +
+ {!docTemplatesLoaded ? ( +

Loading templates...

+ ) : docTemplates.length === 0 ? ( +

+ No templates saved yet. Create one from the Templates page. +

+ ) : ( +
    + {docTemplates.map(t => ( +
  • handleSelectDocTemplate(t)} + className={`px-3 py-2 text-sm cursor-pointer hover:bg-gray-100 ${ + selectedDocTemplate?.id === t.id ? 'bg-blue-50 font-medium' : '' + }`} + > + {t.name} + + {t.formName ?? 'Unknown form'} · {t.fieldCount} field{t.fieldCount !== 1 ? 's' : ''} + +
  • + ))} +
+ )} +
+)} +``` + +The `{activeTab === 'forms' && ( ... )}` wraps around the existing search input, form list `
    `, and custom file upload `
    `. All existing elements are preserved exactly — only wrapped in a conditional. + + + cd teressa-copeland-homes && npx tsc --noEmit + + + - grep "activeTab" src/app/portal/_components/AddDocumentModal.tsx returns at least 4 matches + - grep "My Templates" src/app/portal/_components/AddDocumentModal.tsx returns at least 1 match + - grep "Forms Library" src/app/portal/_components/AddDocumentModal.tsx returns at least 1 match + - grep "selectedDocTemplate" src/app/portal/_components/AddDocumentModal.tsx returns at least 3 matches + - grep "documentTemplateId" src/app/portal/_components/AddDocumentModal.tsx returns at least 1 match + - grep "/api/templates" src/app/portal/_components/AddDocumentModal.tsx returns at least 1 match + - grep "No templates saved yet" src/app/portal/_components/AddDocumentModal.tsx returns 1 match (empty state) + - npx tsc --noEmit exits 0 + + AddDocumentModal has two tabs (Forms Library + My Templates), lazy-loads templates on first tab click, sends documentTemplateId to POST /api/documents when a template is selected + + + + + +1. `cd teressa-copeland-homes && npx tsc --noEmit` — zero type errors +2. `cd teressa-copeland-homes && npm run build` — production build succeeds +3. grep confirms: documentTemplateId in both route.ts and AddDocumentModal.tsx +4. grep confirms: crypto.randomUUID in route.ts field copy +5. grep confirms: SIGNER_COLORS in route.ts +6. grep confirms: activeTab, selectedDocTemplate in AddDocumentModal.tsx + + + +- POST /api/documents accepts documentTemplateId and creates a document with copied fields (fresh UUIDs), mapped signers, and correct formTemplateId +- AddDocumentModal shows two tabs; My Templates tab fetches from GET /api/templates and displays template rows +- Selecting a template and clicking Add Document creates a document via the template branch +- No changes to existing Forms Library tab or custom file upload behavior +- TypeScript compiles with zero errors + + + +After completion, create `.planning/phases/20-apply-template-and-portal-nav/20-01-SUMMARY.md` + diff --git a/.planning/phases/20-apply-template-and-portal-nav/20-02-PLAN.md b/.planning/phases/20-apply-template-and-portal-nav/20-02-PLAN.md new file mode 100644 index 0000000..8b63ba7 --- /dev/null +++ b/.planning/phases/20-apply-template-and-portal-nav/20-02-PLAN.md @@ -0,0 +1,258 @@ +--- +phase: 20-apply-template-and-portal-nav +plan: 02 +type: execute +wave: 2 +depends_on: [20-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 +autonomous: false +requirements: [TMPL-13, TMPL-15, TMPL-16] + +must_haves: + truths: + - "When agent selects a text field that has a hint (from template), a 'Template Hint' chip appears in the Quick Fill section of PreparePanel" + - "Clicking the hint chip fills the selected field with the hint text (same behavior as existing quick-fill)" + - "Fields without hints show no extra chip — existing quick-fill behavior unchanged" + - "Templates nav link exists in portal navigation (Phase 19, already done)" + - "Templates list page shows all templates with form name, field count, last-updated (Phase 19, already done)" + artifacts: + - path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx" + provides: "Fields state fetch + selectedFieldHint derivation" + contains: "selectedFieldHint" + - path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx" + provides: "Template Hint chip in Quick Fill section" + contains: "selectedFieldHint" + key_links: + - from: "DocumentPageClient.tsx" + to: "GET /api/documents/:docId/fields" + via: "fetch on mount + aiPlacementKey change" + pattern: "api/documents.*fields" + - from: "DocumentPageClient.tsx" + to: "PreparePanel" + via: "selectedFieldHint prop" + pattern: "selectedFieldHint=" + - from: "PreparePanel.tsx" + to: "onQuickFill callback" + via: "hint chip onClick" + pattern: "Template Hint" +--- + + +Surface template text hints as quick-fill suggestions in PreparePanel and verify the complete Phase 20 feature set with human testing. + +Purpose: When a document is created from a template, text fields with hints (e.g., "Property Address", "Purchase Price") show those hints as one-click quick-fill chips, saving the agent from remembering what each blank field is for. +Output: Modified DocumentPageClient.tsx (fields state + hint derivation), modified PreparePanel.tsx (hint chip), human verification of the full template-to-document flow. + + + +@$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/20-apply-template-and-portal-nav/20-CONTEXT.md +@.planning/phases/20-apply-template-and-portal-nav/20-RESEARCH.md +@.planning/phases/20-apply-template-and-portal-nav/20-01-SUMMARY.md + + + + +From src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx: +```typescript +interface DocumentPageClientProps { + docId: string; + docStatus: string; + defaultEmail: string; + clientName: string; + agentDownloadUrl?: string | null; + signedAt?: Date | null; + clientPropertyAddress?: string | null; + initialSigners: DocumentSigner[]; + clientContacts?: { name: string; email: string }[]; +} + +// Existing state: +// selectedFieldId: string | null +// textFillData: Record +// aiPlacementKey: number +// signers: DocumentSigner[] +``` + +From src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx: +```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; + textFillData: Record; + selectedFieldId: string | null; + onQuickFill: (fieldId: string, value: string) => void; + onAiAutoPlace: () => Promise; + signers?: DocumentSigner[]; + onSignersChange?: (signers: DocumentSigner[]) => void; + unassignedFieldIds?: Set; + onUnassignedFieldIdsChange?: (ids: Set) => void; +} +``` + +Quick Fill section pattern (PreparePanel lines 313-355): +```typescript +{/* Quick-fill panel — only shown when a text field is selected */} +{selectedFieldId ? ( +
    +

    Click a suggestion to fill the selected field.

    + {clientName && ( )} + {clientPropertyAddress && ( )} + +
    +) : ( +

    Click a text field on the document...

    +)} +``` +
    +
    + + + + + Task 1: Add fields state to DocumentPageClient and pass selectedFieldHint to PreparePanel + + 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/DocumentPageClient.tsx + teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx + teressa-copeland-homes/src/lib/db/schema.ts + + +**In DocumentPageClient.tsx:** + +1. Add import: `import type { SignatureFieldData } from '@/lib/db/schema';` (DocumentSigner is already imported). + +2. Add state for fields: +```typescript +const [fields, setFields] = useState([]); +``` + +3. Add useEffect to fetch fields on mount and when aiPlacementKey changes: +```typescript +useEffect(() => { + fetch(`/api/documents/${docId}/fields`) + .then(r => r.json()) + .then((data: SignatureFieldData[]) => setFields(Array.isArray(data) ? data : [])) + .catch(() => {}); +}, [docId, aiPlacementKey]); +``` + +4. Derive the hint for the selected field: +```typescript +const selectedFieldHint = selectedFieldId + ? fields.find(f => f.id === selectedFieldId)?.hint + : undefined; +``` + +5. Pass new prop to PreparePanel: +```typescript + +``` + +**In PreparePanel.tsx:** + +1. Add `selectedFieldHint?: string;` to the `PreparePanelProps` interface (after `onQuickFill`). + +2. Destructure it in the function signature: add `selectedFieldHint,` to the destructured props. + +3. In the Quick Fill section (inside the `{selectedFieldId ? (` branch), add a new chip AFTER the existing Client Email chip button (before the closing `
    ` of `space-y-1.5`): + +```tsx +{selectedFieldHint && ( + +)} +``` + +This follows the exact same markup pattern as the existing Client Name / Property Address / Client Email chips. The prop is optional with no default — existing callers (DocumentPageClient without template-sourced documents) simply don't pass it, and `undefined` means no chip renders. + + + cd teressa-copeland-homes && npx tsc --noEmit + + + - grep "selectedFieldHint" src/app/portal/\(protected\)/documents/\[docId\]/_components/DocumentPageClient.tsx returns at least 2 matches (derivation + prop pass) + - grep "selectedFieldHint" src/app/portal/\(protected\)/documents/\[docId\]/_components/PreparePanel.tsx returns at least 3 matches (interface + destructure + render) + - grep "Template Hint" src/app/portal/\(protected\)/documents/\[docId\]/_components/PreparePanel.tsx returns 1 match + - grep "api/documents.*fields" src/app/portal/\(protected\)/documents/\[docId\]/_components/DocumentPageClient.tsx returns at least 1 match + - grep "SignatureFieldData" src/app/portal/\(protected\)/documents/\[docId\]/_components/DocumentPageClient.tsx returns at least 1 match (import) + - npx tsc --noEmit exits 0 + + DocumentPageClient fetches fields on mount, derives selectedFieldHint from selected field, passes it to PreparePanel. PreparePanel renders a "Template Hint" quick-fill chip when the hint exists. + + + + Task 2: Human verification of full template-to-document flow + + Complete "Start from template" feature: + - My Templates tab in Add Document modal (Plan 01) + - Template apply with fresh field UUIDs and role mapping (Plan 01) + - Text hint quick-fill chips in PreparePanel (Plan 02 Task 1) + - Templates nav + list page (Phase 19, already live) + + + Prerequisites: At least one saved template with fields and text hints must exist (created in Phase 19 testing). + + 1. Navigate to /portal/templates — verify the list page shows saved templates with form name, field count, and updated date (TMPL-16) + 2. Verify "Templates" appears in the portal top nav (TMPL-15) + 3. Go to a client profile page → click "Add Document" + 4. Verify the modal shows two tabs: "Forms Library" and "My Templates" (TMPL-10) + 5. Click "My Templates" tab → verify saved templates appear with name, form name, and field count + 6. Select a template → verify the document name auto-fills with the template name + 7. Click "Add Document" → verify the document is created and you're returned to the client page + 8. Open the newly created document → verify fields are pre-loaded at the correct positions on the PDF (TMPL-11) + 9. In PreparePanel, click a text field that has a hint → verify a "Template Hint" chip appears in the Quick Fill section alongside Client Name/Address/Email (TMPL-13) + 10. Click the hint chip → verify the field is filled with the hint text + 11. Click the "Forms Library" tab back in a new Add Document modal → verify the existing form library still works exactly as before (D-04) + 12. (TMPL-14 — snapshot independence): Go to /portal/templates → edit the template (change a field position) → go back to the document created in step 7 → verify the document's fields are unchanged (still at original positions) + + Type "approved" or describe any issues found + + + + + +1. `cd teressa-copeland-homes && npx tsc --noEmit` — zero type errors +2. `cd teressa-copeland-homes && npm run build` — production build succeeds +3. Human verification confirms all 12 steps pass +4. TMPL-10 through TMPL-16 all satisfied (TMPL-14/15/16 by design or Phase 19) + + + +- Template Hint chip appears in Quick Fill when a text field with a hint is selected +- Chip click fills the field (same behavior as existing quick-fill chips) +- Fields without hints show no extra chip +- Human confirms full template-to-document flow works end-to-end +- All 7 TMPL requirements (TMPL-10 through TMPL-16) are satisfied + + + +After completion, create `.planning/phases/20-apply-template-and-portal-nav/20-02-SUMMARY.md` +