feat(03-04): add client profile page with edit/delete and documents table

- 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
This commit is contained in:
Chandler Copeland
2026-03-19 16:58:21 -06:00
parent b94720bde8
commit b186ac5f38
3 changed files with 187 additions and 0 deletions

View File

@@ -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<string>`${clients.name}`,
})
.from(documents)
.leftJoin(clients, eq(documents.clientId, clients.id))
.where(eq(documents.clientId, id))
.orderBy(desc(documents.createdAt));
return <ClientProfileClient client={client} docs={docs} />;
}

View File

@@ -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 (
<div>
{/* Header card */}
<div className="bg-white rounded-xl shadow-sm p-6 mb-6">
<div className="mb-4">
<Link
href="/portal/clients"
className="text-[var(--gold)] text-sm hover:opacity-80 flex items-center gap-1"
>
<span aria-hidden="true">&larr;</span> Back to Clients
</Link>
</div>
<div className="flex items-start justify-between">
<div>
<h1 className="text-[var(--navy)] text-2xl font-semibold">
{client.name}
</h1>
<p className="text-gray-500 text-sm mt-1">{client.email}</p>
</div>
<div className="flex gap-3">
<button
type="button"
onClick={() => setIsEditOpen(true)}
className="border border-[var(--navy)] text-[var(--navy)] px-4 py-2 rounded-lg text-sm hover:bg-[var(--navy)] hover:text-white transition-colors"
>
Edit
</button>
<button
type="button"
onClick={() => setIsDeleteOpen(true)}
className="border border-red-300 text-red-600 px-4 py-2 rounded-lg text-sm hover:bg-red-50 transition-colors"
>
Delete Client
</button>
</div>
</div>
</div>
{/* Documents card */}
<div className="bg-white rounded-xl shadow-sm p-6">
<h2 className="text-[var(--navy)] text-lg font-semibold mb-4">
Documents
</h2>
{docs.length === 0 ? (
<p className="text-gray-500 text-sm">No documents yet.</p>
) : (
<DocumentsTable rows={docs} showClientColumn={false} />
)}
</div>
{/* Edit modal */}
<ClientModal
isOpen={isEditOpen}
onClose={() => setIsEditOpen(false)}
mode="edit"
clientId={client.id}
defaultName={client.name}
defaultEmail={client.email}
/>
{/* Delete confirmation */}
<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)}
/>
</div>
);
}

View File

@@ -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 (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-white rounded-xl shadow-xl p-6 w-full max-w-md">
<h2 className="text-[var(--navy)] text-lg font-semibold mb-2">{title}</h2>
<p className="text-gray-600 text-sm mb-6">{message}</p>
<div className="flex justify-end gap-3">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 text-sm text-gray-600 border border-gray-200 rounded-lg hover:bg-gray-50"
>
Cancel
</button>
<button
type="button"
onClick={onConfirm}
className="px-4 py-2 text-sm text-white bg-red-600 rounded-lg hover:bg-red-700"
>
{confirmLabel}
</button>
</div>
</div>
</div>
);
}