Files
red/.planning/phases/03-agent-portal-shell/03-02-PLAN.md

12 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
03-agent-portal-shell 02 execute 2
03-01
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
true
CLIENT-01
DASH-01
DASH-02
truths artifacts key_links
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
path provides contains
teressa-copeland-homes/src/app/portal/(protected)/layout.tsx Portal route-group layout with auth check and PortalNav PortalNav
path provides
teressa-copeland-homes/src/app/portal/_components/PortalNav.tsx Horizontal top nav with links to Dashboard and Clients
path provides
teressa-copeland-homes/src/app/portal/_components/StatusBadge.tsx Color-coded pill for Draft/Sent/Viewed/Signed
path provides
teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx Reusable documents table used by dashboard + client profile
path provides exports
teressa-copeland-homes/src/lib/actions/clients.ts createClient, updateClient, deleteClient server actions
createClient
updateClient
deleteClient
from to via pattern
portal/(protected)/layout.tsx auth() session check import from @/lib/auth auth()
from to via pattern
ClientModal.tsx (next plan) createClient action import from @/lib/actions/clients createClient
from to via pattern
DocumentsTable.tsx StatusBadge import StatusBadge 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.

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

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

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

// Already exists — import and use it in PortalNav for the sign-out action

Brand CSS variables (defined in globals.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 <div className="min-h-screen bg-[var(--cream)]"> wrapping <PortalNav userEmail={session.user?.email ?? ""} /> and <main className="max-w-7xl mx-auto px-6 py-8">{children}</main>
  • 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 <Link> 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<DocumentStatus, string> = { 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 ( <span className={inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${STATUS_STYLES[status]}}> {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 <table> 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:

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:

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

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'

<success_criteria>

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