From 87fa28c4d138006f16a7426912f466f12a79dd6a Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 21 Sep 2026 13:29:31 +0200 Subject: [PATCH] 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 here. --- src/app/admin/clients/[clientId]/page.tsx | 101 ++++++++++++++++++++++ src/app/admin/clients/page.tsx | 23 +++-- 2 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 src/app/admin/clients/[clientId]/page.tsx diff --git a/src/app/admin/clients/[clientId]/page.tsx b/src/app/admin/clients/[clientId]/page.tsx new file mode 100644 index 0000000..0da046f --- /dev/null +++ b/src/app/admin/clients/[clientId]/page.tsx @@ -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(clientId); + } catch { + notFound(); + } + + const projects = await pb + .collection("projects") + .getFullList({ + filter: `client = "${clientId}" && status != "archived"`, + sort: "name", + }); + + return ( + + + + {client.slug} + + + +
+
+
+ Request links panel lands in Task 2. +
+ +
+
+

+ Projects +

+

+ Each project is hidden from the public request page by + default. Flip the toggle to show it. +

+
+
    + {projects.map((p) => ( +
  • +
    +
    {p.name}
    +
    + {p.slug} · {p.status} +
    +
    + +
  • + ))} + {projects.length === 0 && ( +
  • + No projects yet for this client. +
  • + )} +
+
+
+
+
+ ); +} diff --git a/src/app/admin/clients/page.tsx b/src/app/admin/clients/page.tsx index b2db361..466a201 100644 --- a/src/app/admin/clients/page.tsx +++ b/src/app/admin/clients/page.tsx @@ -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() {
- - - {client.name} - - {client.slug} + + + {client.name} + + {client.slug}
{projectCount}