Some checks failed
Dev CI / Unit Tests (${{ matrix.app }}) (backend) (push) Blocked by required conditions
Dev CI / Unit Tests (${{ matrix.app }}) (frontend) (push) Blocked by required conditions
Dev CI / Notify Failure (push) Blocked by required conditions
Dev CI / Quality (${{ matrix.app }}) (backend) (push) Has been cancelled
Dev CI / Quality (${{ matrix.app }}) (frontend) (push) Has been cancelled
Aligns dev with the complete application codebase (cicd branch). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
233 lines
8.0 KiB
TypeScript
233 lines
8.0 KiB
TypeScript
/**
|
|
* CSV Rate Search Page
|
|
*
|
|
* Complete rate search page with:
|
|
* - Volume/Weight/Pallet input
|
|
* - Advanced filters panel
|
|
* - Results table with CSV/API source badges
|
|
*/
|
|
|
|
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
|
import { Loader2, Search } from 'lucide-react';
|
|
import { VolumeWeightInput } from '@/components/rate-search/VolumeWeightInput';
|
|
import { RateFiltersPanel } from '@/components/rate-search/RateFiltersPanel';
|
|
import { RateResultsTable } from '@/components/rate-search/RateResultsTable';
|
|
import { useCsvRateSearch } from '@/hooks/useCsvRateSearch';
|
|
import type { RateSearchFilters } from '@/types/rate-filters';
|
|
|
|
export default function CsvRateSearchPage() {
|
|
// Search parameters
|
|
const [origin, setOrigin] = useState('NLRTM');
|
|
const [destination, setDestination] = useState('USNYC');
|
|
const [volumeCBM, setVolumeCBM] = useState(25.5);
|
|
const [weightKG, setWeightKG] = useState(3500);
|
|
const [palletCount, setPalletCount] = useState(10);
|
|
const [filters, setFilters] = useState<RateSearchFilters>({});
|
|
const [currency, setCurrency] = useState<'USD' | 'EUR'>('USD');
|
|
|
|
const { data, loading, error, search } = useCsvRateSearch();
|
|
|
|
const handleSearch = async () => {
|
|
await search({
|
|
origin,
|
|
destination,
|
|
volumeCBM,
|
|
weightKG,
|
|
palletCount,
|
|
containerType: 'LCL',
|
|
filters,
|
|
});
|
|
};
|
|
|
|
const handleResetFilters = () => {
|
|
setFilters({});
|
|
};
|
|
|
|
const handleBooking = (result: any) => {
|
|
alert(`Booking pour ${result.companyName}: ${result.origin} → ${result.destination}`);
|
|
// TODO: Implement actual booking flow
|
|
};
|
|
|
|
return (
|
|
<div className="container mx-auto py-8 space-y-6">
|
|
{/* Page Header */}
|
|
<div>
|
|
<h1 className="text-3xl font-bold tracking-tight">Recherche de tarifs CSV</h1>
|
|
<p className="text-muted-foreground mt-2">
|
|
Recherchez des tarifs de transport maritime avec filtres avancés
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
|
{/* Left Column: Filters */}
|
|
<div className="lg:col-span-1">
|
|
<RateFiltersPanel
|
|
filters={filters}
|
|
onFiltersChange={setFilters}
|
|
resultsCount={data?.totalResults || 0}
|
|
onReset={handleResetFilters}
|
|
/>
|
|
</div>
|
|
|
|
{/* Right Column: Search Form + Results */}
|
|
<div className="lg:col-span-3 space-y-6">
|
|
{/* Search Form */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Paramètres de recherche</CardTitle>
|
|
<CardDescription>
|
|
Indiquez votre trajet et les dimensions de votre envoi
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{/* Origin and Destination */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="origin">
|
|
Port d'origine <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Input
|
|
id="origin"
|
|
value={origin}
|
|
onChange={e => setOrigin(e.target.value.toUpperCase())}
|
|
placeholder="NLRTM"
|
|
maxLength={5}
|
|
required
|
|
/>
|
|
<p className="text-xs text-muted-foreground">Code UN/LOCODE (5 caractères)</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="destination">
|
|
Port de destination <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Input
|
|
id="destination"
|
|
value={destination}
|
|
onChange={e => setDestination(e.target.value.toUpperCase())}
|
|
placeholder="USNYC"
|
|
maxLength={5}
|
|
required
|
|
/>
|
|
<p className="text-xs text-muted-foreground">Code UN/LOCODE (5 caractères)</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Volume, Weight, Pallets */}
|
|
<VolumeWeightInput
|
|
volumeCBM={volumeCBM}
|
|
weightKG={weightKG}
|
|
palletCount={palletCount}
|
|
onVolumeChange={setVolumeCBM}
|
|
onWeightChange={setWeightKG}
|
|
onPalletChange={setPalletCount}
|
|
disabled={loading}
|
|
/>
|
|
|
|
{/* Currency Selection */}
|
|
<div className="space-y-2">
|
|
<Label>Devise d'affichage</Label>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
type="button"
|
|
variant={currency === 'USD' ? 'default' : 'outline'}
|
|
onClick={() => setCurrency('USD')}
|
|
disabled={loading}
|
|
>
|
|
USD ($)
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant={currency === 'EUR' ? 'default' : 'outline'}
|
|
onClick={() => setCurrency('EUR')}
|
|
disabled={loading}
|
|
>
|
|
EUR (€)
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Search Button */}
|
|
<Button
|
|
onClick={handleSearch}
|
|
disabled={loading || !origin || !destination || volumeCBM <= 0 || weightKG <= 0}
|
|
className="w-full"
|
|
size="lg"
|
|
>
|
|
{loading ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
Recherche en cours...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Search className="mr-2 h-4 w-4" />
|
|
Rechercher des tarifs
|
|
</>
|
|
)}
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Error Alert */}
|
|
{error && (
|
|
<Alert variant="destructive">
|
|
<AlertDescription>{error}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
{/* Search Info */}
|
|
{data && (
|
|
<Alert>
|
|
<AlertDescription>
|
|
Recherche effectuée le {new Date(data.searchedAt).toLocaleString('fr-FR')} •{' '}
|
|
{data.searchedFiles.length} fichier(s) CSV analysé(s) • {data.totalResults} tarif(s)
|
|
trouvé(s)
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
{/* Results Table */}
|
|
{data && data.results.length > 0 && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Résultats de recherche</CardTitle>
|
|
<CardDescription>
|
|
{data.totalResults} tarif{data.totalResults > 1 ? 's' : ''} correspondant à vos
|
|
critères
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<RateResultsTable
|
|
results={data.results}
|
|
currency={currency}
|
|
onBooking={handleBooking}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* No Results */}
|
|
{data && data.results.length === 0 && (
|
|
<Card>
|
|
<CardContent className="py-12 text-center">
|
|
<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>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|