xpeditis2.0/apps/frontend/app/dashboard/bookings/new/page.tsx
2025-11-04 07:30:15 +01:00

861 lines
36 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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<Step>(1);
const [formData, setFormData] = useState<BookingFormData>({
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 (
<div className="max-w-4xl mx-auto space-y-6">
{/* Header */}
<div>
<h1 className="text-2xl font-bold text-gray-900">Create New Booking</h1>
<p className="text-sm text-gray-500 mt-1">Complete the booking process in 4 simple steps</p>
</div>
{/* Progress Steps */}
<div className="bg-white rounded-lg shadow p-6">
<nav aria-label="Progress">
<ol className="flex items-center justify-between">
{[
{ number: 1, name: 'Rate Quote' },
{ number: 2, name: 'Parties' },
{ number: 3, name: 'Containers' },
{ number: 4, name: 'Review' },
].map((step, idx) => (
<li key={step.number} className={`flex items-center ${idx !== 3 ? 'flex-1' : ''}`}>
<div className="flex flex-col items-center">
<div
className={`flex items-center justify-center w-10 h-10 rounded-full border-2 ${
currentStep === step.number
? 'border-blue-600 bg-blue-600 text-white'
: currentStep > step.number
? 'border-green-600 bg-green-600 text-white'
: 'border-gray-300 bg-white text-gray-500'
}`}
>
{currentStep > step.number ? (
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
) : (
<span className="text-sm font-semibold">{step.number}</span>
)}
</div>
<span
className={`mt-2 text-xs font-medium ${
currentStep === step.number
? 'text-blue-600'
: currentStep > step.number
? 'text-green-600'
: 'text-gray-500'
}`}
>
{step.name}
</span>
</div>
{idx !== 3 && (
<div
className={`flex-1 h-0.5 mx-4 ${
currentStep > step.number ? 'bg-green-600' : 'bg-gray-300'
}`}
/>
)}
</li>
))}
</ol>
</nav>
</div>
{/* Error Message */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-md p-4">
<div className="text-sm text-red-800">{error}</div>
</div>
)}
{/* Step Content */}
<div className="bg-white rounded-lg shadow p-6">
{/* Step 1: Rate Quote Selection */}
{currentStep === 1 && (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-4">
Step 1: Select Rate Quote
</h2>
{preselectedQuote ? (
<div className="border border-green-200 bg-green-50 rounded-lg p-4">
<div className="flex items-start justify-between">
<div className="flex items-center space-x-4">
<div className="flex-shrink-0">
{preselectedQuote.carrier.logoUrl ? (
<img
src={preselectedQuote.carrier.logoUrl}
alt={preselectedQuote.carrier.name}
className="h-12 w-12 object-contain"
/>
) : (
<div className="h-12 w-12 bg-blue-100 rounded flex items-center justify-center text-blue-600 font-semibold">
{preselectedQuote.carrier.name.substring(0, 2).toUpperCase()}
</div>
)}
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900">
{preselectedQuote.carrier.name}
</h3>
<p className="text-sm text-gray-500">
{preselectedQuote.route.originPort} {' '}
{preselectedQuote.route.destinationPort}
</p>
</div>
</div>
<div className="text-right">
<div className="text-2xl font-bold text-blue-600">
${preselectedQuote.pricing.totalAmount.toLocaleString()}
</div>
<div className="text-sm text-gray-500">
{preselectedQuote.pricing.currency}
</div>
</div>
</div>
<div className="mt-4 grid grid-cols-3 gap-4 text-sm">
<div>
<span className="text-gray-500">ETD:</span>{' '}
<span className="font-medium">
{new Date(preselectedQuote.route.etd).toLocaleDateString()}
</span>
</div>
<div>
<span className="text-gray-500">Transit:</span>{' '}
<span className="font-medium">{preselectedQuote.route.transitDays} days</span>
</div>
<div>
<span className="text-gray-500">ETA:</span>{' '}
<span className="font-medium">
{new Date(preselectedQuote.route.eta).toLocaleDateString()}
</span>
</div>
</div>
</div>
) : (
<div className="text-center py-8">
<svg
className="mx-auto h-12 w-12 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
<h3 className="mt-2 text-sm font-medium text-gray-900">No rate quote selected</h3>
<p className="mt-1 text-sm text-gray-500">
Please search for rates first and select a quote to book
</p>
<div className="mt-6">
<a
href="/dashboard/search"
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
>
Search Rates
</a>
</div>
</div>
)}
</div>
</div>
)}
{/* Step 2: Shipper & Consignee */}
{currentStep === 2 && (
<div className="space-y-6">
<h2 className="text-lg font-semibold text-gray-900">
Step 2: Shipper & Consignee Information
</h2>
{/* Shipper */}
<div>
<h3 className="text-md font-medium text-gray-900 mb-4">Shipper Details</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700">Company Name *</label>
<input
type="text"
required
value={formData.shipper.name}
onChange={e => 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"
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700">Address *</label>
<input
type="text"
required
value={formData.shipper.address}
onChange={e => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">City *</label>
<input
type="text"
required
value={formData.shipper.city}
onChange={e => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Postal Code *</label>
<input
type="text"
required
value={formData.shipper.postalCode}
onChange={e => 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"
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700">Country *</label>
<input
type="text"
required
value={formData.shipper.country}
onChange={e => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Contact Name *</label>
<input
type="text"
required
value={formData.shipper.contactName}
onChange={e => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Contact Email *</label>
<input
type="email"
required
value={formData.shipper.contactEmail}
onChange={e => 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"
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700">Contact Phone *</label>
<input
type="tel"
required
value={formData.shipper.contactPhone}
onChange={e => 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"
/>
</div>
</div>
</div>
<hr className="border-gray-200" />
{/* Consignee */}
<div>
<h3 className="text-md font-medium text-gray-900 mb-4">Consignee Details</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700">Company Name *</label>
<input
type="text"
required
value={formData.consignee.name}
onChange={e => 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"
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700">Address *</label>
<input
type="text"
required
value={formData.consignee.address}
onChange={e => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">City *</label>
<input
type="text"
required
value={formData.consignee.city}
onChange={e => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Postal Code *</label>
<input
type="text"
required
value={formData.consignee.postalCode}
onChange={e => 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"
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700">Country *</label>
<input
type="text"
required
value={formData.consignee.country}
onChange={e => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Contact Name *</label>
<input
type="text"
required
value={formData.consignee.contactName}
onChange={e => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Contact Email *</label>
<input
type="email"
required
value={formData.consignee.contactEmail}
onChange={e => 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"
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700">Contact Phone *</label>
<input
type="tel"
required
value={formData.consignee.contactPhone}
onChange={e => 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"
/>
</div>
</div>
</div>
</div>
)}
{/* Step 3: Container Details */}
{currentStep === 3 && (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900">Step 3: Container Details</h2>
<button
type="button"
onClick={addContainer}
className="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
>
<span className="mr-1"></span>
Add Container
</button>
</div>
{formData.containers.map((container, index) => (
<div key={index} className="border border-gray-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-md font-medium text-gray-900">Container {index + 1}</h3>
{formData.containers.length > 1 && (
<button
type="button"
onClick={() => removeContainer(index)}
className="text-red-600 hover:text-red-800 text-sm"
>
Remove
</button>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700">
Container Type *
</label>
<select
value={container.type}
onChange={e => updateContainer(index, 'type', 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"
>
<option value="20GP">20' GP</option>
<option value="40GP">40' GP</option>
<option value="40HC">40' HC</option>
<option value="45HC">45' HC</option>
<option value="20RF">20' Reefer</option>
<option value="40RF">40' Reefer</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Quantity *</label>
<input
type="number"
min="1"
value={container.quantity}
onChange={e =>
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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Weight (kg)</label>
<input
type="number"
min="0"
value={container.weight || ''}
onChange={e =>
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"
/>
</div>
{(container.type === '20RF' || container.type === '40RF') && (
<div>
<label className="block text-sm font-medium text-gray-700">
Temperature (°C)
</label>
<input
type="number"
value={container.temperature || ''}
onChange={e =>
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"
/>
</div>
)}
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700">
Commodity Description *
</label>
<textarea
required
rows={2}
value={container.commodityDescription}
onChange={e => updateContainer(index, 'commodityDescription', e.target.value)}
placeholder="e.g., Electronics, Textiles, Machinery..."
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"
/>
</div>
<div className="md:col-span-2">
<div className="flex items-center">
<input
type="checkbox"
id={`hazmat-${index}`}
checked={container.isHazmat}
onChange={e => updateContainer(index, 'isHazmat', e.target.checked)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label
htmlFor={`hazmat-${index}`}
className="ml-2 block text-sm text-gray-900"
>
Contains Hazardous Materials
</label>
</div>
</div>
{container.isHazmat && (
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700">
Hazmat Class (IMO)
</label>
<input
type="text"
value={container.hazmatClass || ''}
onChange={e => updateContainer(index, 'hazmatClass', e.target.value)}
placeholder="e.g., Class 3, Class 8"
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"
/>
</div>
)}
</div>
</div>
))}
</div>
)}
{/* Step 4: Review & Confirmation */}
{currentStep === 4 && (
<div className="space-y-6">
<h2 className="text-lg font-semibold text-gray-900">Step 4: Review & Confirmation</h2>
{/* Rate Quote Summary */}
{preselectedQuote && (
<div>
<h3 className="text-md font-medium text-gray-900 mb-3">Rate Quote</h3>
<div className="bg-gray-50 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<div className="font-semibold">{preselectedQuote.carrier.name}</div>
<div className="text-sm text-gray-600">
{preselectedQuote.route.originPort} {' '}
{preselectedQuote.route.destinationPort}
</div>
<div className="text-sm text-gray-600">
Transit: {preselectedQuote.route.transitDays} days
</div>
</div>
<div className="text-right">
<div className="text-xl font-bold text-blue-600">
${preselectedQuote.pricing.totalAmount.toLocaleString()}
</div>
<div className="text-sm text-gray-500">
{preselectedQuote.pricing.currency}
</div>
</div>
</div>
</div>
</div>
)}
{/* Shipper */}
<div>
<h3 className="text-md font-medium text-gray-900 mb-3">Shipper</h3>
<div className="bg-gray-50 rounded-lg p-4 text-sm">
<div className="font-semibold">{formData.shipper.name}</div>
<div className="text-gray-600">
{formData.shipper.address}, {formData.shipper.city}, {formData.shipper.postalCode}
, {formData.shipper.country}
</div>
<div className="text-gray-600 mt-2">
Contact: {formData.shipper.contactName} ({formData.shipper.contactEmail},{' '}
{formData.shipper.contactPhone})
</div>
</div>
</div>
{/* Consignee */}
<div>
<h3 className="text-md font-medium text-gray-900 mb-3">Consignee</h3>
<div className="bg-gray-50 rounded-lg p-4 text-sm">
<div className="font-semibold">{formData.consignee.name}</div>
<div className="text-gray-600">
{formData.consignee.address}, {formData.consignee.city},{' '}
{formData.consignee.postalCode}, {formData.consignee.country}
</div>
<div className="text-gray-600 mt-2">
Contact: {formData.consignee.contactName} ({formData.consignee.contactEmail},{' '}
{formData.consignee.contactPhone})
</div>
</div>
</div>
{/* Containers */}
<div>
<h3 className="text-md font-medium text-gray-900 mb-3">Containers</h3>
<div className="space-y-2">
{formData.containers.map((container, index) => (
<div key={index} className="bg-gray-50 rounded-lg p-4 text-sm">
<div className="font-semibold">
{container.quantity}x {container.type}
</div>
<div className="text-gray-600">Commodity: {container.commodityDescription}</div>
{container.weight && (
<div className="text-gray-600">Weight: {container.weight} kg</div>
)}
{container.isHazmat && (
<div className="text-red-600 font-medium">
Hazmat {container.hazmatClass && `- ${container.hazmatClass}`}
</div>
)}
</div>
))}
</div>
</div>
{/* Special Instructions */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Special Instructions (Optional)
</label>
<textarea
rows={4}
value={formData.specialInstructions || ''}
onChange={e => setFormData({ ...formData, specialInstructions: e.target.value })}
placeholder="Any special handling requirements, pickup instructions, etc."
className="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"
/>
</div>
{/* Terms */}
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div className="text-sm text-yellow-800">
<p className="font-semibold mb-2">Please review carefully:</p>
<ul className="list-disc list-inside space-y-1">
<li>All information provided is accurate and complete</li>
<li>You agree to the carrier's terms and conditions</li>
<li>Final booking confirmation will be sent via email</li>
<li>Payment details will be provided separately</li>
</ul>
</div>
</div>
</div>
)}
</div>
{/* Navigation Buttons */}
<div className="flex items-center justify-between bg-white rounded-lg shadow p-6">
<button
type="button"
onClick={handleBack}
disabled={currentStep === 1}
className="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
<svg className="mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Back
</button>
{currentStep < 4 ? (
<button
type="button"
onClick={handleNext}
disabled={!isStepValid(currentStep)}
className="inline-flex items-center px-6 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
<svg className="ml-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
) : (
<button
type="button"
onClick={handleSubmit}
disabled={createBookingMutation.isPending || !isStepValid(4)}
className="inline-flex items-center px-6 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{createBookingMutation.isPending ? (
<>
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
Creating Booking...
</>
) : (
<>
<span className="mr-2"></span>
Confirm Booking
</>
)}
</button>
)}
</div>
</div>
);
}