Files
2026-04-03 16:16:07 -06:00

13 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
16-multi-signer-ui 02 execute 2
16-01
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx
true
MSIGN-01
MSIGN-04
truths artifacts key_links
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
path provides contains
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx Signer list UI section, send-block validation logic Add Signer
from to via pattern
PreparePanel signer add onSignersChange callback setSigners call with new signer appended onSignersChange.*email.*color
from to via pattern
PreparePanel send validation onUnassignedFieldIdsChange callback Set of field IDs with no signerEmail 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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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

From DocumentPageClient (after Plan 01):

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

export interface DocumentSigner {
  email: string;
  color: string;
}
export function isClientVisibleField(field: SignatureFieldData): boolean;

Signer Color Palette (D-01, locked):

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<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));
}
```
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

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