168 lines
16 KiB
Markdown
168 lines
16 KiB
Markdown
---
|
||
phase: 16-multi-signer-ui
|
||
verified: 2026-04-03T22:45:00Z
|
||
status: passed
|
||
score: 11/11 must-haves verified
|
||
human_verification:
|
||
- test: "Preview-gate on send-block: Agent adds signers, skips Preview, tries to send — confirm Prepare and Send button is disabled (greyed out) and shows tooltip or stays inaccessible until Preview is clicked"
|
||
expected: "Button is disabled with opacity-50 because previewToken === null; agent cannot reach handlePrepare validation without first clicking Preview"
|
||
why_human: "This is intentional UX but means MSIGN-04 validation only fires post-preview. Need to confirm the UX is clear enough that agents understand they must preview before sending, rather than the validation appearing to be absent."
|
||
- test: "Color-coded fields in FieldPlacer: add two signers (e.g., signer1@test.com, signer2@test.com), select each as active signer, drag a signature field for each — confirm different signer colors appear on the placed fields"
|
||
expected: "First signer's fields show indigo (#6366f1) border/background; second signer's fields show rose (#f43f5e) border/background"
|
||
why_human: "Visual color rendering cannot be verified programmatically"
|
||
- test: "Red validation overlay: add a signer, place a client-visible field without assigning it (existing AI-placed unassigned field works), click Preview then Prepare and Send — confirm red outline appears on unassigned fields"
|
||
expected: "Fields in unassignedFieldIds render with border: 2px solid #ef4444 and background: #ef444414; error message shows 'N field(s) need a signer assigned before sending.'"
|
||
why_human: "Visual validation overlay rendering cannot be verified programmatically"
|
||
- test: "Dashboard N/M badge: send a document with 2 signers, have one signer complete — confirm dashboard shows '1/2 signed' badge next to Sent status"
|
||
expected: "Badge rendered with bg-blue-50 text-blue-700 classes showing '1/2 signed'"
|
||
why_human: "Requires a live signing flow to generate a usedAt token record"
|
||
---
|
||
|
||
# Phase 16: Multi-Signer UI Verification Report
|
||
|
||
**Phase Goal:** Agent can name and add multiple signers from PreparePanel, assign each field to a specific signer in FieldPlacer with color-coded visual distinction, and cannot accidentally send a document with unassigned client-facing fields
|
||
|
||
**Verified:** 2026-04-03T22:45:00Z
|
||
**Status:** passed
|
||
**Re-verification:** No — initial verification
|
||
|
||
---
|
||
|
||
## Goal Achievement
|
||
|
||
### Observable Truths
|
||
|
||
| # | Truth | Status | Evidence |
|
||
|----|-------|--------|---------|
|
||
| 1 | DocumentPageClient holds signers state and threads it to both PreparePanel and PdfViewerWrapper/FieldPlacer | ✓ VERIFIED | `DocumentPageClient.tsx` lines 32-33, 79-80, 98-101: `useState<DocumentSigner[]>(initialSigners)` + props passed to both components |
|
||
| 2 | DocumentPageClient holds unassignedFieldIds state for send-block validation highlighting | ✓ VERIFIED | `DocumentPageClient.tsx` line 33: `useState<Set<string>>(new Set())`, passed as `unassignedFieldIds` to both PdfViewerWrapper and PreparePanel |
|
||
| 3 | Server page reads documents.signers from DB and passes to DocumentPageClient as initialSigners prop | ✓ VERIFIED | `page.tsx` line 63: `initialSigners={doc.signers ?? []}` — doc fetched via `db.query.documents.findFirst` which includes all columns |
|
||
| 4 | PdfViewerWrapper passes signers and unassignedFieldIds through to FieldPlacer | ✓ VERIFIED | PdfViewerWrapper → PdfViewer (lines 32-33, 43-44, 106-107) → FieldPlacer (FieldPlacerProps lines 167-168, destructured at line 171) |
|
||
| 5 | Agent can type an email and click Add Signer to add a signer with auto-assigned color | ✓ VERIFIED | `PreparePanel.tsx` lines 70-84: `handleAddSigner` validates email format, checks duplicate, auto-assigns `SIGNER_COLORS[signers.length % 4]`, calls `onSignersChange` |
|
||
| 6 | Agent sees a colored dot, email, and remove button for each signer in the list | ✓ VERIFIED | `PreparePanel.tsx` lines 338-353: `w-2 h-2 rounded-full` dot with `signer.color`, email text, `aria-label="Remove signer {email}"` × button (w-8 h-8 touch target) |
|
||
| 7 | Agent can remove a signer by clicking the X button | ✓ VERIFIED | `PreparePanel.tsx` lines 86-90: `handleRemoveSigner` filters signer from list and clears `unassignedFieldIds` |
|
||
| 8 | Send is blocked with inline error when client-visible fields have no signerEmail and signers exist | ✓ VERIFIED | `PreparePanel.tsx` lines 210-216: fetches fields, filters `isClientVisibleField`, checks `!f.signerEmail`, calls `onUnassignedFieldIdsChange` and sets error message `"{N} field(s) need a signer assigned before sending."` |
|
||
| 9 | Send is blocked with inline error when signers list is empty but client-visible fields exist | ✓ VERIFIED | `PreparePanel.tsx` lines 204-208: `signers.length === 0 && clientFields.length > 0` guard with message "Add at least one signer before sending." |
|
||
| 10 | Active signer dropdown appears above palette when signers.length > 0; dragged fields get signerEmail | ✓ VERIFIED | `FieldPlacer.tsx` lines 792-830: selector shown when `!readOnly && signers.length > 0`; line 317: `...(activeSignerEmail ? { signerEmail: activeSignerEmail } : {})` in handleDragEnd |
|
||
| 11 | Multi-signer Sent documents show N/M signed badge in Status column; single-signer and fully-signed do not | ✓ VERIFIED | `DocumentsTable.tsx` lines 68-72: conditional render gated on `hasMultipleSigners && status === "Sent" && totalSigners > 0`; dashboard enriches rows with `signedCount`, `totalSigners`, `hasMultipleSigners` from batched `signingTokens` query |
|
||
|
||
**Score:** 11/11 truths verified
|
||
|
||
---
|
||
|
||
### Required Artifacts
|
||
|
||
| Artifact | Expected | Status | Details |
|
||
|----------|----------|--------|---------|
|
||
| `src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx` | signers state, setSigners, unassignedFieldIds state, setUnassignedFieldIds — threaded to PreparePanel and PdfViewerWrapper | ✓ VERIFIED | 107 lines; imports `DocumentSigner`; both state vars initialized; both props threaded correctly to both children |
|
||
| `src/app/portal/(protected)/documents/[docId]/page.tsx` | Server-side documents.signers fetch, passed as initialSigners to DocumentPageClient | ✓ VERIFIED | `db.query.documents.findFirst` fetches all columns including signers; `initialSigners={doc.signers ?? []}` at line 63 |
|
||
| `src/app/portal/(protected)/documents/[docId]/_components/PdfViewerWrapper.tsx` | Passes signers and unassignedFieldIds props through to PdfViewer/FieldPlacer | ✓ VERIFIED | 44 lines; both props accepted and passed through the full chain |
|
||
| `src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx` | Signer list UI section, send-block validation logic | ✓ VERIFIED | 394 lines; contains "Add Signer" button, `SIGNER_COLORS` palette, signer list rendering, send-block validation in `handlePrepare` |
|
||
| `src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx` | Active signer selector, per-signer field coloring, validation overlay | ✓ VERIFIED | 907 lines; `activeSignerEmail` state; selector UI above palette; `signerEmail` assignment in `handleDragEnd`; signer color override in `renderFields`; `#ef4444` validation overlay |
|
||
| `src/app/portal/(protected)/dashboard/page.tsx` | Server-side query joining signingTokens for per-signer completion count | ✓ VERIFIED | Imports `signingTokens`; batched `count(usedAt)` query; `enrichedRows` with `signedCount`, `totalSigners`, `hasMultipleSigners` |
|
||
| `src/app/portal/_components/DocumentsTable.tsx` | N/M signed badge rendering in Status column | ✓ VERIFIED | `DocumentRow` type extended with optional fields; conditional badge with `bg-blue-50 text-blue-700 ml-1.5` classes |
|
||
|
||
---
|
||
|
||
### Key Link Verification
|
||
|
||
| From | To | Via | Status | Details |
|
||
|------|----|-----|--------|---------|
|
||
| `page.tsx` | DocumentPageClient | `initialSigners={doc.signers ?? []}` | ✓ WIRED | page.tsx line 63 |
|
||
| DocumentPageClient | PdfViewerWrapper | `signers={signers}` and `unassignedFieldIds={unassignedFieldIds}` | ✓ WIRED | DocumentPageClient.tsx lines 79-80 |
|
||
| DocumentPageClient | PreparePanel | `signers={signers}`, `onSignersChange={setSigners}`, `unassignedFieldIds`, `onUnassignedFieldIdsChange` | ✓ WIRED | DocumentPageClient.tsx lines 98-101 |
|
||
| PdfViewerWrapper | PdfViewer | `signers={signers}` and `unassignedFieldIds={unassignedFieldIds}` | ✓ WIRED | PdfViewerWrapper.tsx lines 40-41; PdfViewer.tsx lines 106-107 |
|
||
| PdfViewer | FieldPlacer | `signers={signers}` and `unassignedFieldIds={unassignedFieldIds}` | ✓ WIRED | PdfViewer.tsx lines 43-44, 106-107 |
|
||
| PreparePanel signer add | `onSignersChange` callback | `onSignersChange([...signers, { email, color }])` | ✓ WIRED | PreparePanel.tsx line 81 |
|
||
| PreparePanel send validation | `onUnassignedFieldIdsChange` callback | `new Set(unassigned.map(f => f.id))` | ✓ WIRED | PreparePanel.tsx line 212 |
|
||
| FieldPlacer handleDragEnd | `newField.signerEmail` | `...(activeSignerEmail ? { signerEmail: activeSignerEmail } : {})` | ✓ WIRED | FieldPlacer.tsx line 317 |
|
||
| FieldPlacer renderFields | signer color lookup | `signers.find(s => s.email === field.signerEmail)?.color` | ✓ WIRED | FieldPlacer.tsx lines 603-608 |
|
||
| PreparePanel | prepare route body | `signers` included in `body: JSON.stringify({ textFillData, emailAddresses, signers })` | ✓ WIRED | PreparePanel.tsx line 224; prepare route persists `body.signers` to `documents.signers` at route.ts line 82 |
|
||
| dashboard/page.tsx query | DocumentsTable rows | `enrichedRows` with `signedCount`, `totalSigners`, `hasMultipleSigners` | ✓ WIRED | dashboard/page.tsx lines 56-64, 82 |
|
||
| DocumentsTable | N/M badge | conditional render on `hasMultipleSigners && status === 'Sent' && totalSigners > 0` | ✓ WIRED | DocumentsTable.tsx lines 68-72 |
|
||
|
||
---
|
||
|
||
### Data-Flow Trace (Level 4)
|
||
|
||
| Artifact | Data Variable | Source | Produces Real Data | Status |
|
||
|----------|---------------|--------|--------------------|--------|
|
||
| PreparePanel — signer list | `signers` prop | `DocumentPageClient` state initialized from `doc.signers` (DB JSONB) | Yes — seeded from DB on page load; updated via `onSignersChange` and persisted via prepare POST | ✓ FLOWING |
|
||
| FieldPlacer — field colors | `field.signerEmail` + `signers` prop | Fields loaded from DB via `GET /api/documents/{id}/fields`; signers from DocumentPageClient state | Yes — both are real DB values | ✓ FLOWING |
|
||
| DocumentsTable — N/M badge | `signedCount`, `totalSigners`, `hasMultipleSigners` | Dashboard batched `signingTokens` query grouped by `documentId` | Yes — live DB token rows with `count(usedAt)` | ✓ FLOWING |
|
||
|
||
---
|
||
|
||
### Behavioral Spot-Checks
|
||
|
||
Step 7b: SKIPPED (requires running server for API calls and live document state; no static checks applicable beyond TypeScript compilation)
|
||
|
||
**TypeScript compile:** `npx tsc --noEmit` — PASS (no errors, no output)
|
||
|
||
---
|
||
|
||
### Requirements Coverage
|
||
|
||
| Requirement | Source Plan | Description | Status | Evidence |
|
||
|-------------|------------|-------------|--------|---------|
|
||
| MSIGN-01 | 16-01, 16-02 | Agent can add named signers to a document by email address from PreparePanel before sending | ✓ SATISFIED | `handleAddSigner` in PreparePanel.tsx; "Add Signer" button; signer list with colored dots |
|
||
| MSIGN-02 | 16-01, 16-03 | Agent can tag each signature, initials, and date field to a specific signer when placing in FieldPlacer | ✓ SATISFIED | `activeSignerEmail` state in FieldPlacer; `signerEmail: activeSignerEmail` assignment on `handleDragEnd` |
|
||
| MSIGN-03 | 16-01, 16-03 | Fields in FieldPlacer are color-coded by assigned signer for visual distinction | ✓ SATISFIED | `renderFields` color override: `signers.find(s => s.email === field.signerEmail)?.color` overrides `PALETTE_TOKENS` type color |
|
||
| MSIGN-04 | 16-02 | Agent cannot send a document if any client-facing field has no signer assigned — send is blocked with a clear error | ✓ SATISFIED | `handlePrepare` in PreparePanel: fetches fields, filters `isClientVisibleField`, checks `!f.signerEmail`, shows "{N} field(s) need a signer assigned before sending." Note: gated behind preview (button disabled until `previewToken !== null`) — see Human Verification item 1 |
|
||
| MSIGN-09 | 16-04 | Dashboard shows per-signer completion status (who has signed, who hasn't) | ✓ SATISFIED | Dashboard batches `signingTokens` query; enriches rows with `signedCount`/`totalSigners`; DocumentsTable renders "N/M signed" badge for multi-signer Sent documents only |
|
||
|
||
**Note on REQUIREMENTS.md status:** The requirements tracker at `.planning/REQUIREMENTS.md` still shows MSIGN-04 and MSIGN-09 as "Pending" despite both being implemented. The tracker was not updated after plan execution. This is a documentation gap, not an implementation gap.
|
||
|
||
---
|
||
|
||
### Anti-Patterns Found
|
||
|
||
| File | Line | Pattern | Severity | Impact |
|
||
|------|------|---------|----------|--------|
|
||
| PreparePanel.tsx | 376 | `disabled={loading \|\| previewToken === null \|\| ...}` | ℹ️ Info | Send-block validation (`handlePrepare`) can only fire after a Preview has been generated. Agent must click Preview before Prepare and Send is enabled. This is intentional UX but is not the same as blocking at click-time before preview. |
|
||
|
||
No STUB, MISSING, or ORPHANED anti-patterns found. No TODO/FIXME/placeholder comments in implementation code. No empty implementations.
|
||
|
||
---
|
||
|
||
### Human Verification Required
|
||
|
||
#### 1. Preview-gate UX clarity for send-block
|
||
|
||
**Test:** Open a Draft document as agent. Add a signer. Place a client-visible field (e.g., Signature) without assigning a signer (leave active signer empty or use a field from AI placement). Attempt to click "Prepare and Send" without first clicking "Preview."
|
||
**Expected:** Button is greyed out / disabled — agent cannot click it. After clicking "Preview," button becomes active, and if fields are unassigned, the send attempt shows: "N field(s) need a signer assigned before sending." and red outlines appear on unassigned fields.
|
||
**Why human:** The preview-gate is a behavioral UX pattern. Need to confirm the flow is clear to agents — that greyout reason is not confusing — and that the red highlight actually renders when validation fires.
|
||
|
||
#### 2. Color-coded field visual distinction
|
||
|
||
**Test:** Add two signers (signer1@test.com with indigo, signer2@test.com with rose). Select signer1 as active, drag a Signature field. Select signer2 as active, drag another Signature field. Confirm both fields show different colored borders/backgrounds.
|
||
**Expected:** Signer1 field: indigo `#6366f1` border and tinted background. Signer2 field: rose `#f43f5e` border and tinted background. Unassigned fields: type-color (blue `#2563eb` for signature).
|
||
**Why human:** Visual color rendering cannot be verified by code inspection alone.
|
||
|
||
#### 3. Red validation overlay on unassigned fields
|
||
|
||
**Test:** Add a signer, place client-visible fields (some with signer assigned, some without). Click Preview, then Prepare and Send.
|
||
**Expected:** Fields with no `signerEmail` show red border `#ef4444` and red-tinted background `#ef444414`. Error message appears: "N field(s) need a signer assigned before sending."
|
||
**Why human:** CSS border/background rendering requires visual inspection.
|
||
|
||
#### 4. Dashboard N/M badge with live signing data
|
||
|
||
**Test:** Send a document to two signers. Have one signer complete signing via their link. Check dashboard.
|
||
**Expected:** Document row shows "1/2 signed" badge (blue pill, `bg-blue-50 text-blue-700`) next to "Sent" status badge. After both sign, only "Signed" badge shows (no N/M badge).
|
||
**Why human:** Requires a live signing flow to create a `usedAt` token record.
|
||
|
||
---
|
||
|
||
### Gaps Summary
|
||
|
||
No gaps found. All 11 observable truths are verified, all artifacts are substantive and wired, all key links are confirmed present in the actual codebase. TypeScript compiles clean.
|
||
|
||
The only notable finding is a documentation inconsistency: REQUIREMENTS.md still shows MSIGN-04 and MSIGN-09 as "Pending" rather than "Complete," but both requirements are fully implemented in the code. This does not affect goal achievement.
|
||
|
||
The preview-gate on "Prepare and Send" (`disabled` when `previewToken === null`) means agents must preview before sending. This is an intentional UX design choice — the send-block validation fires correctly within `handlePrepare` once the button is enabled. It is flagged for human verification to confirm the UX is sufficiently clear.
|
||
|
||
---
|
||
|
||
_Verified: 2026-04-03T22:45:00Z_
|
||
_Verifier: Claude (gsd-verifier)_
|