Backend foundations were already in place ('generic' CustomDocumentType,
storage-path routing). This wires the UI surface across Documents Hub +
entity file tabs.
- UploadForSigningDialog: interestId now string | null; new entity?,
folderId?, onCreated? props. Generic path POSTs to new endpoint
/api/v1/upload-for-signing; interest-scoped paths unchanged.
- uploadDocumentForSigning service: interestId nullable; skips interest
lookup, pipeline-stage advance, doc-status flip on the generic path.
Routes file FK + auto-filed folder via either interest.clientId or the
caller-supplied entity. Validation enforces the matching invariant
(generic must be interestId=null, type-specific must carry one).
- New menu item in NewDocumentMenu ("Upload & send for signature") on
Documents Hub root + folder views.
- Upload & send-for-signature button on ClientFilesTab + CompanyFilesTab,
gated by documents.send_for_signing.
Existing unit tests for the service still pass (validation paths unchanged).
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
L41 from the 2026-05-21 plan.
Shipped (4 sub-tasks):
- **Dialog width**: already fixed in an earlier session
(max-w-[1400px] w-[95vw] on the DialogContent).
- **Draft persistence to localStorage**: scoped per
interest+documentType (`pn-crm.upload-for-signing.draft.v1:<id>:<type>`),
versioned for future shape evolution. Persists step / title /
recipients / fields / invitationMessage with a 500ms debounce so
rapid edits (typing the custom note, dragging a field) don't
hammer storage. The PDF File object itself is NOT persisted
(large blobs + browser quota); on reopen the rep re-picks the
file but every other piece of state survives. Pristine "no
progress yet" state actively clears any stale draft. Header
surfaces a "Draft saved" indicator + Discard button when a
draft exists. Successful submission clears the draft so the
shadow doesn't outlive the doc.
- **PDF preview error handling + zoom**: `onLoadError` now sets
`pdfLoadError` and replaces the spinner with a useful failure
block (error message + re-pick guidance) so reps don't see an
infinite loading state on a broken file. Toolbar gains zoom
controls (50–200% in 25% steps); field coordinates stay in %
of page dimensions so placements scale automatically with the
canvas.
- **Field-placement keyboard shortcuts**: window-level keydown
handler responds to Delete / Backspace (remove selected field),
arrow keys (nudge 0.5% per press, Shift + arrow = 5% per press).
Ignored when focus is in a real input / textarea / contenteditable
so the shortcuts never steal typing.
Verified: tsc clean, vitest 1454/1454.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six surgical Wave-2-3 wins:
- UploadForSigningDialog: dialog widened to max-w-[1400px] w-[95vw] so
the place-fields step actually has room; recipient row converts from
fixed grid to flex (name flex-1, email flex-[2] for the longer
string, role w-40, delete shrink-0); invitation-message textarea
rows 3 → 6.
- ChartCard becomes flex-col with flex-1 + items-center on CardContent
so charts vertically center when neighbouring cards make the row
taller (e.g. Pipeline Value's full breakdown).
- Berth recommender pill: drops the "Tier {letter} · " prefix; shows
just the plain-English label ("Open" / "Fall-through" / "Active
interest" / "Late stage") as a Popover trigger that explains the
4-state ladder. HelpCircle icon makes the tooltip discoverable.
- Activity feed gains a "See all" link in the header pointing at
/<port>/admin/audit, permission-gated by `admin.view_audit_log`.
- Inbox section order swaps to Reminders above Alerts (rep-noted
priority); PageHeader title flips to "Reminders & Alerts". Section
ids, deep-link hashes, and localStorage open-state keys untouched.
- Inbox ReminderList (embedded mode only): "New Reminder" button now
shares the filter row (right-aligned via ml-auto) instead of
occupying its own dedicated row above the filters.
tsc clean. 1419/1419 vitest pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two of the six Phase 6 polish items shipped in one commit because they
share the data + plumbing path (per-doc message uses the signing-
progress UI's existing layout).
1) Signing-progress activity badges
- Surfaces `invitedAt`, `openedAt`, `lastReminderSentAt` (all
populated by Phase 1+2 webhook handlers) per signer in the
existing progress widget. Each badge renders as
"Invited 2 hours ago / Opened yesterday / Reminded 3 days ago"
via Intl.RelativeTimeFormat.
- Resend button: was silent on success/failure; now uses
useMutation + toast so the rep sees whether the reminder fired
or fell into a cadence cooldown. Honours the existing
sendReminderIfAllowed return shape (`{sent, reason}`).
- Title-tooltips on each badge show the exact ISO timestamp.
2) Per-document custom invitation message
- New `documents.invitation_message` column (migration 0060;
applied via psql per the dev-flow note in CLAUDE.md).
- Textarea in UploadForSigningDialog step 2 (recipient configurator),
1000-char cap, placeholder text shows the expected tone.
- custom-document-upload.service accepts `invitationMessage`,
trims + stores on the documents row.
- sendCascadingInviteForNextSigner now reads
doc.invitationMessage and passes as customMessage so every
cascaded recipient (developer / approver / witness) sees the
same note — not just the first signer.
- send-invitation route (manual resend path) reads the same
column → customMessage so manual reminders match.
- The email template's existing customMessage rendering does
the XSS escape; no other plumbing needed.
Phase 6 items still deferred (each ~2-3h, mostly independent):
- Auto-send delay (`eoi_send_delay_minutes` setting + scheduled
BullMQ job — needs a scheduler hook).
- Document expiration (`documents.expires_at` + Documenso
`expiresAt` passthrough — needs Documenso v2 endpoint shape
verification).
- Failed-webhook recovery admin UI (the BullMQ DLQ exists; needs
an admin page with Replay button).
Tests: 1340 → 1350 ✅; tsc clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 4 lands the visual half of the Documenso build — the upload-
for-signing dialog the Contract + Reservation tabs hand off to. Four
files of new code; the existing tab placeholders point at it.
Files added:
- lib/services/document-field-detector.ts — Phase 4c auto-detect
scanner. Uses pdfjs-dist to extract per-page text + positions, then
matches anchor patterns (Signature, Date, Initials, Email, Name,
underscore-runs) and produces percent-coordinate DetectedField
rows. Recipient label inference walks ±100pt of each match for
Buyer/Seller/Client/Witness/Notary keywords. Returns [] when the
PDF is image-only; UI falls back to manual placement without an
error. 6 unit tests pin the matching + coordinate math.
- app/api/v1/documents/auto-detect-fields/route.ts — multipart POST
endpoint that delegates to detectFields(). Permission-gated by
documents.send_for_signing.
- app/api/v1/documents/signing-defaults/route.ts — GET endpoint that
surfaces just the per-port developer + approver display name/email
+ sendMode flag. No secrets exposed; lets the dialog prefill the
recipient configurator without an admin-scoped settings read.
- components/documents/upload-for-signing-dialog.tsx — the Phase 4
UI. Three-step state machine inside a single Dialog:
1. select-file: drop/click PDF picker + title input
2. configure-recipients: client + developer + approver prefilled,
rep can add/remove/reorder + change role (SIGNER/APPROVER/CC)
3. place-fields: react-pdf renders the source PDF; auto-detect
runs in the background on file load and seeds the overlay;
rep places, drags, resizes, deletes, reassigns fields via the
palette + side panel. Native DOM drag (no dnd-kit dependency
added — the coordinate math stays obvious).
Send fires POST /api/v1/interests/[id]/upload-for-signing (Phase 3
service); success toast reflects port sendMode (auto fires the
invite immediately, manual leaves it for the rep).
Files modified:
- components/interests/interest-contract-tab.tsx + reservation-tab.tsx:
swap the ComingSoonDialog placeholder for the real
UploadForSigningDialog with the matching documentType prop. The
placeholder ComingSoonDialog helper is deleted from both.
- scripts/tsc-staged.mjs: pull src/types/**/*.d.ts into the temp
staged-only tsconfig so side-effect CSS imports (e.g.
react-pdf/dist/Page/AnnotationLayer.css) resolve via the existing
declare-module shim. Without this fix the staged compile reports
TS2882 even though the full tsc --noEmit pass passes.
Design choices noted in code comments:
- Native drag over dnd-kit: the field overlay's percent-based
coordinate math is short enough that adding a drag library adds
complexity without saving lines.
- Auto-detect on file-load (not on demand): runs immediately so the
rep doesn't have to click a second button — empty result drops
back to manual placement silently.
- Per-recipient color swatches indexed by signingOrder.
- Recipient seed via useMemo + user-event handler instead of
useEffect → setRecipients (Wave 3 set-state-in-effect avoidance).
Server-side, Phase 3 plumbing handles the rest: tenant guard, magic-
byte verify, Documenso round-trip with per-port v1/v2 routing,
recipient signingToken capture for Phase 2 webhook cascade, auto-
send when port.sendMode === 'auto'.
Tests: 1334 → 1340 ✅ (6 new for the detector); tsc clean.
Deferred polish (Phase 6):
- Per-field metadata side panel for DROPDOWN/RADIO option lists
- Pinch-zoom + zoom-out controls on the field-placement canvas
- Recipient drag-reorder via dnd-kit
- Required toggle per field
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>