--- 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 After completion, create `.planning/phases/03-agent-portal-shell/03-02-SUMMARY.md`