257 lines
10 KiB
Markdown
257 lines
10 KiB
Markdown
---
|
|
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>
|