"use client"; import { useState } from "react"; import { InfoTooltip } from "~/components/ui/InfoTooltip.js"; import { trpc } from "~/lib/trpc/client.js"; type ClientRow = { id: string; name: string; code: string | null; parentId: string | null; sortOrder: number; isActive: boolean; }; type ClientNode = ClientRow & { children: ClientNode[] }; type EditingClient = { id?: string; name: string; code: string; parentId: string; sortOrder: number; }; function ClientTreeNode({ node, onEdit, onAddChild, depth = 0, }: { node: ClientNode; onEdit: (c: ClientRow) => void; onAddChild: (parentId: string) => void; depth?: number; }) { const [expanded, setExpanded] = useState(depth < 1); const hasChildren = node.children.length > 0; return (
{hasChildren ? ( ) : ( )} {node.name} {node.code && [{node.code}]} {!node.isActive && inactive}
{expanded && node.children.map((child) => ( ))}
); } export function ClientsAdminClient() { const [editing, setEditing] = useState(null); const [search, setSearch] = useState(""); const [error, setError] = useState(null); const utils = trpc.useUtils(); const { data: tree, isLoading } = trpc.clientEntity.getTree.useQuery(); const { data: flatList } = trpc.clientEntity.list.useQuery(); const createMut = trpc.clientEntity.create.useMutation({ onSuccess: () => { void utils.clientEntity.getTree.invalidate(); void utils.clientEntity.list.invalidate(); setEditing(null); }, onError: (e) => setError(e.message), }); const updateMut = trpc.clientEntity.update.useMutation({ onSuccess: () => { void utils.clientEntity.getTree.invalidate(); void utils.clientEntity.list.invalidate(); setEditing(null); }, onError: (e) => setError(e.message), }); const allClients = (flatList ?? []) as unknown as ClientRow[]; function openCreate(parentId?: string) { setEditing({ name: "", code: "", parentId: parentId ?? "", sortOrder: 0 }); setError(null); } function openEdit(c: ClientRow) { setEditing({ id: c.id, name: c.name, code: c.code ?? "", parentId: c.parentId ?? "", sortOrder: c.sortOrder, }); setError(null); } function handleSave() { if (!editing) return; setError(null); if (editing.id) { updateMut.mutate({ id: editing.id, data: { name: editing.name, code: editing.code || undefined, parentId: editing.parentId || undefined, sortOrder: editing.sortOrder, }, }); } else { createMut.mutate({ name: editing.name, code: editing.code || undefined, parentId: editing.parentId || undefined, sortOrder: editing.sortOrder, }); } } const isPending = createMut.isPending || updateMut.isPending; const treeNodes = (tree ?? []) as unknown as ClientNode[]; // Simple client-side filter on tree function filterTree(nodes: ClientNode[], q: string): ClientNode[] { if (!q) return nodes; const lower = q.toLowerCase(); return nodes.reduce((acc, node) => { const filteredChildren = filterTree(node.children, q); if (node.name.toLowerCase().includes(lower) || (node.code ?? "").toLowerCase().includes(lower) || filteredChildren.length > 0) { acc.push({ ...node, children: filteredChildren }); } return acc; }, []); } const filteredTree = filterTree(treeNodes, search); return (

Clients

Client hierarchy for project assignment and chargeability reporting

setSearch(e.target.value)} placeholder="Search clients..." className="border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-brand-400 w-64 bg-white dark:bg-gray-900 dark:text-gray-100" />
{error && (
{error}
)}
{isLoading &&
Loading...
} {!isLoading && filteredTree.length === 0 && (
{search ? "No clients match your search." : "No clients yet."}
)} {filteredTree.map((node) => ( openCreate(pid)} /> ))}
{/* Create/Edit Modal */} {editing && (

{editing.id ? "Edit Client" : "Add Client"}

setEditing({ ...editing, name: e.target.value })} placeholder="e.g. BMW Group" className="border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2 text-sm w-full bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-brand-400" />
setEditing({ ...editing, code: e.target.value })} placeholder="BMW" className="border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2 text-sm w-full bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-brand-400 font-mono" />
setEditing({ ...editing, sortOrder: parseInt(e.target.value) || 0 })} className="border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2 text-sm w-full bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-brand-400" />
)}
); }