xpeditis2.0/apps/frontend/app/dashboard/bookings/page.tsx
2025-11-30 23:27:22 +01:00

417 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Bookings List Page
*
* Display all bookings (standard + CSV) with filters and search
*/
'use client';
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { listBookings, listCsvBookings } from '@/lib/api';
import Link from 'next/link';
type SearchType = 'pallets' | 'weight' | 'route' | 'status' | 'date' | 'quote';
export default function BookingsListPage() {
const [searchTerm, setSearchTerm] = useState('');
const [searchType, setSearchType] = useState<SearchType>('route');
const [statusFilter, setStatusFilter] = useState('');
const [page, setPage] = useState(1);
// Fetch CSV bookings only (without status filter in API, we filter client-side)
const { data: csvData, isLoading, error: csvError } = useQuery({
queryKey: ['csv-bookings', page],
queryFn: () =>
listCsvBookings({
page,
limit: 100, // Fetch more to allow client-side filtering
}),
});
// Log errors for debugging
if (csvError) console.error('CSV bookings error:', csvError);
// Filter bookings based on search term, search type, and status
const filterBookings = (bookings: any[]) => {
let filtered = bookings;
// Filter by status first (if status filter is active)
if (statusFilter) {
filtered = filtered.filter((booking: any) => booking.status === statusFilter);
}
// Then filter by search term if provided
if (searchTerm.trim()) {
const term = searchTerm.toLowerCase();
filtered = filtered.filter((booking: any) => {
switch (searchType) {
case 'pallets':
return booking.palletCount?.toString().includes(term);
case 'weight':
return booking.weightKG?.toString().includes(term);
case 'route':
const origin = booking.originCity?.toLowerCase() || '';
const destination = booking.destinationCity?.toLowerCase() || '';
return origin.includes(term) || destination.includes(term);
case 'status':
return booking.status?.toLowerCase().includes(term);
case 'date':
const date = new Date(booking.requestedPickupDate || booking.requestedAt).toLocaleDateString('fr-FR');
return date.includes(term);
case 'quote':
return booking.id?.toLowerCase().includes(term) || booking.quoteNumber?.toLowerCase().includes(term);
default:
return true;
}
});
}
return filtered;
};
const allBookings = filterBookings((csvData?.bookings || []).map(b => ({ ...b, type: 'csv' as const })));
const statusOptions = [
{ value: '', label: 'Tous les statuts' },
{ value: 'PENDING', label: 'En attente' },
{ value: 'ACCEPTED', label: 'Accepté' },
{ value: 'REJECTED', label: 'Refusé' },
];
const searchTypeOptions = [
{ value: 'route', label: 'Route (Origine/Destination)' },
{ value: 'pallets', label: 'Palettes/Colis' },
{ value: 'weight', label: 'Poids (kg)' },
{ value: 'status', label: 'Statut' },
{ value: 'date', label: 'Date' },
{ value: 'quote', label: 'N° Devis' },
];
const getPlaceholder = () => {
switch (searchType) {
case 'pallets':
return 'Rechercher par nombre de palettes...';
case 'weight':
return 'Rechercher par poids en kg...';
case 'route':
return 'Rechercher par ville (origine ou destination)...';
case 'status':
return 'Rechercher par statut...';
case 'date':
return 'Rechercher par date (JJ/MM/AAAA)...';
case 'quote':
return 'Rechercher par numéro de devis...';
default:
return 'Rechercher...';
}
};
const getStatusColor = (status: string) => {
const colors: Record<string, string> = {
PENDING: 'bg-yellow-100 text-yellow-800',
ACCEPTED: 'bg-green-100 text-green-800',
REJECTED: 'bg-red-100 text-red-800',
};
return colors[status] || 'bg-gray-100 text-gray-800';
};
const getStatusLabel = (status: string) => {
const labels: Record<string, string> = {
PENDING: 'En attente',
ACCEPTED: 'Accepté',
REJECTED: 'Refusé',
};
return labels[status] || status;
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Réservations</h1>
<p className="text-sm text-gray-500 mt-1">Gérez et suivez vos envois</p>
</div>
<Link
href="/dashboard/search-advanced"
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
>
<span className="mr-2"></span>
Nouvelle Réservation
</Link>
</div>
{/* Filters */}
<div className="bg-white rounded-lg shadow p-4">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label htmlFor="searchType" className="sr-only">
Type de recherche
</label>
<select
id="searchType"
value={searchType}
onChange={e => setSearchType(e.target.value as SearchType)}
className="block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md"
>
{searchTypeOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<div className="md:col-span-2">
<label htmlFor="search" className="sr-only">
Rechercher
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg
className="h-5 w-5 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
<input
type="text"
id="search"
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
placeholder={getPlaceholder()}
/>
</div>
</div>
<div>
<label htmlFor="status" className="sr-only">
Statut
</label>
<select
id="status"
value={statusFilter}
onChange={e => setStatusFilter(e.target.value)}
className="block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md"
>
{statusOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
</div>
</div>
{/* Bookings Table */}
<div className="bg-white rounded-lg shadow overflow-hidden">
{isLoading ? (
<div className="px-6 py-12 text-center text-gray-500">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
Chargement des réservations...
</div>
) : allBookings && allBookings.length > 0 ? (
<>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Palettes/Colis
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Poids
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Route
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Statut
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Date
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
N° Devis
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{allBookings.map((booking: any) => (
<tr key={`${booking.type}-${booking.id}`} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
{booking.type === 'csv'
? `${booking.palletCount} palette${booking.palletCount > 1 ? 's' : ''}`
: `${booking.containers?.length || 0} conteneur${booking.containers?.length > 1 ? 's' : ''}`}
</div>
<div className="text-xs text-gray-500">
{booking.type === 'csv' ? 'LCL' : booking.containerType || 'FCL'}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
{booking.type === 'csv'
? `${booking.weightKG} kg`
: booking.totalWeight
? `${booking.totalWeight} kg`
: 'N/A'}
</div>
<div className="text-xs text-gray-500">
{booking.type === 'csv'
? `${booking.volumeCBM} CBM`
: booking.totalVolume
? `${booking.totalVolume} CBM`
: ''}
</div>
</td>
<td className="px-6 py-4">
<div className="text-sm text-gray-900">
{booking.type === 'csv'
? `${booking.origin}${booking.destination}`
: booking.route || 'N/A'}
</div>
<div className="text-sm text-gray-500">
{booking.type === 'csv'
? `${booking.carrierName}`
: booking.carrier || ''}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusColor(
booking.status
)}`}
>
{getStatusLabel(booking.status)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{(booking.createdAt || booking.requestedAt)
? new Date(booking.createdAt || booking.requestedAt).toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
: 'N/A'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<Link
href={
booking.type === 'csv'
? `/dashboard/csv-bookings/${booking.id}`
: `/dashboard/bookings/${booking.id}`
}
className="text-blue-600 hover:text-blue-900"
>
{booking.type === 'csv'
? `#${booking.bookingId || booking.id.slice(0, 8).toUpperCase()}`
: booking.bookingNumber || `#${booking.id.slice(0, 8).toUpperCase()}`}
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Pagination */}
{(csvData?.total || 0) > 20 && (
<div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div className="flex-1 flex justify-between sm:hidden">
<button
onClick={() => setPage(Math.max(1, page - 1))}
disabled={page === 1}
className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:bg-gray-100 disabled:cursor-not-allowed"
>
Précédent
</button>
<button
onClick={() => setPage(page + 1)}
disabled={page * 20 >= (csvData?.total || 0)}
className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:bg-gray-100 disabled:cursor-not-allowed"
>
Suivant
</button>
</div>
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p className="text-sm text-gray-700">
Affichage de <span className="font-medium">{(page - 1) * 20 + 1}</span> à{' '}
<span className="font-medium">
{Math.min(page * 20, csvData?.total || 0)}
</span>{' '}
sur{' '}
<span className="font-medium">
{csvData?.total || 0}
</span>{' '}
résultats
</p>
</div>
<div className="flex space-x-2">
<button
onClick={() => setPage(Math.max(1, page - 1))}
disabled={page === 1}
className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:bg-gray-100 disabled:cursor-not-allowed"
>
Précédent
</button>
<button
onClick={() => setPage(page + 1)}
disabled={page * 20 >= (csvData?.total || 0)}
className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:bg-gray-100 disabled:cursor-not-allowed"
>
Suivant
</button>
</div>
</div>
</div>
)}
</>
) : (
<div className="px-6 py-12 text-center">
<svg
className="mx-auto h-12 w-12 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
/>
</svg>
<h3 className="mt-2 text-sm font-medium text-gray-900">Aucune réservation trouvée</h3>
<p className="mt-1 text-sm text-gray-500">
{searchTerm || statusFilter
? 'Essayez d\'ajuster vos filtres'
: 'Commencez par créer votre première réservation'}
</p>
<div className="mt-6">
<Link
href="/dashboard/search-advanced"
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
>
<span className="mr-2"></span>
Nouvelle Réservation
</Link>
</div>
</div>
)}
</div>
</div>
);
}