xpeditis2.0/apps/frontend/app/dashboard/bookings/page.tsx
David e6b9b42f6c
Some checks failed
CI/CD Pipeline - Xpeditis PreProd / Backend - Build & Test (push) Failing after 5m51s
CI/CD Pipeline - Xpeditis PreProd / Backend - Docker Build & Push (push) Has been skipped
CI/CD Pipeline - Xpeditis PreProd / Frontend - Build & Test (push) Successful in 10m57s
CI/CD Pipeline - Xpeditis PreProd / Frontend - Docker Build & Push (push) Failing after 12m28s
CI/CD Pipeline - Xpeditis PreProd / Deploy to PreProd Server (push) Has been skipped
CI/CD Pipeline - Xpeditis PreProd / Run Smoke Tests (push) Has been skipped
fix
2025-11-13 00:15:45 +01:00

408 lines
18 KiB
TypeScript
Raw Permalink 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 BookingType = 'all' | 'standard' | 'csv';
export default function BookingsListPage() {
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('');
const [bookingType, setBookingType] = useState<BookingType>('csv'); // Start with CSV bookings
const [page, setPage] = useState(1);
// Fetch standard bookings
const { data: standardData, isLoading: standardLoading, error: standardError } = useQuery({
queryKey: ['bookings', page, statusFilter, searchTerm],
queryFn: () =>
listBookings({
page,
limit: 10,
status: statusFilter || undefined,
}),
enabled: bookingType === 'all' || bookingType === 'standard',
retry: false, // Don't retry if it fails
});
// Fetch CSV bookings
const { data: csvData, isLoading: csvLoading, error: csvError } = useQuery({
queryKey: ['csv-bookings', page, statusFilter],
queryFn: () =>
listCsvBookings({
page,
limit: 10,
status: statusFilter as 'PENDING' | 'ACCEPTED' | 'REJECTED' | undefined,
}),
enabled: bookingType === 'all' || bookingType === 'csv',
});
// Log errors for debugging
if (standardError) console.error('Standard bookings error:', standardError);
if (csvError) console.error('CSV bookings error:', csvError);
const isLoading = standardLoading || csvLoading;
// Combine bookings based on filter
const getCombinedBookings = () => {
if (bookingType === 'standard') {
return (standardData?.bookings || []).map(b => ({ ...b, type: 'standard' as const }));
}
if (bookingType === 'csv') {
return (csvData?.bookings || []).map(b => ({ ...b, type: 'csv' as const }));
}
// For 'all', combine both
const standard = (standardData?.bookings || []).map(b => ({ ...b, type: 'standard' as const }));
const csv = (csvData?.bookings || []).map(b => ({ ...b, type: 'csv' as const }));
return [...standard, ...csv].sort((a, b) => {
const dateA = new Date((a as any).createdAt || (a as any).requestedAt || 0).getTime();
const dateB = new Date((b as any).createdAt || (b as any).requestedAt || 0).getTime();
return dateB - dateA;
});
};
const allBookings = getCombinedBookings();
const statusOptions = [
{ value: '', label: 'Tous les statuts' },
// Standard booking statuses
{ value: 'draft', label: 'Brouillon' },
{ value: 'pending', label: 'En attente' },
{ value: 'confirmed', label: 'Confirmé' },
{ value: 'in_transit', label: 'En transit' },
{ value: 'delivered', label: 'Livré' },
{ value: 'cancelled', label: 'Annulé' },
// CSV booking statuses
{ value: 'PENDING', label: 'En attente (CSV)' },
{ value: 'ACCEPTED', label: 'Accepté (CSV)' },
{ value: 'REJECTED', label: 'Refusé (CSV)' },
];
const getStatusColor = (status: string) => {
const colors: Record<string, string> = {
// Standard statuses
draft: 'bg-gray-100 text-gray-800',
pending: 'bg-yellow-100 text-yellow-800',
confirmed: 'bg-green-100 text-green-800',
in_transit: 'bg-blue-100 text-blue-800',
delivered: 'bg-purple-100 text-purple-800',
cancelled: 'bg-red-100 text-red-800',
// CSV statuses
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> = {
// Standard statuses
draft: 'Brouillon',
pending: 'En attente',
confirmed: 'Confirmé',
in_transit: 'En transit',
delivered: 'Livré',
cancelled: 'Annulé',
// CSV statuses
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 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="Rechercher par numéro de réservation..."
/>
</div>
</div>
<div>
<label htmlFor="bookingType" className="sr-only">
Type de réservation
</label>
<select
id="bookingType"
value={bookingType}
onChange={e => setBookingType(e.target.value as BookingType)}
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"
>
<option value="all">Toutes les réservations</option>
<option value="standard">Réservations standard</option>
<option value="csv">Réservations CSV</option>
</select>
</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 */}
{((standardData?.total || 0) + (csvData?.total || 0)) > 10 && (
<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 * 10 >= ((standardData?.total || 0) + (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) * 10 + 1}</span> à{' '}
<span className="font-medium">
{Math.min(page * 10, (standardData?.total || 0) + (csvData?.total || 0))}
</span>{' '}
sur{' '}
<span className="font-medium">
{(standardData?.total || 0) + (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 * 10 >= ((standardData?.total || 0) + (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>
);
}