# Phase 20: Apply Template and Portal Nav - Research **Researched:** 2026-04-06 **Domain:** Next.js App Router, Drizzle ORM, React client-state wiring, JSONB field snapshot copy **Confidence:** HIGH --- ## User Constraints (from CONTEXT.md) ### Locked Decisions **AddDocumentModal Redesign** - D-01: Modal gets two tabs: "Forms Library" (existing, unchanged) and "My Templates" (new). Tab switching is client-side state — no page reload. - D-02: "My Templates" fetches from `GET /api/templates`. Each row shows: template name, form name, field count. Agent clicks a row to select it. - D-03: No second "role mapping" step in the modal. Agent picks template → clicks "Add Document" → document created immediately. Role assignment happens in PreparePanel (existing flow). - D-04: "Forms Library" tab, "Or upload a custom PDF" section, and all existing modal behavior remain completely unchanged. Pure additive change to `AddDocumentModal.tsx`. **Apply Template Operation (API)** - D-05: `POST /api/documents` accepts a new optional body field `documentTemplateId: string`. When present, server reads template's `signatureFields`, copies with new UUIDs, stores as new document's fields. - D-06: Role-to-email mapping: server reads role labels from copied fields' `signerEmail` values. Builds `signers` array: role[0] → `client.email`, role[1] → `client.contacts[0]?.email` (if present), role[2+] → left as-is (role label string). Happens server-side during document creation. - D-07: Every `SignatureFieldData` object copied from template gets `id: crypto.randomUUID()`. No template field ID may appear in the new document's fields. - D-08: Created document's `formTemplateId` set to the template's `formTemplateId`. Document is NOT linked to `documentTemplate` record after creation — snapshot, not live reference. **Text Hints as Quick-Fill** - D-09: Field `hint` values copied verbatim from `documentTemplates.signatureFields[].hint` into new document's fields. - D-10: In PreparePanel, when agent selects a text field with a `hint`, it appears as a clickable suggestion chip in the existing Quick Fill suggestions area. Clicking fills the field with hint text. - D-11: Hints are NOT shown as placeholder text on the field — only in PreparePanel suggestions area. - D-12: PreparePanel already has `textFillData` system and Quick Fill area. Hint chip is added to that same section — no new UI region. **Navigation (Already Delivered — No Work Needed)** - Templates nav link: DONE in Phase 19 - `/portal/templates` list page: DONE in Phase 19 - Phase 20 does NOT touch nav or templates list page. ### Claude's Discretion - Exact tab styling in AddDocumentModal (pill tabs vs underline tabs) - Empty state for "My Templates" tab when no templates exist - How field count is displayed in the template picker row - Order of roles when more than 2 are present in the template ### Deferred Ideas (OUT OF SCOPE) - Template versioning (already addressed by D-08 snapshot approach) - Bulk-apply template to multiple clients at once - Template categories or tags for organizing the My Templates list --- ## Phase Requirements | ID | Description | Research Support | |----|-------------|------------------| | TMPL-10 | Agent can choose "Start from template" when adding a document | D-01/D-02: "My Templates" tab in AddDocumentModal; API in AddDocumentModal.handleSubmit | | TMPL-11 | Applying template creates document with all template fields pre-loaded (fresh IDs, positions, types, role assignments) | D-05/D-07: POST /api/documents copies `signatureFields` with `crypto.randomUUID()` per field | | TMPL-12 | Template signer roles auto-mapped to client contacts in order; agent can override before sending | D-06: server maps roles to `client.email` + `contacts[]`; PreparePanel signer list is the override surface | | TMPL-13 | Text hints from template appear as quick-fill suggestions in PreparePanel | D-09/D-10/D-12: hints copied into document fields; PreparePanel surfaces as chip next to Client Name/Address | | TMPL-14 | Editing a template after documents created from it does NOT change those documents | D-08: snapshot copy at creation — no FK link from document back to documentTemplate | | TMPL-15 | "Templates" in portal nav | Already done Phase 19 — no work needed | | TMPL-16 | Templates list page shows all templates with form name, field count, last-updated date | Already done Phase 19 — no work needed | --- ## Summary Phase 20 is the final wiring of the template system into the document creation flow. The infrastructure is fully in place (Phase 18: schema + API, Phase 19: editor + field saving). This phase adds three incremental features: a second tab in `AddDocumentModal`, an extension to `POST /api/documents`, and a hint chip in `PreparePanel`. The code surface is small and well-bounded. All state management patterns (tabs as `useState`, Quick Fill chips, selectedFieldId threading) are established. The only new data flow is `documentTemplateId` on the POST body causing the server to fetch the template, copy fields with fresh UUIDs, and set `signers` from role-to-email mapping. **TMPL-14 and TMPL-15/16 are already satisfied.** TMPL-14 is satisfied by the snapshot design (D-08) — no new code. TMPL-15 and TMPL-16 were completed in Phase 19 and need no changes. **Primary recommendation:** Three focused changes — AddDocumentModal tab UI, POST /api/documents template branch, PreparePanel hint chip — each independent and non-breaking. --- ## Standard Stack ### Core (already installed — no new dependencies) | Library | Version | Purpose | Why Standard | |---------|---------|---------|--------------| | React `useState` | (React 19) | Tab switching, template selection state | Existing modal pattern | | Next.js App Router `fetch` | (Next.js 16.2) | `GET /api/templates` from modal | Existing pattern in AddDocumentModal | | Drizzle ORM | (existing) | DB read of `documentTemplates` + `clients.contacts` in POST handler | All DB calls use Drizzle in this project | | `crypto.randomUUID()` | Node built-in | Fresh UUID per copied field | Already used in `documents` route | ### No new packages required The phase requires zero new npm dependencies. All libraries are already installed and in use. --- ## Architecture Patterns ### Pattern 1: Two-Tab Modal (additive to AddDocumentModal) **What:** Add a `activeTab: 'forms' | 'templates'` state variable. Render tab buttons at modal top. Conditionally render either the existing forms list (completely unchanged) or the new My Templates list. All existing submit logic is preserved — only the selection state changes. **Key insight from reading the code:** `AddDocumentModal` currently has two submission paths (custom file vs form template), determined by which of `selectedTemplate` (form library) or `customFile` is set. The template tab adds a third selection type: `selectedDocumentTemplate`. When `selectedDocumentTemplate` is set, the JSON POST branch sends `documentTemplateId` instead of `formTemplateId`. **State shape needed:** ```typescript // New state const [activeTab, setActiveTab] = useState<'forms' | 'templates'>('forms'); const [documentTemplates, setDocumentTemplates] = useState([]); const [selectedDocumentTemplate, setSelectedDocumentTemplate] = useState(null); // DocumentTemplateRow type (from GET /api/templates response) type DocumentTemplateRow = { id: string; name: string; formName: string | null; fieldCount: number; updatedAt: string; }; ``` **Fetch:** The `GET /api/templates` endpoint already returns `id, name, formName, fieldCount, updatedAt`. The modal can call it once on mount (alongside the existing forms-library fetch) or lazily on first tab click. Lazy on first click avoids an unnecessary request when agent never uses My Templates. **Submit path for template:** ```typescript // Inside handleSubmit — new third branch: if (selectedDocumentTemplate) { await fetch('/api/documents', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clientId, name: docName.trim(), documentTemplateId: selectedDocumentTemplate.id, }), }); } ``` **Validation:** The submit button's disabled condition must include the new path: ``` disabled={saving || (!selectedTemplate && !customFile && !selectedDocumentTemplate) || !docName.trim()} ``` **Selection mutual exclusivity:** When agent picks a form-library template, clear `selectedDocumentTemplate`. When agent picks a document template, clear `selectedTemplate` and `customFile`. Tab switching itself does NOT clear selections — so switching back preserves prior selection. **ClientId availability:** `AddDocumentModal` receives `clientId` as a prop. The `client` object itself (with `contacts`) is NOT passed into `AddDocumentModal` — it stays in `ClientProfileClient`. The role-to-email mapping is done server-side in `POST /api/documents` by fetching the client record from DB. ### Pattern 2: POST /api/documents — Template Branch **What:** Extend the existing JSON-body path in `POST /api/documents` to handle `documentTemplateId`. This is a new branch inside the existing `else` block (JSON content-type). **Existing flow (JSON path):** reads `formTemplateId`, looks up `formTemplates` to get `filename`, copies PDF file, inserts document with `signatureFields: null`. **New flow (template path):** 1. Parse `documentTemplateId` from body alongside existing fields 2. When `documentTemplateId` is present: a. Fetch `documentTemplates` record with `formTemplateId` (to get `filename` from joined `formTemplates`) and `signatureFields` b. Copy PDF from seeds dir using the form template's `filename` (same as existing path) c. Copy `signatureFields` with fresh UUIDs: `template.signatureFields.map(f => ({ ...f, id: crypto.randomUUID() }))` d. Extract unique role labels (order of first appearance) from copied fields' `signerEmail` values e. Build `signers` array for the document: `roles[0] → client.email`, `roles[1] → contacts[0]?.email ?? roles[1]`, `roles[2+] → roles[n]` (kept as-is) f. Fetch client record to get `email` and `contacts` g. Set `formTemplateId` from the document template's `formTemplateId` h. Insert document with `signatureFields: copiedFields`, `signers: mappedSigners`, `formTemplateId` **Critical: what "roles" means.** Template fields carry role labels (e.g., "Buyer", "Seller") in their `signerEmail` slot. These are NOT real emails. The role-to-email mapping replaces these strings with real emails in the `signers` array on the document record. The `signatureFields` themselves keep the role labels in `signerEmail` — the `signers` array is where real emails live. Verify this matches Phase 16's PreparePanel signer resolution pattern. **Client fetch:** The `POST /api/documents` handler currently does NOT fetch the client record. Adding `documentTemplateId` support requires a new DB query: `db.query.clients.findFirst({ where: eq(clients.id, clientId) })`. **Field ID guarantee:** `crypto.randomUUID()` is Node built-in — already imported implicitly (used on `documents.id` via Drizzle schema defaults). Explicitly call it per field in the map. **Code structure (pseudo):** ```typescript // New branch inside the JSON else block: let signatureFields: SignatureFieldData[] | null = null; let mappedSigners: DocumentSigner[] | null = null; if (documentTemplateId) { 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 }); // Copy PDF using the associated form template's filename const srcPath = path.join(SEEDS_DIR, docTemplate.formTemplate.filename); await copyFile(srcPath, destPath); // Copy fields with fresh UUIDs — hints are preserved verbatim (D-09) const rawFields = docTemplate.signatureFields ?? []; signatureFields = rawFields.map(f => ({ ...f, id: crypto.randomUUID() })); // Build signers from role-to-email mapping (D-06) const client = await db.query.clients.findFirst({ where: eq(clients.id, clientId) }); const uniqueRoles = [...new Set( signatureFields.map(f => f.signerEmail).filter(Boolean) )] as string[]; const contacts = [client?.email, ...(client?.contacts ?? []).map(c => c.email)]; mappedSigners = uniqueRoles.map((role, i) => ({ email: contacts[i] ?? role, // fallback to role label if no contact at that index color: SIGNER_COLORS[i % SIGNER_COLORS.length], })); formTemplateId = docTemplate.formTemplateId; // override for DB insert } else if (formTemplateId) { // existing form-library path unchanged } ``` **SIGNER_COLORS in API route:** `PreparePanel` and `DocumentPageClient` both define `SIGNER_COLORS = ['#6366f1', '#f43f5e', '#10b981', '#f59e0b']`. The API route needs the same palette to assign consistent colors. Define the same constant in the route or extract to a shared util. Recommendation: inline the constant in the route (same value, avoids client/server boundary import complexity). ### Pattern 3: PreparePanel Hint Chip **What:** When `selectedFieldId` is set, look up the field's `hint` in the fields list. If present, render a chip in the Quick Fill section alongside existing suggestions. **Data access challenge:** PreparePanel currently receives `selectedFieldId` and `textFillData` but does NOT receive the list of fields — it doesn't know the hints. There are two approaches: **Option A (server-threaded prop):** Pass `fields: SignatureFieldData[]` from `DocumentPageClient` to `PreparePanel` as a new optional prop. `DocumentPageClient` would need to track fields state. But currently fields are fetched and owned by `FieldPlacer` — `DocumentPageClient` doesn't hold them. **Option B (hint-only prop, preferred):** Pass `fieldHints: Record` from `DocumentPageClient` to `PreparePanel`. `DocumentPageClient` initializes `fieldHints` when it has the hint data. But `DocumentPageClient` also doesn't receive fields on mount — they're fetched by FieldPlacer. **Option C (fetch in PreparePanel, simplest):** When `selectedFieldId` changes (and is non-null), PreparePanel fetches the hint for that field from `/api/documents/:id/fields`, picks out the selected field's `hint`. This adds a round-trip but is completely self-contained. However it fetches ALL fields just for the hint. **Option D (lift fields to DocumentPageClient via callback, recommended):** Introduce a `onFieldsLoaded` callback from `DocumentPageClient` → `FieldPlacer`. When FieldPlacer fetches fields from DB, it also calls this callback. `DocumentPageClient` stores `fields: SignatureFieldData[]` in state and passes them to `PreparePanel`. This is analogous to how `aiPlacementKey` causes FieldPlacer to re-fetch. However this requires more prop threading. **Simplest viable approach for this phase (recommended based on scope):** Pass `selectedFieldHint: string | undefined` as a new prop to `PreparePanel`. In `DocumentPageClient`, add a `fields` state initialized from a one-time fetch of `/api/documents/:docId/fields` on mount. When `selectedFieldId` changes, look up the hint from `fields`. This is one additional fetch (which is already done by FieldPlacer anyway — lightweight deduplication). Actually — the simplest correct approach: `DocumentPageClient` fetches fields once at mount to populate `fields` state. This is also needed for the role auto-seed check (`initialSigners.length === 0` check already there). The hint lookup is then: `const selectedFieldHint = selectedFieldId ? fields.find(f => f.id === selectedFieldId)?.hint : undefined`. Pass `selectedFieldHint?: string` as a new optional prop to `PreparePanel`. In PreparePanel's Quick Fill section: ```typescript {selectedFieldHint && ( )} ``` This is a backward-compatible optional prop (default undefined). No existing call sites break. **Where to fetch fields in DocumentPageClient:** Add a `useEffect` on mount (empty deps) that calls `GET /api/documents/:docId/fields` and stores in `useState([])`. This endpoint already exists (used by PreparePanel's handlePrepare). When `aiPlacementKey` changes (AI re-places), re-fetch to pick up new hints. ### Pattern 4: GET /api/templates — Already Sufficient The existing endpoint already returns all required data: - `id`, `name`, `formName`, `fieldCount`, `updatedAt` — everything the My Templates tab needs. - It does NOT return `signatureFields` in the response (the `signatureFields` column is fetched but only used server-side to compute `fieldCount`). This is correct per D-02 — the modal only needs name/formName/fieldCount. - The server-side POST handler fetches `signatureFields` directly from DB, so no change to GET response needed. **Confidence:** HIGH — confirmed by reading `src/app/api/templates/route.ts` lines 1-36. ### Recommended Project Structure (files touched) ``` teressa-copeland-homes/src/ ├── app/ │ ├── portal/ │ │ └── _components/ │ │ └── AddDocumentModal.tsx ← Add My Templates tab + submit branch │ └── api/ │ └── documents/ │ └── route.ts ← Add documentTemplateId branch └── app/portal/(protected)/documents/[docId]/ └── _components/ ├── DocumentPageClient.tsx ← Add fields state + selectedFieldHint derivation └── PreparePanel.tsx ← Add selectedFieldHint prop + chip render ``` ### Anti-Patterns to Avoid - **Mutating template field IDs in place:** Always create new objects (`{ ...f, id: crypto.randomUUID() }`) — never mutate the JSONB objects fetched from DB. - **Setting signerEmail on signers array entries from role labels:** The `signers` array should always contain real emails (or the role label as fallback if no contact mapped). Fields' `signerEmail` slots keep the role labels — this is intentional for the existing color-assignment system. - **Fetching fields inside PreparePanel on every selectedFieldId change:** One fetch on mount (or on aiPlacementKey change) is sufficient. Don't fetch inside PreparePanel — keep it prop-driven. - **Breaking the existing forms-library tab:** D-04 is explicit — zero changes to existing behavior. Tab switching must not clear existing form/file selections unless the user actively picks something in the other tab. - **Passing `signatureFields` in GET /api/templates response:** The full JSONB column can be large (100+ fields). The modal only needs `id`, `name`, `formName`, `fieldCount`. Don't add `signatureFields` to the GET response. --- ## Don't Hand-Roll | Problem | Don't Build | Use Instead | Why | |---------|-------------|-------------|-----| | UUID generation per field | Custom ID generator | `crypto.randomUUID()` | Already used in schema defaults; Node built-in; no import needed | | Role deduplication in order | Custom Set logic | `[...new Set(array)]` | Standard JS, preserves insertion order | | Tab UI | Custom tab library | `useState` + conditional render | Project uses zero UI component libraries — plain Tailwind/inline styles | | Field lookup by ID | Custom index | `Array.prototype.find` | Fields array is small (< 200 items); no index needed | --- ## Runtime State Inventory This phase is not a rename/refactor. No runtime state inventory needed. All changes are code-only with no stored strings being renamed. --- ## Common Pitfalls ### Pitfall 1: signerEmail slot carries role labels, not emails **What goes wrong:** Developer reads `field.signerEmail` expecting an email address, but in templates it contains "Buyer", "Seller", etc. Code that calls `isValidEmail(field.signerEmail)` will fail. The `getSignerEmail()` utility will return the role label, which is not a real email. **Why it happens:** Template fields use `signerEmail` as a role-label slot (v1.3 design decision). The field type `SignatureFieldData.signerEmail` is typed as `string | undefined` with no discrimination between role labels and real emails. **How to avoid:** In the POST /api/documents template branch, treat `field.signerEmail` values as role labels. The `signers` array on the document is where real emails go. Never pass template field `signerEmail` values directly to email-sending logic. **Warning signs:** `isValidEmail(field.signerEmail)` returning false in the POST handler. Email send step receiving strings like "Buyer" or "Seller". ### Pitfall 2: Field copy shares object references (JSONB mutation) **What goes wrong:** `const copiedFields = template.signatureFields.map(f => ({ ...f, id: crypto.randomUUID() }))` — shallow spread is sufficient because `SignatureFieldData` has only primitive properties (strings, numbers). No nested objects. But if a future field type adds nested JSONB, mutation could propagate to the template record. **Why it happens:** Drizzle returns JSONB as JS objects, not frozen values. **How to avoid:** The shallow spread `{ ...f }` is correct for the current `SignatureFieldData` shape. Document this assumption with a comment in the code. ### Pitfall 3: clientId not validated before client fetch **What goes wrong:** The POST handler fetches the client for role mapping but doesn't guard against an invalid/missing `clientId`. If the client doesn't exist, `db.query.clients.findFirst` returns `undefined` and `client?.email` is undefined, so `contacts[0]` becomes `undefined`. **Why it happens:** The existing POST handler validates `clientId` and `name` are non-empty strings but does not verify the client exists in DB. **How to avoid:** Add a client existence check before the template branch: `if (!clientId || !name) return 400`. The role mapping degrades gracefully (role labels used as fallback `email: contacts[i] ?? role`) — so even if client fetch fails, the insert doesn't crash. But a missing client means the document is orphaned, which is a separate concern the existing code already has. ### Pitfall 4: "My Templates" tab fetch race with modal open **What goes wrong:** Modal opens → fetch to `/api/templates` is fired → agent has no templates → fetch returns empty array → agent is confused by empty state. **Why it happens:** Network latency on modal open. **How to avoid:** Show a loading state or "No templates yet" empty state. The empty state copy should be helpful: "No templates saved yet. Create one from the Templates page." ### Pitfall 5: SIGNER_COLORS constant duplication **What goes wrong:** `PreparePanel`, `DocumentPageClient`, and now `POST /api/documents` all define the same `SIGNER_COLORS` array. If the palette is changed in one place, the others become inconsistent. **Why it happens:** The color array originated in `PreparePanel`, was duplicated in `DocumentPageClient` (Phase 16), and now needs to be in the API route. **How to avoid:** For this phase, inline the constant in the API route as-is (same 4-color array). The duplication is an existing project pattern — Phase 20 should not introduce a shared-util refactor that changes scope. Document the duplication in a comment for future cleanup. --- ## Code Examples ### Apply template — field copy with fresh UUIDs ```typescript // Source: CONTEXT.md D-07, confirmed against schema.ts SignatureFieldData shape const rawFields: SignatureFieldData[] = docTemplate.signatureFields ?? []; const copiedFields: SignatureFieldData[] = rawFields.map(f => ({ ...f, // copies: page, x, y, width, height, type, hint, signerEmail (role label) id: crypto.randomUUID(), // fresh UUID — template field ID must never appear in documents (D-07) })); ``` ### Role-to-email mapping ```typescript // Source: CONTEXT.md D-06 + specifics pseudocode // 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); } } // Build client contacts array: primary email first, then co-buyer emails const clientEmails = [ client?.email, ...(client?.contacts ?? []).map((c: ClientContact) => c.email), ].filter(Boolean) as string[]; const SIGNER_COLORS = ['#6366f1', '#f43f5e', '#10b981', '#f59e0b']; const mappedSigners: DocumentSigner[] = uniqueRoles.map((role, i) => ({ email: clientEmails[i] ?? role, // fallback: keep role label if no client contact at index i color: SIGNER_COLORS[i % SIGNER_COLORS.length], })); ``` ### PreparePanel hint chip (additive) ```typescript // Source: PreparePanel.tsx Quick Fill section pattern (lines 317-355) // New prop added to PreparePanelProps: // selectedFieldHint?: string; // Inside the Quick Fill section, after existing chips: {selectedFieldHint && selectedFieldId && ( )} ``` ### DocumentPageClient — fields state + hint derivation ```typescript // New state in DocumentPageClient const [fields, setFields] = useState([]); // Fetch on mount and on AI placement key change useEffect(() => { fetch(`/api/documents/${docId}/fields`) .then(r => r.json()) .then((data: SignatureFieldData[]) => setFields(data)) .catch(() => {}); }, [docId, aiPlacementKey]); // Derive hint for selected field const selectedFieldHint = selectedFieldId ? fields.find(f => f.id === selectedFieldId)?.hint : undefined; // Pass to PreparePanel: // selectedFieldHint={selectedFieldHint} ``` ### AddDocumentModal — tab state and My Templates fetch ```typescript // New state const [activeTab, setActiveTab] = useState<'forms' | 'templates'>('forms'); const [docTemplates, setDocTemplates] = useState([]); const [docTemplatesLoaded, setDocTemplatesLoaded] = useState(false); const [selectedDocTemplate, setSelectedDocTemplate] = useState(null); // Lazy fetch on first tab click (not on mount, to avoid unnecessary request) function handleSwitchToTemplates() { setActiveTab('templates'); if (!docTemplatesLoaded) { fetch('/api/templates') .then(r => r.json()) .then((data: DocumentTemplateRow[]) => { setDocTemplates(data); setDocTemplatesLoaded(true); }) .catch(console.error); } } // In handleSelectTemplate (existing): also clear selectedDocTemplate // In template row click: clear selectedTemplate, setCustomFile(null), set selectedDocTemplate ``` --- ## State of the Art | Old Approach | Current Approach | When Changed | Impact | |--------------|------------------|--------------|--------| | Single tab in AddDocumentModal | Two tabs: Forms Library + My Templates | Phase 20 | Additive — forms tab unchanged | | POST /api/documents: form-library or custom-file only | Three paths: form-library, custom-file, document-template | Phase 20 | New branch with snapshot copy | | PreparePanel Quick Fill: 3 chips only (Name, Address, Email) | Quick Fill: up to 4 chips (+ Template Hint when applicable) | Phase 20 | Optional — only shown when selectedField has a hint | --- ## Environment Availability Step 2.6: SKIPPED — no external tools or services beyond the existing Next.js stack. All changes are to API routes and React components running in the existing Docker/Node.js environment. --- ## Validation Architecture **nyquist_validation:** Not explicitly set to `false` in config.json — treated as enabled. ### Test Framework | Property | Value | |----------|-------| | Framework | Jest 29.7 + ts-jest | | Config file | `package.json` (jest.preset: ts-jest, testEnvironment: node) | | Quick run command | `cd teressa-copeland-homes && npx jest --testPathPattern=prepare-document` | | Full suite command | `cd teressa-copeland-homes && npx jest` | ### Phase Requirements → Test Map | Req ID | Behavior | Test Type | Automated Command | File Exists? | |--------|----------|-----------|-------------------|-------------| | TMPL-10 | "My Templates" tab appears, fetches templates | manual-only (React UI) | n/a — no component test runner | ❌ not applicable | | TMPL-11 | Fields copied with fresh UUIDs, positions/types preserved | unit | `npx jest --testPathPattern=apply-template` | ❌ Wave 0 | | TMPL-12 | Role-to-email mapping logic | unit | `npx jest --testPathPattern=apply-template` | ❌ Wave 0 | | TMPL-13 | Hint chip appears when selected field has hint | manual-only (React UI) | n/a | ❌ not applicable | | TMPL-14 | Snapshot independence (no live link) | architectural (no code) | n/a — satisfied by D-08 design | n/a | | TMPL-15 | Templates nav link | already complete (Phase 19) | n/a | n/a | | TMPL-16 | Templates list page | already complete (Phase 19) | n/a | n/a | ### Sampling Rate - **Per task commit:** `cd teressa-copeland-homes && npx jest` (full suite — only 1 test file currently) - **Per wave merge:** `cd teressa-copeland-homes && npx jest` - **Phase gate:** Full suite green before `/gsd:verify-work` ### Wave 0 Gaps - [ ] `src/lib/utils/__tests__/apply-template.test.ts` — unit tests for field copy (UUID freshness) and role-to-email mapping logic. These are pure functions that can be extracted from the POST handler and tested in isolation. *(If extracting to a util is too heavy for this phase, the test can inline the logic and test it directly — following the `prepare-document.test.ts` pattern of testing the pure conversion math.)* --- ## Open Questions 1. **Does `/api/documents/:docId/fields` endpoint exist?** - What we know: `PreparePanel.handlePrepare` calls `fetch(`/api/documents/${docId}/fields`)` (line 262) — so the endpoint must exist. - What's unclear: Not verified by reading the route file. - Recommendation: Confirm endpoint exists before DocumentPageClient plan relies on it. Low risk — PreparePanel already uses it. 2. **Where does `SignatureFieldData.hint` appear on the signing page for client-text fields?** - What we know: TMPL-08 (Phase 19) added hint support to template editor. D-11 says hints are NOT shown as placeholder on the field. - What's unclear: Whether the signing page currently renders hints as placeholder text for `client-text` fields — if so, hints will appear there in documents created from templates (which is probably correct behavior, just not PreparePanel's concern). - Recommendation: Out of scope for Phase 20 planning — PreparePanel's D-11 constraint is clear. Note for future reference. 3. **What happens when a document template has zero signatureFields (null/empty)?** - What we know: `documentTemplates.signatureFields` is nullable JSONB (Phase 18 decision: "template starts empty"). An agent could create a template but never open the editor. - What's unclear: Should applying an empty template be blocked or allowed? - Recommendation: Allow it — `copiedFields = []` is valid. Document is created with no fields (agent places them manually). This matches the existing "create document with no fields" flow. --- ## Sources ### Primary (HIGH confidence) - Direct reading of `src/app/portal/_components/AddDocumentModal.tsx` — existing modal structure, state patterns, submit branches - Direct reading of `src/app/api/documents/route.ts` — existing POST handler, DB insert pattern - Direct reading of `src/lib/db/schema.ts` — `documentTemplates`, `SignatureFieldData`, `ClientContact`, `DocumentSigner` types - Direct reading of `src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx` — Quick Fill section implementation - Direct reading of `src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx` — state threading pattern, signers, aiPlacementKey - Direct reading of `src/app/api/templates/route.ts` — GET response shape, JOIN with formTemplates - Direct reading of `src/app/portal/_components/ClientProfileClient.tsx` — how AddDocumentModal is called (clientId only, no client object) - Direct reading of `.planning/phases/20-apply-template-and-portal-nav/20-CONTEXT.md` — locked decisions D-01 through D-12 - Direct reading of `.planning/STATE.md` — accumulated pre-decisions, Phase 19 confirmation - Direct reading of `package.json` jest config — test framework details ### Secondary (MEDIUM confidence) - None needed — all findings are from direct source code inspection. --- ## Metadata **Confidence breakdown:** - Standard stack: HIGH — zero new dependencies; all libraries confirmed in package.json and in active use - Architecture: HIGH — all patterns confirmed by reading actual source files - Pitfalls: HIGH — derived from reading the exact code, not speculation - Test infrastructure: HIGH — confirmed from package.json and existing test file **Research date:** 2026-04-06 **Valid until:** 2026-05-06 (stable codebase — no fast-moving dependencies) ## Project Constraints (from CLAUDE.md) The following directives from `teressa-copeland-homes/CLAUDE.md` are binding on the planner: | Directive | Implication for Phase 20 | |-----------|--------------------------| | Extend existing patterns unless explicitly justified | AddDocumentModal gets additive tabs; PreparePanel gets additive optional prop; POST handler gets additive branch. No new abstractions. | | No greenfield assumptions — inspect repository first | Done: all 7 canonical files read and understood. | | Consistency is mandatory (file structure, naming, patterns) | Tab UI uses plain `useState` + Tailwind, not a component library. Fetch pattern matches existing `useEffect` + `fetch`. | | All solutions must reference real repo structure | Research is grounded in actual file contents. | | Security: path traversal guard | The PDF copy path in the template branch must pass through the same `!destPath.startsWith(UPLOADS_DIR)` guard as existing paths. | | Deviation must be justified | No deviations proposed. |