diff --git a/src/app/admin/clients/page.tsx b/src/app/admin/clients/page.tsx new file mode 100644 index 0000000..b2db361 --- /dev/null +++ b/src/app/admin/clients/page.tsx @@ -0,0 +1,53 @@ +import { AppShell } from "@/components/shell/app-shell"; +import { TopBar } from "@/components/shell/top-bar"; +import { AdminTable, AdminCell } from "@/components/admin/admin-table"; +import { NewClientForm, ToggleClientActive } from "@/components/admin/client-form"; +import { Pill } from "@/components/ui/pill"; +import { loadClients } from "@/lib/data/admin"; + +export default async function AdminClientsPage() { + const rows = await loadClients(); + + return ( + + + + + + + + {rows.map(({ client, projectCount, managerCount }) => ( + + + + + + {client.name} + + {client.slug} + + + {projectCount} + {managerCount} + + + {client.active ? "Active" : "Inactive"} + + + + + + + ))} + + + + ); +} \ No newline at end of file diff --git a/src/components/admin/client-form.tsx b/src/components/admin/client-form.tsx new file mode 100644 index 0000000..edac38f --- /dev/null +++ b/src/components/admin/client-form.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { useState, useTransition } from "react"; +import { Plus } from "lucide-react"; + +import { createClient, updateClient } from "@/app/actions/clients"; +import type { ClientRecord } from "@/lib/pb/types"; + +export function NewClientForm() { + const [open, setOpen] = useState(false); + const [name, setName] = useState(""); + const [brandColor, setBrandColor] = useState("#2f6dad"); + const [pending, startTransition] = useTransition(); + const [error, setError] = useState(null); + + function submit(event: React.FormEvent) { + event.preventDefault(); + if (!name.trim()) return; + setError(null); + + startTransition(async () => { + try { + await createClient({ name, brandColor }); + setName(""); + setOpen(false); + } catch (e) { + setError(e instanceof Error ? e.message : "Could not create that client"); + } + }); + } + + if (!open) { + return ( + setOpen(true)} + className="inline-flex items-center gap-1.5 rounded-lg bg-brand px-3 py-1.5 text-sm font-medium text-brand-fg transition-colors hover:bg-brand-hover" + > + + New client + + ); + } + + return ( + + + Company name + setName(event.target.value)} + className="w-full rounded-lg border border-border-subtle bg-panel px-3 py-2 text-sm text-text-primary outline-none focus:border-brand" + /> + + + + Colour + setBrandColor(event.target.value)} + className="block h-9 w-14 cursor-pointer rounded-lg border border-border-subtle bg-panel" + /> + + + + {pending ? "Creating…" : "Create"} + + setOpen(false)} + className="px-2 py-2 text-sm text-text-muted hover:text-text-secondary" + > + Cancel + + + {error && ( + + {error} + + )} + + ); +} + +export function ToggleClientActive({ client }: { client: ClientRecord }) { + const [pending, startTransition] = useTransition(); + + return ( + + startTransition(async () => { + await updateClient({ clientId: client.id, active: !client.active }); + }) + } + className="text-xs text-text-muted underline-offset-2 hover:text-text-secondary hover:underline disabled:opacity-50" + > + {client.active ? "Deactivate" : "Reactivate"} + + ); +} \ No newline at end of file
{client.slug}
+ {error} +