Files
Chandler Copeland 9fe7936304 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>
2026-03-21 10:30:05 -06:00

14 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
07-audit-trail-and-download 02 execute 2
07-01
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
true
SIGN-07
LEGAL-03
truths artifacts key_links
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
path provides contains
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx Server component that generates agentDownloadUrl and passes it to PreparePanel createAgentDownloadToken
path provides contains
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx Download button rendered only when currentStatus === Signed and agentDownloadUrl is non-null agentDownloadUrl
path provides contains
teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx DocumentRow type with signedAt field + Date Signed column in table signedAt
path provides contains
teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx Select includes signedAt from documents table signedAt: documents.signedAt
from to via pattern
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx teressa-copeland-homes/src/lib/signing/token.ts import { createAgentDownloadToken } from '@/lib/signing/token' createAgentDownloadToken
from to via pattern
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx PreparePanel agentDownloadUrl prop agentDownloadUrl
from to via pattern
teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx DocumentsTable rows prop including signedAt field signedAt
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).

<execution_context> @/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md @/Users/ccopeland/.claude/get-shit-done/templates/summary.md </execution_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
// 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 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)
type DocumentRow = {
  id: string;
  name: string;
  clientName: string | null;
  status: "Draft" | "Sent" | "Viewed" | "Signed";
  sentAt: Date | null;
  clientId: string;
  // ADD: signedAt: Date | null;
};
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)
  // ...
status: documentStatusEnum("status").notNull().default("Draft"),
signedFilePath: text("signed_file_path"),   // null until signed
signedAt: timestamp("signed_at"),            // null until signed
// 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
Task 1: Update PreparePanel props interface and add Download button for Signed status teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx Two changes to PreparePanel.tsx:

1. Extend PreparePanelProps interface:

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:

// 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. cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20 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

Task 2: Wire document detail page, update dashboard table for signedAt 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 **File 1: documents/[docId]/page.tsx**

Add import for createAgentDownloadToken at the top:

import { createAgentDownloadToken } from '@/lib/signing/token';

After the existing const [doc, docClient] = await Promise.all([...]) block, add:

// 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:

<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:

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>:

<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:

<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:

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. cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20 && npm run build 2>&1 | tail -10 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

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

<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>
After completion, create `.planning/phases/07-audit-trail-and-download/07-02-SUMMARY.md`