--- 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) + 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" --- 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. @/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md @/Users/ccopeland/.claude/get-shit-done/templates/summary.md @.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 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`count(${documents.id})::int`, lastActivity: sql`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 } ``` Task 1: Dashboard page with filterable documents table teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx 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:** - `

` with "Dashboard" heading - Filter bar: a `` — inline a simple client component OR use a plain `
` approach: - Since filter state lives in the URL, use a plain ` 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" > ); } ``` - Import `useRouter` and `useSearchParams` from `next/navigation` in the inline client component - Below the filter bar: `` - 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` cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 Dashboard page renders DocumentsTable with seeded rows. Filter select changes URL to ?status=X. TypeScript compiles cleanly. Task 2: Clients list page with card grid and create modal 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 **`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 `` — 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 `` - 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 — `

No clients yet.

` + `` per CONTEXT.md decision - If clients exist: `
` with `` for each - ` 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). cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 ClientCard exported. ClientModal exported with create/edit mode support. Clients page renders grid + empty state + "+ Add Client" button. TypeScript compiles cleanly. Task 3: Extend seed.ts with client and placeholder document rows teressa-copeland-homes/scripts/seed.ts 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. cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run db:seed 2>&1 | tail -20 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. 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' - 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 After completion, create `.planning/phases/03-agent-portal-shell/03-03-SUMMARY.md`