Commit Graph

64 Commits

Author SHA1 Message Date
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
f41db49ff7 feat(06-02): branded signing request email + mailer utilities
- Add SigningRequestEmail.tsx React Email component (navy/gold brand colors, CTA button)
- Add signing-mailer.tsx with sendSigningRequestEmail() and sendAgentNotificationEmail()
- Uses CONTACT_SMTP_* env vars (same SMTP provider as contact form)
- Sender: "Teressa Copeland" <teressa@teressacopelandhomes.com>
2026-03-20 11:29:05 -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
2929581ab9 feat(06-01): create signing utility library (token, audit, embed)
- token.ts: createSigningToken() + verifySigningToken() using jose HS256
- audit.ts: logAuditEvent() inserts typed audit events with server timestamp
- embed-signature.ts: embedSignatureInPdf() embeds PNG sigs, returns SHA-256 hash
- added SIGNING_JWT_SECRET to .env.local (random 32-char base64 secret)
2026-03-20 11:24:56 -06:00
Chandler Copeland
fa68a1bcb4 feat(06-01): install packages + extend schema + generate migration
- installed signature_pad, @react-email/render, @react-email/components
- added signingTokens table (jti pk, documentId, expiresAt, usedAt)
- added auditEvents table with auditEventTypeEnum (6 event types)
- added signedFilePath, pdfHash, signedAt columns to documents table
- generated and applied migration 0005_signing_flow.sql
2026-03-20 11:24:02 -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
c4e8d01784 fix(05-04): move text stamp to 60pt from top, increase font size to 10pt
- Change startY from pageHeight-20 to pageHeight-60 (~0.83 inch from top)
- Increase lineHeight from 12 to 14 for better readability
- Increase stamp font size from 8pt to 10pt for better visibility
2026-03-20 00:42:03 -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
ef10dd5089 fix(05-04): always stamp text fill data into prepared PDF
Strategy A still attempts AcroForm filling by field name (matching
fields in the PDF's form dict). Strategy B is now a mandatory fallback:
any text field entries that did not match an AcroForm field (or when
the PDF has no AcroForm at all) are drawn as 'key: value' text lines
near the top of page 1 using @cantoo/pdf-lib drawText.

This ensures text fill data supplied in the PreparePanel is always
visible in the output PDF regardless of whether the source PDF was
built with interactive form fields.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 00:22:17 -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
34ed0baa43 test(05-01): add unit tests for Y-flip coordinate conversion formula
- 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>
2026-03-19 23:55:27 -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
Chandler Copeland
d67130da20 feat(05-01): extend documents schema with 4 new columns + migration 0003
- Added SignatureFieldData interface export to schema.ts
- Added signatureFields (jsonb), textFillData (jsonb), assignedClientId (text), preparedFilePath (text) nullable columns to documents table
- Added jsonb import to drizzle-orm/pg-core imports
- Generated and applied migration 0003_cool_natasha_romanoff.sql

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 23:53:12 -06:00
Chandler Copeland
ac5b98fe33 wip: skyslope scraper — fix name extraction via body text parsing, preview+download flow ready 2026-03-19 23:06:17 -06:00
Chandler Copeland
27462a0ebb feat: add Playwright script to scrape SkySlope form libraries into seeds/forms/ 2026-03-19 22:27:41 -06:00
Chandler Copeland
6c5e4afd86 fix(04-03): move ssr:false dynamic import into client wrapper to fix DOMMatrix SSR error 2026-03-19 22:01:58 -06:00
Chandler Copeland
025d9896ed fix(04-03): make back button more prominent with bordered pill style 2026-03-19 21:56:36 -06:00
Chandler Copeland
5c7a0fd061 fix(04-03): style Browse files as a visible button with selected filename display 2026-03-19 21:54:07 -06:00
Chandler Copeland
c1f60cadf6 feat(04-03): add AddDocumentModal, PdfViewer, and document detail page
- 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
2026-03-19 21:44:17 -06:00
Chandler Copeland
63e5888968 feat(04-03): install react-pdf and configure Next.js transpilePackages
- npm install react-pdf (v9+ pulls in pdfjs-dist automatically)
- Add transpilePackages: ['react-pdf', 'pdfjs-dist'] to next.config.ts
- Build verified passing after config change
2026-03-19 21:42:06 -06:00
Chandler Copeland
32e129c097 feat(04-02): create POST /api/documents and GET /api/documents/[id]/file routes
- 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
2026-03-19 21:36:54 -06:00
Chandler Copeland
e0f180c3d8 feat(04-02): create GET /api/forms-library authenticated template list
- Queries form_templates ordered by name (asc)
- Returns 401 for unauthenticated requests
- Returns JSON array of { id, name, filename } for authenticated agents
2026-03-19 21:36:21 -06:00
Chandler Copeland
f82364d2c7 feat(04-01): create seed-forms script and npm run seed:forms command
- 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)
2026-03-19 21:33:02 -06:00
Chandler Copeland
bbbbdbed5e feat(04-01): add formTemplates table and extend documents schema
- Add formTemplates table (id text PK, name, filename unique, createdAt, updatedAt)
- Add formTemplateId (nullable FK) and filePath (nullable text) to documents table
- Generate and apply migration 0002_wealthy_zzzax.sql
- Create seeds/forms/.gitkeep to track seed directory in git
2026-03-19 21:32:30 -06:00
Chandler Copeland
b186ac5f38 feat(03-04): add client profile page with edit/delete and documents table
- Create ConfirmDialog component with overlay, title, message, cancel/confirm buttons
- Create ClientProfilePage server component (awaits params Promise — Next.js 16)
- Create ClientProfileClient client component with edit modal and delete confirmation
- Documents section uses DocumentsTable with showClientColumn={false}
- deleteClient called directly from async event handler in client component
2026-03-19 16:58:21 -06:00
Chandler Copeland
3fa2e1c424 feat(03-03): extend seed.ts with client and placeholder document rows
- 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
2026-03-19 16:49:29 -06:00
Chandler Copeland
df1924acc4 feat(03-03): add Clients page with card grid and create modal
- 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
2026-03-19 16:47:28 -06:00
Chandler Copeland
e55d7a1de5 feat(03-03): add Dashboard page with filterable documents table
- 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
2026-03-19 16:46:26 -06:00
Chandler Copeland
5b87201b28 feat(03-02): add createClient, updateClient, deleteClient server actions
- 'use server' file with Zod validation (name min 1 char, valid email)
- createClient: validate, insert, revalidatePath /portal/clients
- updateClient: bind pattern (id, prevState, formData), revalidates client list + profile
- deleteClient: auth check, delete by id, revalidatePath /portal/clients
- Fixed Zod v4 .issues access (not .errors — v4 API change)
2026-03-19 16:38:49 -06:00
Chandler Copeland
28d42f5d9b feat(03-02): add StatusBadge and DocumentsTable shared portal components
- StatusBadge: color-coded pill for Draft=gray, Sent=blue, Viewed=amber, Signed=green
- DocumentsTable: reusable table with optional Client column, StatusBadge integration
- Date format: toLocaleDateString en-US short month; null sentAt renders as em-dash
2026-03-19 16:37:30 -06:00
Chandler Copeland
9c4caeedba feat(03-02): add portal authenticated layout and PortalNav
- 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
2026-03-19 16:37:03 -06:00
Chandler Copeland
00f9c7c9f0 feat(03-01): protect /portal routes and update post-login redirect
- middleware.ts: add /portal/:path* to matcher array
- auth.config.ts: add isPortalRoute check, redirect unauthenticated to /agent/login
- auth.config.ts: change post-login redirect from /agent/dashboard to /portal/dashboard
- agent dashboard page: replace stub with redirect to /portal/dashboard
2026-03-19 16:17:59 -06:00