feat(admin): client admin page with project picker toggles

Phase 9b Task 1. /admin/clients/[clientId] shows the client's active
projects with a 'List on request page' toggle next to each. The
existing /admin/clients list now links each client name into the new
page.

Request-links panel placeholder; Task 2 wires the real
<IntakeLinksPanel mode='client'> here.
This commit is contained in:
Dave 2026-09-21 13:29:31 +02:00
parent 06f3dbafce
commit 87fa28c4d1
2 changed files with 115 additions and 9 deletions

View file

@ -0,0 +1,101 @@
import { notFound } from "next/navigation";
import { AppShell } from "@/components/shell/app-shell";
import { TopBar } from "@/components/shell/top-bar";
import { ShowInIntakeToggle } from "@/components/admin/show-in-intake-toggle";
import { getAdminPb } from "@/lib/pb/admin";
import type { ClientRecord, ProjectRecord } from "@/lib/pb/types";
/**
* Per-client request-link admin page.
*
* Two stacked panels: a Request-links panel (the same component Phase 8
* shipped for the project page, now bound to client instead — wired in
* Task 2) and a Projects list with the "List on request page" toggle
* next to each.
*
* The Phase 8 /admin/projects/[projectId] page keeps its own
* Request-links panel for the legacy per-project link shape. The picker
* is the new default; the legacy path is for owners who haven't
* migrated yet.
*
* Owner-only by virtue of the admin layout, which is already enforced
* upstream.
*/
export default async function ClientAdminPage({
params,
}: {
params: Promise<{ clientId: string }>;
}) {
const { clientId } = await params;
const pb = await getAdminPb();
let client: ClientRecord;
try {
client = await pb.collection("clients").getOne<ClientRecord>(clientId);
} catch {
notFound();
}
const projects = await pb
.collection("projects")
.getFullList<ProjectRecord>({
filter: `client = "${clientId}" && status != "archived"`,
sort: "name",
});
return (
<AppShell role="owner" currentPath="/admin/clients">
<TopBar title={client.name} subtitle="Request links and project picker">
<span className="text-xs text-text-muted">
<code>{client.slug}</code>
</span>
</TopBar>
<div className="flex-1 overflow-y-auto p-6">
<div className="mx-auto max-w-2xl space-y-4">
<section className="rounded-[var(--radius-tile)] border border-border-subtle p-4 text-xs text-text-muted">
Request links panel lands in Task 2.
</section>
<section className="space-y-3 rounded-[var(--radius-tile)] border border-border-subtle p-4">
<div>
<h2 className="text-sm font-semibold text-text-primary">
Projects
</h2>
<p className="mt-0.5 text-xs text-text-muted">
Each project is hidden from the public request page by
default. Flip the toggle to show it.
</p>
</div>
<ul className="space-y-2">
{projects.map((p) => (
<li
key={p.id}
className="flex items-center justify-between gap-3 rounded-lg border border-border-subtle p-3"
>
<div className="min-w-0">
<div className="text-sm text-text-primary">{p.name}</div>
<div className="text-xs text-text-muted">
<code>{p.slug}</code> · {p.status}
</div>
</div>
<ShowInIntakeToggle
projectId={p.id}
initial={Boolean(p.show_in_intake)}
/>
</li>
))}
{projects.length === 0 && (
<li className="text-xs text-text-muted">
No projects yet for this client.
</li>
)}
</ul>
</section>
</div>
</div>
</AppShell>
);
}

View file

@ -1,3 +1,5 @@
import Link from "next/link";
import { AppShell } from "@/components/shell/app-shell";
import { TopBar } from "@/components/shell/top-bar";
import { AdminTable, AdminCell } from "@/components/admin/admin-table";
@ -23,15 +25,18 @@ export default async function AdminClientsPage() {
<tr key={client.id}>
<AdminCell>
<div className="flex items-center gap-2.5">
<span
aria-hidden
className="h-2.5 w-2.5 rounded-full"
style={{ background: client.brand_color || "var(--brand)" }}
/>
<span className="font-medium text-text-primary">
{client.name}
</span>
<code className="text-xs text-text-muted">{client.slug}</code>
<span
aria-hidden
className="h-2.5 w-2.5 rounded-full"
style={{ background: client.brand_color || "var(--brand)" }}
/>
<Link
href={`/admin/clients/${client.id}`}
className="font-medium text-text-primary hover:underline"
>
{client.name}
</Link>
<code className="text-xs text-text-muted">{client.slug}</code>
</div>
</AdminCell>
<AdminCell className="tabular">{projectCount}</AdminCell>