256 lines
7.8 KiB
TypeScript
256 lines
7.8 KiB
TypeScript
/**
|
|
* Rate Results Table Component
|
|
*
|
|
* Displays search results in a table format
|
|
* Shows CSV/API source, prices, transit time, and surcharge details
|
|
*/
|
|
|
|
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from '@/components/ui/dialog';
|
|
import { ArrowUpDown, Info } from 'lucide-react';
|
|
import type { CsvRateResult } from '@/types/rate-filters';
|
|
|
|
interface RateResultsTableProps {
|
|
results: CsvRateResult[];
|
|
currency?: 'USD' | 'EUR';
|
|
onBooking?: (result: CsvRateResult) => void;
|
|
}
|
|
|
|
type SortField = 'price' | 'transit' | 'company' | 'matchScore';
|
|
type SortOrder = 'asc' | 'desc';
|
|
|
|
export function RateResultsTable({ results, currency = 'USD', onBooking }: RateResultsTableProps) {
|
|
const [sortField, setSortField] = useState<SortField>('price');
|
|
const [sortOrder, setSortOrder] = useState<SortOrder>('asc');
|
|
|
|
const handleSort = (field: SortField) => {
|
|
if (sortField === field) {
|
|
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
|
|
} else {
|
|
setSortField(field);
|
|
setSortOrder('asc');
|
|
}
|
|
};
|
|
|
|
const sortedResults = [...results].sort((a, b) => {
|
|
let aValue: number | string;
|
|
let bValue: number | string;
|
|
|
|
switch (sortField) {
|
|
case 'price':
|
|
aValue = currency === 'USD' ? a.priceUSD : a.priceEUR;
|
|
bValue = currency === 'USD' ? b.priceUSD : b.priceEUR;
|
|
break;
|
|
case 'transit':
|
|
aValue = a.transitDays;
|
|
bValue = b.transitDays;
|
|
break;
|
|
case 'company':
|
|
aValue = a.companyName;
|
|
bValue = b.companyName;
|
|
break;
|
|
case 'matchScore':
|
|
aValue = a.matchScore;
|
|
bValue = b.matchScore;
|
|
break;
|
|
default:
|
|
return 0;
|
|
}
|
|
|
|
if (sortOrder === 'asc') {
|
|
return aValue > bValue ? 1 : -1;
|
|
} else {
|
|
return aValue < bValue ? 1 : -1;
|
|
}
|
|
});
|
|
|
|
const formatPrice = (priceUSD: number, priceEUR: number) => {
|
|
if (currency === 'USD') {
|
|
return `$${priceUSD.toFixed(2)}`;
|
|
} else {
|
|
return `€${priceEUR.toFixed(2)}`;
|
|
}
|
|
};
|
|
|
|
const SortButton = ({ field, label }: { field: SortField; label: string }) => (
|
|
<button
|
|
onClick={() => handleSort(field)}
|
|
className="flex items-center gap-1 hover:text-primary transition-colors"
|
|
>
|
|
{label}
|
|
<ArrowUpDown className="h-4 w-4" />
|
|
</button>
|
|
);
|
|
|
|
if (results.length === 0) {
|
|
return (
|
|
<div className="text-center py-12 border rounded-lg">
|
|
<p className="text-muted-foreground">Aucun tarif trouvé pour cette recherche.</p>
|
|
<p className="text-sm text-muted-foreground mt-2">
|
|
Essayez d'ajuster vos critères de recherche ou vos filtres.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="rounded-md border">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>
|
|
<SortButton field="company" label="Compagnie" />
|
|
</TableHead>
|
|
<TableHead>Source</TableHead>
|
|
<TableHead>Trajet</TableHead>
|
|
<TableHead>
|
|
<SortButton field="price" label="Prix" />
|
|
</TableHead>
|
|
<TableHead>Surcharges</TableHead>
|
|
<TableHead>
|
|
<SortButton field="transit" label="Transit" />
|
|
</TableHead>
|
|
<TableHead>Validité</TableHead>
|
|
<TableHead>
|
|
<SortButton field="matchScore" label="Score" />
|
|
</TableHead>
|
|
<TableHead>Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{sortedResults.map((result, index) => (
|
|
<TableRow key={index}>
|
|
{/* Compagnie */}
|
|
<TableCell className="font-medium">{result.companyName}</TableCell>
|
|
|
|
{/* Source (CSV/API) */}
|
|
<TableCell>
|
|
<Badge variant={result.source === 'CSV' ? 'secondary' : 'default'}>
|
|
{result.source}
|
|
</Badge>
|
|
</TableCell>
|
|
|
|
{/* Trajet */}
|
|
<TableCell>
|
|
<div className="text-sm">
|
|
<div>
|
|
{result.origin} → {result.destination}
|
|
</div>
|
|
<div className="text-muted-foreground">{result.containerType}</div>
|
|
</div>
|
|
</TableCell>
|
|
|
|
{/* Prix */}
|
|
<TableCell>
|
|
<div className="font-semibold">{formatPrice(result.priceUSD, result.priceEUR)}</div>
|
|
{result.hasSurcharges && (
|
|
<div className="text-xs text-orange-600">+ surcharges</div>
|
|
)}
|
|
</TableCell>
|
|
|
|
{/* Surcharges */}
|
|
<TableCell>
|
|
{result.hasSurcharges ? (
|
|
<Dialog>
|
|
<DialogTrigger asChild>
|
|
<Button variant="ghost" size="sm" className="gap-1">
|
|
<Info className="h-3 w-3" />
|
|
Détails
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Détails des surcharges</DialogTitle>
|
|
<DialogDescription>
|
|
{result.companyName} - {result.origin} → {result.destination}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="py-4">
|
|
<p className="text-sm">{result.surchargeDetails}</p>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
) : (
|
|
<Badge variant="outline" className="text-green-600 border-green-600">
|
|
All-in
|
|
</Badge>
|
|
)}
|
|
</TableCell>
|
|
|
|
{/* Transit */}
|
|
<TableCell>
|
|
<div className="text-sm">{result.transitDays} jours</div>
|
|
</TableCell>
|
|
|
|
{/* Validité */}
|
|
<TableCell>
|
|
<div className="text-xs text-muted-foreground">
|
|
Jusqu'au {new Date(result.validUntil).toLocaleDateString('fr-FR')}
|
|
</div>
|
|
</TableCell>
|
|
|
|
{/* Score */}
|
|
<TableCell>
|
|
<div className="flex items-center gap-1">
|
|
<div
|
|
className={`text-sm font-medium ${
|
|
result.matchScore >= 90
|
|
? 'text-green-600'
|
|
: result.matchScore >= 75
|
|
? 'text-yellow-600'
|
|
: 'text-gray-600'
|
|
}`}
|
|
>
|
|
{result.matchScore}%
|
|
</div>
|
|
</div>
|
|
</TableCell>
|
|
|
|
{/* Actions */}
|
|
<TableCell>
|
|
<Button size="sm" onClick={() => onBooking?.(result)} disabled={!onBooking}>
|
|
Réserver
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
|
|
{/* Summary footer */}
|
|
<div className="p-4 border-t bg-muted/50">
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="text-muted-foreground">
|
|
{results.length} tarif{results.length > 1 ? 's' : ''} trouvé
|
|
{results.length > 1 ? 's' : ''}
|
|
</span>
|
|
<div className="flex items-center gap-4">
|
|
<span className="text-muted-foreground">
|
|
Prix affichés en <strong>{currency}</strong>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|