---
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"
---

<objective>
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.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.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

<interfaces>
<!-- Contracts from Plan 01 — signers state threaded from DocumentPageClient -->

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<string>     // field IDs with no signerEmail (for send-block)
onUnassignedFieldIdsChange: (ids: Set<string>) => 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.
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add signer list UI section to PreparePanel</name>
  <read_first>
    - 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)
  </read_first>
  <files>teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx</files>
  <action>
    **New props on PreparePanelProps (from Plan 01 wiring):**
    ```typescript
    signers: DocumentSigner[];
    onSignersChange: (signers: DocumentSigner[]) => void;
    unassignedFieldIds: Set<string>;
    onUnassignedFieldIdsChange: (ids: Set<string>) => 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<string | null>(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 `</div>` and the AI Auto-place button. Per UI-SPEC section 1:

    Section label: `<label className="block text-sm font-medium text-gray-700 mb-1">Signers</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: `<p className="text-xs text-gray-400 mt-1">Each signer receives their own signing link.</p>`

    Duplicate error: When `signerInputError === 'duplicate'`, show `<p className="text-sm text-red-600 mt-1">That email is already in the signer list.</p>`

    Signer list (below input): `<div className="space-y-1.5 mt-2">` containing signer rows.

    Each signer row: `<div className="flex items-center gap-2 bg-white border border-gray-200 rounded-md py-1 px-2">`
    - Colored dot: `<span className="w-2 h-2 rounded-full flex-shrink-0" style={{ backgroundColor: signer.color }} />`
    - Email: `<span className="flex-1 text-sm text-gray-700 truncate">{signer.email}</span>`
    - Remove button: `<button type="button" onClick={() => handleRemoveSigner(signer.email)} className="w-8 h-8 flex items-center justify-center text-gray-400 hover:text-red-600 cursor-pointer" style={{ background: 'none', border: 'none', fontSize: '16px', lineHeight: 1 }} aria-label={`Remove signer ${signer.email}`}>x</button>` (use actual multiplication sign character)

    Empty state (when signers.length === 0): Show `<p className="text-sm text-gray-400 italic">No signers added yet.</p>` 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));
    }
    ```
  </action>
  <verify>
    <automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30</automated>
  </verify>
  <acceptance_criteria>
    - `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
  </acceptance_criteria>
  <done>
    - 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
  </done>
</task>

</tasks>

<verification>
- `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
</verification>

<success_criteria>
- 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
</success_criteria>

<output>
After completion, create `.planning/phases/16-multi-signer-ui/16-02-SUMMARY.md`
</output>
