From b186ac5f3857d40794cac89f84c50966c8a87d61 Mon Sep 17 00:00:00 2001 From: Chandler Copeland Date: Thu, 19 Mar 2026 16:58:21 -0600 Subject: [PATCH] feat(03-04): add client profile page with edit/delete and documents table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create ConfirmDialog component with overlay, title, message, cancel/confirm buttons - Create ClientProfilePage server component (awaits params Promise — Next.js 16) - Create ClientProfileClient client component with edit modal and delete confirmation - Documents section uses DocumentsTable with showClientColumn={false} - deleteClient called directly from async event handler in client component --- .../portal/(protected)/clients/[id]/page.tsx | 32 +++++ .../_components/ClientProfileClient.tsx | 109 ++++++++++++++++++ .../app/portal/_components/ConfirmDialog.tsx | 46 ++++++++ 3 files changed, 187 insertions(+) create mode 100644 teressa-copeland-homes/src/app/portal/(protected)/clients/[id]/page.tsx create mode 100644 teressa-copeland-homes/src/app/portal/_components/ClientProfileClient.tsx create mode 100644 teressa-copeland-homes/src/app/portal/_components/ConfirmDialog.tsx diff --git a/teressa-copeland-homes/src/app/portal/(protected)/clients/[id]/page.tsx b/teressa-copeland-homes/src/app/portal/(protected)/clients/[id]/page.tsx new file mode 100644 index 0000000..e459618 --- /dev/null +++ b/teressa-copeland-homes/src/app/portal/(protected)/clients/[id]/page.tsx @@ -0,0 +1,32 @@ +import { db } from "@/lib/db"; +import { clients, documents } from "@/lib/db/schema"; +import { eq, desc, sql } from "drizzle-orm"; +import { notFound } from "next/navigation"; +import { ClientProfileClient } from "../../../_components/ClientProfileClient"; + +export default async function ClientProfilePage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + + const [client] = await db.select().from(clients).where(eq(clients.id, id)); + if (!client) notFound(); + + const docs = await db + .select({ + id: documents.id, + name: documents.name, + status: documents.status, + sentAt: documents.sentAt, + clientId: documents.clientId, + clientName: sql`${clients.name}`, + }) + .from(documents) + .leftJoin(clients, eq(documents.clientId, clients.id)) + .where(eq(documents.clientId, id)) + .orderBy(desc(documents.createdAt)); + + return ; +} diff --git a/teressa-copeland-homes/src/app/portal/_components/ClientProfileClient.tsx b/teressa-copeland-homes/src/app/portal/_components/ClientProfileClient.tsx new file mode 100644 index 0000000..5f750ba --- /dev/null +++ b/teressa-copeland-homes/src/app/portal/_components/ClientProfileClient.tsx @@ -0,0 +1,109 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { ClientModal } from "./ClientModal"; +import { ConfirmDialog } from "./ConfirmDialog"; +import { DocumentsTable } from "./DocumentsTable"; +import { deleteClient } from "@/lib/actions/clients"; + +type DocumentRow = { + id: string; + name: string; + clientName: string | null; + status: "Draft" | "Sent" | "Viewed" | "Signed"; + sentAt: Date | null; + clientId: string; +}; + +type Props = { + client: { id: string; name: string; email: string }; + docs: DocumentRow[]; +}; + +export function ClientProfileClient({ client, docs }: Props) { + const router = useRouter(); + const [isEditOpen, setIsEditOpen] = useState(false); + const [isDeleteOpen, setIsDeleteOpen] = useState(false); + + async function handleDelete() { + await deleteClient(client.id); + router.push("/portal/clients"); + } + + return ( +
+ {/* Header card */} +
+
+ + Back to Clients + +
+
+
+

+ {client.name} +

+

{client.email}

+
+
+ + +
+
+
+ + {/* Documents card */} +
+

+ Documents +

+ {docs.length === 0 ? ( +

No documents yet.

+ ) : ( + + )} +
+ + {/* Edit modal */} + setIsEditOpen(false)} + mode="edit" + clientId={client.id} + defaultName={client.name} + defaultEmail={client.email} + /> + + {/* Delete confirmation */} + setIsDeleteOpen(false)} + /> +
+ ); +} diff --git a/teressa-copeland-homes/src/app/portal/_components/ConfirmDialog.tsx b/teressa-copeland-homes/src/app/portal/_components/ConfirmDialog.tsx new file mode 100644 index 0000000..56b18e2 --- /dev/null +++ b/teressa-copeland-homes/src/app/portal/_components/ConfirmDialog.tsx @@ -0,0 +1,46 @@ +"use client"; + +type Props = { + isOpen: boolean; + title: string; + message: string; + onConfirm: () => void; + onCancel: () => void; + confirmLabel?: string; +}; + +export function ConfirmDialog({ + isOpen, + title, + message, + onConfirm, + onCancel, + confirmLabel = "Delete", +}: Props) { + if (!isOpen) return null; + + return ( +
+
+

{title}

+

{message}

+
+ + +
+
+
+ ); +}