Commit Graph

72 Commits

Author SHA1 Message Date
Chandler Copeland
f383f91445 feat(11-01): AgentSignaturePanel, profile page, PortalNav link, FieldPlacer token
- Create AgentSignaturePanel.tsx with signature_pad canvas, save/update/thumbnail flow
- Create /portal/profile page (server component fetching agentSignatureData)
- Add Profile link to PortalNav navLinks array
- Add red 'Agent Signature' token to FieldPlacer PALETTE_TOKENS (6th entry)
2026-03-21 14:02:51 -06:00
Chandler Copeland
e07ed306cd feat(11-01): DB migration and API routes for agent signature storage
- Add agentSignatureData TEXT column to users table in schema.ts
- Generate migration 0008_windy_cloak.sql (ALTER TABLE users ADD COLUMN agent_signature_data text)
- Apply migration to local postgres database
- Create GET/PUT /api/agent/signature route with auth guard and input validation
2026-03-21 14:02:01 -06:00
Chandler Copeland
e179b9284f fix(10-03): transparent field boxes and fixed-size checkbox in FieldPlacer
- preparePdf: remove opaque fill from all field type rectangles (signature,
  initials, checkbox, date, text) — underlying PDF content now shows through
- preparePdf: checkbox draws X lines only (no border rectangle); date draws
  no placeholder at all; text draws nothing (position marker only)
- sign route: remove white overwrite rectangle on date stamp — date text
  draws directly on existing PDF content
- FieldPlacer: suppress resize corner handles for checkbox fields; hide
  "Checkbox" label (too small at 24x24pt); checkbox is fixed-size only

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 13:41:04 -06:00
Chandler Copeland
a5173fe455 fix(portal): serve signed PDF in viewer after document is signed
When signedFilePath is set, /api/documents/[id]/file now returns the
signed PDF so agent sees embedded signatures and initials in the portal
viewer. Falls back to original PDF when not yet signed.
2026-03-21 13:25:26 -06:00
Chandler Copeland
50f082d20f feat(10-03): extend signing page for initials, overlay suppression, and updated progress counting
- Add optional title prop to SignatureModal (defaults to "Add Signature"); button label derives from title
- Add activeFieldType state and setActiveFieldType in SigningPageClient
- handleFieldClick opens modal for both client-signature and initials fields
- handleSubmit and SigningProgressBar total now count both client-signature and initials
- handleJumpToNext jumps to next unsigned required field (client-signature or initials only)
- Non-interactive fields (text/checkbox/date) suppressed from field overlay rendering via early return
- Initials overlays use purple pulse animation (pulse-border-purple); signature overlays use blue
- SignatureModal receives title prop: "Add Initials" for initials, "Add Signature" for client-signature
2026-03-21 12:54:57 -06:00
Chandler Copeland
d395d85ebb feat(10-02): fix POST handler — signable field filter and date stamping at sign time
- Add getFieldType to schema import
- Add PDFDocument, StandardFonts, rgb from @cantoo/pdf-lib for date stamping
- Add readFile, writeFile, unlink from node:fs/promises
- Hoist const now = new Date() to before step 8 (shared for date stamp + DB update)
- Step 8a: stamp signing date onto date fields in prepared PDF before embed
- Step 8b: filter signableFields to client-signature and initials only
- signaturesWithCoords now maps only signable fields (no 500 on text/checkbox/date)
- Update embedSignatureInPdf call to use dateStampedPath
- Fire-and-forget cleanup of temporary .datestamped.tmp file after embed
2026-03-21 12:50:21 -06:00
Chandler Copeland
1e92ca363a feat(10-01): update handleDragEnd and renderFields for typed field creation
- handleDragEnd: determine droppedType from active.id with validTypes set guard
- handleDragEnd: checkbox fields drop at 24x24pt; all other types drop at 144x36pt
- handleDragEnd: newField now includes type property set to droppedType
- renderFields: use getFieldType() + PALETTE_TOKENS lookup to get per-field color and label
- renderFields: border, background, and text color are now driven by fieldColor
- renderFields: resize handle corners use fieldColor instead of hardcoded blue
- renderFields: span displays fieldLabel instead of hardcoded 'Signature'
2026-03-21 12:50:18 -06:00
Chandler Copeland
4140c220b1 feat(10-01): parameterize DraggableToken and add four new palette tokens
- Add PALETTE_TOKENS array with 5 typed tokens (Signature/blue, Initials/purple, Checkbox/green, Date/amber, Text/slate)
- Update DraggableToken to accept id, label, color props with per-type styling
- Change isDraggingToken from boolean to string | null to track active token id
- Update onDragStart to record active.id instead of just true
- Replace single static token with PALETTE_TOKENS.map() in palette JSX
- Update DragOverlay ghost to show correct label, color, and checkbox-appropriate dimensions (24x24 vs 144x36)
2026-03-21 12:49:25 -06:00
Chandler Copeland
27003af70f fix(09-01): bg-gray-100 on prepare panel, stack field name/value rows to fit narrow panel 2026-03-21 12:31:20 -06:00
Chandler Copeland
28a460e9cc fix(09-01): sticky prepare panel, bg-gray-50 background, max-height scroll 2026-03-21 12:29:01 -06:00
Chandler Copeland
a77a144f6f fix(09-01): fix hydration mismatch in TextFillForm — use useEffect for initialData seed 2026-03-21 12:27:09 -06:00
Chandler Copeland
11f2b80217 fix(09-01): fix propertyAddress pre-seed and polish PreparePanel text fill UI
- TextFillForm: add initialData prop; buildInitialRows seeds rows from
  pre-seeded data so propertyAddress row renders populated on mount
