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:
parent
06f3dbafce
commit
87fa28c4d1
2 changed files with 115 additions and 9 deletions
101
src/app/admin/clients/[clientId]/page.tsx
Normal file
101
src/app/admin/clients/[clientId]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue