---
phase: 16-multi-signer-ui
plan: 02
type: execute
wave: 2
depends_on: ["16-01"]
files_modified:
- teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx
autonomous: true
requirements: [MSIGN-01, MSIGN-04]
must_haves:
truths:
- "Agent can type an email and click Add Signer to add a signer with auto-assigned color"
- "Agent sees a colored dot, email, and remove button for each signer in the list"
- "Agent can remove a signer by clicking the X button"
- "Send is blocked with inline error when client-visible fields have no signerEmail and signers exist"
- "Send is blocked with inline error when signers list is empty but client-visible fields exist"
artifacts:
- path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx"
provides: "Signer list UI section, send-block validation logic"
contains: "Add Signer"
key_links:
- from: "PreparePanel signer add"
to: "onSignersChange callback"
via: "setSigners call with new signer appended"
pattern: "onSignersChange.*email.*color"
- from: "PreparePanel send validation"
to: "onUnassignedFieldIdsChange callback"
via: "Set of field IDs with no signerEmail"
pattern: "unassignedFieldIds|onUnassignedFieldIdsChange"
---
Add the signer list UI to PreparePanel and the send-block validation logic per D-02, D-03, D-04, D-09, D-10.
Purpose: MSIGN-01 (agent adds signers by email) and MSIGN-04 (send blocked when fields unassigned). These are the two core PreparePanel features for multi-signer.
Output: PreparePanel with signer email input, signer list with colored dots and remove buttons, send-block validation with error messages.
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/16-multi-signer-ui/16-CONTEXT.md
@.planning/phases/16-multi-signer-ui/16-UI-SPEC.md
@.planning/phases/16-multi-signer-ui/16-01-SUMMARY.md
From DocumentPageClient (after Plan 01):
```typescript
// Props passed to PreparePanel (new additions from Plan 01):
signers: DocumentSigner[] // current signer list
onSignersChange: (signers: DocumentSigner[]) => void // update signer list
unassignedFieldIds: Set // field IDs with no signerEmail (for send-block)
onUnassignedFieldIdsChange: (ids: Set) => void // set unassigned IDs
```
From schema.ts:
```typescript
export interface DocumentSigner {
email: string;
color: string;
}
export function isClientVisibleField(field: SignatureFieldData): boolean;
```
Signer Color Palette (D-01, locked):
```typescript
const SIGNER_COLORS = ['#6366f1', '#f43f5e', '#10b981', '#f59e0b'];
// Auto-assigned: signers[0] = indigo, signers[1] = rose, etc. Cycle if >4.
```
Task 1: Add signer list UI section to PreparePanel
- teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx
- teressa-copeland-homes/src/lib/db/schema.ts (DocumentSigner, isClientVisibleField)
- .planning/phases/16-multi-signer-ui/16-CONTEXT.md (D-01, D-02, D-03, D-04)
- .planning/phases/16-multi-signer-ui/16-UI-SPEC.md (Component Inventory sections 1 and 4, Copywriting Contract, Interaction Contract)
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx
**New props on PreparePanelProps (from Plan 01 wiring):**
```typescript
signers: DocumentSigner[];
onSignersChange: (signers: DocumentSigner[]) => void;
unassignedFieldIds: Set;
onUnassignedFieldIdsChange: (ids: Set) => void;
```
Import `DocumentSigner` and `isClientVisibleField` from `@/lib/db/schema`.
**Signer color palette constant** (top of file, per D-01):
```typescript
const SIGNER_COLORS = ['#6366f1', '#f43f5e', '#10b981', '#f59e0b'];
```
**Local state for signer input:**
```typescript
const [signerInput, setSignerInput] = useState('');
const [signerInputError, setSignerInputError] = useState(null);
```
**Add signer handler:**
```typescript
function handleAddSigner() {
const email = signerInput.trim().toLowerCase();
if (!email || !isValidEmail(email)) {
setSignerInputError('invalid');
return;
}
if (signers.some(s => s.email === email)) {
setSignerInputError('duplicate');
return;
}
const color = SIGNER_COLORS[signers.length % SIGNER_COLORS.length];
onSignersChange([...signers, { email, color }]);
setSignerInput('');
setSignerInputError(null);
}
```
**Remove signer handler:**
```typescript
function handleRemoveSigner(email: string) {
onSignersChange(signers.filter(s => s.email !== email));
// Clear validation state since signer count changed
onUnassignedFieldIdsChange(new Set());
}
```
**Signer list UI** — insert between "Text field fill" section `` and the AI Auto-place button. Per UI-SPEC section 1:
Section label: ``
Input row (flex row, gap-2):
- Email input: `className="flex-1 border border-gray-300 rounded-md px-2 py-1.5 text-sm"` with `placeholder="signer@example.com"`. Border changes to `border-red-400` when `signerInputError` is truthy. `onKeyDown` handler: Enter triggers `handleAddSigner()`.
- "Add Signer" button: `className="bg-gray-700 text-white px-3 py-1.5 rounded-md text-sm font-medium hover:bg-gray-800 disabled:opacity-50 disabled:cursor-not-allowed"`. Disabled when `signerInput.trim() === ''`.
Helper text: `
Each signer receives their own signing link.
`
Duplicate error: When `signerInputError === 'duplicate'`, show `
That email is already in the signer list.
`
Signer list (below input): `
` containing signer rows.
Each signer row: `
`
- Colored dot: ``
- Email: `{signer.email}`
- Remove button: `` (use actual multiplication sign character)
Empty state (when signers.length === 0): Show `
No signers added yet.
` instead of signer list.
**Send-block validation in handlePrepare:**
Before the existing `prepareRes` fetch call, add validation:
1. Fetch current fields: `const fieldsRes = await fetch(\`/api/documents/${docId}/fields\`); const allFields = await fieldsRes.json();`
2. Filter client-visible: `const clientFields = allFields.filter(isClientVisibleField);`
3. Check no signers: If `signers.length === 0 && clientFields.length > 0`:
- `setResult({ ok: false, message: 'Add at least one signer before sending.' });`
- `setLoading(false); return;`
4. Check unassigned: `const unassigned = clientFields.filter((f: SignatureFieldData) => !f.signerEmail);`
If `unassigned.length > 0 && signers.length > 0`:
- `onUnassignedFieldIdsChange(new Set(unassigned.map((f: SignatureFieldData) => f.id)));`
- `setResult({ ok: false, message: \`${unassigned.length} field(s) need a signer assigned before sending.\` });`
- `setLoading(false); return;`
5. Clear unassigned on success: `onUnassignedFieldIdsChange(new Set());` before proceeding to prepare call.
**Save signers to DB** — per Claude's Discretion (simpler approach): save on Prepare and Send click.
In handlePrepare, right before the prepare fetch, PATCH the document to save signers:
```typescript
await fetch(`/api/documents/${docId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ signers }),
});
```
Note: This requires the existing document update API to accept a `signers` field. Check if `/api/documents/[id]` PATCH route exists. If not, add the signers to the prepare route body instead — pass `signers` alongside `textFillData` and `emailAddresses` in the prepare POST body. The prepare route already writes to the document record.
Actually, the simpler approach per CONTEXT.md: pass `signers` in the existing prepare POST body. Modify the prepare POST `body: JSON.stringify({ textFillData, emailAddresses, signers })`. The prepare route handler will need to save signers to the document record — but that is a backend concern. For this UI-only phase, just send the data. The send route at `/api/documents/[id]/send` already reads `documents.signers` from DB (Phase 15 wiring). So we need to persist signers before send.
Simplest path: Save signers via the prepare route. Add `signers` to the JSON body of the prepare POST. Then in the prepare route handler, add one line to update `documents.signers`. This is a minimal backend touch that is required for the UI to function.
In the prepare route (`/api/documents/[id]/prepare/route.ts`), after parsing the body, add:
```typescript
if (body.signers) {
await db.update(documents).set({ signers: body.signers }).where(eq(documents.id, docId));
}
```
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30
- `grep -n "Add Signer" teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx` returns the button
- `grep -n 'aria-label.*Remove signer' teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx` confirms accessibility attribute
- `grep -n "SIGNER_COLORS" teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx` shows palette constant with exact hex values `#6366f1`, `#f43f5e`, `#10b981`, `#f59e0b`
- `grep -n "w-2 h-2 rounded-full" teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx` shows 8px colored dot
- `grep -n "gap-2.*bg-white.*border.*rounded-md.*py-1 px-2" teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx` shows signer row with exact Tailwind classes
- `grep -n "w-8 h-8" teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx` shows 32px touch target on remove button
- `grep -n "need a signer assigned before sending" teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx` shows send-block error copy
- `grep -n "Add at least one signer before sending" teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx` shows no-signers error copy
- `grep -n "signer@example.com" teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx` shows placeholder text
- `grep -n "Each signer receives their own signing link" teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx` shows helper text
- `npx tsc --noEmit` passes
- Agent can type email, click "Add Signer", see colored dot + email + remove button per D-02/D-03
- Signer colors auto-assigned in order: indigo, rose, emerald, amber per D-01
- Remove button has `aria-label="Remove signer {email}"` per accessibility requirement
- Send blocked with "{N} field(s) need a signer assigned before sending." when unassigned fields exist per D-09
- Send blocked with "Add at least one signer before sending." when no signers per D-04
- Signers saved to document on Prepare and Send click
- TypeScript compiles cleanly
- `npx tsc --noEmit` passes
- PreparePanel renders signer section between text fill and AI Auto-place button
- All copy strings match UI-SPEC Copywriting Contract exactly
- All Tailwind classes match UI-SPEC spacing/color values
- Agent can add/remove signers with auto-assigned colors
- Send-block validation prevents sending documents with unassigned client-visible fields
- All UI-SPEC copy, spacing, and color values are implemented exactly
- TypeScript compiles with no errors