--- phase: 04-pdf-ingest plan: 03 type: execute wave: 3 depends_on: - 04-01 - 04-02 files_modified: - teressa-copeland-homes/src/app/portal/clients/[id]/page.tsx - teressa-copeland-homes/src/app/portal/clients/[id]/_components/AddDocumentModal.tsx - teressa-copeland-homes/src/app/portal/documents/[docId]/page.tsx - teressa-copeland-homes/src/app/portal/documents/[docId]/_components/PdfViewer.tsx - teressa-copeland-homes/package.json - teressa-copeland-homes/next.config.ts autonomous: true requirements: - DOC-01 - DOC-03 must_haves: truths: - "Client profile page has an 'Add Document' button that opens a modal" - "Modal shows searchable list of forms from the library; agent can filter by typing" - "Modal has a 'Browse files' option for custom PDF upload (file picker)" - "After adding a document, the modal closes and the new document appears in the client's documents list without page reload" - "Clicking a document name navigates to the document detail page" - "Document detail page renders the PDF in the browser using react-pdf with page nav and zoom controls" - "Document detail page shows document name, client name, and a back link to the client profile" artifacts: - path: "teressa-copeland-homes/src/app/portal/clients/[id]/_components/AddDocumentModal.tsx" provides: "Client component — searchable forms library modal with file picker fallback" exports: ["AddDocumentModal"] - path: "teressa-copeland-homes/src/app/portal/documents/[docId]/_components/PdfViewer.tsx" provides: "Client component — react-pdf canvas renderer with page nav and zoom" exports: ["PdfViewer"] - path: "teressa-copeland-homes/src/app/portal/documents/[docId]/page.tsx" provides: "Document detail page — server component wrapping PdfViewer" key_links: - from: "AddDocumentModal.tsx" to: "/api/forms-library" via: "fetch on modal open" pattern: "fetch.*forms-library" - from: "AddDocumentModal.tsx" to: "/api/documents" via: "fetch POST on form submit" pattern: "fetch.*api/documents" - from: "PdfViewer.tsx" to: "/api/documents/{docId}/file" via: "react-pdf Document file prop" pattern: "api/documents.*file" --- Install react-pdf, build the "Add Document" modal on the client profile page, and build the PDF viewer document detail page. This delivers the visible, agent-facing portion of Phase 4. Purpose: Agent can browse the forms library, add a document to a client, and view it rendered in the browser. Output: AddDocumentModal component, PdfViewer component, document detail page route. @/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md @/Users/ccopeland/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/phases/04-pdf-ingest/04-CONTEXT.md @.planning/phases/04-pdf-ingest/04-RESEARCH.md @.planning/phases/04-pdf-ingest/04-01-SUMMARY.md @.planning/phases/04-pdf-ingest/04-02-SUMMARY.md Task 1: Install react-pdf and configure Next.js for ESM compatibility teressa-copeland-homes/package.json teressa-copeland-homes/next.config.ts Run: ```bash cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm install react-pdf ``` react-pdf v9+ pulls in pdfjs-dist as a peer dep automatically. Open `next.config.ts` and add `transpilePackages` if not already present: ```typescript const nextConfig = { // ... existing config ... transpilePackages: ['react-pdf', 'pdfjs-dist'], }; ``` This is necessary because react-pdf ships as ESM and Next.js webpack needs to transpile it. Verify the dev server still starts: ```bash cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run build 2>&1 | tail -20 ``` If build errors mention react-pdf or pdfjs-dist, confirm transpilePackages is applied. If errors persist around a specific import, check the react-pdf v9 release notes for Next.js App Router setup — the worker config in PdfViewer (Task 2) must use the `new URL(...)` pattern, not a CDN URL. ```bash cd /Users/ccopeland/temp/red/teressa-copeland-homes && node -e "require('react-pdf')" 2>&1 | head -5 ``` Expected: No error (or ESM warning that resolves at build time — acceptable). react-pdf installed. next.config.ts has transpilePackages. Build does not error on react-pdf imports. Task 2: Add Document modal + PDF viewer page teressa-copeland-homes/src/app/portal/clients/[id]/_components/AddDocumentModal.tsx teressa-copeland-homes/src/app/portal/clients/[id]/page.tsx teressa-copeland-homes/src/app/portal/documents/[docId]/page.tsx teressa-copeland-homes/src/app/portal/documents/[docId]/_components/PdfViewer.tsx **A. Create AddDocumentModal.tsx** `src/app/portal/clients/[id]/_components/AddDocumentModal.tsx`: ```typescript 'use client'; import { useState, useEffect, useTransition } from 'react'; import { useRouter } from 'next/navigation'; type FormTemplate = { id: string; name: string; filename: string }; export function AddDocumentModal({ clientId, onClose }: { clientId: string; onClose: () => void }) { const [templates, setTemplates] = useState([]); const [query, setQuery] = useState(''); const [docName, setDocName] = useState(''); const [selectedTemplate, setSelectedTemplate] = useState(null); const [customFile, setCustomFile] = useState(null); const [isPending, startTransition] = useTransition(); const [saving, setSaving] = useState(false); const router = useRouter(); useEffect(() => { fetch('/api/forms-library') .then(r => r.json()) .then(setTemplates) .catch(console.error); }, []); const filtered = templates.filter(t => t.name.toLowerCase().includes(query.toLowerCase()) ); const handleSelectTemplate = (t: FormTemplate) => { setSelectedTemplate(t); setCustomFile(null); setDocName(t.name); }; const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0] ?? null; setCustomFile(file); setSelectedTemplate(null); if (file) setDocName(file.name.replace(/\.pdf$/i, '')); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!docName.trim() || (!selectedTemplate && !customFile)) return; setSaving(true); try { if (customFile) { const fd = new FormData(); fd.append('clientId', clientId); fd.append('name', docName.trim()); fd.append('file', customFile); await fetch('/api/documents', { method: 'POST', body: fd }); } else { await fetch('/api/documents', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clientId, name: docName.trim(), formTemplateId: selectedTemplate!.id }), }); } startTransition(() => router.refresh()); onClose(); } catch (err) { console.error(err); } finally { setSaving(false); } }; return (

Add Document

setQuery(e.target.value)} className="w-full border rounded px-3 py-2 mb-3 text-sm" />
    {filtered.length === 0 && (
  • No forms found
  • )} {filtered.map(t => (
  • handleSelectTemplate(t)} className={`px-3 py-2 text-sm cursor-pointer hover:bg-gray-100 ${selectedTemplate?.id === t.id ? 'bg-blue-50 font-medium' : ''}`} > {t.name}
  • ))}
setDocName(e.target.value)} required className="w-full border rounded px-3 py-2 mb-4 text-sm" placeholder="e.g. 123 Main St Purchase Agreement" />
); } ``` **B. Wire "Add Document" button into the existing client profile page** Read `src/app/portal/clients/[id]/page.tsx` first to understand the current structure (Phase 3 built this). Then: - Add a state-controlled "Add Document" button near the top of the documents section (or the page header) - The button triggers a `showModal` state that renders ` setShowModal(false)} />` - Since the profile page is likely a server component with a `ClientProfileClient` sub-component (per Phase 3 pattern), add the modal trigger and state inside the existing client component — do NOT convert the server page to a client component. Add the button and modal inside `_components/ClientProfileClient.tsx` or the equivalent client sub-component. **C. Create PdfViewer.tsx** `src/app/portal/documents/[docId]/_components/PdfViewer.tsx`: ```typescript 'use client'; import { useState } from 'react'; import { Document, Page, pdfjs } from 'react-pdf'; import 'react-pdf/dist/Page/AnnotationLayer.css'; import 'react-pdf/dist/Page/TextLayer.css'; // Worker setup — must use import.meta.url for local/Docker environments (no CDN) pdfjs.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url, ).toString(); export function PdfViewer({ docId }: { docId: string }) { const [numPages, setNumPages] = useState(0); const [pageNumber, setPageNumber] = useState(1); const [scale, setScale] = useState(1.0); return (
{pageNumber} / {numPages || '?'} Download
setNumPages(numPages)} className="shadow-lg" >
); } ``` **D. Create document detail page** `src/app/portal/documents/[docId]/page.tsx`: ```typescript import { auth } from '@/lib/auth'; import { redirect } from 'next/navigation'; import { db } from '@/lib/db'; import { documents, clients } from '@/lib/db/schema'; import { eq } from 'drizzle-orm'; import Link from 'next/link'; import { PdfViewer } from './_components/PdfViewer'; export default async function DocumentPage({ params, }: { params: Promise<{ docId: string }>; }) { const session = await auth(); if (!session) redirect('/login'); const { docId } = await params; const doc = await db.query.documents.findFirst({ where: eq(documents.id, docId), with: { client: true }, }); if (!doc) redirect('/portal/dashboard'); return (
← Back to {doc.client?.name ?? 'Client'}

{doc.name}

{doc.client?.name}

); } ``` NOTE: The `with: { client: true }` relation requires that the Drizzle relations are defined in schema.ts. If Phase 3 already defined client/documents relations, use those. If not, add the minimal relation: ```typescript // In schema.ts, add after table definitions: import { relations } from 'drizzle-orm'; export const documentsRelations = relations(documents, ({ one }) => ({ client: one(clients, { fields: [documents.clientId], references: [clients.id] }), })); ``` If relations already exist in schema.ts (check the existing file), extend them rather than overwriting. Also ensure that clicking a document name in the client profile's documents table navigates to `/portal/documents/{doc.id}` — read the existing DocumentsTable component from Phase 3 and add the link if not present.
```bash cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run build 2>&1 | grep -E "error|Error|compiled" | head -20 ``` Expected: "compiled successfully" or "Compiled" with no errors referencing react-pdf, PdfViewer, or AddDocumentModal. - Client profile page has "Add Document" button that opens the modal. - Modal shows searchable template list and file picker fallback. - Submitting the modal creates a document and refreshes the documents list. - Document name in client profile links to `/portal/documents/{id}`. - Document detail page renders PdfViewer with page nav, zoom, and download controls. - Build passes with no errors.
- `npm run build` completes without errors - AddDocumentModal imports from react-pdf DO NOT include CDN worker URLs - Both CSS layers imported in PdfViewer: AnnotationLayer.css and TextLayer.css - Document detail page uses `await params` (Next.js 15 pattern) - PdfViewer uses `new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url)` for worker — not a CDN URL Agent can open the forms library modal, add a document from the library or via file picker, see it appear in the documents list, click it to navigate to the detail page, and see the PDF rendered in the browser with page navigation and zoom. After completion, create `.planning/phases/04-pdf-ingest/04-03-SUMMARY.md`