Files
pn-new-crm/src/components/interests/interest-picker.tsx

116 lines
3.5 KiB
TypeScript
Raw Normal View History

feat(documents-wizard): replace UUID-paste fields with searchable pickers + inline upload Reps no longer have to copy/paste UUIDs into the New-document wizard. Three UUID inputs replaced: - Template id Input → DocumentTemplatePicker (queries /api/v1/document-templates with name search; filters to isActive=true) - Uploaded file id Input → inline FileUploadZone (drop or browse PDF; surfaces the uploaded file id directly to the wizard via the new onUploadComplete signature) - Subject id Input → conditional picker: ClientPicker / CompanyPicker / YachtPicker / InterestPicker depending on the subject-type dropdown. Reservation falls back to Input for now (no ReservationPicker yet). Other polish in the wizard: - SIGNER_ROLES labels capitalized in the role select (client → Client, etc.) via a formatSignerRole() helper. Internal values stay lowercase. - Pinned h-9 on Select triggers so the type/subject row + signer-role select vertically align with their adjacent inputs. - Subject-type change now resets subjectId — picker options are type-specific and a stale id from a different entity table would be invalid. Infrastructure for hub uploads (will be consumed in a follow-up dropdown + drag-drop pass): - /api/v1/files/upload route now parses folderId from FormData (schema already supported it). - FileUploadZone accepts a folderId prop and forwards it, plus a new onUploadComplete(file) callback shape that surfaces { id, filename } on each successful upload. Existing per-entity callers (Files tab on clients, companies, yachts, interests) ignore the arg, no behaviour change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:17:02 +02:00
'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 InterestOption {
id: string;
clientId: string;
clientName?: string;
pipelineStage?: string;
// Some list endpoints surface the linked client inline; we display whatever's
// available with a fallback to a short id.
}
interface InterestPickerProps {
value: string | null;
onChange: (interestId: string | null) => void;
placeholder?: string;
disabled?: boolean;
}
export function InterestPicker({
value,
onChange,
placeholder = 'Select interest...',
disabled,
}: InterestPickerProps) {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState('');
const debounced = useDebounce(search, 300);
const { data } = useQuery<{ data: InterestOption[] }>({
queryKey: ['interest-picker', debounced],
queryFn: () =>
apiFetch(
`/api/v1/interests?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);
if (!match) return `Interest ${value.slice(0, 8)}`;
if (match.clientName) return `${match.clientName}${match.pipelineStage ?? 'open'}`;
return `Interest ${match.id.slice(0, 8)}`;
})();
return (
<Popover open={open} onOpenChange={setOpen}>
<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" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[320px] p-0" align="start">
<Command shouldFilter={false}>
<CommandInput
placeholder="Search by client name…"
value={search}
onValueChange={setSearch}
/>
<CommandList>
<CommandEmpty>No interests found.</CommandEmpty>
<CommandGroup>
{options.map((i) => (
<CommandItem
key={i.id}
value={i.id}
onSelect={() => {
onChange(i.id);
setOpen(false);
}}
>
<Check
className={cn('mr-2 h-4 w-4', value === i.id ? 'opacity-100' : 'opacity-0')}
/>
<span className="truncate">
{i.clientName ?? `Interest ${i.id.slice(0, 8)}`}
{i.pipelineStage ? (
<span className="ml-2 text-xs text-muted-foreground">{i.pipelineStage}</span>
) : null}
</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}