diff --git a/src/components/yachts/yacht-detail-header.tsx b/src/components/yachts/yacht-detail-header.tsx index 5204538..99b8a14 100644 --- a/src/components/yachts/yacht-detail-header.tsx +++ b/src/components/yachts/yacht-detail-header.tsx @@ -9,16 +9,10 @@ import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; import { ArchiveConfirmDialog } from '@/components/shared/archive-confirm-dialog'; +import { PermissionGate } from '@/components/shared/permission-gate'; import { YachtForm } from '@/components/yachts/yacht-form'; +import { YachtTransferDialog } from '@/components/yachts/yacht-transfer-dialog'; import { apiFetch } from '@/lib/api/client'; interface YachtDetailHeaderYacht { @@ -174,15 +168,17 @@ export function YachtDetailHeader({ yacht }: YachtDetailHeaderProps) { Edit - + + + - - - + ); } diff --git a/src/components/yachts/yacht-transfer-dialog.tsx b/src/components/yachts/yacht-transfer-dialog.tsx new file mode 100644 index 0000000..e89ec26 --- /dev/null +++ b/src/components/yachts/yacht-transfer-dialog.tsx @@ -0,0 +1,202 @@ +'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, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} 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 { OwnerPicker, type OwnerRef } from '@/components/shared/owner-picker'; +import { apiFetch } from '@/lib/api/client'; + +type TransferReason = 'sale' | 'inheritance' | 'gift' | 'company_restructure' | 'other'; + +type FormValues = { + newOwner: OwnerRef | undefined; + effectiveDate: string; // ISO date string from + transferReason?: TransferReason; + transferNotes?: string; +}; + +interface YachtTransferDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + yachtId: string; + /** Current owner shown for reference; used to guard against selecting the same owner. */ + currentOwner?: OwnerRef; +} + +const todayIso = (): string => new Date().toISOString().slice(0, 10); + +export function YachtTransferDialog({ + open, + onOpenChange, + yachtId, + currentOwner, +}: YachtTransferDialogProps) { + const queryClient = useQueryClient(); + const [formError, setFormError] = useState(null); + + const { + register, + handleSubmit, + watch, + setValue, + reset, + formState: { errors, isSubmitting }, + } = useForm({ + defaultValues: { + newOwner: undefined, + effectiveDate: todayIso(), + transferReason: undefined, + transferNotes: '', + }, + }); + + useEffect(() => { + if (open) { + setFormError(null); + reset({ + newOwner: undefined, + effectiveDate: todayIso(), + transferReason: undefined, + transferNotes: '', + }); + } + }, [open, reset]); + + const newOwner = watch('newOwner'); + const transferReason = watch('transferReason'); + + const mutation = useMutation({ + mutationFn: async (data: FormValues) => { + if (!data.newOwner) { + throw new Error('Please select a new owner'); + } + if ( + currentOwner && + data.newOwner.type === currentOwner.type && + data.newOwner.id === currentOwner.id + ) { + throw new Error('New owner must be different from the current owner'); + } + await apiFetch(`/api/v1/yachts/${yachtId}/transfer`, { + method: 'POST', + body: { + newOwner: data.newOwner, + effectiveDate: data.effectiveDate, + transferReason: data.transferReason, + transferNotes: data.transferNotes?.trim() || undefined, + }, + }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['yachts', yachtId] }); + queryClient.invalidateQueries({ queryKey: ['yachts', yachtId, 'ownership-history'] }); + queryClient.invalidateQueries({ queryKey: ['yachts'] }); + toast.success('Ownership transferred'); + onOpenChange(false); + }, + onError: (err: unknown) => { + const msg = err instanceof Error ? err.message : 'Failed to transfer ownership'; + setFormError(msg); + }, + }); + + return ( + + + + Transfer ownership + + This will close the current ownership record and open a new one. The change is auditable + and atomic. + + + +
{ + setFormError(null); + mutation.mutate(data); + })} + className="space-y-4" + > +
+ + setValue('newOwner', v ?? undefined)} + /> +
+ +
+ + + {errors.effectiveDate &&

Required

} +
+ +
+ + +
+ +
+ +