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:
@@ -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} />;
|
||||
}
|
||||
@@ -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">←</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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user