feat(interests): add yacht-picker to interest form

This commit is contained in:
Matt Ciaccio
2026-04-24 15:36:27 +02:00
parent f9cb8003b5
commit bcf4c1f797

View File

@@ -18,18 +18,8 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import { import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetFooter } from '@/components/ui/sheet';
Sheet, import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
SheetContent,
SheetHeader,
SheetTitle,
SheetFooter,
} from '@/components/ui/sheet';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { import {
Command, Command,
CommandEmpty, CommandEmpty,
@@ -41,6 +31,7 @@ import {
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { TagPicker } from '@/components/shared/tag-picker'; import { TagPicker } from '@/components/shared/tag-picker';
import { YachtPicker } from '@/components/yachts/yacht-picker';
import { apiFetch } from '@/lib/api/client'; import { apiFetch } from '@/lib/api/client';
import { useEntityOptions } from '@/hooks/use-entity-options'; import { useEntityOptions } from '@/hooks/use-entity-options';
import { createInterestSchema, type CreateInterestInput } from '@/lib/validators/interests'; import { createInterestSchema, type CreateInterestInput } from '@/lib/validators/interests';
@@ -71,6 +62,7 @@ interface InterestFormProps {
id: string; id: string;
clientId: string; clientId: string;
clientName?: string | null; clientName?: string | null;
yachtId?: string | null;
berthId?: string | null; berthId?: string | null;
berthMooringNumber?: string | null; berthMooringNumber?: string | null;
pipelineStage: string; pipelineStage: string;
@@ -101,6 +93,7 @@ export function InterestForm({ open, onOpenChange, interest }: InterestFormProps
resolver: zodResolver(createInterestSchema), resolver: zodResolver(createInterestSchema),
defaultValues: { defaultValues: {
clientId: '', clientId: '',
yachtId: undefined,
pipelineStage: 'open', pipelineStage: 'open',
reminderEnabled: false, reminderEnabled: false,
tagIds: [], tagIds: [],
@@ -111,15 +104,22 @@ export function InterestForm({ open, onOpenChange, interest }: InterestFormProps
const reminderEnabled = watch('reminderEnabled'); const reminderEnabled = watch('reminderEnabled');
const selectedClientId = watch('clientId'); const selectedClientId = watch('clientId');
const selectedBerthId = watch('berthId'); const selectedBerthId = watch('berthId');
const selectedYachtId = watch('yachtId');
const { options: clientOptions, isLoading: clientsLoading, setSearch: setClientSearch } = const {
useEntityOptions({ options: clientOptions,
isLoading: clientsLoading,
setSearch: setClientSearch,
} = useEntityOptions({
endpoint: '/api/v1/clients/options', endpoint: '/api/v1/clients/options',
labelKey: 'fullName', labelKey: 'fullName',
}); });
const { options: berthOptions, isLoading: berthsLoading, setSearch: setBerthSearch } = const {
useEntityOptions({ options: berthOptions,
isLoading: berthsLoading,
setSearch: setBerthSearch,
} = useEntityOptions({
endpoint: '/api/v1/berths/options', endpoint: '/api/v1/berths/options',
labelKey: 'mooringNumber', labelKey: 'mooringNumber',
}); });
@@ -128,9 +128,10 @@ export function InterestForm({ open, onOpenChange, interest }: InterestFormProps
if (interest && open) { if (interest && open) {
reset({ reset({
clientId: interest.clientId, clientId: interest.clientId,
yachtId: interest.yachtId ?? undefined,
berthId: interest.berthId ?? undefined, berthId: interest.berthId ?? undefined,
pipelineStage: interest.pipelineStage as typeof PIPELINE_STAGES[number], pipelineStage: interest.pipelineStage as (typeof PIPELINE_STAGES)[number],
leadCategory: interest.leadCategory as typeof LEAD_CATEGORIES[number] | undefined, leadCategory: interest.leadCategory as (typeof LEAD_CATEGORIES)[number] | undefined,
source: interest.source ?? undefined, source: interest.source ?? undefined,
notes: interest.notes ?? undefined, notes: interest.notes ?? undefined,
reminderEnabled: interest.reminderEnabled ?? false, reminderEnabled: interest.reminderEnabled ?? false,
@@ -140,6 +141,7 @@ export function InterestForm({ open, onOpenChange, interest }: InterestFormProps
} else if (!interest && open) { } else if (!interest && open) {
reset({ reset({
clientId: '', clientId: '',
yachtId: undefined,
pipelineStage: 'open', pipelineStage: 'open',
reminderEnabled: false, reminderEnabled: false,
tagIds: [], tagIds: [],
@@ -178,10 +180,7 @@ export function InterestForm({ open, onOpenChange, interest }: InterestFormProps
<SheetTitle>{isEdit ? 'Edit Interest' : 'New Interest'}</SheetTitle> <SheetTitle>{isEdit ? 'Edit Interest' : 'New Interest'}</SheetTitle>
</SheetHeader> </SheetHeader>
<form <form onSubmit={handleSubmit((data) => mutation.mutate(data))} className="space-y-6 py-6">
onSubmit={handleSubmit((data) => mutation.mutate(data))}
className="space-y-6 py-6"
>
{/* Client */} {/* Client */}
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wide"> <h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
@@ -202,16 +201,13 @@ export function InterestForm({ open, onOpenChange, interest }: InterestFormProps
)} )}
disabled={isEdit} disabled={isEdit}
> >
{selectedClient?.label ?? (interest?.clientName ?? 'Select client...')} {selectedClient?.label ?? interest?.clientName ?? 'Select client...'}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-[400px] p-0"> <PopoverContent className="w-[400px] p-0">
<Command> <Command>
<CommandInput <CommandInput placeholder="Search clients..." onValueChange={setClientSearch} />
placeholder="Search clients..."
onValueChange={setClientSearch}
/>
<CommandList> <CommandList>
<CommandEmpty> <CommandEmpty>
{clientsLoading ? 'Loading...' : 'No clients found.'} {clientsLoading ? 'Loading...' : 'No clients found.'}
@@ -258,16 +254,13 @@ export function InterestForm({ open, onOpenChange, interest }: InterestFormProps
!selectedBerthId && 'text-muted-foreground', !selectedBerthId && 'text-muted-foreground',
)} )}
> >
{selectedBerth?.label ?? (interest?.berthMooringNumber ?? 'Select berth...')} {selectedBerth?.label ?? interest?.berthMooringNumber ?? 'Select berth...'}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-[400px] p-0"> <PopoverContent className="w-[400px] p-0">
<Command> <Command>
<CommandInput <CommandInput placeholder="Search berths..." onValueChange={setBerthSearch} />
placeholder="Search berths..."
onValueChange={setBerthSearch}
/>
<CommandList> <CommandList>
<CommandEmpty> <CommandEmpty>
{berthsLoading ? 'Loading...' : 'No berths found.'} {berthsLoading ? 'Loading...' : 'No berths found.'}
@@ -312,6 +305,24 @@ export function InterestForm({ open, onOpenChange, interest }: InterestFormProps
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</div> </div>
<div className="space-y-2">
<Label>Yacht</Label>
<YachtPicker
value={selectedYachtId ?? null}
onChange={(id) => setValue('yachtId', id ?? undefined)}
ownerFilter={
selectedClientId ? { type: 'client', id: selectedClientId } : undefined
}
disabled={!selectedClientId}
placeholder={selectedClientId ? 'Select yacht...' : 'Select a client first'}
/>
<p className="text-xs text-muted-foreground">
Required before the interest can leave the &quot;Open&quot; stage.
</p>
{/* TODO: also include company-owned yachts where client is a member — requires autocomplete owner=any|company filter */}
{/* TODO: add "Add new yacht" inline shortcut (requires YachtForm integration) */}
</div>
</div> </div>
<Separator /> <Separator />
@@ -326,7 +337,9 @@ export function InterestForm({ open, onOpenChange, interest }: InterestFormProps
<Label>Stage</Label> <Label>Stage</Label>
<Select <Select
value={watch('pipelineStage') ?? 'open'} value={watch('pipelineStage') ?? 'open'}
onValueChange={(v) => setValue('pipelineStage', v as typeof PIPELINE_STAGES[number])} onValueChange={(v) =>
setValue('pipelineStage', v as (typeof PIPELINE_STAGES)[number])
}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select stage" /> <SelectValue placeholder="Select stage" />
@@ -346,7 +359,10 @@ export function InterestForm({ open, onOpenChange, interest }: InterestFormProps
<Select <Select
value={watch('leadCategory') ?? ''} value={watch('leadCategory') ?? ''}
onValueChange={(v) => onValueChange={(v) =>
setValue('leadCategory', v ? v as typeof LEAD_CATEGORIES[number] : undefined) setValue(
'leadCategory',
v ? (v as (typeof LEAD_CATEGORIES)[number]) : undefined,
)
} }
> >
<SelectTrigger> <SelectTrigger>
@@ -427,18 +443,11 @@ export function InterestForm({ open, onOpenChange, interest }: InterestFormProps
{/* Tags */} {/* Tags */}
<div className="space-y-2"> <div className="space-y-2">
<Label>Tags</Label> <Label>Tags</Label>
<TagPicker <TagPicker selectedIds={tagIds} onChange={(ids) => setValue('tagIds', ids)} />
selectedIds={tagIds}
onChange={(ids) => setValue('tagIds', ids)}
/>
</div> </div>
<SheetFooter> <SheetFooter>
<Button <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
>
Cancel Cancel
</Button> </Button>
<Button type="submit" disabled={isSubmitting || mutation.isPending}> <Button type="submit" disabled={isSubmitting || mutation.isPending}>