'use client'; import { useState, useEffect } from 'react'; import { useQuery } from '@tanstack/react-query'; import { listCsvBookings } from '@/lib/api'; import { Link } from '@/i18n/navigation'; import { Plus, Clock, Eye, X, CreditCard, Pencil, MoreVertical, Lock, Sparkles } from 'lucide-react'; import ExportButton from '@/components/ExportButton'; import { useSearchParams } from 'next/navigation'; import { PageHeader } from '@/components/ui/PageHeader'; import { useTranslations, useLocale } from 'next-intl'; import { useReservationQuota } from '@/hooks/useReservationQuota'; type SearchType = 'pallets' | 'weight' | 'route' | 'status' | 'date' | 'quote'; // Statuses considered "not finalized": booking started but not yet paid. const TO_FINALIZE_STATUSES = ['PENDING_PAYMENT', 'PENDING_BANK_TRANSFER']; export default function BookingsListPage() { const t = useTranslations('dashboard.bookingsList'); const locale = useLocale(); const dateLocale = locale === 'fr' ? 'fr-FR' : 'en-US'; const searchParams = useSearchParams(); const [searchTerm, setSearchTerm] = useState(''); const [searchType, setSearchType] = useState('route'); const [statusFilter, setStatusFilter] = useState(''); const [page, setPage] = useState(1); const [showTransferBanner, setShowTransferBanner] = useState(false); const [selectedBooking, setSelectedBooking] = useState(null); const [openMenuId, setOpenMenuId] = useState(null); const [menuPos, setMenuPos] = useState<{ top: number; right: number } | null>(null); const ITEMS_PER_PAGE = 20; // "Modifier" reopens the existing booking flow (search → carrier choice → // payment) pre-filled with the booking's data, so a change re-quotes the price. const buildEditUrl = (b: any) => { const enabledOptions = Object.keys(b.options || {}) .filter(k => b.options[k]) .join(','); const params = new URLSearchParams({ editBookingId: b.id, origin: b.origin || b.originCity || '', destination: b.destination || b.destinationCity || '', volumeCBM: String(b.volumeCBM ?? ''), weightKG: String(b.weightKG ?? ''), palletCount: String(b.palletCount ?? ''), hasDangerousGoods: String(!!(b.options && b.options.dangerousGoods)), }); if (enabledOptions) params.set('options', enabledOptions); return `/dashboard/search-advanced?${params.toString()}`; }; // Kebab (⋮) actions menu — anchored with fixed positioning so it is never // clipped by the scrollable table container. const toggleMenu = (e: React.MouseEvent, bookingId: string) => { e.stopPropagation(); if (openMenuId === bookingId) { setOpenMenuId(null); return; } const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); setMenuPos({ top: rect.bottom + 6, right: window.innerWidth - rect.right }); setOpenMenuId(bookingId); }; useEffect(() => { if (!openMenuId) return; const close = () => setOpenMenuId(null); window.addEventListener('click', close); window.addEventListener('scroll', close, true); window.addEventListener('resize', close); return () => { window.removeEventListener('click', close); window.removeEventListener('scroll', close, true); window.removeEventListener('resize', close); }; }, [openMenuId]); useEffect(() => { if (searchParams.get('transfer') === 'declared') { setShowTransferBanner(true); } }, [searchParams]); const { data: csvData, isLoading, error: csvError, } = useQuery({ queryKey: ['csv-bookings'], queryFn: () => listCsvBookings({ page: 1, limit: 1000, }), }); if (csvError) console.error('CSV bookings error:', csvError); const quota = useReservationQuota(); // When the free-plan reservation limit is reached, the "New reservation" // entry point is replaced by an upgrade call-to-action. const newReservationAction = quota.blocked ? ( {t('limit.upgrade')} {t('limit.upgradeShort')} ) : ( {t('new')} ); const filterBookings = (bookings: any[]) => { let filtered = bookings; if (statusFilter) { if (statusFilter === 'to_finalize') { filtered = filtered.filter((booking: any) => TO_FINALIZE_STATUSES.includes(booking.status) ); } else { filtered = filtered.filter((booking: any) => booking.status === statusFilter); } } 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(dateLocale); return date.includes(term); case 'quote': return ( booking.id?.toLowerCase().includes(term) || booking.quoteNumber?.toLowerCase().includes(term) ); default: return true; } }); } return filtered; }; const filteredBookings = filterBookings( (csvData?.bookings || []).map(b => ({ ...b, type: 'csv' as const })) ); const totalBookings = filteredBookings.length; const totalPages = Math.ceil(totalBookings / ITEMS_PER_PAGE); const startIndex = (page - 1) * ITEMS_PER_PAGE; const endIndex = startIndex + ITEMS_PER_PAGE; const paginatedBookings = filteredBookings.slice(startIndex, endIndex); const resetPage = () => setPage(1); const statusOptions = [ { value: '', label: t('statusFilter.all') }, { value: 'to_finalize', label: t('statusFilter.toFinalize') }, { value: 'PENDING_PAYMENT', label: t('status.pendingPayment') }, { value: 'PENDING_BANK_TRANSFER', label: t('status.pendingBankTransfer') }, { value: 'PENDING', label: t('status.pending') }, { value: 'ACCEPTED', label: t('status.accepted') }, { value: 'REJECTED', label: t('status.rejected') }, { value: 'CANCELLED', label: t('status.cancelled') }, ]; const searchTypeOptions: { value: SearchType; label: string }[] = [ { value: 'route', label: t('searchType.route') }, { value: 'pallets', label: t('searchType.pallets') }, { value: 'weight', label: t('searchType.weight') }, { value: 'status', label: t('searchType.status') }, { value: 'date', label: t('searchType.date') }, { value: 'quote', label: t('searchType.quote') }, ]; const getPlaceholder = () => { const keyMap: Record = { route: 'searchPlaceholder.route', pallets: 'searchPlaceholder.pallets', weight: 'searchPlaceholder.weight', status: 'searchPlaceholder.status', date: 'searchPlaceholder.date', quote: 'searchPlaceholder.quote', }; return t(keyMap[searchType] as any); }; const getStatusColor = (status: string) => { const colors: Record = { PENDING_PAYMENT: 'bg-orange-100 text-orange-800', PENDING_BANK_TRANSFER: 'bg-blue-100 text-blue-800', PENDING: 'bg-yellow-100 text-yellow-800', ACCEPTED: 'bg-green-100 text-green-800', REJECTED: 'bg-red-100 text-red-800', CANCELLED: 'bg-gray-100 text-gray-800', }; return colors[status] || 'bg-gray-100 text-gray-800'; }; const getStatusLabel = (status: string) => { const map: Record = { PENDING_PAYMENT: t('status.pendingPayment'), PENDING_BANK_TRANSFER: t('status.pendingBankTransfer'), PENDING: t('status.pending'), ACCEPTED: t('status.accepted'), REJECTED: t('status.rejected'), CANCELLED: t('status.cancelled'), }; return map[status] || status; }; return (
{showTransferBanner && (

{t('transferBanner.title')}

{t('transferBanner.message')}

)} `${v || 0}` }, { key: 'weightKG', label: t('export.weight'), format: v => `${v || 0}` }, { key: 'volumeCBM', label: t('export.volume'), format: v => `${v || 0}` }, { key: 'origin', label: t('export.origin') }, { key: 'destination', label: t('export.destination') }, { key: 'carrierName', label: t('export.carrier') }, { key: 'status', label: t('export.status'), format: v => getStatusLabel(v) }, { key: 'createdAt', label: t('export.createdAt'), format: v => (v ? new Date(v).toLocaleDateString(dateLocale) : ''), }, ]} /> {newReservationAction} } /> {quota.blocked && (

{t('limit.title')}

{t('limit.message', { max: quota.max })}

{t('limit.upgrade')}
)}
{ setSearchTerm(e.target.value); resetPage(); }} 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()} />
{isLoading ? (
{t('loading')}
) : paginatedBookings && paginatedBookings.length > 0 ? ( <>
{paginatedBookings.map((booking: any) => (
{booking.type === 'csv' ? `${booking.origin} → ${booking.destination}` : booking.route || 'N/A'}
{booking.type === 'csv' ? booking.carrierName : booking.carrier || ''}
{getStatusLabel(booking.status)}
{t('mobile.pallets')}
{booking.type === 'csv' ? t('units.palletsShort', { count: booking.palletCount }) : t('units.containersShort', { count: booking.containers?.length || 0 })}
{t('mobile.weight')}
{booking.type === 'csv' ? t('units.kg', { value: booking.weightKG }) : booking.totalWeight ? t('units.kg', { value: booking.totalWeight }) : 'N/A'}
{t('mobile.date')}
{booking.createdAt || booking.requestedAt ? new Date(booking.createdAt || booking.requestedAt).toLocaleDateString( dateLocale, { day: '2-digit', month: '2-digit', year: '2-digit' } ) : 'N/A'}
{booking.type === 'csv' ? t('mobile.ref', { id: booking.bookingId || booking.id.slice(0, 8).toUpperCase(), }) : t('mobile.booking', { number: booking.bookingNumber || '-' })}
))}
{paginatedBookings.map((booking: any) => { const reference = booking.type === 'csv' ? `#${booking.bookingId || booking.id.slice(0, 8).toUpperCase()}` : booking.bookingNumber || `#${booking.id.slice(0, 8).toUpperCase()}`; return ( ); })}
{t('columns.reference')} {t('columns.route')} {t('columns.cargo')} {t('columns.status')} {t('columns.date')} {t('columns.actions')}
{booking.bookingNumber || reference}
{booking.bookingNumber && (
{reference}
)}
{booking.type === 'csv' ? `${booking.origin} → ${booking.destination}` : booking.route || 'N/A'}
{booking.type === 'csv' ? `${booking.carrierName}` : booking.carrier || ''}
{booking.type === 'csv' ? t('units.palletsCount', { count: booking.palletCount }) : t('units.containersCount', { count: booking.containers?.length || 0, })} {' · '} {booking.type === 'csv' ? 'LCL' : booking.containerType || 'FCL'}
{booking.type === 'csv' ? `${t('units.kg', { value: booking.weightKG })} · ${t('units.cbm', { value: booking.volumeCBM })}` : booking.totalWeight ? t('units.kg', { value: booking.totalWeight }) : ''}
{getStatusLabel(booking.status)} {booking.createdAt || booking.requestedAt ? new Date( booking.createdAt || booking.requestedAt ).toLocaleDateString(dateLocale, { day: '2-digit', month: '2-digit', year: 'numeric', }) : 'N/A'}
{totalPages > 1 && (

{t.rich('pagination.showing', { start: startIndex + 1, end: Math.min(endIndex, totalBookings), total: totalBookings, b: chunks => {chunks}, })}

)} ) : (

{t('empty.title')}

{searchTerm || statusFilter ? t('empty.hasFilters') : t('empty.noBookings')}

{quota.blocked ? ( {t('limit.upgrade')} ) : ( {t('new')} )}
)}
{/* Actions menu (⋮) — fixed-positioned to avoid clipping */} {openMenuId && menuPos && (() => { const b = paginatedBookings.find((x: any) => x.id === openMenuId); if (!b) return null; return (
e.stopPropagation()} > {b.status === 'PENDING_PAYMENT' && ( <> {t('actions.edit')} {t('actions.pay')} )}
); })()} {/* Booking Detail Modal */} {selectedBooking && (
setSelectedBooking(null)} />

{t('detail.title')} {selectedBooking.bookingId ? ` #${selectedBooking.bookingId}` : ` #${selectedBooking.id?.slice(0, 8).toUpperCase()}`}

{getStatusLabel(selectedBooking.status)} {selectedBooking.carrierName && ( {selectedBooking.carrierName} )}

{t('detail.route')}

{selectedBooking.origin || selectedBooking.originCity || 'N/A'} {' → '} {selectedBooking.destination || selectedBooking.destinationCity || 'N/A'}

{t('detail.date')}

{selectedBooking.createdAt || selectedBooking.requestedAt ? new Date( selectedBooking.createdAt || selectedBooking.requestedAt ).toLocaleDateString(dateLocale, { day: '2-digit', month: 'long', year: 'numeric', }) : 'N/A'}

{t('detail.cargo')}

{selectedBooking.palletCount ? t('units.palletsCount', { count: selectedBooking.palletCount }) : '-'}

{t('detail.weight')}

{selectedBooking.weightKG ? t('units.kg', { value: selectedBooking.weightKG }) : 'N/A'} {selectedBooking.volumeCBM ? ` / ${t('units.cbm', { value: selectedBooking.volumeCBM })}` : ''}

{selectedBooking.requestedPickupDate && (

{t('detail.pickupDate')}

{new Date(selectedBooking.requestedPickupDate).toLocaleDateString(dateLocale, { day: '2-digit', month: 'long', year: 'numeric', })}

)} {(selectedBooking.priceEUR || selectedBooking.priceUSD) && (

{t('detail.price')}

{selectedBooking.priceEUR ? `${selectedBooking.priceEUR} €` : `${selectedBooking.priceUSD} $`}

)}
{selectedBooking.status === 'PENDING_PAYMENT' && (

{t('detail.pendingPaymentNotice')}

setSelectedBooking(null)} className="inline-flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-700 text-sm font-medium rounded-lg hover:bg-gray-200 transition-colors" > {t('actions.edit')} setSelectedBooking(null)} className="inline-flex items-center gap-2 px-4 py-2 bg-orange-500 text-white text-sm font-medium rounded-lg hover:bg-orange-600 transition-colors" > {t('detail.payNow')}
)}
)}
); }