feat(interests): add yacht-picker to interest form
This commit is contained in:
@@ -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 "Open" 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}>
|
||||||
|
|||||||
Reference in New Issue
Block a user