docs(16): create phase plan — 4 plans in 3 waves
This commit is contained in:
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