docs(phase-20): create phase plan — apply template and portal nav
This commit is contained in:
@@ -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
|
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
|
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
|
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
|
**UI hint**: yes
|
||||||
|
|
||||||
## Progress
|
## Progress
|
||||||
|
|||||||
422
.planning/phases/20-apply-template-and-portal-nav/20-01-PLAN.md
Normal file
422
.planning/phases/20-apply-template-and-portal-nav/20-01-PLAN.md
Normal file
@@ -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"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
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.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.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
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- Key types and contracts. Extracted from codebase. Executor should use directly. -->
|
||||||
|
|
||||||
|
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<SignatureFieldData[]>(),
|
||||||
|
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
|
||||||
|
```
|
||||||
|
</interfaces>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Extend POST /api/documents with documentTemplateId branch</name>
|
||||||
|
<files>teressa-copeland-homes/src/app/api/documents/route.ts</files>
|
||||||
|
<read_first>
|
||||||
|
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
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
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<string>();
|
||||||
|
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.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd teressa-copeland-homes && npx tsc --noEmit</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- 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
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>POST /api/documents accepts documentTemplateId, copies fields with fresh UUIDs, maps roles to client contacts, inserts document with signatureFields and signers</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Add My Templates tab to AddDocumentModal</name>
|
||||||
|
<files>teressa-copeland-homes/src/app/portal/_components/AddDocumentModal.tsx</files>
|
||||||
|
<read_first>
|
||||||
|
teressa-copeland-homes/src/app/portal/_components/AddDocumentModal.tsx
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
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<DocumentTemplateRow[]>([]);
|
||||||
|
const [docTemplatesLoaded, setDocTemplatesLoaded] = useState(false);
|
||||||
|
const [selectedDocTemplate, setSelectedDocTemplate] = useState<DocumentTemplateRow | null>(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
|
||||||
|
<div className="flex border-b mb-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveTab('forms')}
|
||||||
|
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${
|
||||||
|
activeTab === 'forms'
|
||||||
|
? 'border-blue-600 text-blue-600'
|
||||||
|
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Forms Library
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSwitchToTemplates}
|
||||||
|
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${
|
||||||
|
activeTab === 'templates'
|
||||||
|
? 'border-blue-600 text-blue-600'
|
||||||
|
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
My Templates
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
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' && (
|
||||||
|
<div className="mb-4">
|
||||||
|
{!docTemplatesLoaded ? (
|
||||||
|
<p className="text-sm text-gray-500 py-4 text-center">Loading templates...</p>
|
||||||
|
) : docTemplates.length === 0 ? (
|
||||||
|
<p className="text-sm text-gray-500 py-4 text-center">
|
||||||
|
No templates saved yet. Create one from the Templates page.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<ul className="border rounded max-h-48 overflow-y-auto">
|
||||||
|
{docTemplates.map(t => (
|
||||||
|
<li
|
||||||
|
key={t.id}
|
||||||
|
onClick={() => handleSelectDocTemplate(t)}
|
||||||
|
className={`px-3 py-2 text-sm cursor-pointer hover:bg-gray-100 ${
|
||||||
|
selectedDocTemplate?.id === t.id ? 'bg-blue-50 font-medium' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="block">{t.name}</span>
|
||||||
|
<span className="text-xs text-gray-400">
|
||||||
|
{t.formName ?? 'Unknown form'} · {t.fieldCount} field{t.fieldCount !== 1 ? 's' : ''}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `{activeTab === 'forms' && ( ... )}` wraps around the existing search input, form list `<ul>`, and custom file upload `<div>`. All existing elements are preserved exactly — only wrapped in a conditional.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd teressa-copeland-homes && npx tsc --noEmit</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- 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
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>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</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
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
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- 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
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/20-apply-template-and-portal-nav/20-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
258
.planning/phases/20-apply-template-and-portal-nav/20-02-PLAN.md
Normal file
258
.planning/phases/20-apply-template-and-portal-nav/20-02-PLAN.md
Normal file
@@ -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"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
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.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.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
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- Key types and contracts from codebase. -->
|
||||||
|
|
||||||
|
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<string, string>
|
||||||
|
// 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<string, string>;
|
||||||
|
selectedFieldId: string | null;
|
||||||
|
onQuickFill: (fieldId: string, value: string) => void;
|
||||||
|
onAiAutoPlace: () => Promise<void>;
|
||||||
|
signers?: DocumentSigner[];
|
||||||
|
onSignersChange?: (signers: DocumentSigner[]) => void;
|
||||||
|
unassignedFieldIds?: Set<string>;
|
||||||
|
onUnassignedFieldIdsChange?: (ids: Set<string>) => void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Quick Fill section pattern (PreparePanel lines 313-355):
|
||||||
|
```typescript
|
||||||
|
{/* Quick-fill panel — only shown when a text field is selected */}
|
||||||
|
{selectedFieldId ? (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<p className="text-xs text-gray-400">Click a suggestion to fill the selected field.</p>
|
||||||
|
{clientName && ( <button onClick={() => onQuickFill(selectedFieldId, clientName)}>...</button> )}
|
||||||
|
{clientPropertyAddress && ( <button onClick={() => onQuickFill(selectedFieldId, clientPropertyAddress)}>...</button> )}
|
||||||
|
<button onClick={() => onQuickFill(selectedFieldId, defaultEmail)}>Client Email chip</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p>Click a text field on the document...</p>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
</interfaces>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Add fields state to DocumentPageClient and pass selectedFieldHint to PreparePanel</name>
|
||||||
|
<files>
|
||||||
|
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx
|
||||||
|
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx
|
||||||
|
</files>
|
||||||
|
<read_first>
|
||||||
|
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
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
**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<SignatureFieldData[]>([]);
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
<PreparePanel
|
||||||
|
// ... all existing props unchanged ...
|
||||||
|
selectedFieldHint={selectedFieldHint}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
**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 `</div>` of `space-y-1.5`):
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
{selectedFieldHint && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onQuickFill(selectedFieldId, selectedFieldHint)}
|
||||||
|
className="w-full text-left px-3 py-2 text-sm border rounded bg-white hover:bg-blue-50 hover:border-blue-300 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="text-xs text-gray-400 block">Template Hint</span>
|
||||||
|
<span className="truncate block">{selectedFieldHint}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd teressa-copeland-homes && npx tsc --noEmit</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- 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
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>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.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="checkpoint:human-verify" gate="blocking">
|
||||||
|
<name>Task 2: Human verification of full template-to-document flow</name>
|
||||||
|
<what-built>
|
||||||
|
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)
|
||||||
|
</what-built>
|
||||||
|
<how-to-verify>
|
||||||
|
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)
|
||||||
|
</how-to-verify>
|
||||||
|
<resume-signal>Type "approved" or describe any issues found</resume-signal>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
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)
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- 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
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/20-apply-template-and-portal-nav/20-02-SUMMARY.md`
|
||||||
|
</output>
|
||||||
Reference in New Issue
Block a user