---
phase: 03-agent-portal-shell
plan: 03
type: execute
wave: 3
depends_on: [03-01, 03-02]
files_modified:
  - teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx
  - teressa-copeland-homes/src/app/portal/(protected)/clients/page.tsx
  - teressa-copeland-homes/src/app/portal/_components/ClientCard.tsx
  - teressa-copeland-homes/src/app/portal/_components/ClientModal.tsx
  - teressa-copeland-homes/scripts/seed.ts
autonomous: true
requirements: [CLIENT-01, CLIENT-02, DASH-01, DASH-02]

must_haves:
  truths:
    - "Agent visits /portal/clients and sees a card grid of clients — each card shows name, email, document count, and last activity date"
    - "Agent clicks '+ Add Client' on the clients page, a modal opens, fills name + email, clicks submit — client appears in the grid"
    - "Agent visits /portal/dashboard and sees a table of documents with Document Name, Client, Status badge, and Date Sent columns"
    - "Dashboard filter dropdown (All / Draft / Sent / Viewed / Signed) filters the table; URL reflects the filter (?status=Signed)"
    - "After running npm run db:seed, the database contains at least 2 clients and 4 placeholder documents"
  artifacts:
    - path: "teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx"
      provides: "Dashboard with documents table, status filter, date sort"
    - path: "teressa-copeland-homes/src/app/portal/(protected)/clients/page.tsx"
      provides: "Clients card grid with '+ Add Client' modal trigger and empty state"
    - path: "teressa-copeland-homes/src/app/portal/_components/ClientCard.tsx"
      provides: "Individual client card with name, email, doc count, last activity"
    - path: "teressa-copeland-homes/src/app/portal/_components/ClientModal.tsx"
      provides: "Create/edit modal using useActionState with createClient action"
    - path: "teressa-copeland-homes/scripts/seed.ts"
      provides: "Seed data: 2 clients + 4 placeholder documents"
  key_links:
    - from: "clients/page.tsx"
      to: "ClientModal"
      via: "useState(isOpen) + <ClientModal isOpen={isOpen} onClose={() => setIsOpen(false)} />"
      pattern: "ClientModal"
    - from: "ClientModal"
      to: "createClient server action"
      via: "useActionState(createClient, null)"
      pattern: "useActionState.*createClient"
    - from: "dashboard/page.tsx"
      to: "DocumentsTable"
      via: "pass rows prop from Drizzle JOIN query"
      pattern: "DocumentsTable"
    - from: "dashboard/page.tsx searchParams"
      to: "Drizzle WHERE clause"
      via: "status filter applied in server component before passing to DocumentsTable"
      pattern: "searchParams.*status"
---

<objective>
Build the Dashboard page and Clients list page — the two primary portal views. Also seed the database with placeholder data so the UI looks populated from the first run.

Purpose: These are the two main entry points of the portal (agent lands on dashboard post-login; clients is the client management hub). Seed data is needed so the dashboard table is not empty on first load.
Output: Working /portal/dashboard with filterable documents table; working /portal/clients with card grid and create modal; extended seed.ts.
</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/STATE.md
@.planning/phases/03-agent-portal-shell/03-CONTEXT.md
@.planning/phases/03-agent-portal-shell/03-RESEARCH.md
@.planning/phases/03-agent-portal-shell/03-02-SUMMARY.md

<interfaces>
<!-- From Plan 02 — use these exactly -->

From 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;
};
type Props = { rows: DocumentRow[]; showClientColumn?: boolean };
export function DocumentsTable({ rows, showClientColumn = true }: Props): JSX.Element
```

From src/app/portal/_components/StatusBadge.tsx:
```typescript
export function StatusBadge({ status }: { status: "Draft" | "Sent" | "Viewed" | "Signed" }): JSX.Element
```

From src/lib/actions/clients.ts:
```typescript
export async function createClient(
  _prevState: { error?: string } | null,
  formData: FormData
): Promise<{ error?: string; success?: boolean }>
```

From src/lib/db/schema.ts (Plan 01):
```typescript
export const clients  // id, name, email, createdAt, updatedAt
export const documents // id, name, clientId, status, sentAt, createdAt
export const documentStatusEnum // "Draft" | "Sent" | "Viewed" | "Signed"
```

Drizzle query patterns (from RESEARCH.md):
```typescript
import { db } from "@/lib/db";
import { documents, clients } from "@/lib/db/schema";
import { eq, desc, sql } from "drizzle-orm";

// Dashboard: all documents with client name
const rows = await db
  .select({
    id: documents.id,
    name: documents.name,
    status: documents.status,
    sentAt: documents.sentAt,
    clientName: clients.name,
    clientId: documents.clientId,
  })
  .from(documents)
  .leftJoin(clients, eq(documents.clientId, clients.id))
  .orderBy(desc(documents.createdAt));

