xpeditis2.0/apps/frontend/app/[locale]/dashboard/search-advanced/page.tsx
David 64de600292 fix(booking-edit): reconstruct pallet dims with 120x80 base (not a cube)
La reprise reconstruisait un cube (cbrt du volume) au lieu de la base palette
standard, d'ou des longueur/largeur/hauteur incorrectes. Pour une palette on
reconstitue desormais 120x80 cm et on deduit la hauteur du volume (ex: 0.96 CBM
-> 120x80x100). Les colis restent en cube equivalent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 16:12:28 +02:00

885 lines
34 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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.

'use client';
import { useState, useEffect } from 'react';
import { useRouter } from '@/i18n/navigation';
import { useSearchParams } from 'next/navigation';
import { Search, Loader2 } from 'lucide-react';
import { useQuery } from '@tanstack/react-query';
import { useTranslations } from 'next-intl';
import { getAvailableOrigins, getAvailableDestinations, RoutePortInfo } from '@/lib/api/rates';
import dynamic from 'next/dynamic';
const PortRouteMapLoader = () => {
const t = useTranslations('dashboard.rateSearch');
return (
<div className="h-80 bg-gray-100 rounded-lg flex items-center justify-center">
{t('mapLoading')}
</div>
);
};
const PortRouteMap = dynamic(() => import('@/components/PortRouteMap'), {
ssr: false,
loading: PortRouteMapLoader,
});
interface Package {
type: 'caisse' | 'colis' | 'palette' | 'autre';
quantity: number;
length: number;
width: number;
height: number;
weight: number;
stackable: boolean;
}
interface SearchForm {
origin: string;
destination: string;
packages: Package[];
eurDocument: boolean;
customsStop: boolean;
exportAssistance: boolean;
dangerousGoods: boolean;
specialHandling: boolean;
tailgate: boolean;
straps: boolean;
thermalCover: boolean;
regulatedProducts: boolean;
appointment: boolean;
insurance: boolean;
t1Document: boolean;
}
// Options & services persisted with the booking (order irrelevant).
const OPTION_KEYS: (keyof SearchForm)[] = [
'eurDocument',
't1Document',
'customsStop',
'exportAssistance',
'dangerousGoods',
'regulatedProducts',
'specialHandling',
'tailgate',
'straps',
'thermalCover',
'appointment',
'insurance',
];
export default function AdvancedSearchPage() {
const t = useTranslations('dashboard.rateSearch');
const router = useRouter();
const searchParams = useSearchParams();
const [editBookingId, setEditBookingId] = useState<string | null>(null);
const [searchForm, setSearchForm] = useState<SearchForm>({
origin: '',
destination: '',
packages: [
{
type: 'palette',
quantity: 1,
length: 120,
width: 80,
height: 100,
weight: 500,
stackable: true,
},
],
eurDocument: false,
customsStop: false,
exportAssistance: false,
dangerousGoods: false,
specialHandling: false,
tailgate: false,
straps: false,
thermalCover: false,
regulatedProducts: false,
appointment: false,
insurance: false,
t1Document: false,
});
const [currentStep, setCurrentStep] = useState(1);
const [originSearch, setOriginSearch] = useState('');
const [destinationSearch, setDestinationSearch] = useState('');
const [showOriginDropdown, setShowOriginDropdown] = useState(false);
const [showDestinationDropdown, setShowDestinationDropdown] = useState(false);
const [selectedOriginPort, setSelectedOriginPort] = useState<RoutePortInfo | null>(null);
const [selectedDestinationPort, setSelectedDestinationPort] = useState<RoutePortInfo | null>(
null
);
const { data: originsData, isLoading: isLoadingOrigins } = useQuery({
queryKey: ['available-origins'],
queryFn: getAvailableOrigins,
});
const { data: destinationsData, isLoading: isLoadingDestinations } = useQuery({
queryKey: ['available-destinations', searchForm.origin],
queryFn: () => getAvailableDestinations(searchForm.origin),
enabled: !!searchForm.origin,
});
const filteredOrigins = (originsData?.origins || []).filter(port => {
if (!originSearch || originSearch.length < 1) return true;
const searchLower = originSearch.toLowerCase();
return (
port.code.toLowerCase().includes(searchLower) ||
port.name.toLowerCase().includes(searchLower) ||
port.city.toLowerCase().includes(searchLower) ||
port.countryName.toLowerCase().includes(searchLower)
);
});
const filteredDestinations = (destinationsData?.destinations || []).filter(port => {
if (!destinationSearch || destinationSearch.length < 1) return true;
const searchLower = destinationSearch.toLowerCase();
return (
port.code.toLowerCase().includes(searchLower) ||
port.name.toLowerCase().includes(searchLower) ||
port.city.toLowerCase().includes(searchLower) ||
port.countryName.toLowerCase().includes(searchLower)
);
});
useEffect(() => {
if (searchForm.origin && selectedDestinationPort) {
const isValidDestination = destinationsData?.destinations?.some(
d => d.code === searchForm.destination
);
if (!isValidDestination) {
setSearchForm(prev => ({ ...prev, destination: '' }));
setSelectedDestinationPort(null);
setDestinationSearch('');
}
}
}, [searchForm.origin, destinationsData]);
// Prefill the form when re-editing an existing unpaid booking (or from any
// deep link with query params). Runs once on mount.
useEffect(() => {
const originParam = searchParams.get('origin');
const destinationParam = searchParams.get('destination');
const editId = searchParams.get('editBookingId');
if (!originParam && !destinationParam && !editId) return;
if (editId) setEditBookingId(editId);
const vol = parseFloat(searchParams.get('volumeCBM') || '0');
const wgt = parseFloat(searchParams.get('weightKG') || '0');
const pallets = parseInt(searchParams.get('palletCount') || '0', 10);
const dg = searchParams.get('hasDangerousGoods') === 'true';
// Prefill the options & services from the "options" CSV param.
const enabledOptions = (searchParams.get('options') || '')
.split(',')
.map(s => s.trim())
.filter(Boolean);
const optionFlags = OPTION_KEYS.reduce(
(acc, k) => ({ ...acc, [k]: enabledOptions.includes(k) }),
{} as Record<string, boolean>
);
// Reconstruct a single representative package that reproduces the aggregate
// totals (the booking only stores totals, not individual packages).
const q = pallets > 0 ? pallets : 1;
const perVolM3 = vol > 0 ? vol / q : 0;
const isPalette = pallets > 0;
// The booking only stores aggregate totals, not per-package dimensions. For
// pallets we reconstruct the standard EUR pallet footprint (120×80 cm) and
// derive the height from the volume — so a default 0.96 CBM pallet comes back
// as 120×80×100. For loose parcels we fall back to a cube of equal volume.
let length: number;
let width: number;
let height: number;
if (isPalette) {
length = 120;
width = 80;
const baseM2 = (length / 100) * (width / 100); // 0.96 m²
height = perVolM3 > 0 ? Math.round((perVolM3 / baseM2) * 100 * 100) / 100 : 100;
} else {
const sideCm = perVolM3 > 0 ? Math.round(Math.cbrt(perVolM3) * 100 * 100) / 100 : 0;
length = sideCm;
width = sideCm;
height = sideCm;
}
const pkg = {
type: (isPalette ? 'palette' : 'colis') as Package['type'],
quantity: q,
length,
width,
height,
weight: wgt > 0 ? Math.round((wgt / q) * 100) / 100 : 0,
stackable: true,
};
setSearchForm(prev => ({
...prev,
...optionFlags,
origin: originParam || prev.origin,
destination: destinationParam || prev.destination,
dangerousGoods: dg || !!optionFlags.dangerousGoods,
packages: vol > 0 || wgt > 0 || pallets > 0 ? [pkg] : prev.packages,
}));
if (originParam) setOriginSearch(originParam);
if (destinationParam) setDestinationSearch(destinationParam);
// Start at the cargo (packages) step as requested.
setCurrentStep(2);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Once port lists load, resolve the prefilled codes to full port objects so
// the selection UI reflects them (and destination validation passes).
useEffect(() => {
if (searchForm.origin && !selectedOriginPort) {
const p = (originsData?.origins || []).find(o => o.code === searchForm.origin);
if (p) {
setSelectedOriginPort(p);
setOriginSearch(p.displayName);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [originsData, searchForm.origin]);
useEffect(() => {
if (searchForm.destination && !selectedDestinationPort) {
const p = (destinationsData?.destinations || []).find(
d => d.code === searchForm.destination
);
if (p) {
setSelectedDestinationPort(p);
setDestinationSearch(p.displayName);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [destinationsData, searchForm.destination]);
const calculateTotals = () => {
let totalVolumeCBM = 0;
let totalWeightKG = 0;
let totalPallets = 0;
searchForm.packages.forEach(pkg => {
const volumeM3 = (pkg.length * pkg.width * pkg.height) / 1000000;
totalVolumeCBM += volumeM3 * pkg.quantity;
totalWeightKG += pkg.weight * pkg.quantity;
if (pkg.type === 'palette') {
totalPallets += pkg.quantity;
}
});
return { totalVolumeCBM, totalWeightKG, totalPallets };
};
const handleSearch = () => {
const { totalVolumeCBM, totalWeightKG, totalPallets } = calculateTotals();
const params = new URLSearchParams({
origin: searchForm.origin,
destination: searchForm.destination,
volumeCBM: totalVolumeCBM.toString(),
weightKG: totalWeightKG.toString(),
palletCount: totalPallets.toString(),
hasDangerousGoods: searchForm.dangerousGoods.toString(),
requiresSpecialHandling: searchForm.specialHandling.toString(),
requiresTailgate: searchForm.tailgate.toString(),
requiresStraps: searchForm.straps.toString(),
requiresThermalCover: searchForm.thermalCover.toString(),
hasRegulatedProducts: searchForm.regulatedProducts.toString(),
requiresAppointment: searchForm.appointment.toString(),
});
if (editBookingId) {
params.set('editBookingId', editBookingId);
}
// Carry the selected options & services so they can be persisted.
const enabledOptions = OPTION_KEYS.filter(k => searchForm[k]).join(',');
if (enabledOptions) params.set('options', enabledOptions);
router.push(`/dashboard/search-advanced/results?${params.toString()}`);
};
const addPackage = () => {
setSearchForm({
...searchForm,
packages: [
...searchForm.packages,
{
type: 'palette',
quantity: 1,
length: 120,
width: 80,
height: 100,
weight: 500,
stackable: true,
},
],
});
};
const removePackage = (index: number) => {
setSearchForm({
...searchForm,
packages: searchForm.packages.filter((_, i) => i !== index),
});
};
const updatePackage = (index: number, field: keyof Package, value: any) => {
const newPackages = [...searchForm.packages];
newPackages[index] = { ...newPackages[index], [field]: value };
setSearchForm({ ...searchForm, packages: newPackages });
};
const renderStep1 = () => (
<div className="space-y-6">
<h2 className="text-xl font-semibold text-gray-900">{t('step1.title')}</h2>
<div className="grid grid-cols-2 gap-4">
<div className="relative">
<label className="block text-sm font-medium text-gray-700 mb-2">
{t('step1.originLabel')}{' '}
{searchForm.origin && (
<span className="text-green-600 text-xs">{t('step1.selected')}</span>
)}
</label>
<div className="relative">
<input
type="text"
value={originSearch}
onChange={e => {
setOriginSearch(e.target.value);
setShowOriginDropdown(true);
if (selectedOriginPort && e.target.value !== selectedOriginPort.displayName) {
setSearchForm({ ...searchForm, origin: '', destination: '' });
setSelectedOriginPort(null);
setSelectedDestinationPort(null);
setDestinationSearch('');
}
}}
onFocus={() => setShowOriginDropdown(true)}
onBlur={() => setTimeout(() => setShowOriginDropdown(false), 200)}
placeholder={t('step1.originPlaceholder')}
className={`w-full px-3 py-2 border rounded-md focus:ring-blue-500 focus:border-blue-500 ${
searchForm.origin ? 'border-green-500 bg-green-50' : 'border-gray-300'
}`}
/>
{isLoadingOrigins && (
<div className="absolute right-3 top-1/2 -translate-y-1/2">
<Loader2 className="h-4 w-4 animate-spin text-gray-400" />
</div>
)}
</div>
{showOriginDropdown && filteredOrigins.length > 0 && (
<div className="absolute left-0 right-0 mt-2 bg-white border border-gray-200 rounded-md shadow-lg max-h-60 overflow-y-auto z-50">
{filteredOrigins.slice(0, 15).map((port: RoutePortInfo) => (
<button
key={port.code}
type="button"
onClick={() => {
setSearchForm({ ...searchForm, origin: port.code, destination: '' });
setOriginSearch(port.displayName);
setSelectedOriginPort(port);
setSelectedDestinationPort(null);
setDestinationSearch('');
setShowOriginDropdown(false);
}}
className="w-full text-left px-4 py-3 hover:bg-blue-50 border-b border-gray-100 last:border-b-0"
>
<div className="font-medium text-gray-900">{port.name}</div>
<div className="text-gray-500 text-xs mt-1">
{port.code} - {port.city}, {port.countryName}
</div>
</button>
))}
{filteredOrigins.length > 15 && (
<div className="px-4 py-2 text-xs text-gray-500 bg-gray-50">
{t('step1.moreResults', { count: filteredOrigins.length - 15 })}
</div>
)}
</div>
)}
{showOriginDropdown &&
filteredOrigins.length === 0 &&
!isLoadingOrigins &&
originsData && (
<div className="absolute left-0 right-0 mt-2 bg-white border border-gray-200 rounded-md shadow-lg p-4 z-50">
<p className="text-sm text-gray-500">
{t('step1.noOrigin', { query: originSearch })}
</p>
</div>
)}
</div>
<div className="relative">
<label className="block text-sm font-medium text-gray-700 mb-2">
{t('step1.destinationLabel')}{' '}
{searchForm.destination && (
<span className="text-green-600 text-xs">{t('step1.selected')}</span>
)}
</label>
<div className="relative">
<input
type="text"
value={destinationSearch}
onChange={e => {
setDestinationSearch(e.target.value);
setShowDestinationDropdown(true);
if (
selectedDestinationPort &&
e.target.value !== selectedDestinationPort.displayName
) {
setSearchForm({ ...searchForm, destination: '' });
setSelectedDestinationPort(null);
}
}}
onFocus={() => setShowDestinationDropdown(true)}
onBlur={() => setTimeout(() => setShowDestinationDropdown(false), 200)}
disabled={!searchForm.origin}
placeholder={
searchForm.origin
? t('step1.destinationPlaceholder')
: t('step1.destinationDisabled')
}
className={`w-full px-3 py-2 border rounded-md focus:ring-blue-500 focus:border-blue-500 ${
searchForm.destination ? 'border-green-500 bg-green-50' : 'border-gray-300'
} ${!searchForm.origin ? 'bg-gray-100 cursor-not-allowed' : ''}`}
/>
{isLoadingDestinations && searchForm.origin && (
<div className="absolute right-3 top-1/2 -translate-y-1/2">
<Loader2 className="h-4 w-4 animate-spin text-gray-400" />
</div>
)}
</div>
{searchForm.origin && destinationsData?.total !== undefined && (
<p className="text-xs text-gray-500 mt-1">
{t('step1.availableDestinations', {
count: destinationsData.total,
port: selectedOriginPort?.name || searchForm.origin,
})}
</p>
)}
{showDestinationDropdown && filteredDestinations.length > 0 && (
<div className="absolute left-0 right-0 mt-2 bg-white border border-gray-200 rounded-md shadow-lg max-h-60 overflow-y-auto z-50">
{filteredDestinations.slice(0, 15).map((port: RoutePortInfo) => (
<button
key={port.code}
type="button"
onClick={() => {
setSearchForm({ ...searchForm, destination: port.code });
setDestinationSearch(port.displayName);
setSelectedDestinationPort(port);
setShowDestinationDropdown(false);
}}
className="w-full text-left px-4 py-3 hover:bg-blue-50 border-b border-gray-100 last:border-b-0"
>
<div className="font-medium text-gray-900">{port.name}</div>
<div className="text-gray-500 text-xs mt-1">
{port.code} - {port.city}, {port.countryName}
</div>
</button>
))}
{filteredDestinations.length > 15 && (
<div className="px-4 py-2 text-xs text-gray-500 bg-gray-50">
{t('step1.moreResults', { count: filteredDestinations.length - 15 })}
</div>
)}
</div>
)}
{showDestinationDropdown &&
filteredDestinations.length === 0 &&
!isLoadingDestinations &&
searchForm.origin &&
destinationsData && (
<div className="absolute left-0 right-0 mt-2 bg-white border border-gray-200 rounded-md shadow-lg p-4 z-50">
<p className="text-sm text-gray-500">
{t('step1.noDestination', { query: destinationSearch })}
</p>
</div>
)}
</div>
</div>
{selectedOriginPort &&
selectedDestinationPort &&
selectedOriginPort.latitude &&
selectedDestinationPort.latitude && (
<div className="mt-6 border border-gray-200 rounded-lg overflow-hidden">
<div className="bg-gray-50 px-4 py-3 border-b border-gray-200">
<h3 className="text-sm font-semibold text-gray-900">
{t('step1.routeTitle', {
origin: selectedOriginPort.name,
destination: selectedDestinationPort.name,
})}
</h3>
<p className="text-xs text-gray-500 mt-1">{t('step1.routeDescription')}</p>
</div>
<PortRouteMap
portA={{
lat: selectedOriginPort.latitude,
lng: selectedOriginPort.longitude!,
}}
portB={{
lat: selectedDestinationPort.latitude,
lng: selectedDestinationPort.longitude!,
}}
height="400px"
/>
</div>
)}
</div>
);
const renderStep2 = () => {
const totals = calculateTotals();
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-gray-900">{t('step2.title')}</h2>
<button
type="button"
onClick={addPackage}
className="px-4 py-2 text-sm font-medium text-blue-700 bg-blue-50 rounded-md hover:bg-blue-100"
>
{t('step2.addPackage')}
</button>
</div>
{searchForm.packages.map((pkg, index) => (
<div key={index} className="border border-gray-200 rounded-lg p-4 space-y-4">
<div className="flex items-center justify-between">
<h3 className="font-medium text-gray-900">
{t('step2.packageNumber', { number: index + 1 })}
</h3>
{searchForm.packages.length > 1 && (
<button
type="button"
onClick={() => removePackage(index)}
className="text-sm text-red-600 hover:text-red-700"
>
{t('step2.remove')}
</button>
)}
</div>
<div className="grid grid-cols-5 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
{t('step2.type')}
</label>
<select
value={pkg.type}
onChange={e => updatePackage(index, 'type', e.target.value)}
className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded-md"
>
<option value="caisse">{t('step2.packageTypes.caisse')}</option>
<option value="colis">{t('step2.packageTypes.colis')}</option>
<option value="palette">{t('step2.packageTypes.palette')}</option>
<option value="autre">{t('step2.packageTypes.autre')}</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
{t('step2.quantity')}
</label>
<input
type="number"
min="1"
value={pkg.quantity}
onChange={e => updatePackage(index, 'quantity', parseInt(e.target.value) || 1)}
className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded-md"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
{t('step2.length')}
</label>
<input
type="number"
min="1"
value={pkg.length}
onChange={e => updatePackage(index, 'length', parseInt(e.target.value) || 0)}
className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded-md"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
{t('step2.width')}
</label>
<input
type="number"
min="1"
value={pkg.width}
onChange={e => updatePackage(index, 'width', parseInt(e.target.value) || 0)}
className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded-md"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
{t('step2.height')}
</label>
<input
type="number"
min="1"
value={pkg.height}
onChange={e => updatePackage(index, 'height', parseInt(e.target.value) || 0)}
className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded-md"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
{t('step2.weight')}
</label>
<input
type="number"
min="1"
value={pkg.weight}
onChange={e => updatePackage(index, 'weight', parseInt(e.target.value) || 0)}
className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded-md"
/>
</div>
<div className="flex items-center pt-6">
<input
type="checkbox"
checked={pkg.stackable}
onChange={e => updatePackage(index, 'stackable', e.target.checked)}
className="h-4 w-4 text-blue-600 border-gray-300 rounded"
/>
<label className="ml-2 text-sm text-gray-700">{t('step2.stackable')}</label>
</div>
</div>
</div>
))}
<div className="bg-blue-50 border border-blue-200 rounded-md p-4">
<h3 className="text-sm font-medium text-blue-900 mb-2">{t('step2.summary.title')}</h3>
<div className="text-sm text-blue-800 space-y-1">
<div>{t('step2.summary.volume', { value: totals.totalVolumeCBM.toFixed(2) })}</div>
<div>{t('step2.summary.weight', { value: totals.totalWeightKG })}</div>
<div>{t('step2.summary.pallets', { value: totals.totalPallets })}</div>
</div>
</div>
</div>
);
};
const renderStep3 = () => (
<div className="space-y-6">
<h2 className="text-xl font-semibold text-gray-900">{t('step3.title')}</h2>
<div className="space-y-4">
<div className="border-b pb-4">
<h3 className="text-sm font-semibold text-gray-900 mb-3">{t('step3.customs.title')}</h3>
<div className="space-y-2">
<label className="flex items-center">
<input
type="checkbox"
checked={searchForm.eurDocument}
onChange={e => setSearchForm({ ...searchForm, eurDocument: e.target.checked })}
className="h-4 w-4 text-blue-600 border-gray-300 rounded"
/>
<span className="ml-2 text-sm text-gray-700">{t('step3.customs.eurDocument')}</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={searchForm.t1Document}
onChange={e => setSearchForm({ ...searchForm, t1Document: e.target.checked })}
className="h-4 w-4 text-blue-600 border-gray-300 rounded"
/>
<span className="ml-2 text-sm text-gray-700">{t('step3.customs.t1Document')}</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={searchForm.customsStop}
onChange={e => setSearchForm({ ...searchForm, customsStop: e.target.checked })}
className="h-4 w-4 text-blue-600 border-gray-300 rounded"
/>
<span className="ml-2 text-sm text-gray-700">{t('step3.customs.customsStop')}</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={searchForm.exportAssistance}
onChange={e => setSearchForm({ ...searchForm, exportAssistance: e.target.checked })}
className="h-4 w-4 text-blue-600 border-gray-300 rounded"
/>
<span className="ml-2 text-sm text-gray-700">
{t('step3.customs.exportAssistance')}
</span>
</label>
</div>
</div>
<div className="border-b pb-4">
<h3 className="text-sm font-semibold text-gray-900 mb-3">{t('step3.goods.title')}</h3>
<div className="space-y-2">
<label className="flex items-center">
<input
type="checkbox"
checked={searchForm.dangerousGoods}
onChange={e => setSearchForm({ ...searchForm, dangerousGoods: e.target.checked })}
className="h-4 w-4 text-blue-600 border-gray-300 rounded"
/>
<span className="ml-2 text-sm text-gray-700">{t('step3.goods.dangerous')}</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={searchForm.regulatedProducts}
onChange={e =>
setSearchForm({ ...searchForm, regulatedProducts: e.target.checked })
}
className="h-4 w-4 text-blue-600 border-gray-300 rounded"
/>
<span className="ml-2 text-sm text-gray-700">{t('step3.goods.regulated')}</span>
</label>
</div>
</div>
<div className="border-b pb-4">
<h3 className="text-sm font-semibold text-gray-900 mb-3">{t('step3.handling.title')}</h3>
<div className="space-y-2">
<label className="flex items-center">
<input
type="checkbox"
checked={searchForm.specialHandling}
onChange={e => setSearchForm({ ...searchForm, specialHandling: e.target.checked })}
className="h-4 w-4 text-blue-600 border-gray-300 rounded"
/>
<span className="ml-2 text-sm text-gray-700">{t('step3.handling.special')}</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={searchForm.tailgate}
onChange={e => setSearchForm({ ...searchForm, tailgate: e.target.checked })}
className="h-4 w-4 text-blue-600 border-gray-300 rounded"
/>
<span className="ml-2 text-sm text-gray-700">{t('step3.handling.tailgate')}</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={searchForm.straps}
onChange={e => setSearchForm({ ...searchForm, straps: e.target.checked })}
className="h-4 w-4 text-blue-600 border-gray-300 rounded"
/>
<span className="ml-2 text-sm text-gray-700">{t('step3.handling.straps')}</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={searchForm.thermalCover}
onChange={e => setSearchForm({ ...searchForm, thermalCover: e.target.checked })}
className="h-4 w-4 text-blue-600 border-gray-300 rounded"
/>
<span className="ml-2 text-sm text-gray-700">{t('step3.handling.thermalCover')}</span>
</label>
</div>
</div>
<div>
<h3 className="text-sm font-semibold text-gray-900 mb-3">{t('step3.other.title')}</h3>
<div className="space-y-2">
<label className="flex items-center">
<input
type="checkbox"
checked={searchForm.appointment}
onChange={e => setSearchForm({ ...searchForm, appointment: e.target.checked })}
className="h-4 w-4 text-blue-600 border-gray-300 rounded"
/>
<span className="ml-2 text-sm text-gray-700">{t('step3.other.appointment')}</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={searchForm.insurance}
onChange={e => setSearchForm({ ...searchForm, insurance: e.target.checked })}
className="h-4 w-4 text-blue-600 border-gray-300 rounded"
/>
<span className="ml-2 text-sm text-gray-700">{t('step3.other.insurance')}</span>
</label>
</div>
</div>
</div>
</div>
);
return (
<div className="max-w-7xl mx-auto space-y-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">{t('title')}</h1>
<p className="text-sm text-gray-500 mt-1">{t('subtitle')}</p>
</div>
<div className="flex items-center justify-center space-x-4">
{[1, 2, 3].map(step => (
<div key={step} className="flex items-center">
<div
className={`flex items-center justify-center w-10 h-10 rounded-full ${
currentStep >= step ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-600'
}`}
>
{step}
</div>
{step < 3 && (
<div
className={`w-20 h-1 mx-2 ${currentStep > step ? 'bg-blue-600' : 'bg-gray-200'}`}
/>
)}
</div>
))}
</div>
<div className="bg-white rounded-lg shadow p-8">
{currentStep === 1 && renderStep1()}
{currentStep === 2 && renderStep2()}
{currentStep === 3 && renderStep3()}
<div className="mt-8 flex items-center justify-between pt-6 border-t">
<button
type="button"
onClick={() => setCurrentStep(Math.max(1, currentStep - 1))}
disabled={currentStep === 1}
className="px-6 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
{t('navigation.previous')}
</button>
{currentStep < 3 ? (
<button
type="button"
onClick={() => setCurrentStep(currentStep + 1)}
disabled={!searchForm.origin || !searchForm.destination}
className="px-6 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{t('navigation.next')}
</button>
) : (
<button
type="button"
onClick={handleSearch}
disabled={!searchForm.origin || !searchForm.destination}
className="px-6 py-3 text-base font-medium text-white bg-green-600 rounded-md hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center"
>
<Search className="h-5 w-5 mr-2" /> {t('navigation.search')}
</button>
)}
</div>
</div>
</div>
);
}