Files
pn-new-crm/src/components/shared/client-picker.tsx
Matt 221ae5784e chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:

- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
  never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
  after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
  redirects (ocr to ai, reports to dashboard, invitations to users),
  docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
  flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
  let-reassign), set-state-in-effect disables in CountryFlag and
  UploadForSigning preview-bytes effect, unused 'confirm' destructures in
  interest contract + reservation tabs, unescaped apostrophe in test-template
  card copy
2026-05-23 00:52:59 +02:00

128 lines
4.1 KiB
TypeScript

'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;
}
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,
});
// The search results are paginated and only fetched while the popover
// is open, so a `value` set from outside (e.g. an existing reservation)
// can't be name-resolved from them. A second query targets the picked
// client directly so the trigger label reads as the rep's name, not a
// UUID-prefix fallback.
const { data: selectedData } = useQuery<{ data: { id: string; fullName: string } }>({
queryKey: ['client-picker', 'selected', value],
queryFn: () => apiFetch(`/api/v1/clients/${value}`),
enabled: !!value,
staleTime: 5 * 60_000,
});
const options = data?.data ?? [];
const selectedLabel = (() => {
if (!value) return placeholder;
const match = options.find((o) => o.id === value);
return match?.fullName ?? selectedData?.data?.fullName ?? `Client ${value.slice(0, 8)}`;
})();
return (
// `modal` is required when this picker is rendered inside a Sheet /
// Dialog - without it the CommandInput stays focus-blocked by the
// outer Sheet's focus trap and clicks/typing are silently dropped.
<Popover open={open} onOpenChange={setOpen} modal>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
disabled={disabled}
className={cn('w-full justify-between', !value && 'text-muted-foreground')}
>
<span className="truncate">{selectedLabel}</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" aria-hidden />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[320px] p-0" align="start">
<Command shouldFilter={false}>
<CommandInput placeholder="Search clients…" value={search} onValueChange={setSearch} />
<CommandList>
<CommandEmpty>No clients found.</CommandEmpty>
<CommandGroup>
{value ? (
<CommandItem
value="__clear__"
onSelect={() => {
onChange(null);
setOpen(false);
}}
className="text-muted-foreground"
>
Clear selection
</CommandItem>
) : null}
{options.map((c) => (
<CommandItem
key={c.id}
value={c.id}
onSelect={() => {
onChange(c.id);
setOpen(false);
}}
>
<Check
className={cn('mr-2 h-4 w-4', value === c.id ? 'opacity-100' : 'opacity-0')}
/>
<span>{c.fullName}</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}