/** * Multi-Step Booking Form * * Create a new booking in 4 steps: * 1. Select Rate Quote * 2. Shipper & Consignee Information * 3. Container Details * 4. Review & Confirmation */ 'use client'; import { useState, useEffect } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import { useMutation, useQuery } from '@tanstack/react-query'; import { bookingsApi, ratesApi } from '@/lib/api'; type Step = 1 | 2 | 3 | 4; interface Party { name: string; address: string; city: string; postalCode: string; country: string; contactName: string; contactEmail: string; contactPhone: string; } interface Container { type: string; quantity: number; weight?: number; temperature?: number; isHazmat: boolean; hazmatClass?: string; commodityDescription: string; } interface BookingFormData { rateQuoteId: string; shipper: Party; consignee: Party; containers: Container[]; specialInstructions?: string; } const emptyParty: Party = { name: '', address: '', city: '', postalCode: '', country: '', contactName: '', contactEmail: '', contactPhone: '', }; const emptyContainer: Container = { type: '40HC', quantity: 1, isHazmat: false, commodityDescription: '', }; export default function NewBookingPage() { const router = useRouter(); const searchParams = useSearchParams(); const preselectedQuoteId = searchParams.get('quoteId'); const [currentStep, setCurrentStep] = useState(1); const [formData, setFormData] = useState({ rateQuoteId: preselectedQuoteId || '', shipper: { ...emptyParty }, consignee: { ...emptyParty }, containers: [{ ...emptyContainer }], specialInstructions: '', }); const [error, setError] = useState(''); // Fetch preselected quote if provided const { data: preselectedQuote } = useQuery({ queryKey: ['rate-quote', preselectedQuoteId], queryFn: () => ratesApi.getById(preselectedQuoteId!), enabled: !!preselectedQuoteId, }); useEffect(() => { if (preselectedQuote) { setFormData(prev => ({ ...prev, rateQuoteId: preselectedQuote.id })); } }, [preselectedQuote]); // Create booking mutation const createBookingMutation = useMutation({ mutationFn: (data: BookingFormData) => bookingsApi.create(data), onSuccess: booking => { router.push(`/dashboard/bookings/${booking.id}`); }, onError: (err: any) => { setError(err.response?.data?.message || 'Failed to create booking'); }, }); const handleNext = () => { setError(''); if (currentStep < 4) { setCurrentStep(prev => (prev + 1) as Step); } }; const handleBack = () => { setError(''); if (currentStep > 1) { setCurrentStep(prev => (prev - 1) as Step); } }; const handleSubmit = () => { setError(''); createBookingMutation.mutate(formData); }; const updateParty = (type: 'shipper' | 'consignee', field: keyof Party, value: string) => { setFormData(prev => ({ ...prev, [type]: { ...prev[type], [field]: value, }, })); }; const updateContainer = (index: number, field: keyof Container, value: any) => { setFormData(prev => ({ ...prev, containers: prev.containers.map((c, i) => (i === index ? { ...c, [field]: value } : c)), })); }; const addContainer = () => { setFormData(prev => ({ ...prev, containers: [...prev.containers, { ...emptyContainer }], })); }; const removeContainer = (index: number) => { if (formData.containers.length > 1) { setFormData(prev => ({ ...prev, containers: prev.containers.filter((_, i) => i !== index), })); } }; const isStepValid = (step: Step): boolean => { switch (step) { case 1: return !!formData.rateQuoteId; case 2: return ( formData.shipper.name.trim() !== '' && formData.shipper.contactEmail.trim() !== '' && formData.consignee.name.trim() !== '' && formData.consignee.contactEmail.trim() !== '' ); case 3: return formData.containers.every( c => c.commodityDescription.trim() !== '' && c.quantity > 0 ); case 4: return true; default: return false; } }; return (
{/* Header */}

Create New Booking

Complete the booking process in 4 simple steps

{/* Progress Steps */}
{/* Error Message */} {error && (
{error}
)} {/* Step Content */}
{/* Step 1: Rate Quote Selection */} {currentStep === 1 && (

Step 1: Select Rate Quote

{preselectedQuote ? (
{preselectedQuote.carrier.logoUrl ? ( {preselectedQuote.carrier.name} ) : (
{preselectedQuote.carrier.name.substring(0, 2).toUpperCase()}
)}

{preselectedQuote.carrier.name}

{preselectedQuote.route.originPort} →{' '} {preselectedQuote.route.destinationPort}

${preselectedQuote.pricing.totalAmount.toLocaleString()}
{preselectedQuote.pricing.currency}
ETD:{' '} {new Date(preselectedQuote.route.etd).toLocaleDateString()}
Transit:{' '} {preselectedQuote.route.transitDays} days
ETA:{' '} {new Date(preselectedQuote.route.eta).toLocaleDateString()}
) : (

No rate quote selected

Please search for rates first and select a quote to book

)}
)} {/* Step 2: Shipper & Consignee */} {currentStep === 2 && (

Step 2: Shipper & Consignee Information

{/* Shipper */}

Shipper Details

updateParty('shipper', 'name', e.target.value)} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border" />
updateParty('shipper', 'address', e.target.value)} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border" />
updateParty('shipper', 'city', e.target.value)} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border" />
updateParty('shipper', 'postalCode', e.target.value)} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border" />
updateParty('shipper', 'country', e.target.value)} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border" />
updateParty('shipper', 'contactName', e.target.value)} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border" />
updateParty('shipper', 'contactEmail', e.target.value)} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border" />
updateParty('shipper', 'contactPhone', e.target.value)} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border" />

{/* Consignee */}

Consignee Details

updateParty('consignee', 'name', e.target.value)} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border" />
updateParty('consignee', 'address', e.target.value)} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border" />
updateParty('consignee', 'city', e.target.value)} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border" />
updateParty('consignee', 'postalCode', e.target.value)} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border" />
updateParty('consignee', 'country', e.target.value)} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border" />
updateParty('consignee', 'contactName', e.target.value)} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border" />
updateParty('consignee', 'contactEmail', e.target.value)} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border" />
updateParty('consignee', 'contactPhone', e.target.value)} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border" />
)} {/* Step 3: Container Details */} {currentStep === 3 && (

Step 3: Container Details

{formData.containers.map((container, index) => (

Container {index + 1}

{formData.containers.length > 1 && ( )}
updateContainer(index, 'quantity', parseInt(e.target.value) || 1) } className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border" />
updateContainer( index, 'weight', e.target.value ? parseFloat(e.target.value) : undefined ) } className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border" />
{(container.type === '20RF' || container.type === '40RF') && (
updateContainer( index, 'temperature', e.target.value ? parseFloat(e.target.value) : undefined ) } className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border" />
)}