/** * Advanced Rate Search Page * * Complete search form with all filters and best options display * Uses only ports available in CSV rates for origin/destination selection */ 'use client'; import { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { Search, Loader2 } from 'lucide-react'; import { useQuery } from '@tanstack/react-query'; import { getAvailableOrigins, getAvailableDestinations, RoutePortInfo, } from '@/lib/api/rates'; import dynamic from 'next/dynamic'; // Import dynamique pour éviter les erreurs SSR avec Leaflet const PortRouteMap = dynamic(() => import('@/components/PortRouteMap'), { ssr: false, loading: () =>
Chargement de la carte...
, }); interface Package { type: 'caisse' | 'colis' | 'palette' | 'autre'; quantity: number; length: number; width: number; height: number; weight: number; stackable: boolean; } interface SearchForm { // General origin: string; destination: string; // Conditionnement packages: Package[]; // Douane eurDocument: boolean; customsStop: boolean; exportAssistance: boolean; // Marchandise dangerousGoods: boolean; specialHandling: boolean; // Manutention tailgate: boolean; straps: boolean; thermalCover: boolean; // Autres regulatedProducts: boolean; appointment: boolean; insurance: boolean; t1Document: boolean; } export default function AdvancedSearchPage() { const router = useRouter(); const [searchForm, setSearchForm] = useState({ 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(null); const [selectedDestinationPort, setSelectedDestinationPort] = useState(null); // Fetch available origins from CSV rates const { data: originsData, isLoading: isLoadingOrigins } = useQuery({ queryKey: ['available-origins'], queryFn: getAvailableOrigins, }); // Fetch available destinations based on selected origin const { data: destinationsData, isLoading: isLoadingDestinations } = useQuery({ queryKey: ['available-destinations', searchForm.origin], queryFn: () => getAvailableDestinations(searchForm.origin), enabled: !!searchForm.origin, }); // Filter origins based on search input 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) ); }); // Filter destinations based on search input 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) ); }); // Reset destination when origin changes useEffect(() => { if (searchForm.origin && selectedDestinationPort) { // Check if current destination is still valid for new origin const isValidDestination = destinationsData?.destinations?.some( d => d.code === searchForm.destination ); if (!isValidDestination) { setSearchForm(prev => ({ ...prev, destination: '' })); setSelectedDestinationPort(null); setDestinationSearch(''); } } }, [searchForm.origin, destinationsData]); // Calculate total volume and weight 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(); // Build query parameters 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(), }); // Redirect to results page 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 = () => (

1. Informations Générales

{/* Info banner about available routes */}

Seuls les ports ayant des tarifs disponibles dans notre système sont proposés. {originsData?.total && ( ({originsData.total} ports d'origine disponibles) )}

{/* Origin Port with Autocomplete - Limited to CSV routes */}
{ setOriginSearch(e.target.value); setShowOriginDropdown(true); // Clear selection if user modifies the input 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="Rechercher un port d'origine..." 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 && (
)}
{showOriginDropdown && filteredOrigins.length > 0 && (
{filteredOrigins.slice(0, 15).map((port: RoutePortInfo) => ( ))} {filteredOrigins.length > 15 && (
+{filteredOrigins.length - 15} autres résultats. Affinez votre recherche.
)}
)} {showOriginDropdown && filteredOrigins.length === 0 && !isLoadingOrigins && originsData && (

Aucun port d'origine trouvé pour "{originSearch}"

)}
{/* Destination Port with Autocomplete - Limited to routes from selected origin */}
{ setDestinationSearch(e.target.value); setShowDestinationDropdown(true); // Clear selection if user modifies the input 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 ? 'Rechercher une destination...' : 'Sélectionnez d\'abord un port d\'origine'} 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 && (
)}
{searchForm.origin && destinationsData?.total !== undefined && (

{destinationsData.total} destination{destinationsData.total > 1 ? 's' : ''} disponible{destinationsData.total > 1 ? 's' : ''} depuis {selectedOriginPort?.name || searchForm.origin}

)} {showDestinationDropdown && filteredDestinations.length > 0 && (
{filteredDestinations.slice(0, 15).map((port: RoutePortInfo) => ( ))} {filteredDestinations.length > 15 && (
+{filteredDestinations.length - 15} autres résultats. Affinez votre recherche.
)}
)} {showDestinationDropdown && filteredDestinations.length === 0 && !isLoadingDestinations && searchForm.origin && destinationsData && (

Aucune destination trouvée pour "{destinationSearch}"

)}
{/* Carte interactive de la route maritime */} {selectedOriginPort && selectedDestinationPort && selectedOriginPort.latitude && selectedDestinationPort.latitude && (

Route maritime : {selectedOriginPort.name} → {selectedDestinationPort.name}

Distance approximative et visualisation de la route

)}
); const renderStep2 = () => (

2. Conditionnement

{searchForm.packages.map((pkg, index) => (

Colis #{index + 1}

{searchForm.packages.length > 1 && ( )}
updatePackage(index, 'quantity', parseInt(e.target.value) || 1)} className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded-md" />
updatePackage(index, 'length', parseInt(e.target.value) || 0)} className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded-md" />
updatePackage(index, 'width', parseInt(e.target.value) || 0)} className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded-md" />
updatePackage(index, 'height', parseInt(e.target.value) || 0)} className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded-md" />
updatePackage(index, 'weight', parseInt(e.target.value) || 0)} className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded-md" />
updatePackage(index, 'stackable', e.target.checked)} className="h-4 w-4 text-blue-600 border-gray-300 rounded" />
))}

Récapitulatif

Volume total: {calculateTotals().totalVolumeCBM.toFixed(2)} m³
Poids total: {calculateTotals().totalWeightKG} kg
Palettes: {calculateTotals().totalPallets}
); const renderStep3 = () => (

3. Options & Services

Douane Import / Export

Marchandise

Manutention particulière

Autres options

); return (
{/* Header */}

Recherche Avancée de Tarifs

Formulaire complet avec toutes les options de transport

{/* Progress Steps */}
{[1, 2, 3].map(step => (
= step ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-600' }`} > {step}
{step < 3 && (
step ? 'bg-blue-600' : 'bg-gray-200' }`} /> )}
))}
{/* Form */}
{currentStep === 1 && renderStep1()} {currentStep === 2 && renderStep2()} {currentStep === 3 && renderStep3()} {/* Navigation */}
{currentStep < 3 ? ( ) : ( )}
); }