`
+ - Colored dot: `
`
+ - Email: `
{signer.email} `
+ - Remove button: `
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 ` (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`
+
diff --git a/.planning/phases/16-multi-signer-ui/16-03-PLAN.md b/.planning/phases/16-multi-signer-ui/16-03-PLAN.md
new file mode 100644
index 0000000..9823ad8
--- /dev/null
+++ b/.planning/phases/16-multi-signer-ui/16-03-PLAN.md
@@ -0,0 +1,280 @@
+---
+phase: 16-multi-signer-ui
+plan: 03
+type: execute
+wave: 2
+depends_on: ["16-01"]
+files_modified:
+ - teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx
+autonomous: true
+requirements: [MSIGN-02, MSIGN-03]
+
+must_haves:
+ truths:
+ - "Active signer dropdown appears above palette when signers.length > 0"
+ - "Dragged fields auto-get signerEmail of the active signer"
+ - "Field boxes with signerEmail render in signer color instead of type color"
+ - "Fields with no signerEmail render in existing type color (unchanged)"
+ - "Fields in unassignedFieldIds set show red validation outline"
+ - "Active signer defaults to first signer on load"
+ artifacts:
+ - path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx"
+ provides: "Active signer selector, per-signer field coloring, validation overlay"
+ contains: "Active signer:"
+ key_links:
+ - from: "FieldPlacer handleDragEnd"
+ to: "newField.signerEmail"
+ via: "activeSigner.email assignment on field creation"
+ pattern: "signerEmail.*activeSigner"
+ - from: "FieldPlacer renderFields"
+ to: "signer color lookup"
+ via: "signers.find matching field.signerEmail for color"
+ pattern: "signers.*find.*signerEmail.*color"
+---
+
+
+Add the active signer selector dropdown and per-signer field coloring to FieldPlacer per D-05, D-06, D-07, D-08, D-09/D-10.
+
+Purpose: MSIGN-02 (tag fields to signers) and MSIGN-03 (color-coded fields by signer). When a signer is active, every field dropped gets that signer's email. Field boxes render in the signer's color when assigned.
+
+Output: FieldPlacer with active signer dropdown, signer-aware field creation, per-signer color rendering, and red validation overlay for unassigned fields.
+
+
+
+@$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 FieldPlacer (after Plan 01):
+```typescript
+interface FieldPlacerProps {
+ // ... existing props ...
+ signers?: DocumentSigner[]; // from DocumentPageClient via PdfViewerWrapper
+ unassignedFieldIds?: Set; // from DocumentPageClient for send-block validation
+}
+```
+
+From schema.ts:
+```typescript
+export interface DocumentSigner {
+ email: string;
+ color: string;
+}
+export interface SignatureFieldData {
+ id: string; page: number; x: number; y: number;
+ width: number; height: number;
+ type?: SignatureFieldType;
+ signerEmail?: string;
+}
+```
+
+FieldPlacer existing color system (lines 70-78):
+```typescript
+const PALETTE_TOKENS: Array<{ id: SignatureFieldType; label: string; color: string }> = [
+ { id: 'client-signature', label: 'Signature', color: '#2563eb' },
+ { id: 'initials', label: 'Initials', color: '#7c3aed' },
+ { id: 'checkbox', label: 'Checkbox', color: '#059669' },
+ { id: 'date', label: 'Date', color: '#d97706' },
+ { id: 'text', label: 'Text', color: '#64748b' },
+ { id: 'agent-signature', label: 'Agent Signature', color: '#dc2626' },
+ { id: 'agent-initials', label: 'Agent Initials', color: '#ea580c' },
+];
+```
+
+Color Override Rule (D-06, D-07):
+- field.signerEmail set AND matching signer in signers[] => use signer.color
+- field.signerEmail absent => use PALETTE_TOKENS type color (unchanged)
+
+Validation overlay (D-09/D-10, UI-SPEC section 3):
+- unassignedFieldIds contains field.id => border: 2px solid #ef4444, bg: #ef444414
+
+
+
+
+
+
+ Task 1: Add active signer selector and signer-aware field coloring
+
+ - teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx (FULL FILE — 822 lines)
+ - teressa-copeland-homes/src/lib/db/schema.ts (DocumentSigner, SignatureFieldData)
+ - .planning/phases/16-multi-signer-ui/16-CONTEXT.md (D-05, D-06, D-07, D-08, D-09, D-10)
+ - .planning/phases/16-multi-signer-ui/16-UI-SPEC.md (Component Inventory sections 2 and 3)
+
+ teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx
+
+ **1. Active signer state (inside FieldPlacer function body):**
+ ```typescript
+ const [activeSignerEmail, setActiveSignerEmail] = useState(null);
+ ```
+ Default to first signer on load and when signers change (per D-08):
+ ```typescript
+ useEffect(() => {
+ if (signers && signers.length > 0) {
+ setActiveSignerEmail(prev => {
+ // Keep current selection if still valid
+ if (prev && signers.some(s => s.email === prev)) return prev;
+ return signers[0].email;
+ });
+ } else {
+ setActiveSignerEmail(null);
+ }
+ }, [signers]);
+ ```
+
+ **2. Active signer selector UI** — insert ABOVE the palette div (line ~756), ONLY when `signers && signers.length > 0`:
+ ```tsx
+ {!readOnly && signers && signers.length > 0 && (
+
+
+ Active signer:
+
+ setActiveSignerEmail(e.target.value)}
+ style={{
+ flex: 1,
+ height: '32px',
+ border: '1px solid #D1D5DB',
+ borderRadius: '0.375rem',
+ fontSize: '0.875rem',
+ padding: '0 8px',
+ background: 'white',
+ }}
+ >
+ {signers.map(s => (
+
+ {s.email}
+
+ ))}
+
+ {/* Color indicator dot next to the dropdown */}
+ {(() => {
+ const activeSigner = signers.find(s => s.email === activeSignerEmail);
+ return activeSigner ? (
+
+ ) : null;
+ })()}
+
+ )}
+ ```
+
+ **3. handleDragEnd modification** — at line ~294 where `newField` is created, add `signerEmail`:
+ Change the `newField` construction to include:
+ ```typescript
+ const newField: SignatureFieldData = {
+ id: crypto.randomUUID(),
+ page: currentPage,
+ x: pdfX,
+ y: pdfY,
+ width: fieldW,
+ height: fieldH,
+ type: droppedType,
+ ...(activeSignerEmail ? { signerEmail: activeSignerEmail } : {}),
+ };
+ ```
+ Add `activeSignerEmail` to the useCallback dependency array of handleDragEnd.
+
+ **4. renderFields color override** — in the renderFields function (around line 581-584), replace the existing color lookup:
+ ```typescript
+ // Per-type color and label
+ const fieldType = getFieldType(field);
+ const tokenMeta = PALETTE_TOKENS.find((t) => t.id === fieldType);
+
+ // Color override: signer color when signerEmail is set (D-06), else type color (D-07)
+ let fieldColor = tokenMeta?.color ?? '#2563eb';
+ if (field.signerEmail && signers) {
+ const matchedSigner = signers.find(s => s.email === field.signerEmail);
+ if (matchedSigner) {
+ fieldColor = matchedSigner.color;
+ }
+ }
+
+ // Validation overlay: unassigned field highlight (D-09/D-10)
+ const isUnassigned = unassignedFieldIds?.has(field.id) ?? false;
+ const fieldLabel = tokenMeta?.label ?? 'Signature';
+ ```
+
+ **5. Field box style override for validation** — in the field box's `style` object (around line 627-653):
+ - Change `border` line to: `border: isUnassigned ? '2px solid #ef4444' : \`2px solid ${fieldColor}\``,
+ - Change `background` line to: `background: isUnassigned ? '#ef444414' : (readOnly ? \`${fieldColor}0d\` : \`${fieldColor}1a\`)`,
+
+ **6. DragOverlay ghost color** — in the DragOverlay section (around line 796-798), the ghost should also use signer color if active signer is set. Update:
+ ```typescript
+ {isDraggingToken ? (() => {
+ const tokenMeta = PALETTE_TOKENS.find((t) => t.id === isDraggingToken);
+ const label = tokenMeta?.label ?? 'Field';
+ // Ghost uses active signer color if one is selected, else type color
+ let ghostColor = tokenMeta?.color ?? '#2563eb';
+ if (activeSignerEmail && signers) {
+ const activeSigner = signers.find(s => s.email === activeSignerEmail);
+ if (activeSigner) ghostColor = activeSigner.color;
+ }
+ const isCheckbox = isDraggingToken === 'checkbox';
+ // ... rest uses ghostColor instead of color
+ ```
+
+
+ cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30
+
+
+ - `grep -n "Active signer:" teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx` shows dropdown label
+ - `grep -n "activeSignerEmail" teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx` shows state variable
+ - `grep -n "signerEmail.*activeSignerEmail" teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx` shows field creation sets signerEmail
+ - `grep -n "signers.*find.*signerEmail" teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx` shows signer color lookup in renderFields
+ - `grep -n "#ef4444" teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx` shows validation overlay border color
+ - `grep -n "#ef444414" teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx` shows validation overlay background
+ - `grep -n "height.*32px" teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx` shows dropdown is 32px tall
+ - `npx tsc --noEmit` passes
+
+
+ - Active signer dropdown appears above palette when signers exist, hidden otherwise per D-05
+ - Dropdown defaults to first signer per D-08
+ - Dragged fields get signerEmail = active signer's email per D-06
+ - Fields with signerEmail render in signer color; fields without use type color per D-06/D-07
+ - Fields in unassignedFieldIds set show red #ef4444 outline and #ef444414 background per D-09/D-10
+ - TypeScript compiles cleanly
+
+
+
+
+
+
+- `npx tsc --noEmit` passes
+- Active signer selector renders above palette with correct styling
+- Field color follows signer color when assigned, type color when not
+- Validation overlay visible on unassigned fields
+
+
+
+- Agent can select active signer from dropdown before placing fields
+- Fields placed with active signer are tagged and colored accordingly
+- Unassigned fields show red validation highlight when send-block triggers
+- Existing behavior unchanged when no signers configured
+- TypeScript compiles with no errors
+
+
+
+After completion, create `.planning/phases/16-multi-signer-ui/16-03-SUMMARY.md`
+
diff --git a/.planning/phases/16-multi-signer-ui/16-04-PLAN.md b/.planning/phases/16-multi-signer-ui/16-04-PLAN.md
new file mode 100644
index 0000000..f6b0ab5
--- /dev/null
+++ b/.planning/phases/16-multi-signer-ui/16-04-PLAN.md
@@ -0,0 +1,256 @@
+---
+phase: 16-multi-signer-ui
+plan: 04
+type: execute
+wave: 3
+depends_on: ["16-02", "16-03"]
+files_modified:
+ - teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx
+ - teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx
+autonomous: true
+requirements: [MSIGN-09]
+
+must_haves:
+ truths:
+ - "Multi-signer Sent documents show N/M signed badge in Status column"
+ - "Single-signer documents show no N/M badge (unchanged)"
+ - "Fully signed documents show existing Signed badge only (no N/M)"
+ - "N/M is computed from signingTokens with usedAt IS NOT NULL"
+ artifacts:
+ - path: "teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx"
+ provides: "Server-side query joining signingTokens for per-signer completion count"
+ contains: "signingTokens"
+ - path: "teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx"
+ provides: "N/M signed badge rendering in Status column"
+ contains: "signed"
+ key_links:
+ - from: "dashboard/page.tsx query"
+ to: "DocumentsTable rows"
+ via: "signedCount and totalSigners fields on row data"
+ pattern: "signedCount|totalSigners"
+ - from: "DocumentsTable"
+ to: "N/M badge"
+ via: "conditional render when totalSigners > 0 and status === Sent"
+ pattern: "totalSigners.*>.*0"
+---
+
+
+Add the N/M signed badge to the dashboard documents table for multi-signer documents per D-11, D-12, D-13.
+
+Purpose: MSIGN-09 (dashboard shows per-signer completion status). Agent sees at a glance how many signers have completed for documents that are partially signed.
+
+Output: Dashboard query includes signing token counts; DocumentsTable renders "N/M signed" badge next to Status for multi-signer Sent documents.
+
+
+
+@$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
+
+
+From schema.ts:
+```typescript
+export const documents = pgTable("documents", {
+ id: text("id").primaryKey(),
+ // ...
+ signers: jsonb("signers").$type(),
+ status: text("status").$type<"Draft" | "Sent" | "Viewed" | "Signed">().default("Draft").notNull(),
+ // ...
+});
+
+export const signingTokens = pgTable('signing_tokens', {
+ id: text('id').primaryKey(),
+ documentId: text('document_id').notNull().references(() => documents.id, { onDelete: 'cascade' }),
+ signerEmail: text('signer_email'),
+ createdAt: timestamp('created_at').defaultNow().notNull(),
+ expiresAt: timestamp('expires_at').notNull(),
+ usedAt: timestamp('used_at'),
+});
+```
+
+DocumentsTable current row type:
+```typescript
+type DocumentRow = {
+ id: string;
+ name: string;
+ clientName: string | null;
+ status: "Draft" | "Sent" | "Viewed" | "Signed";
+ sentAt: Date | null;
+ signedAt: Date | null;
+ clientId: string;
+};
+```
+
+StatusBadge:
+```typescript
+export function StatusBadge({ status }: { status: "Draft" | "Sent" | "Viewed" | "Signed" });
+```
+
+UI-SPEC badge spec:
+- Style: `inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-700 ml-1.5`
+- Text: "N/M signed"
+- Only for multi-signer (signers non-empty) AND status "Sent"
+- Not for status "Signed" (existing badge sufficient)
+
+
+
+
+
+
+ Task 1: Add signing token counts to dashboard query
+
+ - teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx
+ - teressa-copeland-homes/src/lib/db/schema.ts (signingTokens table, documents.signers)
+ - .planning/phases/16-multi-signer-ui/16-CONTEXT.md (D-11, D-12, D-13)
+ - .planning/phases/16-multi-signer-ui/16-UI-SPEC.md (Component Inventory section 5)
+
+ teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx
+
+ Import `signingTokens` from `@/lib/db/schema` and `sql, isNotNull, count` from `drizzle-orm`.
+
+ The dashboard currently uses a simple select+leftJoin query. To add signer counts, use a subquery approach to avoid N+1:
+
+ After the main `allRows` query, compute signing progress for multi-signer documents. Use a separate query for signing token counts to keep it simple:
+
+ ```typescript
+ import { signingTokens } from "@/lib/db/schema";
+ import { sql } from "drizzle-orm";
+
+ // After allRows query, fetch signing token progress for all documents in one query
+ const tokenCounts = await db
+ .select({
+ documentId: signingTokens.documentId,
+ total: sql`count(*)`.as('total'),
+ signed: sql`count(${signingTokens.usedAt})`.as('signed'),
+ })
+ .from(signingTokens)
+ .groupBy(signingTokens.documentId);
+
+ const tokenMap = new Map(tokenCounts.map(t => [t.documentId, { total: Number(t.total), signed: Number(t.signed) }]));
+ ```
+
+ Then augment the rows passed to DocumentsTable:
+ ```typescript
+ const enrichedRows = filteredRows.map(row => {
+ const tc = tokenMap.get(row.id);
+ return {
+ ...row,
+ signedCount: tc?.signed ?? null,
+ totalSigners: tc?.total ?? null,
+ };
+ });
+ ```
+
+ Pass `enrichedRows` to `` instead of `filteredRows`.
+
+ Also need to add `signers` to the allRows select to know which documents are multi-signer:
+ ```typescript
+ signers: documents.signers,
+ ```
+ And include it in enrichedRows:
+ ```typescript
+ hasMultipleSigners: Array.isArray(row.signers) && row.signers.length > 0,
+ ```
+
+
+ cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30
+
+
+ - `grep -n "signingTokens" teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx` shows import and query
+ - `grep -n "signedCount" teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx` shows field enrichment
+ - `grep -n "totalSigners" teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx` shows field enrichment
+ - `grep -n "hasMultipleSigners" teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx` shows multi-signer detection
+ - `npx tsc --noEmit` passes
+
+ Dashboard query fetches signing token counts per document and passes enriched rows to DocumentsTable
+
+
+
+ Task 2: Render N/M signed badge in DocumentsTable Status column
+
+ - teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx
+ - teressa-copeland-homes/src/app/portal/_components/StatusBadge.tsx
+ - .planning/phases/16-multi-signer-ui/16-CONTEXT.md (D-11, D-12, D-13)
+ - .planning/phases/16-multi-signer-ui/16-UI-SPEC.md (Component Inventory section 5, badge spec)
+
+ teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx
+
+ Update the `DocumentRow` type to include optional new fields:
+ ```typescript
+ type DocumentRow = {
+ id: string;
+ name: string;
+ clientName: string | null;
+ status: "Draft" | "Sent" | "Viewed" | "Signed";
+ sentAt: Date | null;
+ signedAt: Date | null;
+ clientId: string;
+ signedCount?: number | null;
+ totalSigners?: number | null;
+ hasMultipleSigners?: boolean;
+ };
+ ```
+
+ In the Status `` cell (currently just ` `), add the N/M badge after the StatusBadge:
+
+ ```tsx
+
+
+ {row.hasMultipleSigners && row.status === 'Sent' && row.totalSigners != null && row.totalSigners > 0 && (
+
+ {row.signedCount ?? 0}/{row.totalSigners} signed
+
+ )}
+
+ ```
+
+ Per D-12: Only show for multi-signer documents (`hasMultipleSigners`).
+ Per D-13: Only show for status "Sent" — not for "Signed" (existing Signed badge sufficient).
+ The UI-SPEC specifies `bg-blue-50 text-blue-700` for the badge (matching the blue theme of Sent status).
+
+
+ cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30
+
+
+ - `grep -n "signedCount" teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx` shows field in type and render
+ - `grep -n "totalSigners" teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx` shows field in type and render
+ - `grep -n "hasMultipleSigners" teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx` shows conditional
+ - `grep -n "bg-blue-50 text-blue-700" teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx` shows badge styles
+ - `grep -n "ml-1.5" teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx` shows margin from StatusBadge
+ - `grep -n "signed" teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx` shows "signed" text in badge
+ - `npx tsc --noEmit` passes
+
+
+ - Multi-signer Sent documents show "N/M signed" badge next to Sent status badge per D-11
+ - Single-signer documents show no N/M badge per D-12
+ - Signed documents show only existing "Signed" badge per D-13
+ - Badge uses exact UI-SPEC classes: `inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-700 ml-1.5`
+ - TypeScript compiles cleanly
+
+
+
+
+
+
+- `npx tsc --noEmit` passes
+- Dashboard query includes token count subquery
+- N/M badge renders only for multi-signer Sent documents
+- Badge styling matches UI-SPEC exactly
+
+
+
+- Dashboard shows "1/2 signed" badge for partially-signed multi-signer documents
+- No badge for single-signer or fully-signed documents
+- Badge computed server-side from signingTokens count
+- TypeScript compiles with no errors
+
+
+
+After completion, create `.planning/phases/16-multi-signer-ui/16-04-SUMMARY.md`
+