diff --git a/src/components/reservations/berth-reserve-dialog.tsx b/src/components/reservations/berth-reserve-dialog.tsx new file mode 100644 index 0000000..1567c13 --- /dev/null +++ b/src/components/reservations/berth-reserve-dialog.tsx @@ -0,0 +1,251 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { Loader2 } from 'lucide-react'; +import { toast } from 'sonner'; + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogDescription, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Label } from '@/components/ui/label'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { ClientPicker } from '@/components/shared/client-picker'; +import { YachtPicker } from '@/components/yachts/yacht-picker'; +import { apiFetch } from '@/lib/api/client'; + +type TenureType = 'permanent' | 'fixed_term' | 'seasonal'; + +type FormValues = { + clientId: string | null; + yachtId: string | null; + startDate: string; // YYYY-MM-DD + tenureType: TenureType; + notes?: string; +}; + +interface BerthReserveDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + berthId: string; +} + +export function BerthReserveDialog({ open, onOpenChange, berthId }: BerthReserveDialogProps) { + const queryClient = useQueryClient(); + const [formError, setFormError] = useState(null); + + const { + register, + handleSubmit, + watch, + setValue, + reset, + formState: { errors, isSubmitting }, + } = useForm({ + defaultValues: { + clientId: null, + yachtId: null, + startDate: new Date().toISOString().slice(0, 10), + tenureType: 'permanent', + notes: '', + }, + }); + + useEffect(() => { + if (open) { + setFormError(null); + reset({ + clientId: null, + yachtId: null, + startDate: new Date().toISOString().slice(0, 10), + tenureType: 'permanent', + notes: '', + }); + } + }, [open, reset]); + + const clientId = watch('clientId'); + const yachtId = watch('yachtId'); + const tenureType = watch('tenureType'); + + // When client changes, clear yacht (since yacht-picker is filtered to owner=client) + useEffect(() => { + setValue('yachtId', null); + }, [clientId, setValue]); + + function validate(data: FormValues): string | null { + if (!data.clientId) return 'Please select a client'; + if (!data.yachtId) return 'Please select a yacht'; + return null; + } + + async function createPending(data: FormValues): Promise<{ id: string }> { + const res = await apiFetch<{ data: { id: string } }>(`/api/v1/berths/${berthId}/reservations`, { + method: 'POST', + body: { + clientId: data.clientId!, + yachtId: data.yachtId!, + startDate: data.startDate, + tenureType: data.tenureType, + notes: data.notes?.trim() || undefined, + }, + }); + return res.data; + } + + const createMutation = useMutation({ + mutationFn: async (data: FormValues) => { + const err = validate(data); + if (err) throw new Error(err); + await createPending(data); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['berths', berthId, 'reservations'] }); + queryClient.invalidateQueries({ queryKey: ['berth-reservations'] }); + toast.success('Reservation created'); + onOpenChange(false); + }, + onError: (err: unknown) => { + const msg = err instanceof Error ? err.message : 'Failed to create reservation'; + setFormError(msg); + }, + }); + + const createAndActivateMutation = useMutation({ + mutationFn: async (data: FormValues) => { + const err = validate(data); + if (err) throw new Error(err); + const pending = await createPending(data); + // Immediately activate + await apiFetch(`/api/v1/berth-reservations/${pending.id}`, { + method: 'PATCH', + body: { action: 'activate' }, + }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['berths', berthId, 'reservations'] }); + queryClient.invalidateQueries({ queryKey: ['berth-reservations'] }); + toast.success('Reservation created and activated'); + onOpenChange(false); + }, + onError: (err: unknown) => { + const msg = err instanceof Error ? err.message : 'Failed to activate'; + if (/active reservation|conflict|409/i.test(msg)) { + setFormError( + 'This berth already has an active reservation. The pending record was created — activate it manually once the other reservation ends.', + ); + } else { + setFormError(msg); + } + }, + }); + + const isPending = isSubmitting || createMutation.isPending || createAndActivateMutation.isPending; + + return ( + + + + Reserve this berth + + Create a pending reservation or activate it immediately. + + + +
+
+ + setValue('clientId', id)} /> +
+ +
+ + setValue('yachtId', id)} + ownerFilter={clientId ? { type: 'client', id: clientId } : undefined} + disabled={!clientId} + placeholder={clientId ? 'Select yacht...' : 'Select a client first'} + /> +
+ +
+ + + {errors.startDate &&

Required

} +
+ +
+ + +
+ +
+ +