281 lines
12 KiB
Markdown
281 lines
12 KiB
Markdown
---
|
|
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"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</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>
|
|
<!-- From Plan 01: signers and unassignedFieldIds are now props on FieldPlacer -->
|
|
|
|
From FieldPlacer (after Plan 01):
|
|
```typescript
|
|
interface FieldPlacerProps {
|
|
// ... existing props ...
|
|
signers?: DocumentSigner[]; // from DocumentPageClient via PdfViewerWrapper
|
|
unassignedFieldIds?: Set<string>; // 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
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Add active signer selector and signer-aware field coloring</name>
|
|
<read_first>
|
|
- 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)
|
|
</read_first>
|
|
<files>teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx</files>
|
|
<action>
|
|
**1. Active signer state (inside FieldPlacer function body):**
|
|
```typescript
|
|
const [activeSignerEmail, setActiveSignerEmail] = useState<string | null>(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 && (
|
|
<div style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '8px',
|
|
marginBottom: '8px',
|
|
padding: '8px 12px',
|
|
background: '#f8fafc',
|
|
border: '1px solid #e2e8f0',
|
|
borderRadius: '6px',
|
|
}}>
|
|
<span style={{ fontSize: '12px', color: '#6b7280', fontWeight: 500, whiteSpace: 'nowrap' }}>
|
|
Active signer:
|
|
</span>
|
|
<select
|
|
value={activeSignerEmail ?? ''}
|
|
onChange={(e) => 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 => (
|
|
<option key={s.email} value={s.email}>
|
|
{s.email}
|
|
</option>
|
|
))}
|
|
</select>
|
|
{/* Color indicator dot next to the dropdown */}
|
|
{(() => {
|
|
const activeSigner = signers.find(s => s.email === activeSignerEmail);
|
|
return activeSigner ? (
|
|
<span style={{
|
|
width: '8px', height: '8px', borderRadius: '50%',
|
|
backgroundColor: activeSigner.color, flexShrink: 0,
|
|
}} />
|
|
) : null;
|
|
})()}
|
|
</div>
|
|
)}
|
|
```
|
|
|
|
**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
|
|
```
|
|
</action>
|
|
<verify>
|
|
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `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
|
|
</acceptance_criteria>
|
|
<done>
|
|
- 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
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `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
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- 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
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/16-multi-signer-ui/16-03-SUMMARY.md`
|
|
</output>
|