289 lines
12 KiB
TypeScript
289 lines
12 KiB
TypeScript
/**
|
||
* Bookings List Page
|
||
*
|
||
* Display all bookings with filters and search
|
||
*/
|
||
|
||
'use client';
|
||
|
||
import { useState } from 'react';
|
||
import { useQuery } from '@tanstack/react-query';
|
||
import { bookingsApi } from '@/lib/api';
|
||
import Link from 'next/link';
|
||
|
||
export default function BookingsListPage() {
|
||
const [searchTerm, setSearchTerm] = useState('');
|
||
const [statusFilter, setStatusFilter] = useState('');
|
||
const [page, setPage] = useState(1);
|
||
|
||
const { data, isLoading } = useQuery({
|
||
queryKey: ['bookings', page, statusFilter, searchTerm],
|
||
queryFn: () =>
|
||
bookingsApi.list({
|
||
page,
|
||
limit: 10,
|
||
status: statusFilter || undefined,
|
||
search: searchTerm || undefined,
|
||
}),
|
||
});
|
||
|
||
const statusOptions = [
|
||
{ value: '', label: 'All Statuses' },
|
||
{ value: 'draft', label: 'Draft' },
|
||
{ value: 'pending', label: 'Pending' },
|
||
{ value: 'confirmed', label: 'Confirmed' },
|
||
{ value: 'in_transit', label: 'In Transit' },
|
||
{ value: 'delivered', label: 'Delivered' },
|
||
{ value: 'cancelled', label: 'Cancelled' },
|
||
];
|
||
|
||
const getStatusColor = (status: string) => {
|
||
const colors: Record<string, string> = {
|
||
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',
|
||
};
|
||
return colors[status] || 'bg-gray-100 text-gray-800';
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-gray-900">Bookings</h1>
|
||
<p className="text-sm text-gray-500 mt-1">
|
||
Manage and track your shipments
|
||
</p>
|
||
</div>
|
||
<Link
|
||
href="/dashboard/bookings/new"
|
||
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>
|
||
New Booking
|
||
</Link>
|
||
</div>
|
||
|
||
{/* Filters */}
|
||
<div className="bg-white rounded-lg shadow p-4">
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||
<div className="md:col-span-2">
|
||
<label htmlFor="search" className="sr-only">
|
||
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)}
|
||
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="Search by booking number or description..."
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label htmlFor="status" className="sr-only">
|
||
Status
|
||
</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>
|
||
Loading bookings...
|
||
</div>
|
||
) : data?.data && data.data.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">
|
||
Booking Number
|
||
</th>
|
||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||
Cargo
|
||
</th>
|
||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||
Status
|
||
</th>
|
||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||
Created
|
||
</th>
|
||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||
Actions
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="bg-white divide-y divide-gray-200">
|
||
{data.data.map((booking) => (
|
||
<tr key={booking.id} className="hover:bg-gray-50">
|
||
<td className="px-6 py-4 whitespace-nowrap">
|
||
<Link
|
||
href={`/dashboard/bookings/${booking.id}`}
|
||
className="text-sm font-medium text-blue-600 hover:text-blue-700"
|
||
>
|
||
{booking.bookingNumber}
|
||
</Link>
|
||
</td>
|
||
<td className="px-6 py-4">
|
||
<div className="text-sm text-gray-900 max-w-xs truncate">
|
||
{booking.cargoDescription}
|
||
</div>
|
||
<div className="text-sm text-gray-500">
|
||
{booking.containers?.length || 0} container(s)
|
||
</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
|
||
)}`}
|
||
>
|
||
{booking.status}
|
||
</span>
|
||
</td>
|
||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||
{new Date(booking.createdAt).toLocaleDateString()}
|
||
</td>
|
||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||
<Link
|
||
href={`/dashboard/bookings/${booking.id}`}
|
||
className="text-blue-600 hover:text-blue-900 mr-4"
|
||
>
|
||
View
|
||
</Link>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{/* Pagination */}
|
||
{data.total > 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"
|
||
>
|
||
Previous
|
||
</button>
|
||
<button
|
||
onClick={() => setPage(page + 1)}
|
||
disabled={page * 10 >= data.total}
|
||
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"
|
||
>
|
||
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">
|
||
Showing{' '}
|
||
<span className="font-medium">{(page - 1) * 10 + 1}</span>{' '}
|
||
to{' '}
|
||
<span className="font-medium">
|
||
{Math.min(page * 10, data.total)}
|
||
</span>{' '}
|
||
of <span className="font-medium">{data.total}</span>{' '}
|
||
results
|
||
</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"
|
||
>
|
||
Previous
|
||
</button>
|
||
<button
|
||
onClick={() => setPage(page + 1)}
|
||
disabled={page * 10 >= data.total}
|
||
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"
|
||
>
|
||
Next
|
||
</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">
|
||
No bookings found
|
||
</h3>
|
||
<p className="mt-1 text-sm text-gray-500">
|
||
{searchTerm || statusFilter
|
||
? 'Try adjusting your filters'
|
||
: 'Get started by creating your first booking'}
|
||
</p>
|
||
<div className="mt-6">
|
||
<Link
|
||
href="/dashboard/bookings/new"
|
||
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>
|
||
New Booking
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|