From 29a7fc88574dffe4525626e94f375753f0cda9f3 Mon Sep 17 00:00:00 2001 From: Matt Ciaccio Date: Fri, 24 Apr 2026 14:02:00 +0200 Subject: [PATCH] feat(ui): add shared client-picker autocomplete --- src/components/shared/client-picker.tsx | 106 ++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 src/components/shared/client-picker.tsx diff --git a/src/components/shared/client-picker.tsx b/src/components/shared/client-picker.tsx new file mode 100644 index 0000000..6739f40 --- /dev/null +++ b/src/components/shared/client-picker.tsx @@ -0,0 +1,106 @@ +'use client'; + +import { useState } from 'react'; +import { Check, ChevronsUpDown } from 'lucide-react'; +import { useQuery } from '@tanstack/react-query'; + +import { Button } from '@/components/ui/button'; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/components/ui/command'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { useDebounce } from '@/hooks/use-debounce'; +import { apiFetch } from '@/lib/api/client'; +import { cn } from '@/lib/utils'; + +interface ClientOption { + id: string; + fullName: string; + companyName?: string | null; +} + +interface ClientPickerProps { + value: string | null; + onChange: (clientId: string | null) => void; + placeholder?: string; + disabled?: boolean; +} + +export function ClientPicker({ + value, + onChange, + placeholder = 'Select client...', + disabled, +}: ClientPickerProps) { + const [open, setOpen] = useState(false); + const [search, setSearch] = useState(''); + const debounced = useDebounce(search, 300); + + const { data } = useQuery<{ data: ClientOption[] }>({ + queryKey: ['client-picker', debounced], + queryFn: () => + apiFetch( + `/api/v1/clients?search=${encodeURIComponent(debounced)}&page=1&limit=10&order=desc&includeArchived=false`, + ), + enabled: open, + }); + + const options = data?.data ?? []; + + const selectedLabel = (() => { + if (!value) return placeholder; + const match = options.find((o) => o.id === value); + return match?.fullName ?? `Client ${value.slice(0, 8)}`; + })(); + + return ( + + + + + + + + + No clients found. + + {options.map((c) => ( + { + onChange(c.id); + setOpen(false); + }} + > + + + {c.fullName} + {c.companyName ? ( + {c.companyName} + ) : null} + + + ))} + + + + + + ); +}