fix search booking
This commit is contained in:
parent
2da0f0210d
commit
a27b1d6cfa
@ -11,90 +11,105 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { listBookings, listCsvBookings } from '@/lib/api';
|
import { listBookings, listCsvBookings } from '@/lib/api';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
type BookingType = 'all' | 'standard' | 'csv';
|
type SearchType = 'pallets' | 'weight' | 'route' | 'status' | 'date' | 'quote';
|
||||||
|
|
||||||
export default function BookingsListPage() {
|
export default function BookingsListPage() {
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [searchType, setSearchType] = useState<SearchType>('route');
|
||||||
const [statusFilter, setStatusFilter] = useState('');
|
const [statusFilter, setStatusFilter] = useState('');
|
||||||
const [bookingType, setBookingType] = useState<BookingType>('csv'); // Start with CSV bookings
|
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
|
|
||||||
// Fetch standard bookings
|
// Fetch CSV bookings only (without status filter in API, we filter client-side)
|
||||||
const { data: standardData, isLoading: standardLoading, error: standardError } = useQuery({
|
const { data: csvData, isLoading, error: csvError } = useQuery({
|
||||||
queryKey: ['bookings', page, statusFilter, searchTerm],
|
queryKey: ['csv-bookings', page],
|
||||||
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: () =>
|
queryFn: () =>
|
||||||
listCsvBookings({
|
listCsvBookings({
|
||||||
page,
|
page,
|
||||||
limit: 10,
|
limit: 100, // Fetch more to allow client-side filtering
|
||||||
status: statusFilter as 'PENDING' | 'ACCEPTED' | 'REJECTED' | undefined,
|
|
||||||
}),
|
}),
|
||||||
enabled: bookingType === 'all' || bookingType === 'csv',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log errors for debugging
|
// Log errors for debugging
|
||||||
if (standardError) console.error('Standard bookings error:', standardError);
|
|
||||||
if (csvError) console.error('CSV bookings error:', csvError);
|
if (csvError) console.error('CSV bookings error:', csvError);
|
||||||
|
|
||||||
const isLoading = standardLoading || csvLoading;
|
// Filter bookings based on search term, search type, and status
|
||||||
|
const filterBookings = (bookings: any[]) => {
|
||||||
|
let filtered = bookings;
|
||||||
|
|
||||||
// Combine bookings based on filter
|
// Filter by status first (if status filter is active)
|
||||||
const getCombinedBookings = () => {
|
if (statusFilter) {
|
||||||
if (bookingType === 'standard') {
|
filtered = filtered.filter((booking: any) => booking.status === statusFilter);
|
||||||
return (standardData?.bookings || []).map(b => ({ ...b, type: 'standard' as const }));
|
|
||||||
}
|
}
|
||||||
if (bookingType === 'csv') {
|
|
||||||
return (csvData?.bookings || []).map(b => ({ ...b, type: 'csv' as const }));
|
// 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// For 'all', combine both
|
|
||||||
const standard = (standardData?.bookings || []).map(b => ({ ...b, type: 'standard' as const }));
|
return filtered;
|
||||||
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 allBookings = filterBookings((csvData?.bookings || []).map(b => ({ ...b, type: 'csv' as const })));
|
||||||
|
|
||||||
const statusOptions = [
|
const statusOptions = [
|
||||||
{ value: '', label: 'Tous les statuts' },
|
{ value: '', label: 'Tous les statuts' },
|
||||||
// Standard booking statuses
|
{ value: 'PENDING', label: 'En attente' },
|
||||||
{ value: 'draft', label: 'Brouillon' },
|
{ value: 'ACCEPTED', label: 'Accepté' },
|
||||||
{ value: 'pending', label: 'En attente' },
|
{ value: 'REJECTED', label: 'Refusé' },
|
||||||
{ 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 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 getStatusColor = (status: string) => {
|
||||||
const colors: Record<string, 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',
|
PENDING: 'bg-yellow-100 text-yellow-800',
|
||||||
ACCEPTED: 'bg-green-100 text-green-800',
|
ACCEPTED: 'bg-green-100 text-green-800',
|
||||||
REJECTED: 'bg-red-100 text-red-800',
|
REJECTED: 'bg-red-100 text-red-800',
|
||||||
@ -104,14 +119,6 @@ export default function BookingsListPage() {
|
|||||||
|
|
||||||
const getStatusLabel = (status: string) => {
|
const getStatusLabel = (status: string) => {
|
||||||
const labels: Record<string, 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',
|
PENDING: 'En attente',
|
||||||
ACCEPTED: 'Accepté',
|
ACCEPTED: 'Accepté',
|
||||||
REJECTED: 'Refusé',
|
REJECTED: 'Refusé',
|
||||||
@ -139,6 +146,23 @@ export default function BookingsListPage() {
|
|||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<div className="bg-white rounded-lg shadow p-4">
|
<div className="bg-white rounded-lg shadow p-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-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">
|
<div className="md:col-span-2">
|
||||||
<label htmlFor="search" className="sr-only">
|
<label htmlFor="search" className="sr-only">
|
||||||
Rechercher
|
Rechercher
|
||||||
@ -165,25 +189,10 @@ export default function BookingsListPage() {
|
|||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={e => setSearchTerm(e.target.value)}
|
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"
|
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..."
|
placeholder={getPlaceholder()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<div>
|
||||||
<label htmlFor="status" className="sr-only">
|
<label htmlFor="status" className="sr-only">
|
||||||
Statut
|
Statut
|
||||||
@ -317,7 +326,7 @@ export default function BookingsListPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
{((standardData?.total || 0) + (csvData?.total || 0)) > 10 && (
|
{(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="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">
|
<div className="flex-1 flex justify-between sm:hidden">
|
||||||
<button
|
<button
|
||||||
@ -329,7 +338,7 @@ export default function BookingsListPage() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setPage(page + 1)}
|
onClick={() => setPage(page + 1)}
|
||||||
disabled={page * 10 >= ((standardData?.total || 0) + (csvData?.total || 0))}
|
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"
|
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
|
Suivant
|
||||||
@ -338,13 +347,13 @@ export default function BookingsListPage() {
|
|||||||
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-700">
|
<p className="text-sm text-gray-700">
|
||||||
Affichage de <span className="font-medium">{(page - 1) * 10 + 1}</span> à{' '}
|
Affichage de <span className="font-medium">{(page - 1) * 20 + 1}</span> à{' '}
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{Math.min(page * 10, (standardData?.total || 0) + (csvData?.total || 0))}
|
{Math.min(page * 20, csvData?.total || 0)}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
sur{' '}
|
sur{' '}
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{(standardData?.total || 0) + (csvData?.total || 0)}
|
{csvData?.total || 0}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
résultats
|
résultats
|
||||||
</p>
|
</p>
|
||||||
@ -359,7 +368,7 @@ export default function BookingsListPage() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setPage(page + 1)}
|
onClick={() => setPage(page + 1)}
|
||||||
disabled={page * 10 >= ((standardData?.total || 0) + (csvData?.total || 0))}
|
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"
|
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
|
Suivant
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user