docs(16): create phase plan — 4 plans in 3 waves
This commit is contained in:
@@ -350,14 +350,19 @@ Plans:
|
|||||||
2. Each field placed in FieldPlacer has a signer assignment dropdown; selecting a signer changes the field's color to that signer's assigned color
|
2. Each field placed in FieldPlacer has a signer assignment dropdown; selecting a signer changes the field's color to that signer's assigned color
|
||||||
3. If the agent clicks Send with any client-facing field (signature, initials, date, text) having no signer assigned, the send is blocked and a clear error message identifies the unassigned fields
|
3. If the agent clicks Send with any client-facing field (signature, initials, date, text) having no signer assigned, the send is blocked and a clear error message identifies the unassigned fields
|
||||||
4. The dashboard document detail view shows a per-signer completion row for each signer (who has signed, who hasn't, with timestamps)
|
4. The dashboard document detail view shows a per-signer completion row for each signer (who has signed, who hasn't, with timestamps)
|
||||||
**Plans**: 3 plans
|
**Plans**: 4 plans
|
||||||
|
|
||||||
Plans:
|
Plans:
|
||||||
- [x] 15-01-PLAN.md — Token utility extensions (signerEmail param, signer-download JWT), sendSignerCompletionEmail mailer, public signer download route
|
- [ ] 16-01-PLAN.md — DocumentPageClient signers state threading, server page initialSigners prop, PdfViewerWrapper prop pass-through
|
||||||
- [x] 15-02-PLAN.md — Send route rewrite: multi-signer token loop with Promise.all dispatch, legacy single-signer fallback, APP_BASE_URL rename
|
- [ ] 16-02-PLAN.md — PreparePanel signer list UI (add/remove by email, colored dots, auto-assigned colors) + send-block validation
|
||||||
- [ ] 15-03-PLAN.md — Sign handler rewrite: GET signer-filtered fields, POST signer-scoped operations + accumulate PDF + atomic completion + notifications
|
- [ ] 16-03-PLAN.md — FieldPlacer active signer selector dropdown + per-signer field coloring + validation overlay
|
||||||
|
- [ ] 16-04-PLAN.md — Dashboard N/M signed badge (signingTokens count query, DocumentsTable badge rendering)
|
||||||
**UI hint**: yes
|
**UI hint**: yes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Phase 17: Docker Deployment
|
### Phase 17: Docker Deployment
|
||||||
**Goal**: The application runs reliably in a Docker container on the production server — secrets are injected at runtime, email delivers correctly, uploaded files survive container restarts, and a health check confirms database connectivity
|
**Goal**: The application runs reliably in a Docker container on the production server — secrets are injected at runtime, email delivers correctly, uploaded files survive container restarts, and a health check confirms database connectivity
|
||||||
**Depends on**: Phase 16
|
**Depends on**: Phase 16
|
||||||
@@ -400,5 +405,5 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 →
|
|||||||
| 13. AI Field Placement and Pre-fill | v1.1 | 3/4 | In Progress | - |
|
| 13. AI Field Placement and Pre-fill | v1.1 | 3/4 | In Progress | - |
|
||||||
| 14. Multi-Signer Schema | v1.2 | 1/1 | Complete | 2026-04-03 |
|
| 14. Multi-Signer Schema | v1.2 | 1/1 | Complete | 2026-04-03 |
|
||||||
| 15. Multi-Signer Backend | v1.2 | 3/3 | Complete | 2026-04-03 |
|
| 15. Multi-Signer Backend | v1.2 | 3/3 | Complete | 2026-04-03 |
|
||||||
| 16. Multi-Signer UI | v1.2 | 0/TBD | Not started | - |
|
| 16. Multi-Signer UI | v1.2 | 0/4 | Not started | - |
|
||||||
| 17. Docker Deployment | v1.2 | 0/TBD | Not started | - |
|
| 17. Docker Deployment | v1.2 | 0/TBD | Not started | - |
|
||||||
|
|||||||
266
.planning/phases/16-multi-signer-ui/16-01-PLAN.md
Normal file
266
.planning/phases/16-multi-signer-ui/16-01-PLAN.md
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
---
|
||||||
|
phase: 16-multi-signer-ui
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx
|
||||||
|
- teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx
|
||||||
|
- teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewerWrapper.tsx
|
||||||
|
autonomous: true
|
||||||
|
requirements: [MSIGN-01, MSIGN-02, MSIGN-03]
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "DocumentPageClient holds signers state and threads it to both PreparePanel and PdfViewerWrapper/FieldPlacer"
|
||||||
|
- "DocumentPageClient holds unassignedFieldIds state for send-block validation highlighting"
|
||||||
|
- "Server page reads documents.signers from DB and passes to DocumentPageClient as initialSigners prop"
|
||||||
|
- "PdfViewerWrapper passes signers and unassignedFieldIds through to FieldPlacer"
|
||||||
|
artifacts:
|
||||||
|
- path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx"
|
||||||
|
provides: "signers state, setSigners, unassignedFieldIds state, setUnassignedFieldIds — threaded to PreparePanel and PdfViewerWrapper"
|
||||||
|
exports: ["DocumentPageClient"]
|
||||||
|
- path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx"
|
||||||
|
provides: "Server-side documents.signers fetch, passed as initialSigners to DocumentPageClient"
|
||||||
|
- path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewerWrapper.tsx"
|
||||||
|
provides: "Passes signers and unassignedFieldIds props through to PdfViewer/FieldPlacer"
|
||||||
|
key_links:
|
||||||
|
- from: "page.tsx"
|
||||||
|
to: "DocumentPageClient"
|
||||||
|
via: "initialSigners prop from db.query.documents"
|
||||||
|
pattern: "initialSigners=.*doc\\.signers"
|
||||||
|
- from: "DocumentPageClient"
|
||||||
|
to: "PdfViewerWrapper"
|
||||||
|
via: "signers and unassignedFieldIds props"
|
||||||
|
pattern: "signers=.*unassignedFieldIds="
|
||||||
|
- from: "DocumentPageClient"
|
||||||
|
to: "PreparePanel"
|
||||||
|
via: "signers, onSignersChange, unassignedFieldIds, onUnassignedFieldIdsChange props"
|
||||||
|
pattern: "signers=.*onSignersChange="
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Thread multi-signer state through the component tree so PreparePanel and FieldPlacer can consume signers data in Wave 2.
|
||||||
|
|
||||||
|
Purpose: Foundation wiring — DocumentPageClient is the state owner for signers (from Phase 14 schema) and unassignedFieldIds (for send-block validation). Without this, Wave 2 plans cannot receive or modify signer state.
|
||||||
|
|
||||||
|
Output: Updated DocumentPageClient with signers + unassignedFieldIds state; server page fetching documents.signers; PdfViewerWrapper passing new props through.
|
||||||
|
</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/STATE.md
|
||||||
|
@.planning/phases/16-multi-signer-ui/16-CONTEXT.md
|
||||||
|
@.planning/phases/16-multi-signer-ui/16-UI-SPEC.md
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
|
||||||
|
|
||||||
|
From src/lib/db/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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isClientVisibleField(field: SignatureFieldData): boolean;
|
||||||
|
export function getFieldType(field: SignatureFieldData): SignatureFieldType;
|
||||||
|
export function getSignerEmail(field: SignatureFieldData, fallbackEmail: string): string;
|
||||||
|
```
|
||||||
|
|
||||||
|
From DocumentPageClient.tsx (current props):
|
||||||
|
```typescript
|
||||||
|
interface DocumentPageClientProps {
|
||||||
|
docId: string;
|
||||||
|
docStatus: string;
|
||||||
|
defaultEmail: string;
|
||||||
|
clientName: string;
|
||||||
|
agentDownloadUrl?: string | null;
|
||||||
|
signedAt?: Date | null;
|
||||||
|
clientPropertyAddress?: string | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
From PdfViewerWrapper.tsx (current props):
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
docId: string;
|
||||||
|
docStatus?: string;
|
||||||
|
onFieldsChanged?: () => void;
|
||||||
|
selectedFieldId?: string | null;
|
||||||
|
textFillData?: Record<string, string>;
|
||||||
|
onFieldSelect?: (fieldId: string | null) => void;
|
||||||
|
onFieldValueChange?: (fieldId: string, value: string) => void;
|
||||||
|
aiPlacementKey?: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
From FieldPlacer.tsx (current props — FieldPlacerProps interface at line 155):
|
||||||
|
```typescript
|
||||||
|
interface FieldPlacerProps {
|
||||||
|
docId: string;
|
||||||
|
pageInfo: PageInfo | null;
|
||||||
|
currentPage: number;
|
||||||
|
children: React.ReactNode;
|
||||||
|
readOnly?: boolean;
|
||||||
|
onFieldsChanged?: () => void;
|
||||||
|
selectedFieldId?: string | null;
|
||||||
|
textFillData?: Record<string, string>;
|
||||||
|
onFieldSelect?: (fieldId: string | null) => void;
|
||||||
|
onFieldValueChange?: (fieldId: string, value: string) => void;
|
||||||
|
aiPlacementKey?: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
From PreparePanel.tsx (current props):
|
||||||
|
```typescript
|
||||||
|
interface PreparePanelProps {
|
||||||
|
docId: string;
|
||||||
|
defaultEmail: string;
|
||||||
|
clientName: string;
|
||||||
|
currentStatus: string;
|
||||||
|
agentDownloadUrl?: string | null;
|
||||||
|
signedAt?: Date | null;
|
||||||
|
clientPropertyAddress?: string | null;
|
||||||
|
previewToken: string | null;
|
||||||
|
onPreviewTokenChange: (token: string | null) => void;
|
||||||
|
textFillData: Record<string, string>;
|
||||||
|
selectedFieldId: string | null;
|
||||||
|
onQuickFill: (fieldId: string, value: string) => void;
|
||||||
|
onAiAutoPlace: () => Promise<void>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
From server page.tsx:
|
||||||
|
```typescript
|
||||||
|
// doc fetched via db.query.documents.findFirst — has all document columns including signers
|
||||||
|
const doc = await db.query.documents.findFirst({ where: eq(documents.id, docId) });
|
||||||
|
// doc.signers is DocumentSigner[] | null (JSONB column from Phase 14)
|
||||||
|
```
|
||||||
|
</interfaces>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Add signers prop to server page and thread state through DocumentPageClient</name>
|
||||||
|
<read_first>
|
||||||
|
- teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx
|
||||||
|
- teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx
|
||||||
|
- teressa-copeland-homes/src/lib/db/schema.ts (for DocumentSigner interface)
|
||||||
|
- .planning/phases/16-multi-signer-ui/16-CONTEXT.md
|
||||||
|
- .planning/phases/16-multi-signer-ui/16-UI-SPEC.md
|
||||||
|
</read_first>
|
||||||
|
<files>
|
||||||
|
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx,
|
||||||
|
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
**server page.tsx:**
|
||||||
|
- `doc` is already fetched via `db.query.documents.findFirst` which returns all columns including `signers`. Pass `doc.signers` to DocumentPageClient as `initialSigners`:
|
||||||
|
```
|
||||||
|
initialSigners={doc.signers ?? []}
|
||||||
|
```
|
||||||
|
|
||||||
|
**DocumentPageClient.tsx:**
|
||||||
|
- Import `DocumentSigner` from `@/lib/db/schema`
|
||||||
|
- Add `initialSigners: DocumentSigner[]` to `DocumentPageClientProps`
|
||||||
|
- Add state: `const [signers, setSigners] = useState<DocumentSigner[]>(initialSigners);`
|
||||||
|
- Add state: `const [unassignedFieldIds, setUnassignedFieldIds] = useState<Set<string>>(new Set());`
|
||||||
|
- Pass to PreparePanel: `signers={signers}`, `onSignersChange={setSigners}`, `unassignedFieldIds={unassignedFieldIds}`, `onUnassignedFieldIdsChange={setUnassignedFieldIds}`
|
||||||
|
- Pass to PdfViewerWrapper: `signers={signers}`, `unassignedFieldIds={unassignedFieldIds}`
|
||||||
|
- No other changes to existing props or callbacks
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- `grep -n "initialSigners" teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx` shows prop being passed
|
||||||
|
- `grep -n "DocumentSigner" teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx` shows import
|
||||||
|
- `grep -n "signers={signers}" teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx` shows props threaded to both PreparePanel and PdfViewerWrapper
|
||||||
|
- `grep -n "unassignedFieldIds" teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx` shows state and prop threading
|
||||||
|
- `npx tsc --noEmit` passes
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>DocumentPageClient has signers + unassignedFieldIds state initialized from server; props threaded to PreparePanel and PdfViewerWrapper; TypeScript compiles cleanly</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Thread signers and unassignedFieldIds through PdfViewerWrapper to FieldPlacer</name>
|
||||||
|
<read_first>
|
||||||
|
- teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewerWrapper.tsx
|
||||||
|
- teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx (to understand its prop interface — it wraps FieldPlacer)
|
||||||
|
- teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx (lines 155-167 for FieldPlacerProps)
|
||||||
|
- teressa-copeland-homes/src/lib/db/schema.ts (for DocumentSigner type)
|
||||||
|
- .planning/phases/16-multi-signer-ui/16-UI-SPEC.md
|
||||||
|
</read_first>
|
||||||
|
<files>
|
||||||
|
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewerWrapper.tsx,
|
||||||
|
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx,
|
||||||
|
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
**PdfViewerWrapper.tsx:**
|
||||||
|
- Import `DocumentSigner` from `@/lib/db/schema`
|
||||||
|
- Add optional props: `signers?: DocumentSigner[]`, `unassignedFieldIds?: Set<string>`
|
||||||
|
- Pass both through to `<PdfViewer signers={signers} unassignedFieldIds={unassignedFieldIds} ... />`
|
||||||
|
|
||||||
|
**PdfViewer.tsx:**
|
||||||
|
- Add same optional props to PdfViewer's prop interface: `signers?: DocumentSigner[]`, `unassignedFieldIds?: Set<string>`
|
||||||
|
- Pass both through to `<FieldPlacer signers={signers} unassignedFieldIds={unassignedFieldIds} ... />`
|
||||||
|
|
||||||
|
**FieldPlacer.tsx:**
|
||||||
|
- Import `DocumentSigner` from `@/lib/db/schema`
|
||||||
|
- Add to FieldPlacerProps: `signers?: DocumentSigner[]`, `unassignedFieldIds?: Set<string>`
|
||||||
|
- Destructure in component function signature (default `signers = []`, `unassignedFieldIds = new Set()`)
|
||||||
|
- Do NOT implement rendering changes yet (that is Plan 03) — just accept the props
|
||||||
|
|
||||||
|
All three files: add the two new optional props and pass them through. No rendering or behavioral changes.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- `grep -n "signers" teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewerWrapper.tsx` shows prop accepted and passed through
|
||||||
|
- `grep -n "signers" teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx` shows signers in FieldPlacerProps
|
||||||
|
- `grep -n "unassignedFieldIds" teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx` shows unassignedFieldIds in FieldPlacerProps
|
||||||
|
- `npx tsc --noEmit` passes
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>signers and unassignedFieldIds props flow from DocumentPageClient through PdfViewerWrapper and PdfViewer into FieldPlacer; TypeScript compiles cleanly</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- `npx tsc --noEmit` passes with no errors
|
||||||
|
- `grep -rn "signers" teressa-copeland-homes/src/app/portal/(protected)/documents/` confirms signers threaded through page.tsx -> DocumentPageClient -> PdfViewerWrapper -> PdfViewer -> FieldPlacer AND DocumentPageClient -> PreparePanel
|
||||||
|
- `grep -rn "unassignedFieldIds" teressa-copeland-homes/src/app/portal/(protected)/documents/` confirms unassignedFieldIds threaded through the same paths
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- DocumentPageClient has `signers` and `unassignedFieldIds` state
|
||||||
|
- Server page passes `initialSigners` from `doc.signers`
|
||||||
|
- Both PreparePanel and FieldPlacer (via PdfViewerWrapper chain) receive signers and unassignedFieldIds as props
|
||||||
|
- TypeScript compiles with no errors
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/16-multi-signer-ui/16-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
246
.planning/phases/16-multi-signer-ui/16-02-PLAN.md
Normal file
246
.planning/phases/16-multi-signer-ui/16-02-PLAN.md
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
---
|
||||||
|
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>
|
||||||
280
.planning/phases/16-multi-signer-ui/16-03-PLAN.md
Normal file
280
.planning/phases/16-multi-signer-ui/16-03-PLAN.md
Normal file
@@ -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"
|
||||||
|
---
|
||||||
|
|
||||||
|
<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>
|
||||||
256
.planning/phases/16-multi-signer-ui/16-04-PLAN.md
Normal file
256
.planning/phases/16-multi-signer-ui/16-04-PLAN.md
Normal file
@@ -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"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
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.
|
||||||
|
</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
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
From schema.ts:
|
||||||
|
```typescript
|
||||||
|
export const documents = pgTable("documents", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
// ...
|
||||||
|
signers: jsonb("signers").$type<DocumentSigner[]>(),
|
||||||
|
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)
|
||||||
|
</interfaces>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Add signing token counts to dashboard query</name>
|
||||||
|
<read_first>
|
||||||
|
- 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)
|
||||||
|
</read_first>
|
||||||
|
<files>teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx</files>
|
||||||
|
<action>
|
||||||
|
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<number>`count(*)`.as('total'),
|
||||||
|
signed: sql<number>`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 `<DocumentsTable rows={enrichedRows} ...>` 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,
|
||||||
|
```
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- `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
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>Dashboard query fetches signing token counts per document and passes enriched rows to DocumentsTable</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Render N/M signed badge in DocumentsTable Status column</name>
|
||||||
|
<read_first>
|
||||||
|
- 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)
|
||||||
|
</read_first>
|
||||||
|
<files>teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx</files>
|
||||||
|
<action>
|
||||||
|
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 `<td>` cell (currently just `<StatusBadge status={row.status} />`), add the N/M badge after the StatusBadge:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<td style={{ padding: "0.875rem 1.5rem" }}>
|
||||||
|
<StatusBadge status={row.status} />
|
||||||
|
{row.hasMultipleSigners && row.status === 'Sent' && row.totalSigners != null && row.totalSigners > 0 && (
|
||||||
|
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-700 ml-1.5">
|
||||||
|
{row.signedCount ?? 0}/{row.totalSigners} signed
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
```
|
||||||
|
|
||||||
|
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).
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- `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
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>
|
||||||
|
- 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
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- `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
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- 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
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/16-multi-signer-ui/16-04-SUMMARY.md`
|
||||||
|
</output>
|
||||||
Reference in New Issue
Block a user