- PreparePanel: pass initialData={...} to TextFillForm so the lazy
  useState in PreparePanel correctly flows through to the visible UI
- TextFillForm: replace AcroForm jargon instruction with friendly copy
- TextFillForm: add Field name / Value column headers for clear layout
- TextFillForm: improve spacing (py-1.5), softer remove button (gray→red on hover)
- TypeScript: npx tsc --noEmit passes clean
2026-03-21 12:20:08 -06:00
Chandler Copeland
fa9981edd9 feat(09-01): UI layer — property address modal input, profile display, PreparePanel pre-seed
- ClientModal: add defaultPropertyAddress prop, property address input field after email
- ClientProfileClient: add propertyAddress to Props type, display address under email when non-null, pass defaultPropertyAddress to edit modal
- documents/[docId]/page.tsx: extend client select to include propertyAddress, pass as clientPropertyAddress to PreparePanel
- PreparePanel: add clientPropertyAddress prop, lazy-initialize textFillData with { propertyAddress } when client has address
2026-03-21 12:15:27 -06:00
Chandler Copeland
06e477b455 feat(08-02): add type-branching guards to SigningPageClient
- Import getFieldType from @/lib/db/schema
- handleFieldClick returns early for non-client-signature fields (defense-in-depth)
- handleSubmit counts only client-signature fields for completeness check
- SigningProgressBar total reflects client-signature count only
- signatureFields added to handleFieldClick and handleSubmit dependency arrays
2026-03-21 11:52:09 -06:00
Chandler Copeland
ea3365feb4 feat(08-02): add isClientVisibleField server-side filter to GET /api/sign/[token]
- Import isClientVisibleField from @/lib/db/schema
- Filter signatureFields in GET response to exclude agent-signature fields
- POST handler untouched — reads signatureFields from DB directly
2026-03-21 11:51:30 -06:00
Chandler Copeland
cac5d5bbb6 fix(07-04): hide Download anchor in PdfViewer for Signed documents (LEGAL-03)
- Wrap Download anchor in conditional: only rendered when docStatus !== 'Signed'
- PDF viewer still loads original via /file for in-browser display regardless of status
- PreparePanel presigned URL remains sole download path for signed PDFs
2026-03-21 10:54:43 -06:00
Chandler Copeland
6775cc76eb fix(07-04): restrict /file route to original PDF only (LEGAL-03)
- Remove signedFilePath fallback from /file route
- Route now always serves doc.filePath (unsigned original)
- Signed PDF exclusively available via presigned /download?adt=[token]
2026-03-21 10:53:56 -06:00
Chandler Copeland
68d94a779f feat(07-02): wire agentDownloadUrl to doc detail page, add signedAt to dashboard and client profile
- document detail page: import createAgentDownloadToken, generate agentDownloadUrl server-side for
  signed docs (signedFilePath present), pass agentDownloadUrl and signedAt props to PreparePanel