// Clients with document count + last activity
const clientRows = await db
  .select({
    id: clients.id,
    name: clients.name,
    email: clients.email,
    createdAt: clients.createdAt,
    docCount: sql<number>`count(${documents.id})::int`,
    lastActivity: sql<Date | null>`max(${documents.sentAt})`,
  })
  .from(clients)
  .leftJoin(documents, eq(documents.clientId, clients.id))
  .groupBy(clients.id, clients.name, clients.email, clients.createdAt)
  .orderBy(desc(clients.createdAt));
```

Next.js 16 searchParams (server component):
```typescript
// searchParams is a Promise in Next.js 16
export default async function DashboardPage({
  searchParams,
}: {
  searchParams: Promise<{ status?: string; sort?: string }>;
}) {
  const { status } = await searchParams;
  // filter rows by status if provided
}
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Dashboard page with filterable documents table</name>
  <files>teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx</files>
  <action>
Create `src/app/portal/(protected)/dashboard/page.tsx` as an async server component.

**Data fetching:**
- Accept `searchParams: Promise<{ status?: string }>` as a prop (Next.js 16 — must await)
- Await searchParams to get `{ status }`
- Query all documents with LEFT JOIN to clients (use the Drizzle pattern from the interfaces block above)
- If `status` param is set and is one of ["Draft", "Sent", "Viewed", "Signed"], filter the rows in JavaScript after fetch (simplest approach — the data set is tiny in Phase 3): `rows.filter(r => r.status === status)`
- Sort by `documents.createdAt` DESC in the query

**Page layout:**
- `<h1>` with "Dashboard" heading
- Filter bar: a `<StatusFilterBar>` — inline a simple client component OR use a plain `<form>` approach:
  - Since filter state lives in the URL, use a plain `<select>` that triggers navigation via a client component wrapper. Create a small inline `"use client"` component `DashboardFilters` in the SAME FILE (below the default export) that uses `useRouter` + `useSearchParams` to push `?status=X` on change:
    ```typescript
    "use client";
    function DashboardFilters({ currentStatus }: { currentStatus?: string }) {
      const router = useRouter();
      return (
        <select
          value={currentStatus ?? ""}
          onChange={(e) => router.push(e.target.value ? `?status=${e.target.value}` : "/portal/dashboard")}
          className="border border-gray-200 rounded-lg px-3 py-1.5 text-sm"
        >
          <option value="">All statuses</option>
          <option value="Draft">Draft</option>
          <option value="Sent">Sent</option>
          <option value="Viewed">Viewed</option>
          <option value="Signed">Signed</option>
        </select>
      );
    }
    ```
  - Import `useRouter` and `useSearchParams` from `next/navigation` in the inline client component
- Below the filter bar: `<DocumentsTable rows={filteredRows} showClientColumn={true} />`
- Page heading area should also show the agent's first name: extract from session via `await auth()` (already done in layout, but page can also call it — session is cached by next-auth in the same request)

**Styling:**
- Use `bg-[var(--cream)]` page background (inherited from layout)
- Section heading `text-[var(--navy)] text-2xl font-semibold mb-6`
- Filter bar: `flex items-center gap-4 mb-6`
  </action>
  <verify>
    <automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1</automated>
  </verify>
  <done>Dashboard page renders DocumentsTable with seeded rows. Filter select changes URL to ?status=X. TypeScript compiles cleanly.</done>
</task>

<task type="auto">
  <name>Task 2: Clients list page with card grid and create modal</name>
  <files>
    teressa-copeland-homes/src/app/portal/(protected)/clients/page.tsx
    teressa-copeland-homes/src/app/portal/_components/ClientCard.tsx
    teressa-copeland-homes/src/app/portal/_components/ClientModal.tsx
  </files>
  <action>
**`src/app/portal/_components/ClientCard.tsx`** (server component):
Props: `{ id: string; name: string; email: string; docCount: number; lastActivity: Date | null }`

Render a white card with rounded-xl shadow-sm:
- Client name: `text-[var(--navy)] font-semibold text-base`
- Email: `text-gray-500 text-sm`
- Row: "Documents: {docCount}" (small, gray)
- Row: "Last activity: {lastActivity formatted}" or "No activity yet" if null — use `toLocaleDateString` for date
- Wrap entire card in a Next.js `<Link href={"/portal/clients/" + id}>` — clicking the card navigates to the profile page
- Card styling: `bg-white rounded-xl shadow-sm p-5 hover:shadow-md transition-shadow`

**`src/app/portal/_components/ClientModal.tsx`** ("use client"):
Props: `{ isOpen: boolean; onClose: () => void; mode?: "create" | "edit"; clientId?: string; defaultName?: string; defaultEmail?: string }`

- If `mode === "edit"` and `clientId` exists, bind `updateClient` to the id: `const boundAction = updateClient.bind(null, clientId)` — import `updateClient` from `@/lib/actions/clients`
- If `mode === "create"`, use `createClient` directly
- `const [state, formAction, pending] = useActionState(mode === "edit" ? boundAction : createClient, null)` — import `useActionState` from `'react'` NOT `'react-dom'`
- `useEffect`: if `state?.success` → call `onClose()`
- Modal overlay: `fixed inset-0 z-50 flex items-center justify-center bg-black/40`
- Modal card: `bg-white rounded-xl shadow-xl p-6 w-full max-w-md`
- Two inputs: name (defaultValue={defaultName}), email (type="email", defaultValue={defaultEmail})
- Error display if `state?.error`
- Cancel + Submit buttons (Submit shows "Saving..." when pending, disabled when pending)
- Title: "Add Client" for create mode, "Edit Client" for edit mode

