xpeditis2.0/apps/frontend/app/dashboard/bookings/page.tsx
David-Henri ARNAUD b31d325646 feature phase 2
2025-10-10 15:07:05 +02:00

289 lines
12 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 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>
);
}