- Use pageInfo.width/height (authoritative rendered canvas size from
react-pdf) instead of containerRect.width/height for all coordinate
math; containerRect dimensions could differ if the wrapper div has
extra decoration not matching the canvas
- Track containerSize in state (updated via useLayoutEffect when
pageInfo changes) so renderFields() uses stable values from state
instead of calling getBoundingClientRect() during render
- Refactor screenToPdfCoords/pdfToScreenCoords to take renderedW/H
as explicit params instead of a DOMRect, making the contract clearer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fetch allClients in parallel with doc via Promise.all
- Import PreparePanel and render in 1/3 right column of 2/3 + 1/3 grid
- currentClientId defaults to doc.assignedClientId ?? doc.clientId
- asc import added from drizzle-orm for ordered client list
- npm run build is clean with no TypeScript errors
- TextFillForm: key-value pair builder (up to 10 rows, individually removable)
- PreparePanel: client selector + text fill form + Prepare and Send button
- PreparePanel calls POST /api/documents/[id]/prepare and calls router.refresh() on success
- PreparePanel shows read-only message for non-Draft documents
- Rule 1 fix: PdfViewer page.scale -> scale (PageCallback has no .scale property; use state var)
- FieldPlacer.tsx: dnd-kit drag-and-drop field placer with Y-flip coordinate conversion
- PdfViewer.tsx: extended with pageInfo state and FieldPlacer integration
- @dnd-kit/core and @dnd-kit/utilities installed
- Fields persist via PUT /api/documents/[id]/fields on every add/remove
- 05-02-SUMMARY.md created, STATE.md and ROADMAP.md updated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add pageInfo state (PageInfo | null) to track rendered PDF page dimensions
- Wire onLoadSuccess callback on Page to capture originalWidth/Height using Math.max mediaBox pattern
- Import and wrap Document/Page tree inside FieldPlacer component
- Pass docId, pageInfo, currentPage to FieldPlacer for coordinate conversion
- Only scale prop used on Page (not both width+scale — avoids double scaling)
- All existing controls (Prev, Next, Zoom In/Out, Download) preserved unchanged
- Install @dnd-kit/core@^6.3.1 and @dnd-kit/utilities@^3.2.2
- Create FieldPlacer.tsx with DndContext, draggable palette token, droppable PDF zone
- Implements Y-flip coordinate conversion (screenToPdfCoords / pdfToScreenCoords)
- Fetches existing fields from GET /api/documents/[id]/fields on mount
- Persists fields via PUT /api/documents/[id]/fields on every add/remove
- Renders placed fields as absolute-positioned blue-bordered overlays with remove button
- Default field size: 144x36 PDF points (2in x 0.5in at 72 DPI)
- Created src/lib/pdf/__tests__/prepare-document.test.ts with 10 test cases
- Tests verify Y-axis flip: screenY=0 → pdfY≈792, screenY=792 → pdfY≈0
- Tests verify scale-invariance at both 1:1 and 50% zoom ratios
- Installed jest, ts-jest, @types/jest and added jest config to package.json
- All 10 tests pass
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Installed @cantoo/pdf-lib for server-side PDF mutation
- Created src/lib/pdf/prepare-document.ts with preparePdf function using atomic tmp->rename write pattern
- form.flatten() called before drawing signature rectangles
- Created GET/PUT /api/documents/[id]/fields routes for signature field storage
- Created POST /api/documents/[id]/prepare route that calls preparePdf and transitions status to Sent
- Fixed pre-existing null check error in scripts/debug-inspect2.ts (Rule 3: blocking build)
- Build compiles successfully with 2 new API routes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Create AddDocumentModal: searchable forms library list + custom file picker
- Wire Add Document button into ClientProfileClient in Documents section header
- Update DocumentsTable: document names now link to /portal/documents/{id}
- Create PdfViewer with page nav, zoom, and download controls (pdfjs worker via import.meta.url)
- Create /portal/documents/[docId] page: server component with auth check, doc/client query
- Add documentsRelations and clientsRelations to schema.ts for db.query with-relations support
- Build verified: /portal/documents/[docId] route present, no errors
- Created 04-02-SUMMARY.md with task commits, decisions, and dependency graph
- Updated STATE.md: position advanced to 04-02 complete, decisions added
- Updated ROADMAP.md: phase 4 progress to 2/4 plans
- POST handles both form-data (custom upload) and JSON (template copy) paths
- Copies seed PDF or writes uploaded file to uploads/clients/{clientId}/{uuid}.pdf
- Path traversal guard on destPath before writing
- GET streams PDF bytes with Content-Type: application/pdf
- Path traversal guard on filePath before reading
- Both routes return 401 for unauthenticated requests
- Create scripts/seed-forms.ts: reads seeds/forms/, upserts PDFs into form_templates via onConflictDoUpdate on filename
- Add seed:forms script to package.json with DOTENV_CONFIG_PATH=.env.local prefix
- Empty seeds/forms/ prints guidance message and exits 0 (monthly-sync workflow ready)
Plans 04-01 through 04-04 cover DOC-01, DOC-02, DOC-03:
schema/seed, API routes, UI modal + PDF viewer, human verification.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Added import of clients, documents, inArray from drizzle-orm
- Seeds 2 clients: Sarah Johnson and Mike Torres (onConflictDoNothing)
- Queries back seeded client IDs, then seeds 4 placeholder documents
- Documents cover Signed/Sent/Draft statuses across both clients
- Seed is idempotent via onConflictDoNothing guard
- ClientCard.tsx: server component with name, email, doc count, last activity; wrapped in Link to /portal/clients/[id]
- ClientModal.tsx: use client component with useActionState from react; supports create/edit modes via bind pattern; closes on success
- ClientsPageClient.tsx: use client wrapper holding isOpen modal state, renders card grid or empty state CTA
- clients/page.tsx: async server component fetching clients with docCount + lastActivity via Drizzle LEFT JOIN + GROUP BY
- Async server component queries all documents with LEFT JOIN to clients
- Filter state lives in URL (?status=Draft|Sent|Viewed|Signed) via DashboardFilters client component
- Rows filtered in JavaScript after fetch (tiny dataset in Phase 3)
- DashboardFilters extracted to _components/DashboardFilters.tsx (use client + useRouter)
- Displays agent first name extracted from session email
- 03-02-SUMMARY.md: plan summary with task commits, deviations, and decisions
- STATE.md: updated current position to Phase 3 Plan 02 complete, added decisions
- ROADMAP.md: Phase 3 progress updated (2/4 plans complete)
- Create portal/(protected)/layout.tsx with auth() check and redirect to /agent/login
- Create PortalNav.tsx as client component with Dashboard/Clients links and active state
- Nav uses usePathname() for active gold underline, LogoutButton for sign-out
- CSS variables --navy/--gold/--cream applied throughout portal shell