275 lines
13 KiB
Markdown
275 lines
13 KiB
Markdown
|
|
---
|
||
|
|
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"
|
||
|
|
---
|
||
|
|
|
||
|
|
<objective>
|
||
|
|
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.
|
||
|
|
</objective>
|
||
|
|
|
||
|
|
<execution_context>
|
||
|
|
@/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md
|
||
|
|
@/Users/ccopeland/.claude/get-shit-done/templates/summary.md
|
||
|
|
</execution_context>
|
||
|
|
|
||
|
|
<context>
|
||
|
|
@.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
|
||
|
|
|
||
|
|
<interfaces>
|
||
|
|
<!-- From Plans 02 and 03 — use these exactly -->
|
||
|
|
|
||
|
|
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<string>`${clients.name}`,
|
||
|
|
})
|
||
|
|
.from(documents)
|
||
|
|
.leftJoin(clients, eq(documents.clientId, clients.id))
|
||
|
|
.where(eq(documents.clientId, id))
|
||
|
|
.orderBy(desc(documents.createdAt));
|
||
|
|
```
|
||
|
|
</interfaces>
|
||
|
|
</context>
|
||
|
|
|
||
|
|
<tasks>
|
||
|
|
|
||
|
|
<task type="auto">
|
||
|
|
<name>Task 1: Client profile page with edit/delete and documents table</name>
|
||
|
|
<files>
|
||
|
|
teressa-copeland-homes/src/app/portal/(protected)/clients/[id]/page.tsx
|
||
|
|
teressa-copeland-homes/src/app/portal/_components/ConfirmDialog.tsx
|
||
|
|
</files>
|
||
|
|
<action>
|
||
|
|
**`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 `<ClientProfileClient client={client} docs={docs} />`
|
||
|
|
|
||
|
|
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 `<Link href="/portal/clients">` with `text-[var(--gold)] text-sm`
|
||
|
|
- Client name as `<h1>` (`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`)
|
||
|
|
- `<h2>Documents</h2>` heading
|
||
|
|
- If `docs.length === 0`: "No documents yet." gray text
|
||
|
|
- If `docs.length > 0`: `<DocumentsTable rows={docs} showClientColumn={false} />`
|
||
|
|
|
||
|
|
**Edit modal**: `<ClientModal isOpen={isEditOpen} onClose={() => setIsEditOpen(false)} mode="edit" clientId={client.id} defaultName={client.name} defaultEmail={client.email} />`
|
||
|
|
|
||
|
|
**Delete flow**:
|
||
|
|
- `<ConfirmDialog isOpen={isDeleteOpen} title="Delete client?" message={"Delete " + client.name + "? This will also delete all associated documents. This cannot be undone."} onConfirm={handleDelete} onCancel={() => 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.
|
||
|
|
</action>
|
||
|
|
<verify>
|
||
|
|
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1</automated>
|
||
|
|
</verify>
|
||
|
|
<done>Client profile page renders with client details header, edit/delete buttons, and documents table. ConfirmDialog component exported. TypeScript compiles cleanly.</done>
|
||
|
|
</task>
|
||
|
|
|
||
|
|
<task type="checkpoint:human-verify" gate="blocking">
|
||
|
|
<name>Task 2: Full Phase 3 portal verification in browser</name>
|
||
|
|
<action>Start the dev server and walk through the complete portal verification checklist below.</action>
|
||
|
|
<files>none</files>
|
||
|
|
<verify>Human approval of all 5 checklist sections below.</verify>
|
||
|
|
<done>Agent approves all 5 portal sections: authentication, dashboard, clients list, client profile, navigation.</done>
|
||
|
|
<what-built>
|
||
|
|
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
|
||
|
|
</what-built>
|
||
|
|
<how-to-verify>
|
||
|
|
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
|
||
|
|
</how-to-verify>
|
||
|
|
<resume-signal>Type "approved" if all checklist items pass. Describe any issues found and I will fix them before you approve.</resume-signal>
|
||
|
|
</task>
|
||
|
|
|
||
|
|
</tasks>
|
||
|
|
|
||
|
|
<verification>
|
||
|
|
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
|
||
|
|
</verification>
|
||
|
|
|
||
|
|
<success_criteria>
|
||
|
|
- 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
|
||
|
|
</success_criteria>
|
||
|
|
|
||
|
|
<output>
|
||
|
|
After completion, create `.planning/phases/03-agent-portal-shell/03-04-SUMMARY.md`
|
||
|
|
</output>
|