247 lines
13 KiB
Markdown
247 lines
13 KiB
Markdown
|
|
---
|
||
|
|
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>
|