xpeditis2.0/apps/frontend/app/dashboard/admin/bookings/page.tsx
David a1e255e816
Some checks failed
CI/CD Pipeline / Discord Notification (Failure) (push) Blocked by required conditions
CI/CD Pipeline / Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Deployment Summary (push) Blocked by required conditions
CI/CD Pipeline / Deploy to Portainer (push) Blocked by required conditions
CI/CD Pipeline / Discord Notification (Success) (push) Blocked by required conditions
CI/CD Pipeline / Backend - Build, Test & Push (push) Failing after 1m20s
CI/CD Pipeline / Frontend - Build, Test & Push (push) Has been cancelled
fix v1.0.0
2025-12-23 11:49:57 +01:00

414 lines
17 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { getAllBookings } from '@/lib/api/admin';
interface Booking {
id: string;
bookingNumber?: string;
bookingId?: string;
type?: string;
status: string;
// CSV bookings use these fields
origin?: string;
destination?: string;
carrierName?: string;
// Regular bookings use these fields
originPort?: {
code: string;
name: string;
};
destinationPort?: {
code: string;
name: string;
};
carrier?: string;
containerType: string;
quantity?: number;
price?: number;
primaryCurrency?: string;
totalPrice?: {
amount: number;
currency: string;
};
createdAt?: string;
updatedAt?: string;
requestedAt?: string;
organizationId?: string;
userId?: string;
}
export default function AdminBookingsPage() {
const [bookings, setBookings] = useState<Booking[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedBooking, setSelectedBooking] = useState<Booking | null>(null);
const [showDetailsModal, setShowDetailsModal] = useState(false);
const [filterStatus, setFilterStatus] = useState('all');
const [searchTerm, setSearchTerm] = useState('');
// Helper function to get formatted quote number
const getQuoteNumber = (booking: Booking): string => {
if (booking.type === 'csv') {
return `#${booking.bookingId || booking.id.slice(0, 8).toUpperCase()}`;
}
return booking.bookingNumber || `#${booking.id.slice(0, 8).toUpperCase()}`;
};
useEffect(() => {
fetchBookings();
}, []);
const fetchBookings = async () => {
try {
setLoading(true);
const response = await getAllBookings();
setBookings(response.bookings || []);
setError(null);
} catch (err: any) {
setError(err.message || 'Failed to load bookings');
} finally {
setLoading(false);
}
};
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-blue-100 text-blue-800',
in_transit: 'bg-purple-100 text-purple-800',
delivered: 'bg-green-100 text-green-800',
cancelled: 'bg-red-100 text-red-800',
};
return colors[status.toLowerCase()] || 'bg-gray-100 text-gray-800';
};
const filteredBookings = bookings
.filter(booking => filterStatus === 'all' || booking.status.toLowerCase() === filterStatus)
.filter(booking => {
if (searchTerm === '') return true;
const searchLower = searchTerm.toLowerCase();
const quoteNumber = getQuoteNumber(booking).toLowerCase();
return (
quoteNumber.includes(searchLower) ||
booking.bookingNumber?.toLowerCase().includes(searchLower) ||
booking.carrier?.toLowerCase().includes(searchLower) ||
booking.carrierName?.toLowerCase().includes(searchLower) ||
booking.origin?.toLowerCase().includes(searchLower) ||
booking.destination?.toLowerCase().includes(searchLower)
);
});
if (loading) {
return (
<div className="flex items-center justify-center h-96">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading bookings...</p>
</div>
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Booking Management</h1>
<p className="mt-1 text-sm text-gray-500">
View and manage all bookings across the platform
</p>
</div>
</div>
{/* Filters */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Search</label>
<input
type="text"
placeholder="Search by booking number or carrier..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Status Filter</label>
<select
value={filterStatus}
onChange={e => setFilterStatus(e.target.value)}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
>
<option value="all">All Statuses</option>
<option value="draft">Draft</option>
<option value="pending">Pending</option>
<option value="confirmed">Confirmed</option>
<option value="in_transit">In Transit</option>
<option value="delivered">Delivered</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="text-sm text-gray-500">Total Réservations</div>
<div className="text-2xl font-bold text-gray-900">{bookings.length}</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="text-sm text-gray-500">En Attente</div>
<div className="text-2xl font-bold text-yellow-600">
{bookings.filter(b => b.status.toUpperCase() === 'PENDING').length}
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="text-sm text-gray-500">Acceptées</div>
<div className="text-2xl font-bold text-green-600">
{bookings.filter(b => b.status.toUpperCase() === 'ACCEPTED').length}
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="text-sm text-gray-500">Rejetées</div>
<div className="text-2xl font-bold text-red-600">
{bookings.filter(b => b.status.toUpperCase() === 'REJECTED').length}
</div>
</div>
</div>
{/* Error Message */}
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg">
{error}
</div>
)}
{/* Bookings Table */}
<div className="bg-white rounded-lg shadow overflow-hidden">
<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">
Numéro de devis
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Route
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Transporteur
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Conteneur
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Statut
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Prix
</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">
{filteredBookings.map(booking => (
<tr key={booking.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
{getQuoteNumber(booking)}
</div>
<div className="text-xs text-gray-500">
{new Date(booking.createdAt || booking.requestedAt || '').toLocaleDateString()}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">
{booking.originPort ? `${booking.originPort.code}${booking.destinationPort?.code}` : `${booking.origin}${booking.destination}`}
</div>
<div className="text-xs text-gray-500">
{booking.originPort ? `${booking.originPort.name}${booking.destinationPort?.name}` : ''}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{booking.carrier || booking.carrierName || 'N/A'}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{booking.containerType}</div>
<div className="text-xs text-gray-500">
{booking.quantity ? `Qty: ${booking.quantity}` : ''}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 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-900">
{booking.totalPrice
? `${booking.totalPrice.amount.toLocaleString()} ${booking.totalPrice.currency}`
: booking.price
? `${booking.price.toLocaleString()} ${booking.primaryCurrency || 'USD'}`
: 'N/A'
}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
onClick={() => {
setSelectedBooking(booking);
setShowDetailsModal(true);
}}
className="text-blue-600 hover:text-blue-900"
>
View Details
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Details Modal */}
{showDetailsModal && selectedBooking && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 overflow-y-auto">
<div className="bg-white rounded-lg p-6 max-w-2xl w-full m-4">
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-bold">Booking Details</h2>
<button
onClick={() => {
setShowDetailsModal(false);
setSelectedBooking(null);
}}
className="text-gray-400 hover:text-gray-600"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-500">Numéro de devis</label>
<div className="mt-1 text-lg font-semibold">
{getQuoteNumber(selectedBooking)}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-500">Statut</label>
<span className={`mt-1 inline-block px-3 py-1 text-sm font-semibold rounded-full ${getStatusColor(selectedBooking.status)}`}>
{selectedBooking.status}
</span>
</div>
</div>
<div className="border-t pt-4">
<h3 className="text-sm font-medium text-gray-900 mb-3">Route Information</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-500">Origin</label>
<div className="mt-1">
{selectedBooking.originPort ? (
<>
<div className="font-semibold">{selectedBooking.originPort.code}</div>
<div className="text-sm text-gray-600">{selectedBooking.originPort.name}</div>
</>
) : (
<div className="font-semibold">{selectedBooking.origin}</div>
)}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-500">Destination</label>
<div className="mt-1">
{selectedBooking.destinationPort ? (
<>
<div className="font-semibold">{selectedBooking.destinationPort.code}</div>
<div className="text-sm text-gray-600">{selectedBooking.destinationPort.name}</div>
</>
) : (
<div className="font-semibold">{selectedBooking.destination}</div>
)}
</div>
</div>
</div>
</div>
<div className="border-t pt-4">
<h3 className="text-sm font-medium text-gray-900 mb-3">Shipping Details</h3>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-500">Carrier</label>
<div className="mt-1 font-semibold">
{selectedBooking.carrier || selectedBooking.carrierName || 'N/A'}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-500">Container Type</label>
<div className="mt-1 font-semibold">{selectedBooking.containerType}</div>
</div>
{selectedBooking.quantity && (
<div>
<label className="block text-sm font-medium text-gray-500">Quantity</label>
<div className="mt-1 font-semibold">{selectedBooking.quantity}</div>
</div>
)}
</div>
</div>
<div className="border-t pt-4">
<h3 className="text-sm font-medium text-gray-900 mb-3">Pricing</h3>
<div className="text-2xl font-bold text-blue-600">
{selectedBooking.totalPrice
? `${selectedBooking.totalPrice.amount.toLocaleString()} ${selectedBooking.totalPrice.currency}`
: selectedBooking.price
? `${selectedBooking.price.toLocaleString()} ${selectedBooking.primaryCurrency || 'USD'}`
: 'N/A'
}
</div>
</div>
<div className="border-t pt-4">
<h3 className="text-sm font-medium text-gray-900 mb-3">Timeline</h3>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<label className="block text-gray-500">Created</label>
<div className="mt-1">
{new Date(selectedBooking.createdAt || selectedBooking.requestedAt || '').toLocaleString()}
</div>
</div>
{selectedBooking.updatedAt && (
<div>
<label className="block text-gray-500">Last Updated</label>
<div className="mt-1">{new Date(selectedBooking.updatedAt).toLocaleString()}</div>
</div>
)}
</div>
</div>
</div>
<div className="flex justify-end space-x-2 mt-6 pt-4 border-t">
<button
onClick={() => {
setShowDetailsModal(false);
setSelectedBooking(null);
}}
className="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"
>
Close
</button>
</div>
</div>
</div>
)}
</div>
);
}