---
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)_