**`src/app/portal/(protected)/clients/page.tsx`** (async server component with a "use client" wrapper for modal state):
Since this page needs both server data AND client modal state, use a common pattern:
- `clients/page.tsx` is a server component that fetches data and renders `<ClientsPageClient clients={clientRows} />`
- Inline a `ClientsPageClient` "use client" component below the default export in the same file, OR extract it to `_components/ClientsPageClient.tsx`

Either approach: the client wrapper holds `const [isOpen, setIsOpen] = useState(false)`.

Server part fetches clients with docCount + lastActivity using the Drizzle query from interfaces block.

Client part renders:
- Page heading + "+ Add Client" button (gold background: `bg-[var(--gold)] text-white px-4 py-2 rounded-lg text-sm font-medium`)
- If no clients: empty state — `<p>No clients yet.</p>` + `<button onClick={() => setIsOpen(true)}>+ Add your first client</button>` per CONTEXT.md decision
- If clients exist: `<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">` with `<ClientCard>` for each
- `<ClientModal isOpen={isOpen} onClose={() => setIsOpen(false)} mode="create" />`

PITFALL: Do NOT use `onMouseEnter`/`onMouseLeave` on server component props. `ClientCard` should use Tailwind `hover:` classes (it's a server component — no event handlers).
  </action>
  <verify>
    <automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1</automated>
  </verify>
  <done>ClientCard exported. ClientModal exported with create/edit mode support. Clients page renders grid + empty state + "+ Add Client" button. TypeScript compiles cleanly.</done>
</task>

<task type="auto">
  <name>Task 3: Extend seed.ts with client and placeholder document rows</name>
  <files>teressa-copeland-homes/scripts/seed.ts</files>
  <action>
Read the existing `scripts/seed.ts` to understand its structure. It already seeds the admin user. Add, AFTER the existing user seed:

**2 clients:**
```typescript
await db.insert(clients).values([
  { name: "Sarah Johnson", email: "sarah.j@example.com" },
  { name: "Mike Torres", email: "m.torres@example.com" },
]).onConflictDoNothing();
```

**4 placeholder documents** (use the client IDs just inserted — query them back after insert):
```typescript
const [sarah, mike] = await db.select({ id: clients.id })
  .from(clients)
  .where(inArray(clients.email, ["sarah.j@example.com", "m.torres@example.com"]))
  .orderBy(clients.createdAt);
// Handle case where seeded clients not found (e.g., already exist from prior seed run)
if (sarah && mike) {
  await db.insert(documents).values([
    { name: "Purchase Agreement - 842 Maple Dr", clientId: sarah.id, status: "Signed", sentAt: new Date("2026-02-15") },
    { name: "Seller Disclosure - 842 Maple Dr", clientId: sarah.id, status: "Signed", sentAt: new Date("2026-02-14") },
    { name: "Buyer Rep Agreement", clientId: mike.id, status: "Sent", sentAt: new Date("2026-03-10") },
    { name: "Purchase Agreement - 1205 Oak Ave", clientId: mike.id, status: "Draft", sentAt: null },
  ]).onConflictDoNothing();
}
```

Import `clients`, `documents` from `@/lib/db/schema` (or relative path — match the existing import style in seed.ts).
Import `inArray` from `drizzle-orm` if needed.

Then run the seed:
```
cd teressa-copeland-homes && npm run db:seed
```

Verify it completes without error.
  </action>
  <verify>
    <automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run db:seed 2>&1 | tail -20</automated>
  </verify>
  <done>seed.ts runs without error. Database contains 2 client rows and 4 document rows. The clients page shows populated cards and the dashboard shows the 4 placeholder documents.</done>
</task>

</tasks>

<verification>
1. `npx tsc --noEmit` produces zero errors
2. `npm run db:seed` completes without error
3. `src/app/portal/(protected)/dashboard/page.tsx` exists and imports DocumentsTable
4. `src/app/portal/(protected)/clients/page.tsx` exists with card grid and modal trigger
5. `src/app/portal/_components/ClientCard.tsx` exported with Link to /portal/clients/[id]
6. `src/app/portal/_components/ClientModal.tsx` exported with useActionState from 'react'
</verification>

<success_criteria>
- Dashboard page renders document table with filter dropdown that uses URL search params
- Clients page renders a card grid with name, email, doc count, last activity on each card
- "+ Add Client" button opens modal; modal submits createClient action and closes on success
- Empty state shows friendly message with CTA when no clients exist
- Seed produces 2 clients and 4 placeholder documents
- TypeScript compiles cleanly
</success_criteria>

<output>
After completion, create `.planning/phases/03-agent-portal-shell/03-03-SUMMARY.md`
</output>
