xpeditis2.0/apps/frontend/src/components/rate-search/RateResultsTable.tsx
2025-11-04 07:30:15 +01:00

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>
);
}