xpeditis2.0/apps/frontend/app/dashboard/search-advanced/results/page.tsx
2025-11-04 22:52:42 +01:00

362 lines
14 KiB
TypeScript
Raw 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.

'use client';
import { useEffect, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { searchCsvRates } from '@/lib/api/rates';
import type { CsvRateSearchResult } from '@/types/rates';
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');
useEffect(() => {
if (!origin || !destination || !volumeCBM || !weightKG) {
router.push('/dashboard/search-advanced');
return;
}
performSearch();
}, [origin, destination, volumeCBM, weightKG, palletCount]);
const performSearch = async () => {
setIsLoading(true);
setError(null);
try {
const response = await searchCsvRates({
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);
}
};
const getBestOptions = (): BestOptions | null => {
if (results.length === 0) return null;
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="text-6xl mb-4"></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="text-6xl mb-4">🔍</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">💡 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: '💰',
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: '⚖️',
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: '⚡',
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">
<span className="text-3xl mr-3">🏆</span>
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 className="text-4xl">{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 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"> Surcharges applicables</span>}
</div>
<button 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>
);
}