diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index dab55bb..9957363 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -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 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) -**Plans**: 3 plans +**Plans**: 4 plans Plans: -- [x] 15-01-PLAN.md — Token utility extensions (signerEmail param, signer-download JWT), sendSignerCompletionEmail mailer, public signer download route -- [x] 15-02-PLAN.md — Send route rewrite: multi-signer token loop with Promise.all dispatch, legacy single-signer fallback, APP_BASE_URL rename -- [ ] 15-03-PLAN.md — Sign handler rewrite: GET signer-filtered fields, POST signer-scoped operations + accumulate PDF + atomic completion + notifications +- [ ] 16-01-PLAN.md — DocumentPageClient signers state threading, server page initialSigners prop, PdfViewerWrapper prop pass-through +- [ ] 16-02-PLAN.md — PreparePanel signer list UI (add/remove by email, colored dots, auto-assigned colors) + send-block validation +- [ ] 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 + + + + ### 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 **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 | - | | 14. Multi-Signer Schema | v1.2 | 1/1 | 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 | - | diff --git a/.planning/phases/16-multi-signer-ui/16-01-PLAN.md b/.planning/phases/16-multi-signer-ui/16-01-PLAN.md new file mode 100644 index 0000000..145bfbb --- /dev/null +++ b/.planning/phases/16-multi-signer-ui/16-01-PLAN.md @@ -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=" +--- + + +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. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.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 + + + + +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; + 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; + 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; + selectedFieldId: string | null; + onQuickFill: (fieldId: string, value: string) => void; + onAiAutoPlace: () => Promise; +} +``` + +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) +``` + + + + + + + Task 1: Add signers prop to server page and thread state through DocumentPageClient + + - 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 + + + teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx, + teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx + + + **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(initialSigners);` + - Add state: `const [unassignedFieldIds, setUnassignedFieldIds] = useState>(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 + + + cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30 + + + - `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 + + DocumentPageClient has signers + unassignedFieldIds state initialized from server; props threaded to PreparePanel and PdfViewerWrapper; TypeScript compiles cleanly + + + + Task 2: Thread signers and unassignedFieldIds through PdfViewerWrapper to FieldPlacer + + - 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 + + + 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 + + + **PdfViewerWrapper.tsx:** + - Import `DocumentSigner` from `@/lib/db/schema` + - Add optional props: `signers?: DocumentSigner[]`, `unassignedFieldIds?: Set` + - Pass both through to `` + + **PdfViewer.tsx:** + - Add same optional props to PdfViewer's prop interface: `signers?: DocumentSigner[]`, `unassignedFieldIds?: Set` + - Pass both through to `` + + **FieldPlacer.tsx:** + - Import `DocumentSigner` from `@/lib/db/schema` + - Add to FieldPlacerProps: `signers?: DocumentSigner[]`, `unassignedFieldIds?: Set` + - 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. + + + cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30 + + + - `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 + + signers and unassignedFieldIds props flow from DocumentPageClient through PdfViewerWrapper and PdfViewer into FieldPlacer; TypeScript compiles cleanly + + + + + +- `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 + + + +- 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 + + + +After completion, create `.planning/phases/16-multi-signer-ui/16-01-SUMMARY.md` + diff --git a/.planning/phases/16-multi-signer-ui/16-02-PLAN.md b/.planning/phases/16-multi-signer-ui/16-02-PLAN.md new file mode 100644 index 0000000..93a0a52 --- /dev/null +++ b/.planning/phases/16-multi-signer-ui/16-02-PLAN.md @@ -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" +--- + + +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. + + + +@$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 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 // field IDs with no signerEmail (for send-block) +onUnassignedFieldIdsChange: (ids: Set) => 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. +``` + + + + + + + Task 1: Add signer list UI section to PreparePanel + + - teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx + - teressa-copeland-homes/src/lib/db/schema.ts (DocumentSigner, isClientVisibleField) + - .planning/phases/16-multi-signer-ui/16-CONTEXT.md (D-01, D-02, D-03, D-04) + - .planning/phases/16-multi-signer-ui/16-UI-SPEC.md (Component Inventory sections 1 and 4, Copywriting Contract, Interaction Contract) + + teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx + + **New props on PreparePanelProps (from Plan 01 wiring):** + ```typescript + signers: DocumentSigner[]; + onSignersChange: (signers: DocumentSigner[]) => void; + unassignedFieldIds: Set; + onUnassignedFieldIdsChange: (ids: Set) => void; + ``` + Import `DocumentSigner` and `isClientVisibleField` from `@/lib/db/schema`. + + **Signer color palette constant** (top of file, per D-01): + ```typescript + const SIGNER_COLORS = ['#6366f1', '#f43f5e', '#10b981', '#f59e0b']; + ``` + + **Local state for signer input:** + ```typescript + const [signerInput, setSignerInput] = useState(''); + const [signerInputError, setSignerInputError] = useState(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 `` and the AI Auto-place button. Per UI-SPEC section 1: + + Section 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: `

Each signer receives their own signing link.

` + + Duplicate error: When `signerInputError === 'duplicate'`, show `

That email is already in the signer list.

` + + Signer list (below input): `
` containing signer rows. + + Each signer row: `
` + - Colored dot: `` + - Email: `{signer.email}` + - Remove button: `` (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: + + + {/* 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` +