diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index fc0d1c2..2d677b7 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -64,9 +64,13 @@ Plans:
2. Agent can view a list of all clients
3. Agent can view a client's profile page and see the documents associated with that client
4. Agent can see all documents in the dashboard with their current status (Draft / Sent / Viewed / Signed) and which client each was sent to and when
-**Plans**: TBD
+**Plans**: 4 plans
-Plans: none yet
+Plans:
+- [ ] 03-01-PLAN.md — Drizzle schema (clients + documents tables), DB migration, middleware + auth config updated for /portal prefix
+- [ ] 03-02-PLAN.md — Portal layout with top nav, StatusBadge, DocumentsTable shared components, client server actions
+- [ ] 03-03-PLAN.md — Dashboard page (filterable documents table) + Clients list page (card grid + create modal) + seed data
+- [ ] 03-04-PLAN.md — Client profile page (edit/delete/documents table) + full Phase 3 human verification checkpoint
### Phase 4: PDF Ingest
**Goal**: Agent can upload a PDF form, see it rendered in the browser, and it is stored safely on the local filesystem / Docker volume
@@ -132,7 +136,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7
|-------|----------------|--------|-----------|
| 1. Foundation | 1/3 | Complete | 2026-03-19 |
| 2. Marketing Site | 2/3 | In Progress| |
-| 3. Agent Portal Shell | 0/? | Not started | - |
+| 3. Agent Portal Shell | 0/4 | Not started | - |
| 4. PDF Ingest | 0/? | Not started | - |
| 5. PDF Fill and Field Mapping | 0/? | Not started | - |
| 6. Signing Flow | 0/? | Not started | - |
diff --git a/.planning/phases/03-agent-portal-shell/03-01-PLAN.md b/.planning/phases/03-agent-portal-shell/03-01-PLAN.md
new file mode 100644
index 0000000..b35ce92
--- /dev/null
+++ b/.planning/phases/03-agent-portal-shell/03-01-PLAN.md
@@ -0,0 +1,199 @@
+---
+phase: 03-agent-portal-shell
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - teressa-copeland-homes/src/lib/db/schema.ts
+ - teressa-copeland-homes/middleware.ts
+ - teressa-copeland-homes/src/lib/auth.config.ts
+ - teressa-copeland-homes/src/app/agent/(protected)/dashboard/page.tsx
+ - teressa-copeland-homes/drizzle/0001_clients_documents.sql
+autonomous: true
+requirements: [CLIENT-01, CLIENT-02, CLIENT-03, DASH-01, DASH-02]
+
+must_haves:
+ truths:
+ - "Running `npm run db:generate && npm run db:migrate` produces a migration that creates the `clients` and `documents` tables with no errors"
+ - "Visiting /portal/dashboard while unauthenticated redirects to /agent/login — not a 404 or blank page"
+ - "After login, agent is redirected to /portal/dashboard (not /agent/dashboard)"
+ - "The document_status PostgreSQL enum exists with values Draft, Sent, Viewed, Signed"
+ artifacts:
+ - path: "teressa-copeland-homes/src/lib/db/schema.ts"
+ provides: "clients and documents table definitions + documentStatusEnum"
+ contains: "pgTable(\"clients\""
+ - path: "teressa-copeland-homes/middleware.ts"
+ provides: "Route protection for /portal/:path*"
+ contains: "/portal/:path*"
+ - path: "teressa-copeland-homes/src/lib/auth.config.ts"
+ provides: "Post-login redirect to /portal/dashboard"
+ contains: "/portal/dashboard"
+ key_links:
+ - from: "middleware.ts matcher"
+ to: "/portal/:path* routes"
+ via: "matcher array"
+ pattern: "portal.*path"
+ - from: "auth.config.ts authorized callback"
+ to: "session check for /portal routes"
+ via: "nextUrl.pathname.startsWith('/portal')"
+ pattern: "startsWith.*portal"
+---
+
+
+Add the `clients` and `documents` stub tables to the Drizzle schema, generate and run the migration, and update the middleware + auth config so all `/portal/` routes are protected and post-login redirect lands on `/portal/dashboard`.
+
+Purpose: Every subsequent Phase 3 plan depends on these two tables and on the `/portal` route prefix being protected. This plan lays the data and routing foundations.
+Output: Working Drizzle migration for clients + documents tables; middleware protecting `/portal/*`; post-login redirect updated.
+
+
+
+@/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md
+@/Users/ccopeland/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/03-agent-portal-shell/03-CONTEXT.md
+@.planning/phases/03-agent-portal-shell/03-RESEARCH.md
+
+
+
+
+
+Existing users table (for reference pattern):
+```typescript
+import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
+// users table already exists — do NOT redefine it
+
+// ADD below the existing users table:
+export const clients = pgTable("clients", {
+ id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
+ name: text("name").notNull(),
+ email: text("email").notNull(),
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
+});
+
+export const documentStatusEnum = pgEnum("document_status", [
+ "Draft", "Sent", "Viewed", "Signed"
+]);
+
+export const documents = pgTable("documents", {
+ id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
+ name: text("name").notNull(),
+ clientId: text("client_id").notNull().references(() => clients.id, { onDelete: "cascade" }),
+ status: documentStatusEnum("status").notNull().default("Draft"),
+ sentAt: timestamp("sent_at"),
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+});
+```
+
+Existing middleware.ts pattern (matcher array to extend):
+```typescript
+export const config = {
+ matcher: ["/agent/:path*"], // ADD "/portal/:path*" here
+};
+```
+
+Existing auth.config.ts authorized callback (redirect to update):
+```typescript
+// Change: redirect(new URL("/agent/dashboard", nextUrl))
+// To: redirect(new URL("/portal/dashboard", nextUrl))
+// Also add: nextUrl.pathname.startsWith("/portal") to protected routes check
+```
+
+
+
+
+
+
+ Task 1: Extend Drizzle schema with clients and documents tables
+ teressa-copeland-homes/src/lib/db/schema.ts
+
+Read the current schema.ts first. Then add, below the existing users table definition:
+
+1. Import `pgEnum` from `drizzle-orm/pg-core` (add to existing import).
+2. Export `documentStatusEnum` using `pgEnum("document_status", ["Draft", "Sent", "Viewed", "Signed"])`.
+3. Export `clients` table: id (text PK with crypto.randomUUID), name (text not null), email (text not null), createdAt (timestamp defaultNow not null), updatedAt (timestamp defaultNow not null).
+4. Export `documents` table (stub — no PDF content yet): id (text PK with crypto.randomUUID), name (text not null), clientId (text not null, FK to clients.id with onDelete: "cascade"), status (documentStatusEnum column default "Draft" not null), sentAt (timestamp nullable), createdAt (timestamp defaultNow not null).
+
+CRITICAL: Export the enum BEFORE the documents table (it is referenced by the documents table column). Export both tables so they are importable by other modules.
+
+Then generate and run the migration:
+```
+cd teressa-copeland-homes && npm run db:generate && npm run db:migrate
+```
+
+The migration SQL file will be created automatically in the `drizzle/` directory. Do NOT hand-write SQL.
+
+PITFALL: pgEnum must be exported AND referenced by a table column or drizzle-kit generate may omit it. Confirm the generated SQL contains `CREATE TYPE document_status AS ENUM`.
+
+
+ cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run db:generate && npm run db:migrate 2>&1 | tail -20
+
+ Migration runs without error. `clients` and `documents` tables exist in the database. `document_status` enum exists in PostgreSQL. schema.ts exports `clients`, `documents`, and `documentStatusEnum`.
+
+
+
+ Task 2: Update middleware and auth config to protect /portal routes
+
+ teressa-copeland-homes/middleware.ts
+ teressa-copeland-homes/src/lib/auth.config.ts
+ teressa-copeland-homes/src/app/agent/(protected)/dashboard/page.tsx
+
+
+**middleware.ts**: Read the file. Change the matcher to include both `/agent/:path*` and `/portal/:path*`:
+```typescript
+export const config = {
+ matcher: ["/agent/:path*", "/portal/:path*"],
+};
+```
+
+**auth.config.ts**: Read the file. Make two changes:
+1. In the `authorized` callback, extend the protected route check to also cover `/portal` routes:
+ ```typescript
+ // Add alongside the existing /agent check:
+ if (nextUrl.pathname.startsWith("/portal")) {
+ if (!isLoggedIn) return Response.redirect(new URL("/agent/login", nextUrl));
+ }
+ ```
+2. Change the post-login redirect (where a logged-in user visiting the login page is sent) from `/agent/dashboard` to `/portal/dashboard`:
+ ```typescript
+ // Change: new URL("/agent/dashboard", nextUrl)
+ // To: new URL("/portal/dashboard", nextUrl)
+ ```
+
+PITFALL (from RESEARCH.md): The `authorized` callback in Auth.js v5 beta may handle the redirect differently than a plain middleware — read the existing callback logic carefully and mirror the existing `/agent` protection pattern rather than rewriting it.
+
+**src/app/agent/(protected)/dashboard/page.tsx**: Replace the dashboard stub with a redirect to `/portal/dashboard`. Import `redirect` from `next/navigation` and call `redirect("/portal/dashboard")` as the only content. This ensures any old link to `/agent/dashboard` silently forwards to the new location.
+
+
+ cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1
+
+ TypeScript compiles with zero errors. middleware.ts matcher includes `/portal/:path*`. auth.config.ts post-login redirect is `/portal/dashboard`. /agent/dashboard/page.tsx redirects to /portal/dashboard.
+
+
+
+
+
+1. `npm run db:generate && npm run db:migrate` completes without error
+2. `npx tsc --noEmit` produces zero TypeScript errors
+3. schema.ts contains exports for `clients`, `documents`, and `documentStatusEnum`
+4. middleware.ts matcher array contains `"/portal/:path*"`
+5. auth.config.ts contains `startsWith("/portal")` in protected route check
+6. auth.config.ts post-login redirect is `/portal/dashboard`
+
+
+
+- Drizzle migration creates `clients` and `documents` tables and `document_status` enum in PostgreSQL
+- All `/portal/` routes require authentication (middleware protects them)
+- After login, agent is redirected to `/portal/dashboard`
+- TypeScript compiles cleanly
+
+
+
diff --git a/.planning/phases/03-agent-portal-shell/03-02-PLAN.md b/.planning/phases/03-agent-portal-shell/03-02-PLAN.md
new file mode 100644
index 0000000..72296ee
--- /dev/null
+++ b/.planning/phases/03-agent-portal-shell/03-02-PLAN.md
@@ -0,0 +1,273 @@
+---
+phase: 03-agent-portal-shell
+plan: 02
+type: execute
+wave: 2
+depends_on: [03-01]
+files_modified:
+ - teressa-copeland-homes/src/app/portal/(protected)/layout.tsx
+ - teressa-copeland-homes/src/app/portal/_components/PortalNav.tsx
+ - teressa-copeland-homes/src/app/portal/_components/StatusBadge.tsx
+ - teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx
+ - teressa-copeland-homes/src/lib/actions/clients.ts
+autonomous: true
+requirements: [CLIENT-01, DASH-01, DASH-02]
+
+must_haves:
+ truths:
+ - "Every page under /portal/(protected)/ renders the top nav bar without writing any nav code in the page itself"
+ - "StatusBadge renders Draft=gray, Sent=blue, Viewed=amber, Signed=green with correct Tailwind classes"
+ - "createClient server action validates name (min 1 char) + email (valid format) and inserts into clients table"
+ - "updateClient and deleteClient server actions call revalidatePath after mutation"
+ artifacts:
+ - path: "teressa-copeland-homes/src/app/portal/(protected)/layout.tsx"
+ provides: "Portal route-group layout with auth check and PortalNav"
+ contains: "PortalNav"
+ - path: "teressa-copeland-homes/src/app/portal/_components/PortalNav.tsx"
+ provides: "Horizontal top nav with links to Dashboard and Clients"
+ - path: "teressa-copeland-homes/src/app/portal/_components/StatusBadge.tsx"
+ provides: "Color-coded pill for Draft/Sent/Viewed/Signed"
+ - path: "teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx"
+ provides: "Reusable documents table used by dashboard + client profile"
+ - path: "teressa-copeland-homes/src/lib/actions/clients.ts"
+ provides: "createClient, updateClient, deleteClient server actions"
+ exports: ["createClient", "updateClient", "deleteClient"]
+ key_links:
+ - from: "portal/(protected)/layout.tsx"
+ to: "auth() session check"
+ via: "import from @/lib/auth"
+ pattern: "auth\\(\\)"
+ - from: "ClientModal.tsx (next plan)"
+ to: "createClient action"
+ via: "import from @/lib/actions/clients"
+ pattern: "createClient"
+ - from: "DocumentsTable.tsx"
+ to: "StatusBadge"
+ via: "import StatusBadge"
+ pattern: "StatusBadge"
+---
+
+
+Build the portal shell: the authenticated layout with top nav, the shared UI components (StatusBadge, DocumentsTable), and the client server actions. These are the shared building blocks that all three portal pages (dashboard, clients, profile) depend on.
+
+Purpose: Pages cannot be built without a layout to render in, a StatusBadge to display document state, and server actions to mutate client data. This plan creates the contracts downstream plans implement against.
+Output: Portal layout, PortalNav, StatusBadge, DocumentsTable, and all three client mutation actions.
+
+
+
+@/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-01-SUMMARY.md
+
+
+
+
+From src/lib/db/schema.ts (created in Plan 01):
+```typescript
+export type DocumentStatus = "Draft" | "Sent" | "Viewed" | "Signed";
+// documentStatusEnum is a pgEnum — the TypeScript union above is how you type it in components
+
+export const clients: PgTable<...> // id, name, email, createdAt, updatedAt
+export const documents: PgTable<...> // id, name, clientId, status, sentAt, createdAt
+```
+
+From src/lib/auth.ts (existing Phase 1):
+```typescript
+export { auth, signIn, signOut } from "./auth";
+// auth() returns session | null
+// session.user?.email is the agent email
+```
+
+From src/components/ui/LogoutButton.tsx (existing Phase 1):
+```typescript
+// Already exists — import and use it in PortalNav for the sign-out action
+```
+
+Brand CSS variables (defined in globals.css):
+```css
+--navy: #1B2B4B
+--gold: #C9A84C
+--cream: #FAF9F7
+```
+IMPORTANT: Use CSS variable references (bg-[var(--navy)]) not hardcoded hex values — Tailwind v4 JIT pitfall from STATE.md.
+
+
+
+
+
+
+ Task 1: Portal layout and PortalNav
+
+ teressa-copeland-homes/src/app/portal/(protected)/layout.tsx
+ teressa-copeland-homes/src/app/portal/_components/PortalNav.tsx
+
+
+Create the directory structure: `src/app/portal/(protected)/` and `src/app/portal/_components/`.
+
+**`src/app/portal/(protected)/layout.tsx`** (server component — no "use client"):
+- Import `auth` from `@/lib/auth` and `redirect` from `next/navigation`
+- Async function: call `const session = await auth()` — if no session, `redirect("/agent/login")`
+- Render `
` wrapping `` and `{children}`
+- Import `PortalNav` from `../\_components/PortalNav`
+
+**`src/app/portal/_components/PortalNav.tsx`** ("use client" — needs interactive logout):
+- Props: `{ userEmail: string }`
+- Top-level nav bar with `bg-[var(--navy)]` background, white text
+- Left side: site name "Teressa Copeland" as text (not a logo image)
+- Nav links: "Dashboard" → `/portal/dashboard`, "Clients" → `/portal/clients` — use Next.js `` from `next/link`
+- Mark active link with a subtle gold underline: use `usePathname()` from `next/navigation` to compare current route
+- Right side: agent email (small, muted) + LogoutButton component (import from `@/components/ui/LogoutButton`)
+- Layout: `flex items-center justify-between px-6 py-3` — utilitarian, not the marketing site style
+- Use `hover:opacity-80` (Tailwind class) not `onMouseEnter`/`onMouseLeave` — server component pitfall from STATE.md does not apply here (this IS a client component) but the hover: class approach is still cleaner
+
+
+ cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1
+
+ Layout file exists with auth check and PortalNav rendering. PortalNav has nav links to /portal/dashboard and /portal/clients. TypeScript compiles cleanly.
+
+
+
+ Task 2: StatusBadge and DocumentsTable shared components
+
+ teressa-copeland-homes/src/app/portal/_components/StatusBadge.tsx
+ teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx
+
+
+**`src/app/portal/_components/StatusBadge.tsx`** (server component — no state needed):
+```typescript
+type DocumentStatus = "Draft" | "Sent" | "Viewed" | "Signed";
+
+const STATUS_STYLES: Record = {
+ Draft: "bg-gray-100 text-gray-600",
+ Sent: "bg-blue-100 text-blue-700",
+ Viewed: "bg-amber-100 text-amber-700",
+ Signed: "bg-green-100 text-green-700",
+};
+
+export function StatusBadge({ status }: { status: DocumentStatus }) {
+ return (
+
+ {status}
+
+ );
+}
+```
+
+**`src/app/portal/_components/DocumentsTable.tsx`** (server component):
+Props interface:
+```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 };
+```
+
+Render a `
` with:
+- `showClientColumn` (default true): show/hide the "Client" column — the client profile page may pass `false` since client is implied
+- Columns: Document Name, Client (conditional), Status (uses StatusBadge), Date Sent
+- Date format: use `toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })` on `sentAt` — show "—" if null
+- Table styling: `w-full text-sm`, `th` with `text-left text-xs font-medium text-gray-500 uppercase px-4 py-3`, `td` with `px-4 py-3 border-t border-gray-100`
+- No empty state needed (dashboard is always seeded; profile page handles its own empty state)
+
+
+ cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1
+
+ StatusBadge exported with correct color mapping for all 4 status values. DocumentsTable exported with DocumentRow type. TypeScript compiles cleanly.
+
+
+
+ Task 3: Client server actions (createClient, updateClient, deleteClient)
+ teressa-copeland-homes/src/lib/actions/clients.ts
+
+Create `src/lib/actions/clients.ts` with `"use server"` at the top.
+
+Imports: `z` from `zod`; `db` from `@/lib/db`; `clients` from `@/lib/db/schema`; `auth` from `@/lib/auth`; `revalidatePath` from `next/cache`; `eq` from `drizzle-orm`.
+
+**Schema:**
+```typescript
+const clientSchema = z.object({
+ name: z.string().min(1, "Name is required"),
+ email: z.string().email("Valid email address required"),
+});
+```
+
+**`createClient`** (signature: `(prevState, formData: FormData) => Promise<{error?: string; success?: boolean}>`):
+1. `const session = await auth()` — return `{ error: "Unauthorized" }` if no session
+2. Parse formData fields `name` and `email` with `clientSchema.safeParse`
+3. Return first field error if invalid
+4. `await db.insert(clients).values({ name: parsed.data.name, email: parsed.data.email })`
+5. `revalidatePath("/portal/clients")`
+6. Return `{ success: true }`
+
+**`updateClient`** (takes `id: string` in addition to prevState + formData):
+1. Same auth check
+2. Same Zod validation
+3. `await db.update(clients).set({ name, email, updatedAt: new Date() }).where(eq(clients.id, id))`
+4. `revalidatePath("/portal/clients")` + `revalidatePath("/portal/clients/" + id)`
+5. Return `{ success: true }`
+
+IMPORTANT: `updateClient` cannot receive `id` via FormData alongside useActionState. Use `bind` pattern:
+```typescript
+// In the modal component (next plan), caller uses:
+// const boundUpdateClient = updateClient.bind(null, clientId);
+// Then passes boundUpdateClient to useActionState
+// So the signature is: updateClient(id: string, prevState, formData)
+export async function updateClient(
+ id: string,
+ _prevState: { error?: string } | null,
+ formData: FormData
+)
+```
+
+**`deleteClient`** (takes `id: string`, no formData needed):
+```typescript
+export async function deleteClient(id: string): Promise<{ error?: string; success?: boolean }>
+```
+1. Auth check
+2. `await db.delete(clients).where(eq(clients.id, id))`
+3. `revalidatePath("/portal/clients")`
+4. Return `{ success: true }`
+
+PITFALL: `revalidatePath` must stay inside this `'use server'` file. Do NOT import or call it from client components.
+PITFALL: `useActionState` imported from `'react'` not `'react-dom'` — this is a known Phase 2 gotcha.
+
+
+ cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1
+
+ clients.ts exports createClient, updateClient (with bound id pattern), and deleteClient. Zod validates name and email. revalidatePath called after each mutation. TypeScript compiles cleanly.
+
+
+
+
+
+1. `npx tsc --noEmit` compiles with zero errors
+2. `src/app/portal/(protected)/layout.tsx` exists with auth() check
+3. `src/app/portal/_components/PortalNav.tsx` has Dashboard + Clients links
+4. `src/app/portal/_components/StatusBadge.tsx` exports StatusBadge with 4 color mappings
+5. `src/app/portal/_components/DocumentsTable.tsx` exports DocumentsTable with DocumentRow type
+6. `src/lib/actions/clients.ts` exports createClient, updateClient, deleteClient with 'use server'
+
+
+
+- Portal layout renders PortalNav for every /portal/(protected)/ page
+- StatusBadge maps Draft/Sent/Viewed/Signed to gray/blue/amber/green
+- DocumentsTable renders a reusable table usable by both dashboard and client profile
+- All three client actions validate input with Zod and call revalidatePath
+- TypeScript compiles cleanly
+
+
+
diff --git a/.planning/phases/03-agent-portal-shell/03-03-PLAN.md b/.planning/phases/03-agent-portal-shell/03-03-PLAN.md
new file mode 100644
index 0000000..ea3b72c
--- /dev/null
+++ b/.planning/phases/03-agent-portal-shell/03-03-PLAN.md
@@ -0,0 +1,338 @@
+---
+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 `
+
+ 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
+
+
+
diff --git a/.planning/phases/03-agent-portal-shell/03-04-PLAN.md b/.planning/phases/03-agent-portal-shell/03-04-PLAN.md
new file mode 100644
index 0000000..2275269
--- /dev/null
+++ b/.planning/phases/03-agent-portal-shell/03-04-PLAN.md
@@ -0,0 +1,274 @@
+---
+phase: 03-agent-portal-shell
+plan: 04
+type: execute
+wave: 4
+depends_on: [03-03]
+files_modified:
+ - teressa-copeland-homes/src/app/portal/(protected)/clients/[id]/page.tsx
+ - teressa-copeland-homes/src/app/portal/_components/ConfirmDialog.tsx
+autonomous: false
+requirements: [CLIENT-03]
+
+must_haves:
+ truths:
+ - "Agent clicks a client card and lands on /portal/clients/{id} — sees the client's name, email, and an Edit button"
+ - "Agent clicks Edit on the client profile — a modal opens pre-filled with the client's current name and email"
+ - "Agent submits the edit modal — the profile page updates with the new values"
+ - "Agent clicks Delete Client — a confirmation dialog appears; confirming deletes the client and redirects to /portal/clients"
+ - "Documents section on the profile page shows the same table style as the dashboard (status badge, date sent)"
+ - "Agent can verify the full portal flow end-to-end in the browser: login → dashboard → clients → profile → edit → delete"
+ artifacts:
+ - path: "teressa-copeland-homes/src/app/portal/(protected)/clients/[id]/page.tsx"
+ provides: "Client profile page: client header + edit/delete actions + documents table"
+ - path: "teressa-copeland-homes/src/app/portal/_components/ConfirmDialog.tsx"
+ provides: "Reusable confirmation dialog for destructive actions"
+ key_links:
+ - from: "clients/[id]/page.tsx"
+ to: "updateClient (bound with id)"
+ via: "ClientModal with mode='edit' + clientId prop"
+ pattern: "ClientModal.*mode.*edit"
+ - from: "ConfirmDialog confirm button"
+ to: "deleteClient server action"
+ via: "form action calling deleteClient(id) then redirect to /portal/clients"
+ pattern: "deleteClient"
+ - from: "clients/[id]/page.tsx"
+ to: "DocumentsTable"
+ via: "documents query filtered by clientId"
+ pattern: "DocumentsTable.*showClientColumn.*false"
+---
+
+
+Build the client profile page with edit and delete capabilities, then run a human verification checkpoint covering the complete Phase 3 portal flow.
+
+Purpose: CLIENT-03 requires the agent to view a client's profile and associated documents. Edit + delete round out the client management feature set. The checkpoint verifies all 4 Phase 3 success criteria in a real browser.
+Output: Working /portal/clients/[id] page with edit modal, delete confirmation, and documents table; human approval of complete Phase 3 portal.
+
+
+
+@/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-03-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;
+};
+export function DocumentsTable({ rows, showClientColumn }: Props): JSX.Element
+// Pass showClientColumn={false} on the profile page — client is implied
+```
+
+From src/app/portal/_components/ClientModal.tsx:
+```typescript
+// Already supports edit mode:
+type Props = {
+ isOpen: boolean;
+ onClose: () => void;
+ mode?: "create" | "edit";
+ clientId?: string;
+ defaultName?: string;
+ defaultEmail?: string;
+};
+export function ClientModal(props: Props): JSX.Element | null
+```
+
+From src/lib/actions/clients.ts:
+```typescript
+// updateClient uses bind pattern — in profile page:
+// const boundUpdate = updateClient.bind(null, client.id);
+export async function updateClient(
+ id: string,
+ _prevState: { error?: string } | null,
+ formData: FormData
+): Promise<{ error?: string; success?: boolean }>
+
+export async function deleteClient(id: string): Promise<{ error?: string; success?: boolean }>
+```
+
+Next.js 16 dynamic params (MUST await):
+```typescript
+export default async function ClientProfilePage({
+ params,
+}: {
+ params: Promise<{ id: string }>;
+}) {
+ const { id } = await params; // CRITICAL — sync access throws at build time
+}
+```
+
+Drizzle query for client profile:
+```typescript
+import { eq, desc, sql } from "drizzle-orm";
+
+// Fetch client
+const [client] = await db.select().from(clients).where(eq(clients.id, id));
+if (!client) notFound(); // import from next/navigation
+
+// Fetch documents for this client
+const docs = await db
+ .select({
+ id: documents.id,
+ name: documents.name,
+ status: documents.status,
+ sentAt: documents.sentAt,
+ clientId: documents.clientId,
+ clientName: sql`${clients.name}`,
+ })
+ .from(documents)
+ .leftJoin(clients, eq(documents.clientId, clients.id))
+ .where(eq(documents.clientId, id))
+ .orderBy(desc(documents.createdAt));
+```
+
+
+
+
+
+
+ Task 1: Client profile page with edit/delete and documents table
+
+ teressa-copeland-homes/src/app/portal/(protected)/clients/[id]/page.tsx
+ teressa-copeland-homes/src/app/portal/_components/ConfirmDialog.tsx
+
+
+**`src/app/portal/_components/ConfirmDialog.tsx`** ("use client"):
+Props: `{ isOpen: boolean; title: string; message: string; onConfirm: () => void; onCancel: () => void; confirmLabel?: string }`
+
+- Overlay + centered dialog box (same overlay pattern as ClientModal: `fixed inset-0 z-50 bg-black/40`)
+- Title, message text, Cancel button + Confirm button
+- Confirm button: red/destructive styling (`bg-red-600 text-white hover:bg-red-700`)
+- `confirmLabel` defaults to "Delete"
+
+**`src/app/portal/(protected)/clients/[id]/page.tsx`**:
+
+This page needs both server data AND client modal/dialog state. Use the same pattern as clients/page.tsx: server component fetches data, renders a "use client" inner component with the interactive parts.
+
+Server component (`default export`):
+1. `const { id } = await params` — CRITICAL: params is a Promise in Next.js 16
+2. Fetch client by id (use `notFound()` if not found)
+3. Fetch documents for this client with LEFT JOIN
+4. Render ``
+
+Inline "use client" component (`ClientProfileClient`) in the same file OR in `_components/ClientProfileClient.tsx`:
+Props: `{ client: { id: string; name: string; email: string }; docs: DocumentRow[] }`
+
+State: `const [isEditOpen, setIsEditOpen] = useState(false)` and `const [isDeleteOpen, setIsDeleteOpen] = useState(false)`
+
+Layout (two-section layout per CONTEXT.md decision):
+- **Header section**: white card (`bg-white rounded-xl shadow-sm p-6 mb-6`)
+ - Back link: arrow + "Back to Clients" as a Next.js `` with `text-[var(--gold)] text-sm`
+ - Client name as `
` (`text-[var(--navy)] text-2xl font-semibold`)
+ - Email below the name (gray text)
+ - Two buttons on the right: "Edit" button (opens edit modal) + "Delete Client" button (opens confirm dialog)
+ - Edit button: outlined style (`border border-[var(--navy)] text-[var(--navy)] px-4 py-2 rounded-lg text-sm`)
+ - Delete button: red outlined style (`border border-red-300 text-red-600 px-4 py-2 rounded-lg text-sm`)
+- **Documents section**: white card (`bg-white rounded-xl shadow-sm p-6`)
+ - `
Documents
` heading
+ - If `docs.length === 0`: "No documents yet." gray text
+ - If `docs.length > 0`: ``
+
+**Edit modal**: ` setIsEditOpen(false)} mode="edit" clientId={client.id} defaultName={client.name} defaultEmail={client.email} />`
+
+**Delete flow**:
+- ` setIsDeleteOpen(false)} />`
+- `handleDelete`: call `await deleteClient(client.id)` then `router.push("/portal/clients")` — import `useRouter` from `next/navigation`
+
+PITFALL: `deleteClient` is a server action. To call it from a client component, import it and call it directly in an async event handler (Next.js 15+ allows this). The action runs server-side; the client awaits the promise.
+
+PITFALL: `await params` before any usage of `id` — synchronous access causes build failure in Next.js 16.
+
+
+ cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1
+
+ Client profile page renders with client details header, edit/delete buttons, and documents table. ConfirmDialog component exported. TypeScript compiles cleanly.
+
+
+
+ Task 2: Full Phase 3 portal verification in browser
+ Start the dev server and walk through the complete portal verification checklist below.
+ none
+ Human approval of all 5 checklist sections below.
+ Agent approves all 5 portal sections: authentication, dashboard, clients list, client profile, navigation.
+
+Complete Phase 3 Agent Portal Shell:
+- /portal/dashboard — documents table with status filter dropdown; 4 seeded placeholder documents visible
+- /portal/clients — card grid with 2 seeded clients; "+ Add Client" button opens modal
+- /portal/clients/[id] — client profile with edit button (opens pre-filled modal), delete button (confirmation dialog), documents table for that client
+- Top nav on all portal pages with Dashboard and Clients links + sign-out button
+- Middleware protects all /portal/* routes
+
+
+Start the dev server: `cd teressa-copeland-homes && npm run dev`
+
+**1. Authentication and routing (Phase 1 preserved)**
+- Visit http://localhost:3000/portal/dashboard while logged out → should redirect to /agent/login
+- Log in with the seeded agent account → should land on /portal/dashboard (not /agent/dashboard)
+
+**2. Dashboard (DASH-01 + DASH-02)**
+- Verify you see a table with 4 documents: "Purchase Agreement - 842 Maple Dr", "Seller Disclosure - 842 Maple Dr", "Buyer Rep Agreement", "Purchase Agreement - 1205 Oak Ave"
+- Verify status badges: two "Signed" (green), one "Sent" (blue), one "Draft" (gray)
+- Verify Client column shows "Sarah Johnson" and "Mike Torres"
+- Select "Signed" in the filter dropdown → URL changes to ?status=Signed → table shows only 2 rows
+- Select "All statuses" → table returns to 4 rows
+
+**3. Clients list (CLIENT-01 + CLIENT-02)**
+- Visit http://localhost:3000/portal/clients
+- Verify card grid shows 2 client cards: Sarah Johnson and Mike Torres
+- Each card shows name, email, document count, and last activity date
+- Click "+ Add Client" → modal opens with name + email fields
+- Fill in a test client (e.g., "Test User" + "test@example.com") → click "Add Client" → modal closes → new card appears in grid
+
+**4. Client profile (CLIENT-03)**
+- Click on "Sarah Johnson" card → lands on /portal/clients/[sarah's id]
+- Verify header shows "Sarah Johnson", her email, Edit and Delete Client buttons
+- Verify documents section shows Sarah's 2 documents with status badges
+- Click "Edit" → modal opens pre-filled with "Sarah Johnson" and her email → change name → save → header updates
+- Click "Delete Client" → confirmation dialog appears → click Cancel → client is NOT deleted
+- (Optional) Delete the test client you created in step 3 → confirm → redirected to /portal/clients
+
+**5. Navigation**
+- Verify top nav is visible on all three portal pages
+- Click "Dashboard" nav link → navigates to /portal/dashboard
+- Click "Clients" nav link → navigates to /portal/clients
+- Current page should be highlighted in nav (gold underline or similar)
+- Sign out → redirects to /agent/login
+
+ Type "approved" if all checklist items pass. Describe any issues found and I will fix them before you approve.
+
+
+
+
+
+1. `npx tsc --noEmit` produces zero TypeScript errors
+2. Client profile page uses `await params` (not synchronous params.id)
+3. ConfirmDialog shows title, message, Cancel + Delete buttons
+4. Edit modal opens pre-filled with client's current name and email
+5. Human verifier approves all 5 portal sections in the browser
+
+
+
+- Agent can see client profile with name, email, and associated documents (CLIENT-03)
+- Agent can edit client name/email via modal — profile updates immediately
+- Agent can delete a client via confirmation dialog — redirected to clients list after
+- All four Phase 3 success criteria confirmed in real browser by agent
+- TypeScript compiles cleanly with zero errors
+
+
+