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 |
|
|
true |
|
|
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.mdFrom 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
authfrom@/lib/authandredirectfromnext/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
PortalNavfrom../\_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>fromnext/link - Mark active link with a subtle gold underline: use
usePathname()fromnext/navigationto 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) notonMouseEnter/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.
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 passfalsesince 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" })onsentAt— show "—" if null - Table styling:
w-full text-sm,thwithtext-left text-xs font-medium text-gray-500 uppercase px-4 py-3,tdwithpx-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.
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}>):
const session = await auth()— return{ error: "Unauthorized" }if no session- Parse formData fields
nameandemailwithclientSchema.safeParse - Return first field error if invalid
await db.insert(clients).values({ name: parsed.data.name, email: parsed.data.email })revalidatePath("/portal/clients")- Return
{ success: true }
updateClient (takes id: string in addition to prevState + formData):
- Same auth check
- Same Zod validation
await db.update(clients).set({ name, email, updatedAt: new Date() }).where(eq(clients.id, id))revalidatePath("/portal/clients")+revalidatePath("/portal/clients/" + id)- 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 }>
- Auth check
await db.delete(clients).where(eq(clients.id, id))revalidatePath("/portal/clients")- 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.
<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>