All checks were successful
Dev CI / Backend — Lint (push) Successful in 10m23s
Dev CI / Backend — Unit Tests (push) Successful in 10m17s
Dev CI / Frontend — Lint & Type-check (push) Successful in 11m3s
Dev CI / Frontend — Unit Tests (push) Successful in 10m33s
Dev CI / Notify Failure (push) Has been skipped
545 lines
25 KiB
TypeScript
545 lines
25 KiB
TypeScript
'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 } 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';
|
|
|
|
type SearchType = 'pallets' | 'weight' | 'route' | 'status' | 'date' | 'quote';
|
|
|
|
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<SearchType>('route');
|
|
const [statusFilter, setStatusFilter] = useState('');
|
|
const [page, setPage] = useState(1);
|
|
const [showTransferBanner, setShowTransferBanner] = useState(false);
|
|
const ITEMS_PER_PAGE = 20;
|
|
|
|
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 filterBookings = (bookings: any[]) => {
|
|
let filtered = bookings;
|
|
|
|
if (statusFilter) {
|
|
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: 'PENDING', label: t('status.pending') },
|
|
{ value: 'ACCEPTED', label: t('status.accepted') },
|
|
{ value: 'REJECTED', label: t('status.rejected') },
|
|
];
|
|
|
|
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<SearchType, string> = {
|
|
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<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 map: Record<string, string> = {
|
|
PENDING: t('status.pending'),
|
|
ACCEPTED: t('status.accepted'),
|
|
REJECTED: t('status.rejected'),
|
|
};
|
|
return map[status] || status;
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{showTransferBanner && (
|
|
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 flex items-start justify-between">
|
|
<div className="flex items-start space-x-3">
|
|
<Clock className="h-5 w-5 text-amber-600 flex-shrink-0 mt-0.5" />
|
|
<div>
|
|
<p className="font-medium text-amber-800">{t('transferBanner.title')}</p>
|
|
<p className="text-sm text-amber-700 mt-0.5">
|
|
{t('transferBanner.message')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button onClick={() => setShowTransferBanner(false)} className="text-amber-500 hover:text-amber-700 ml-4 flex-shrink-0">✕</button>
|
|
</div>
|
|
)}
|
|
<PageHeader
|
|
title={t('title')}
|
|
description={t('description')}
|
|
actions={
|
|
<>
|
|
<ExportButton
|
|
data={filteredBookings}
|
|
filename={t('exportFilename')}
|
|
columns={[
|
|
{ key: 'id', label: t('export.id') },
|
|
{ key: 'palletCount', label: t('export.pallets'), format: (v) => `${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) : '' },
|
|
]}
|
|
/>
|
|
<Link
|
|
href="/dashboard/search-advanced"
|
|
className="inline-flex items-center px-3 sm:px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
|
|
>
|
|
<Plus className="h-4 w-4 sm:mr-2" />
|
|
<span className="hidden sm:inline">{t('new')}</span>
|
|
</Link>
|
|
</>
|
|
}
|
|
/>
|
|
|
|
<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">
|
|
{t('searchType.label')}
|
|
</label>
|
|
<select
|
|
id="searchType"
|
|
value={searchType}
|
|
onChange={e => {
|
|
setSearchType(e.target.value as SearchType);
|
|
resetPage();
|
|
}}
|
|
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">
|
|
{t('search')}
|
|
</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);
|
|
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()}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="status" className="sr-only">
|
|
{t('statusFilter.label')}
|
|
</label>
|
|
<select
|
|
id="status"
|
|
value={statusFilter}
|
|
onChange={e => {
|
|
setStatusFilter(e.target.value);
|
|
resetPage();
|
|
}}
|
|
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>
|
|
|
|
<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>
|
|
{t('loading')}
|
|
</div>
|
|
) : paginatedBookings && paginatedBookings.length > 0 ? (
|
|
<>
|
|
<div className="md:hidden divide-y divide-gray-200">
|
|
{paginatedBookings.map((booking: any) => (
|
|
<div key={`${booking.type}-${booking.id}`} className="p-4 space-y-3">
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<div className="text-sm font-semibold text-gray-900">
|
|
{booking.type === 'csv'
|
|
? `${booking.origin} → ${booking.destination}`
|
|
: booking.route || 'N/A'}
|
|
</div>
|
|
<div className="text-xs text-gray-500 mt-0.5">
|
|
{booking.type === 'csv' ? booking.carrierName : booking.carrier || ''}
|
|
</div>
|
|
</div>
|
|
<span className={`px-2 py-1 text-xs font-semibold rounded-full flex-shrink-0 ${getStatusColor(booking.status)}`}>
|
|
{getStatusLabel(booking.status)}
|
|
</span>
|
|
</div>
|
|
<div className="grid grid-cols-3 gap-2 text-xs">
|
|
<div>
|
|
<div className="text-gray-400 uppercase tracking-wide">{t('mobile.pallets')}</div>
|
|
<div className="font-medium text-gray-900 mt-0.5">
|
|
{booking.type === 'csv'
|
|
? t('units.palletsShort', { count: booking.palletCount })
|
|
: t('units.containersShort', { count: booking.containers?.length || 0 })}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-gray-400 uppercase tracking-wide">{t('mobile.weight')}</div>
|
|
<div className="font-medium text-gray-900 mt-0.5">
|
|
{booking.type === 'csv'
|
|
? t('units.kg', { value: booking.weightKG })
|
|
: booking.totalWeight ? t('units.kg', { value: booking.totalWeight }) : 'N/A'}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-gray-400 uppercase tracking-wide">{t('mobile.date')}</div>
|
|
<div className="font-medium text-gray-900 mt-0.5">
|
|
{(booking.createdAt || booking.requestedAt)
|
|
? new Date(booking.createdAt || booking.requestedAt).toLocaleDateString(dateLocale, { day: '2-digit', month: '2-digit', year: '2-digit' })
|
|
: 'N/A'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="text-xs text-gray-400">
|
|
{booking.type === 'csv'
|
|
? t('mobile.ref', { id: booking.bookingId || booking.id.slice(0, 8).toUpperCase() })
|
|
: t('mobile.booking', { number: booking.bookingNumber || '-' })}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="hidden md:block 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">
|
|
{t('columns.palletsPackages')}
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
{t('columns.weight')}
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
{t('columns.route')}
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
{t('columns.status')}
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
{t('columns.date')}
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
{t('columns.quoteNumber')}
|
|
</th>
|
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
{t('columns.bookingNumber')}
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{paginatedBookings.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'
|
|
? t('units.palletsCount', { count: booking.palletCount })
|
|
: t('units.containersCount', { count: booking.containers?.length || 0 })}
|
|
</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'
|
|
? t('units.kg', { value: booking.weightKG })
|
|
: booking.totalWeight
|
|
? t('units.kg', { value: booking.totalWeight })
|
|
: 'N/A'}
|
|
</div>
|
|
<div className="text-xs text-gray-500">
|
|
{booking.type === 'csv'
|
|
? t('units.cbm', { value: booking.volumeCBM })
|
|
: booking.totalVolume
|
|
? t('units.cbm', { value: booking.totalVolume })
|
|
: ''}
|
|
</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(dateLocale, {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
})
|
|
: 'N/A'}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-left text-sm font-medium">
|
|
{booking.type === 'csv'
|
|
? `#${booking.bookingId || booking.id.slice(0, 8).toUpperCase()}`
|
|
: booking.bookingNumber || `#${booking.id.slice(0, 8).toUpperCase()}`}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium text-brand-navy">
|
|
{booking.bookingNumber || '-'}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{totalPages > 1 && (
|
|
<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(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:text-gray-400 disabled:cursor-not-allowed"
|
|
>
|
|
{t('pagination.previous')}
|
|
</button>
|
|
<button
|
|
onClick={() => setPage(page + 1)}
|
|
disabled={page >= totalPages}
|
|
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:text-gray-400 disabled:cursor-not-allowed"
|
|
>
|
|
{t('pagination.next')}
|
|
</button>
|
|
</div>
|
|
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
|
<div>
|
|
<p className="text-sm text-gray-700">
|
|
{t.rich('pagination.showing', {
|
|
start: startIndex + 1,
|
|
end: Math.min(endIndex, totalBookings),
|
|
total: totalBookings,
|
|
b: (chunks) => <span className="font-medium">{chunks}</span>,
|
|
})}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
|
|
<button
|
|
onClick={() => setPage(page - 1)}
|
|
disabled={page === 1}
|
|
className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed"
|
|
>
|
|
<span className="sr-only">{t('pagination.previous')}</span>
|
|
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clipRule="evenodd" />
|
|
</svg>
|
|
</button>
|
|
|
|
{[...Array(totalPages)].map((_, idx) => {
|
|
const pageNum = idx + 1;
|
|
const showPage = pageNum === 1 ||
|
|
pageNum === totalPages ||
|
|
(pageNum >= page - 1 && pageNum <= page + 1);
|
|
|
|
if (!showPage && pageNum === 2) {
|
|
return <span key={pageNum} className="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">...</span>;
|
|
}
|
|
if (!showPage && pageNum === totalPages - 1) {
|
|
return <span key={pageNum} className="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">...</span>;
|
|
}
|
|
if (!showPage) return null;
|
|
|
|
return (
|
|
<button
|
|
key={pageNum}
|
|
onClick={() => setPage(pageNum)}
|
|
className={`relative inline-flex items-center px-4 py-2 border text-sm font-medium ${
|
|
page === pageNum
|
|
? 'z-10 bg-blue-50 border-blue-500 text-blue-600'
|
|
: 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50'
|
|
}`}
|
|
>
|
|
{pageNum}
|
|
</button>
|
|
);
|
|
})}
|
|
|
|
<button
|
|
onClick={() => setPage(page + 1)}
|
|
disabled={page >= totalPages}
|
|
className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed"
|
|
>
|
|
<span className="sr-only">{t('pagination.next')}</span>
|
|
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
|
|
</svg>
|
|
</button>
|
|
</nav>
|
|
</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">{t('empty.title')}</h3>
|
|
<p className="mt-1 text-sm text-gray-500">
|
|
{searchTerm || statusFilter
|
|
? t('empty.hasFilters')
|
|
: t('empty.noBookings')}
|
|
</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"
|
|
>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
{t('new')}
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|