- DocumentsTable: add signedAt to DocumentRow type, add Date Signed column header and cell
- dashboard page: add signedAt to db select so allRows includes signed date for Signed documents
- ClientProfileClient: add signedAt to local DocumentRow type (fixes type mismatch with DocumentsTable)
- clients/[id]/page: add signedAt to query select to satisfy updated DocumentRow type
2026-03-21 10:39:03 -06:00
Chandler Copeland
b823ae5c58 feat(07-02): extend PreparePanel with agentDownloadUrl/signedAt props and Signed download section
- Added agentDownloadUrl and signedAt to PreparePanelProps interface (optional, nullable)
- Destructure new props in function signature
- Added Signed status branch: green panel with signed timestamp and Download Signed PDF anchor
- Kept Sent/Viewed branch: gray read-only message
- Draft status: existing prepare form unchanged
- Download is a plain <a href> anchor — no fetch/onClick; browser follows link directly
2026-03-21 10:37:37 -06:00
Chandler Copeland
ebc47ae954 feat(07-01): create GET /api/documents/[id]/download agent download route
- Streams signed PDF via short-lived agent-download JWT (adt query param)
- Returns 401 for missing/expired token, 403 for ID mismatch or path traversal
- Returns 404 for unsigned documents or missing files on disk
- Path traversal guard: absPath.startsWith(UPLOADS_DIR) before readFile
- Token/route ID cross-check: documentId !== id returns 403
- new Uint8Array(fileBuffer) for Next.js 16 TypeScript strict mode compatibility
2026-03-21 10:34:43 -06:00
Chandler Copeland
bf6d361973 fix(06): log audit events and set Viewed status in signing page server component 2026-03-21 10:15:16 -06:00
Chandler Copeland
1171b2fa86 fix(06): update status to Viewed on link open; serve signedFilePath in agent portal after signing 2026-03-21 10:01:46 -06:00
Chandler Copeland
5aef96786a fix(06): wire /send route after /prepare in PreparePanel — signing email was never being sent 2026-03-21 09:53:38 -06:00
Chandler Copeland
04c3720096 fix(06): correct clientName in agent notification email 2026-03-21 09:49:06 -06:00
Chandler Copeland
4cdd9eea80 feat(06-05): confirmation page + router.push redirect after signing
- Rewrite confirmed/page.tsx: verifies signing token, shows document name + signed timestamp + download button
- Generate 15-min download token server-side; pass as dt= query param to /api/sign/[token]/download
- Success checkmark (navy circle + gold checkmark), document name, formatted signed date
- Download link valid for 15 minutes note shown below button
- Update SigningPageClient.tsx: replace window.location.href with router.push for SPA navigation
2026-03-20 11:42:24 -06:00
Chandler Copeland
a276da0da1 feat(06-05): download token utilities + download API route
- Add createDownloadToken and verifyDownloadToken to token.ts (15-min TTL, purpose:'download' claim)
- Create GET /api/sign/[token]/download route: validates dt query param JWT, streams signedFilePath as PDF
- Path traversal guard: signedFilePath must start with UPLOADS_DIR
- Auto-fix: Buffer cast to Uint8Array for Response BodyInit compatibility (Next.js 16 / TypeScript strict)
2026-03-20 11:41:18 -06:00
Chandler Copeland
d445c282c1 feat(06-04): POST /api/sign/[token] atomic submission + confirmed page
- Add POST handler to sign/[token]/route.ts with atomic one-time enforcement
- UPDATE signing_tokens SET usedAt WHERE usedAt IS NULL RETURNING — 0 rows = 409
- Log signature_submitted and pdf_hash_computed audit events
- Merge client dataURLs with server-stored field coordinates (NEVER trust client coords)
- Call embedSignatureInPdf, store pdfHash + signedFilePath in documents table
- Update document status to Signed with signedAt timestamp
- Fire-and-forget sendAgentNotificationEmail (catches errors without failing response)
- Create /sign/[token]/confirmed success page for POST redirect destination
2026-03-20 11:37:00 -06:00
Chandler Copeland
05b5207305 feat(06-04): SignatureModal with Draw/Type/Use Saved tabs + wire SigningPageClient
- Create SignatureModal.tsx with signature_pad Draw tab (devicePixelRatio scaling, touch-none)
- Type tab renders name in Dancing Script cursive via offscreen canvas
- Use Saved tab conditionally shown when localStorage has saved signature
- Save for later checkbox persists drawn/typed sig to localStorage on confirm
- Update SigningPageClient.tsx: import modal, track signedFields as Map<string,string>
- Field overlay shows signature preview image after signing
- handleSubmit POSTs to /api/sign/[token] and redirects to /sign/[token]/confirmed on 200
2026-03-20 11:35:40 -06:00
Chandler Copeland
dcf503dfea feat(06-03): signing page — server component, PDF viewer, field overlays, progress bar
- page.tsx: server component validates JWT + one-time-use before rendering any UI
- Three error states (expired/used/invalid) show static pages with no canvas
- SigningPageClientWrapper: dynamic import (ssr:false) for react-pdf browser requirement
- SigningPageClient: full-scroll PDF viewer with pulsing blue field overlays
- Field overlay coordinates convert PDF user-space (bottom-left) to screen (top-left)
- SigningProgressBar: sticky bottom bar with X/Y count + jump-to-next + submit button
- api/sign/[token]/pdf: token-authenticated PDF streaming route (no agent auth)
2026-03-20 11:30:38 -06:00
Chandler Copeland
877ad66ead feat(06-02): POST /api/documents/[id]/send + document_prepared audit log
- Add send/route.ts: creates signing token, sends branded email, logs email_sent, updates status to Sent
- Auth guard returns 401 for unauthenticated requests; 422 if not prepared; 409 if already signed
- Wraps sendMail in try/catch — returns 502 without DB update if email delivery fails
- Add logAuditEvent(document_prepared) to prepare/route.ts after successful PDF preparation
2026-03-20 11:29:54 -06:00
Chandler Copeland
e1306dab69 feat(06-03): GET /api/sign/[token] route — token validation + audit logging
- Validates JWT with verifySigningToken(); returns expired/invalid/used/pending
- Checks signingTokens.usedAt for one-time-use enforcement
- Logs link_opened + document_viewed audit events on valid pending access
- Extracts IP from x-forwarded-for/x-real-ip headers for audit trail
- Public route — no auth() import or session required
2026-03-20 11:28:51 -06:00
Chandler Copeland
d24dd54062 fix(05-04): reject field updates when document status is not Draft 2026-03-20 10:44:21 -06:00
Chandler Copeland
bd73f0cc76 feat(05-04): lock field placer to read-only when document is Sent
- Add readOnly prop to FieldPlacer; when true: hide palette, disable all pointer
  events on field boxes, show fields at 60% opacity, suppress delete button and
  all four resize handles
