docs(07-audit-trail-and-download): create phase 7 plan
3 plans in 3 sequential waves: agent download token + API route (01), UI wiring for download button + signedAt column (02), human verification checkpoint (03). Covers SIGN-07 and LEGAL-03. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
396
.planning/phases/07-audit-trail-and-download/07-02-PLAN.md
Normal file
396
.planning/phases/07-audit-trail-and-download/07-02-PLAN.md
Normal file
@@ -0,0 +1,396 @@
|
||||
---
|
||||
phase: 07-audit-trail-and-download
|
||||
plan: "02"
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on:
|
||||
- "07-01"
|
||||
files_modified:
|
||||
- teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx
|
||||
- teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx
|
||||
- teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx
|
||||
- teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx
|
||||
autonomous: true
|
||||
requirements:
|
||||
- SIGN-07
|
||||
- LEGAL-03
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Agent sees a Download button on the document detail page when document status is Signed"
|
||||
- "Clicking the Download button triggers browser PDF download dialog (no login prompt, no 404)"
|
||||
- "Download button is absent when document status is Draft, Sent, or Viewed"
|
||||
- "Dashboard table shows a Date Signed column populated for Signed documents"
|
||||
- "Dashboard StatusBadge shows Signed for documents that have completed signing"
|
||||
artifacts:
|
||||
- path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx"
|
||||
provides: "Server component that generates agentDownloadUrl and passes it to PreparePanel"
|
||||
contains: "createAgentDownloadToken"
|
||||
- path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx"
|
||||
provides: "Download button rendered only when currentStatus === Signed and agentDownloadUrl is non-null"
|
||||
contains: "agentDownloadUrl"
|
||||
- path: "teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx"
|
||||
provides: "DocumentRow type with signedAt field + Date Signed column in table"
|
||||
contains: "signedAt"
|
||||
- path: "teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx"
|
||||
provides: "Select includes signedAt from documents table"
|
||||
contains: "signedAt: documents.signedAt"
|
||||
key_links:
|
||||
- from: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx"
|
||||
to: "teressa-copeland-homes/src/lib/signing/token.ts"
|
||||
via: "import { createAgentDownloadToken } from '@/lib/signing/token'"
|
||||
pattern: "createAgentDownloadToken"
|
||||
- from: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx"
|
||||
to: "PreparePanel"
|
||||
via: "agentDownloadUrl prop"
|
||||
pattern: "agentDownloadUrl"
|
||||
- from: "teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx"
|
||||
to: "DocumentsTable"
|
||||
via: "rows prop including signedAt field"
|
||||
pattern: "signedAt"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Wire the agent-facing download UI: generate a presigned download URL in the document detail server component and pass it to PreparePanel for Signed documents; add signedAt to the dashboard table.
|
||||
|
||||
Purpose: Complete SIGN-07 (agent can download signed PDF) by surfacing the API from Plan 01 in the portal UI. Satisfies LEGAL-03 by ensuring the only download path is the presigned token route — no direct file URLs anywhere in the UI.
|
||||
|
||||
Output: Four modified files — document detail page (token generation), PreparePanel (Download button), DocumentsTable (signedAt type + column), dashboard page (signedAt select).
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/Users/ccopeland/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/07-audit-trail-and-download/07-RESEARCH.md
|
||||
@.planning/phases/07-audit-trail-and-download/07-01-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Current document detail page server component -->
|
||||
<!-- Source: teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx -->
|
||||
|
||||
```typescript
|
||||
// Current: fetches doc + docClient, renders PdfViewerWrapper + PreparePanel
|
||||
// Phase 7 change: generate agentDownloadUrl server-side for Signed docs and pass as prop
|
||||
|
||||
export default async function DocumentPage({ params }: { params: Promise<{ docId: string }> }) {
|
||||
// ...existing auth, db fetch...
|
||||
// ADD after fetching doc:
|
||||
const agentDownloadUrl = doc.signedFilePath
|
||||
? `/api/documents/${docId}/download?adt=${await createAgentDownloadToken(docId)}`
|
||||
: null;
|
||||
|
||||
// MODIFY PreparePanel call to pass new props:
|
||||
<PreparePanel
|
||||
docId={docId}
|
||||
defaultEmail={docClient?.email ?? ''}
|
||||
clientName={docClient?.name ?? ''}
|
||||
currentStatus={doc.status}
|
||||
agentDownloadUrl={agentDownloadUrl} // ADD
|
||||
signedAt={doc.signedAt ?? null} // ADD
|
||||
/>
|
||||
}
|
||||
```
|
||||
|
||||
<!-- Current PreparePanel — 'use client' component -->
|
||||
<!-- Source: teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx -->
|
||||
|
||||
```typescript
|
||||
// Current interface:
|
||||
interface PreparePanelProps {
|
||||
docId: string;
|
||||
defaultEmail: string;
|
||||
clientName: string;
|
||||
currentStatus: string;
|
||||
}
|
||||
|
||||
// Phase 7 change: extend interface, add Download button section for Signed status
|
||||
// CRITICAL: Token generation must NOT happen in PreparePanel — PreparePanel is 'use client'
|
||||
// PreparePanel only renders an <a href={agentDownloadUrl}> anchor — it does not call createAgentDownloadToken
|
||||
|
||||
// Current non-Draft status return (replace with status-aware rendering):
|
||||
if (currentStatus !== 'Draft') {
|
||||
return (
|
||||
<div ...>
|
||||
Document status is <strong>{currentStatus}</strong> — preparation is only available for Draft documents.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// Phase 7: When currentStatus === 'Signed' and agentDownloadUrl !== null, show download section instead
|
||||
// When currentStatus === 'Sent' or 'Viewed', keep the read-only message (no download button)
|
||||
```
|
||||
|
||||
<!-- Current DocumentsTable row type -->
|
||||
<!-- Source: teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx -->
|
||||
|
||||
```typescript
|
||||
type DocumentRow = {
|
||||
id: string;
|
||||
name: string;
|
||||
clientName: string | null;
|
||||
status: "Draft" | "Sent" | "Viewed" | "Signed";
|
||||
sentAt: Date | null;
|
||||
clientId: string;
|
||||
// ADD: signedAt: Date | null;
|
||||
};
|
||||
```
|
||||
|
||||
<!-- Current dashboard page select -->
|
||||
<!-- Source: teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx -->
|
||||
|
||||
```typescript
|
||||
const allRows = await db
|
||||
.select({
|
||||
id: documents.id,
|
||||
name: documents.name,
|
||||
status: documents.status,
|
||||
sentAt: documents.sentAt,
|
||||
clientName: clients.name,
|
||||
clientId: documents.clientId,
|
||||
// ADD: signedAt: documents.signedAt,
|
||||
})
|
||||
.from(documents)
|
||||
// ...
|
||||
```
|
||||
|
||||
<!-- documents table columns (schema) -->
|
||||
```typescript
|
||||
status: documentStatusEnum("status").notNull().default("Draft"),
|
||||
signedFilePath: text("signed_file_path"), // null until signed
|
||||
signedAt: timestamp("signed_at"), // null until signed
|
||||
```
|
||||
|
||||
<!-- Token function created in Plan 01 -->
|
||||
```typescript
|
||||
// From teressa-copeland-homes/src/lib/signing/token.ts (after Plan 01):
|
||||
export async function createAgentDownloadToken(documentId: string): Promise<string>
|
||||
// Returns a JWT with purpose:'agent-download', 5-min TTL
|
||||
// Used in server component only — import forbidden in 'use client' files
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Update PreparePanel props interface and add Download button for Signed status</name>
|
||||
<files>teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx</files>
|
||||
<action>
|
||||
Two changes to PreparePanel.tsx:
|
||||
|
||||
**1. Extend PreparePanelProps interface:**
|
||||
```typescript
|
||||
interface PreparePanelProps {
|
||||
docId: string;
|
||||
defaultEmail: string;
|
||||
clientName: string;
|
||||
currentStatus: string;
|
||||
agentDownloadUrl?: string | null; // ADD
|
||||
signedAt?: Date | null; // ADD
|
||||
}
|
||||
```
|
||||
|
||||
**2. Replace the non-Draft early return block with status-aware rendering.** Currently the function returns a generic message for any non-Draft status. Replace with:
|
||||
|
||||
```typescript
|
||||
// For Signed status: show download section with optional date
|
||||
if (currentStatus === 'Signed') {
|
||||
return (
|
||||
<div style={{ borderRadius: '0.5rem', border: '1px solid #D1FAE5', padding: '1rem', backgroundColor: '#F0FDF4' }}>
|
||||
<p style={{ fontSize: '0.875rem', color: '#065F46', fontWeight: 600, marginBottom: '0.5rem' }}>
|
||||
Document Signed
|
||||
</p>
|
||||
{signedAt && (
|
||||
<p style={{ fontSize: '0.75rem', color: '#6B7280', marginBottom: '0.75rem' }}>
|
||||
Signed on{' '}
|
||||
{new Date(signedAt).toLocaleString('en-US', {
|
||||
timeZone: 'America/Denver',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
{agentDownloadUrl ? (
|
||||
<a
|
||||
href={agentDownloadUrl}
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
padding: '0.5rem 1rem',
|
||||
backgroundColor: '#1B2B4B',
|
||||
color: '#FFFFFF',
|
||||
borderRadius: '0.375rem',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 500,
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
Download Signed PDF
|
||||
</a>
|
||||
) : (
|
||||
<p style={{ fontSize: '0.75rem', color: '#9CA3AF' }}>Signed PDF not available.</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// For Sent/Viewed: keep existing read-only message
|
||||
if (currentStatus !== 'Draft') {
|
||||
return (
|
||||
<div style={{ borderRadius: '0.5rem', border: '1px solid #E5E7EB', padding: '1rem', backgroundColor: '#F9FAFB', fontSize: '0.875rem', color: '#6B7280' }}>
|
||||
Document status is <strong>{currentStatus}</strong> — preparation is only available for Draft documents.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Important: The Download button is a plain `<a href={agentDownloadUrl}>` anchor — no fetch(), no onClick handler. The browser follows the link directly, which triggers the Content-Disposition: attachment response from the API route.
|
||||
|
||||
Do not destructure `agentDownloadUrl` or `signedAt` from props in the function signature until you have updated the interface. Update interface first.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20</automated>
|
||||
</verify>
|
||||
<done>PreparePanel accepts agentDownloadUrl and signedAt props; TypeScript passes; Signed status renders download section; Sent/Viewed status renders read-only message; Draft status renders full prepare form unchanged</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Wire document detail page, update dashboard table for signedAt</name>
|
||||
<files>
|
||||
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx
|
||||
teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx
|
||||
teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx
|
||||
</files>
|
||||
<action>
|
||||
**File 1: documents/[docId]/page.tsx**
|
||||
|
||||
Add import for createAgentDownloadToken at the top:
|
||||
```typescript
|
||||
import { createAgentDownloadToken } from '@/lib/signing/token';
|
||||
```
|
||||
|
||||
After the existing `const [doc, docClient] = await Promise.all([...])` block, add:
|
||||
```typescript
|
||||
// Generate agent download URL server-side for Signed documents
|
||||
// Must be done here (server component) — PreparePanel is 'use client' and cannot call createAgentDownloadToken
|
||||
const agentDownloadUrl = doc.signedFilePath
|
||||
? `/api/documents/${docId}/download?adt=${await createAgentDownloadToken(docId)}`
|
||||
: null;
|
||||
```
|
||||
|
||||
Pass the two new props to PreparePanel:
|
||||
```tsx
|
||||
<PreparePanel
|
||||
docId={docId}
|
||||
defaultEmail={docClient?.email ?? ''}
|
||||
clientName={docClient?.name ?? ''}
|
||||
currentStatus={doc.status}
|
||||
agentDownloadUrl={agentDownloadUrl}
|
||||
signedAt={doc.signedAt ?? null}
|
||||
/>
|
||||
```
|
||||
|
||||
Note: `doc.signedAt` is available on the document object — it's a column in the documents table (timestamp("signed_at")).
|
||||
|
||||
---
|
||||
|
||||
**File 2: DocumentsTable.tsx**
|
||||
|
||||
Add `signedAt: Date | null` to the DocumentRow type:
|
||||
```typescript
|
||||
type DocumentRow = {
|
||||
id: string;
|
||||
name: string;
|
||||
clientName: string | null;
|
||||
status: "Draft" | "Sent" | "Viewed" | "Signed";
|
||||
sentAt: Date | null;
|
||||
signedAt: Date | null; // ADD
|
||||
clientId: string;
|
||||
};
|
||||
```
|
||||
|
||||
Add a "Date Signed" column header after the "Date Sent" `<th>`:
|
||||
```tsx
|
||||
<th style={{ textAlign: "left", fontSize: "0.75rem", fontWeight: 600, color: "#6B7280", textTransform: "uppercase", letterSpacing: "0.05em", padding: "0.75rem 1.5rem" }}>
|
||||
Date Signed
|
||||
</th>
|
||||
```
|
||||
|
||||
Add a "Date Signed" `<td>` in the row map after the "Date Sent" cell:
|
||||
```tsx
|
||||
<td style={{ padding: "0.875rem 1.5rem", color: "#6B7280" }}>
|
||||
{row.signedAt
|
||||
? new Date(row.signedAt).toLocaleDateString("en-US", {
|
||||
timeZone: "America/Denver",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})
|
||||
: "—"}
|
||||
</td>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**File 3: dashboard/page.tsx**
|
||||
|
||||
Add `signedAt: documents.signedAt` to the select object:
|
||||
```typescript
|
||||
const allRows = await db
|
||||
.select({
|
||||
id: documents.id,
|
||||
name: documents.name,
|
||||
status: documents.status,
|
||||
sentAt: documents.sentAt,
|
||||
signedAt: documents.signedAt, // ADD
|
||||
clientName: clients.name,
|
||||
clientId: documents.clientId,
|
||||
})
|
||||
.from(documents)
|
||||
.leftJoin(clients, eq(documents.clientId, clients.id))
|
||||
.orderBy(desc(documents.createdAt));
|
||||
```
|
||||
|
||||
The `allRows` inferred type will now include `signedAt: Date | null`, which matches the updated DocumentRow type in DocumentsTable.tsx.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20 && npm run build 2>&1 | tail -10</automated>
|
||||
</verify>
|
||||
<done>
|
||||
tsc --noEmit passes with no errors;
|
||||
npm run build passes cleanly;
|
||||
DocumentsTable accepts rows with signedAt field;
|
||||
Dashboard query selects signedAt;
|
||||
Document detail page imports and calls createAgentDownloadToken for signed docs;
|
||||
PreparePanel receives agentDownloadUrl and signedAt props
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. `npx tsc --noEmit` passes — no type errors across all four modified files
|
||||
2. `npm run build` completes successfully
|
||||
3. PreparePanel renders three distinct states: Draft (prepare form), Sent/Viewed (read-only message), Signed (green panel with download link)
|
||||
4. agentDownloadUrl is generated in the server component (page.tsx), not in PreparePanel
|
||||
5. DocumentsTable has Date Signed column
|
||||
6. Dashboard query includes signedAt in select
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Agent can navigate to a Signed document detail page and see a green "Document Signed" panel with signed timestamp and "Download Signed PDF" anchor link
|
||||
- Download button is absent for Draft/Sent/Viewed documents
|
||||
- Dashboard table shows "Date Signed" column with date for Signed documents, "—" for others
|
||||
- Build passes with no TypeScript errors
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/07-audit-trail-and-download/07-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user