Files

16 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 03 execute 3
03-01
03-02
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
true
CLIENT-01
CLIENT-02
DASH-01
DASH-02
truths artifacts key_links
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
path provides
teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx Dashboard with documents table, status filter, date sort
path provides
teressa-copeland-homes/src/app/portal/(protected)/clients/page.tsx Clients card grid with '+ Add Client' modal trigger and empty state
path provides
teressa-copeland-homes/src/app/portal/_components/ClientCard.tsx Individual client card with name, email, doc count, last activity
path provides
teressa-copeland-homes/src/app/portal/_components/ClientModal.tsx Create/edit modal using useActionState with createClient action
path provides
teressa-copeland-homes/scripts/seed.ts Seed data: 2 clients + 4 placeholder documents
from to via pattern
clients/page.tsx ClientModal useState(isOpen) + <ClientModal isOpen={isOpen} onClose={() => setIsOpen(false)} /> ClientModal
from to via pattern
ClientModal createClient server action useActionState(createClient, null) useActionState.*createClient
from to via pattern
dashboard/page.tsx DocumentsTable pass rows prop from Drizzle JOIN query DocumentsTable
from to via pattern
dashboard/page.tsx searchParams Drizzle WHERE clause status filter applied in server component before passing to DocumentsTable 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.

<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-02-SUMMARY.md

From src/app/portal/_components/DocumentsTable.tsx:

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:

export function StatusBadge({ status }: { status: "Draft" | "Sent" | "Viewed" | "Signed" }): JSX.Element

From src/lib/actions/clients.ts:

export async function createClient(
  _prevState: { error?: string } | null,
  formData: FormData
): Promise<{ error?: string; success?: boolean }>

From src/lib/db/schema.ts (Plan 01):

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

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<number>`count(${documents.id})::int`,
    lastActivity: sql<Date | null>`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):

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

  • <h1> with "Dashboard" heading
  • Filter bar: a <StatusFilterBar> — inline a simple client component OR use a plain <form> approach:
    • Since filter state lives in the URL, use a plain <select> that triggers navigation via a client component wrapper. Create a small inline "use client" component DashboardFilters in the SAME FILE (below the default export) that uses useRouter + useSearchParams to push ?status=X on change:
      "use client";
      function DashboardFilters({ currentStatus }: { currentStatus?: string }) {
        const router = useRouter();
        return (
          <select
            value={currentStatus ?? ""}
            onChange={(e) => 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"
          >
            <option value="">All statuses</option>
            <option value="Draft">Draft</option>
            <option value="Sent">Sent</option>
            <option value="Viewed">Viewed</option>
            <option value="Signed">Signed</option>
          </select>
        );
      }
      
    • Import useRouter and useSearchParams from next/navigation in the inline client component
  • Below the filter bar: <DocumentsTable rows={filteredRows} showClientColumn={true} />
  • 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 <Link href={"/portal/clients/" + id}> — 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 <ClientsPageClient clients={clientRows} />
  • 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 — <p>No clients yet.</p> + <button onClick={() => setIsOpen(true)}>+ Add your first client</button> per CONTEXT.md decision
  • If clients exist: <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"> with <ClientCard> for each
  • <ClientModal isOpen={isOpen} onClose={() => 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:

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

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'

<success_criteria>

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