xpeditis2.0/apps/frontend/app/dashboard/search-advanced/results/page.tsx
2026-02-03 16:08:00 +01:00

390 lines
16 KiB
TypeScript

'use client';
import { useEffect, useState, useCallback } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { searchCsvRatesWithOffers } from '@/lib/api/rates';
import type { CsvRateSearchResult } from '@/types/rates';
import { Search, Lightbulb, DollarSign, Scale, Zap, Trophy, XCircle, AlertTriangle } from 'lucide-react';
interface BestOptions {
eco: CsvRateSearchResult;
standard: CsvRateSearchResult;
fast: CsvRateSearchResult;
}
export default function SearchResultsPage() {
const router = useRouter();
const searchParams = useSearchParams();
const [results, setResults] = useState<CsvRateSearchResult[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Parse search parameters from URL
const origin = searchParams.get('origin') || '';
const destination = searchParams.get('destination') || '';
const volumeCBM = parseFloat(searchParams.get('volumeCBM') || '0');
const weightKG = parseFloat(searchParams.get('weightKG') || '0');
const palletCount = parseInt(searchParams.get('palletCount') || '0');
const performSearch = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const response = await searchCsvRatesWithOffers({
origin,
destination,
volumeCBM,
weightKG,
palletCount,
hasDangerousGoods: searchParams.get('hasDangerousGoods') === 'true',
requiresSpecialHandling: searchParams.get('requiresSpecialHandling') === 'true',
requiresTailgate: searchParams.get('requiresTailgate') === 'true',
requiresStraps: searchParams.get('requiresStraps') === 'true',
requiresThermalCover: searchParams.get('requiresThermalCover') === 'true',
hasRegulatedProducts: searchParams.get('hasRegulatedProducts') === 'true',
requiresAppointment: searchParams.get('requiresAppointment') === 'true',
});
setResults(response.results);
} catch (err) {
console.error('Search error:', err);
setError(err instanceof Error ? err.message : 'Une erreur est survenue lors de la recherche');
} finally {
setIsLoading(false);
}
}, [origin, destination, volumeCBM, weightKG, palletCount, searchParams]);
useEffect(() => {
if (!origin || !destination || !volumeCBM || !weightKG) {
router.push('/dashboard/search-advanced');
return;
}
performSearch();
}, [origin, destination, volumeCBM, weightKG, performSearch, router]);
const getBestOptions = (): BestOptions | null => {
if (results.length === 0) return null;
// Filter results by serviceLevel (backend generates 3 offers per rate)
const economic = results.find(r => r.serviceLevel === 'ECONOMIC');
const standard = results.find(r => r.serviceLevel === 'STANDARD');
const rapid = results.find(r => r.serviceLevel === 'RAPID');
// If we have all 3 service levels, return them
if (economic && standard && rapid) {
return {
eco: economic,
standard: standard,
fast: rapid,
};
}
// Fallback: if serviceLevel is not present (old endpoint), use sorting
const sorted = [...results].sort((a, b) => a.priceEUR - b.priceEUR);
const fastest = [...results].sort((a, b) => a.transitDays - b.transitDays);
return {
eco: sorted[0],
standard: sorted[Math.floor(sorted.length / 2)] || sorted[0],
fast: fastest[0],
};
};
const bestOptions = getBestOptions();
const formatPrice = (price: number) => {
return new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR',
}).format(price);
};
if (isLoading) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 py-12 px-4">
<div className="max-w-7xl mx-auto">
<div className="text-center">
<div className="inline-block animate-spin rounded-full h-16 w-16 border-b-4 border-blue-600 mb-4"></div>
<p className="text-xl text-gray-700 font-medium">Recherche des meilleurs tarifs en cours...</p>
<p className="text-gray-500 mt-2">
{origin} {destination}
</p>
</div>
</div>
</div>
);
}
if (error) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 py-12 px-4">
<div className="max-w-7xl mx-auto">
<div className="bg-red-50 border-2 border-red-200 rounded-lg p-8 text-center">
<div className="mb-4 flex justify-center"><XCircle className="h-16 w-16 text-red-500" /></div>
<h3 className="text-xl font-bold text-red-900 mb-2">Erreur</h3>
<p className="text-red-700 mb-4">{error}</p>
<button
onClick={() => router.push('/dashboard/search-advanced')}
className="px-6 py-3 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
>
Retour à la recherche
</button>
</div>
</div>
</div>
);
}
if (results.length === 0) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 py-12 px-4">
<div className="max-w-7xl mx-auto">
<button
onClick={() => router.back()}
className="mb-6 flex items-center text-blue-600 hover:text-blue-800 font-medium"
>
Retour à la recherche
</button>
<div className="bg-yellow-50 border-2 border-yellow-200 rounded-lg p-8 text-center">
<div className="mb-4 flex justify-center"><Search className="h-16 w-16 text-yellow-500" /></div>
<h3 className="text-xl font-bold text-gray-900 mb-2">Aucun résultat trouvé</h3>
<p className="text-gray-600 mb-4">
Aucun tarif ne correspond à votre recherche pour le trajet {origin} {destination}
</p>
<div className="bg-white border border-yellow-300 rounded-lg p-4 text-left max-w-2xl mx-auto mb-6">
<h4 className="font-semibold text-gray-900 mb-2 flex items-center"><Lightbulb className="h-5 w-5 mr-2 text-yellow-500" /> Suggestions :</h4>
<ul className="text-sm text-gray-700 space-y-2">
<li>
<strong>Ports disponibles :</strong> NLRTM, DEHAM, FRLEH, BEGNE (origine) USNYC, USLAX,
CNSHG, SGSIN (destination)
</li>
<li>
<strong>Volume :</strong> Essayez entre 1 et 200 CBM
</li>
<li>
<strong>Poids :</strong> Essayez entre 100 et 30000 kg
</li>
</ul>
</div>
<button
onClick={() => router.back()}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Modifier la recherche
</button>
</div>
</div>
</div>
);
}
const optionCards = [
{
type: 'Économique',
option: bestOptions?.eco,
colors: {
border: 'border-green-200',
bg: 'bg-green-50',
text: 'text-green-800',
button: 'bg-green-600 hover:bg-green-700',
},
icon: <DollarSign className="h-10 w-10 text-green-600" />,
badge: 'Le moins cher',
},
{
type: 'Standard',
option: bestOptions?.standard,
colors: {
border: 'border-blue-200',
bg: 'bg-blue-50',
text: 'text-blue-800',
button: 'bg-blue-600 hover:bg-blue-700',
},
icon: <Scale className="h-10 w-10 text-blue-600" />,
badge: 'Équilibré',
},
{
type: 'Rapide',
option: bestOptions?.fast,
colors: {
border: 'border-purple-200',
bg: 'bg-purple-50',
text: 'text-purple-800',
button: 'bg-purple-600 hover:bg-purple-700',
},
icon: <Zap className="h-10 w-10 text-purple-600" />,
badge: 'Le plus rapide',
},
];
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 py-12 px-4">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<button
onClick={() => router.back()}
className="mb-4 flex items-center text-blue-600 hover:text-blue-800 font-medium"
>
Retour à la recherche
</button>
<div className="bg-white rounded-lg shadow-md p-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">Résultats de recherche</h1>
<p className="text-gray-600">
<span className="font-semibold">{origin}</span> <span className="font-semibold">{destination}</span>{' '}
{volumeCBM} CBM {weightKG} kg
{palletCount > 0 && `${palletCount} palette${palletCount > 1 ? 's' : ''}`}
</p>
</div>
<div className="text-right">
<p className="text-sm text-gray-500">Tarifs trouvés</p>
<p className="text-3xl font-bold text-blue-600">{results.length}</p>
</div>
</div>
</div>
</div>
{/* Best Options */}
{bestOptions && (
<div className="mb-12">
<h2 className="text-2xl font-bold text-gray-900 mb-6 flex items-center">
<Trophy className="h-8 w-8 mr-3 text-yellow-500" />
Meilleurs choix pour votre recherche
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{optionCards.map(card => {
if (!card.option) return null;
return (
<div
key={card.type}
className={`border-2 ${card.colors.border} ${card.colors.bg} rounded-xl shadow-lg overflow-hidden transform transition-all duration-300 hover:scale-105 hover:shadow-2xl`}
>
<div className={`p-6 ${card.colors.bg}`}>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3">
<span>{card.icon}</span>
<div>
<h3 className={`text-xl font-bold ${card.colors.text}`}>{card.type}</h3>
<span className="text-xs bg-white px-2 py-1 rounded-full text-gray-600 font-medium">
{card.badge}
</span>
</div>
</div>
</div>
<div className="bg-white rounded-lg p-4 mb-4">
<div className="text-center mb-3">
<p className="text-sm text-gray-600 mb-1">Prix total</p>
<p className="text-3xl font-bold text-gray-900">{formatPrice(card.option.priceEUR)}</p>
</div>
<div className="border-t border-gray-200 pt-3 space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">Transporteur :</span>
<span className="font-semibold text-gray-900">{card.option.companyName}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Transit :</span>
<span className="font-semibold text-gray-900">{card.option.transitDays} jours</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Type :</span>
<span className="font-semibold text-gray-900">{card.option.containerType}</span>
</div>
</div>
</div>
<button
onClick={() => {
const rateData = encodeURIComponent(JSON.stringify(card.option));
router.push(`/dashboard/booking/new?rateData=${rateData}&volumeCBM=${volumeCBM}&weightKG=${weightKG}&palletCount=${palletCount}`);
}}
className={`w-full py-3 ${card.colors.button} text-white rounded-lg font-semibold transition-colors`}
>
Sélectionner cette option
</button>
</div>
</div>
);
})}
</div>
</div>
)}
{/* All Results */}
<div>
<h2 className="text-2xl font-bold text-gray-900 mb-6">Tous les tarifs disponibles ({results.length})</h2>
<div className="space-y-4">
{results.map((result, index) => (
<div key={index} className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-xl font-bold text-gray-900">{result.companyName}</h3>
<p className="text-sm text-gray-500">
{result.origin} {result.destination} {result.containerType}
</p>
</div>
<div className="text-right">
<p className="text-3xl font-bold text-blue-600">{formatPrice(result.priceEUR)}</p>
<p className="text-sm text-gray-500">Prix total</p>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
<div className="bg-gray-50 rounded-lg p-3">
<p className="text-xs text-gray-600 mb-1">Prix de base</p>
<p className="font-semibold text-gray-900">
{formatPrice(result.priceBreakdown.basePrice)}
</p>
</div>
<div className="bg-gray-50 rounded-lg p-3">
<p className="text-xs text-gray-600 mb-1">Frais volume</p>
<p className="font-semibold text-gray-900">
{formatPrice(result.priceBreakdown.volumeCharge)}
</p>
</div>
<div className="bg-gray-50 rounded-lg p-3">
<p className="text-xs text-gray-600 mb-1">Frais poids</p>
<p className="font-semibold text-gray-900">
{formatPrice(result.priceBreakdown.weightCharge)}
</p>
</div>
<div className="bg-gray-50 rounded-lg p-3">
<p className="text-xs text-gray-600 mb-1">Délai transit</p>
<p className="font-semibold text-gray-900">{result.transitDays} jours</p>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4 text-sm text-gray-600">
<span> Valide jusqu'au {new Date(result.validUntil).toLocaleDateString('fr-FR')}</span>
{result.hasSurcharges && <span className="text-orange-600 flex items-center"><AlertTriangle className="h-4 w-4 mr-1" /> Surcharges applicables</span>}
</div>
<button
onClick={() => {
const rateData = encodeURIComponent(JSON.stringify(result));
router.push(`/dashboard/booking/new?rateData=${rateData}&volumeCBM=${volumeCBM}&weightKG=${weightKG}&palletCount=${palletCount}`);
}}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Sélectionner
</button>
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
}