- PdfViewer accepts docStatus prop and derives readOnly={docStatus==='Sent'||'Signed'}
- PdfViewerWrapper forwards docStatus prop to PdfViewer
- page.tsx passes docStatus={doc.status} to PdfViewerWrapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 10:36:25 -06:00
Chandler Copeland
08719a6109 feat(05-04): replace single resize handle with 4-corner resize handles
- Add ResizeCorner type ('se' | 'sw' | 'ne' | 'nw')
- handleResizeStart now accepts a corner argument, stored in DraggingState
- Screen-space math: compute start rect (top/left/right/bottom px), apply delta
  per corner, enforce 40px/20px minimums, then convert back to PDF units on pointerup
- renderFields renders four 10x10 blue square handles at each corner with the
  correct CSS resize cursor (nw-resize, ne-resize, sw-resize, se-resize)
- Opposite corner is always the anchor; only the two edges adjacent to the dragged
  corner move

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 10:36:04 -06:00
Chandler Copeland
51a77ef7d2 feat(05-04): add move and resize to placed signature fields
- Native pointer events on field body for move drag (no dnd-kit conflict)
- 10x10 resize handle in bottom-right corner with se-resize cursor
- Event delegation via DroppableZone onPointerMove/onPointerUp
- DOM mutation during drag for smooth performance; commit to state on pointerUp
- data-no-move attribute prevents resize handle and delete button from triggering move
- draggingRef tracks in-progress drag; activeDragFieldId drives grabbing cursor + box-shadow
- Minimum field size enforced: 40x20 PDF units
2026-03-20 01:02:45 -06:00
Chandler Copeland
3d6f0ea68c fix(05-04): disable DragOverlay drop animation to eliminate snap-back 2026-03-20 01:00:02 -06:00
Chandler Copeland
cbad0a1a34 fix(05-04): remove transform from DraggableToken to prevent snap-back animation 2026-03-20 00:59:27 -06:00
Chandler Copeland
f8897cce34 fix(05-04): sync defaultEmail into state after hydration 2026-03-20 00:55:22 -06:00
Chandler Copeland
0a719c9d60 fix(05-04): fetch client email via direct join, improve recipients hint text 2026-03-20 00:53:46 -06:00
Chandler Copeland
1319d4310e fix(05-04): guard parseEmails against undefined defaultEmail 2026-03-20 00:51:32 -06:00
Chandler Copeland
7f97bbc5e5 fix(05-04): simplify recipients to single pre-filled textarea, remove client dropdown 2026-03-20 00:50:41 -06:00
Chandler Copeland
73ba6d5a0d fix(05-04): replace locked client display with editable email input
- Remove isLocked variable and read-only div for assigned client
- Add primaryEmail state pre-filled with assigned client's email, user-editable
- Show client name as helper text below the input for reference
- Update buildEmailAddresses() to use primaryEmail when assignedClientId is set
- Update button disabled logic to gate on primaryEmail when assigned client exists
2026-03-20 00:41:49 -06:00
Chandler Copeland
f0ecfd1545 fix(05-04): use ghost rect for field placement, canvas offset for overlays
- Replace cursor+delta calculation with active.rect.current.translated (ghost bounding rect)
- Measure coordinates relative to the inner <canvas> element, not the DroppableZone wrapper
- Add canvasOffset state + useLayoutEffect to offset field overlays by canvas position within wrapper
- Inline PDF coordinate math in handleDragEnd; clamp uses field pixel dimensions
2026-03-20 00:40:18 -06:00
Chandler Copeland
05915aa562 fix(05-04): pre-select document client and add manual email entry
- PreparePanel now receives separate assignedClientId (nullable) and
  defaultClientId props so it can distinguish an explicitly locked
  client from just a default
