import { useState, useRef, useEffect } from "react"; import { useSearchParams } from "react-router-dom"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Files, User, Users, Folder, Plus, Settings2, Check, X } from "lucide-react"; import { listCategories, createCategory, getMyGroups, type CategoryOut, ApiError, } from "@/api/client"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; import ManageCategoriesDialog from "@/components/ManageCategoriesDialog"; // PascalCase-with-dashes naming convention const NAME_RE = /^[A-Z][a-zA-Z0-9]*(-[A-Z][a-zA-Z0-9]*)*$/; export default function SourcePanel() { const [searchParams, setSearchParams] = useSearchParams(); const queryClient = useQueryClient(); const currentView = searchParams.get("view") ?? "all"; const currentCategoryId = searchParams.get("category_id"); const [catSearch, setCatSearch] = useState(""); const [addingCat, setAddingCat] = useState(false); const [newCatName, setNewCatName] = useState(""); const [newCatGroupId, setNewCatGroupId] = useState(""); const [nameError, setNameError] = useState(null); const [manageOpen, setManageOpen] = useState(false); const addInputRef = useRef(null); const { data: categories = [] } = useQuery({ queryKey: ["categories"], queryFn: listCategories, }); const { data: myGroups = [] } = useQuery({ queryKey: ["my-groups"], queryFn: getMyGroups, }); const createMut = useMutation({ mutationFn: ({ name, groupId }: { name: string; groupId?: string }) => createCategory(name, groupId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["categories"] }); setNewCatName(""); setNewCatGroupId(""); setAddingCat(false); setNameError(null); }, onError: (err) => { setNameError(err instanceof ApiError ? err.message : "Failed to create category"); }, }); useEffect(() => { if (addingCat) addInputRef.current?.focus(); }, [addingCat]); const filteredCats = catSearch ? categories.filter((c) => c.name.toLowerCase().includes(catSearch.toLowerCase())) : categories; // Split by scope for display const personalCats = filteredCats.filter((c) => c.scope === "personal"); const systemCats = filteredCats.filter((c) => c.scope === "system"); const groupCatMap = new Map(); for (const cat of filteredCats.filter((c) => c.scope === "group")) { if (!cat.group_id) continue; if (!groupCatMap.has(cat.group_id)) { const grp = myGroups.find((g) => g.id === cat.group_id); groupCatMap.set(cat.group_id, { name: grp?.name ?? cat.group_id, cats: [] }); } groupCatMap.get(cat.group_id)!.cats.push(cat); } function setView(view: string) { setSearchParams((prev) => { const next = new URLSearchParams(prev); next.set("view", view); next.delete("category_id"); next.set("page", "1"); return next; }); } function selectCategory(cat: CategoryOut) { setSearchParams((prev) => { const next = new URLSearchParams(prev); next.delete("view"); next.set("category_id", cat.id); next.set("page", "1"); return next; }); } function handleAddSubmit(e: React.FormEvent) { e.preventDefault(); const name = newCatName.trim(); if (!name) return; if (!NAME_RE.test(name)) { setNameError( "Must start with a capital letter. Join multiple words with dashes, each capitalised (e.g. Vendor-Invoices)." ); return; } setNameError(null); createMut.mutate({ name, groupId: newCatGroupId || undefined }); } const viewItemClass = (active: boolean) => cn( "flex items-center gap-2 w-full px-2 py-1.5 rounded-md text-sm transition-colors", active ? "bg-primary/10 text-primary font-medium" : "text-muted hover:bg-muted/20 hover:text-foreground" ); const catItemClass = (active: boolean) => cn( "flex items-center gap-2 w-full px-2 py-1 rounded-md text-sm transition-colors", active ? "bg-primary/10 text-primary font-medium" : "text-muted hover:bg-muted/20 hover:text-foreground" ); function renderCatSection(label: string, cats: CategoryOut[]) { if (cats.length === 0) return null; return (

{label}

{cats.map((cat) => ( ))}
); } const hasSections = personalCats.length > 0 || systemCats.length > 0 || groupCatMap.size > 0; return ( <> {manageOpen && ( setManageOpen(false)} /> )} ); }