--- 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 After completion, create `.planning/phases/16-multi-signer-ui/16-02-SUMMARY.md`