- When document already has assignedClientId: show locked read-only
  display; user cannot change the primary recipient
- When document has no assignedClientId: default to document owner in
  dropdown but allow changing; option to clear and enter email manually
- Added textarea for additional/CC email addresses (comma or newline
  separated) that is always visible for either mode
- POST /api/documents/[id]/prepare now accepts and stores emailAddresses
  array alongside assignedClientId
- Added email_addresses jsonb column to documents table via migration
  0004_military_maximus.sql

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 00:21:34 -06:00
Chandler Copeland
126e10dc1d fix(05-04): fix signature field placement coordinate math
- 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>
2026-03-20 00:18:23 -06:00
Chandler Copeland
296ef482bb feat(05-03): extend document detail page with PreparePanel
- 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
2026-03-20 00:04:55 -06:00
Chandler Copeland
df6eb76bd0 feat(05-03): create TextFillForm and PreparePanel client components
- 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)
2026-03-20 00:04:08 -06:00
Chandler Copeland
7a367363b1 feat(05-02): extend PdfViewer with pageInfo state and FieldPlacer integration
- 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
2026-03-20 00:00:22 -06:00
Chandler Copeland
6069ae5e06 feat(05-02): install dnd-kit and create FieldPlacer component
- 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)
2026-03-19 23:59:51 -06:00
Chandler Copeland
c81e8ea838 feat(05-01): add preparePdf utility and fields/prepare API routes
- 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>
2026-03-19 23:54:41 -06:00