This commit is contained in:
David 2026-02-03 16:08:00 +01:00
parent 3e654af8a3
commit cf19c36586
31 changed files with 1320 additions and 558 deletions

View File

@ -2,6 +2,8 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { getAllBookings, getAllUsers } from '@/lib/api/admin'; import { getAllBookings, getAllUsers } from '@/lib/api/admin';
import { FileText, Image as ImageIcon, FileEdit, FileSpreadsheet, Paperclip } from 'lucide-react';
import type { ReactNode } from 'react';
interface Document { interface Document {
id: string; id: string;
@ -226,30 +228,31 @@ export default function AdminDocumentsPage() {
setCurrentPage(1); setCurrentPage(1);
}, [searchTerm, filterUserId, filterQuoteNumber]); }, [searchTerm, filterUserId, filterQuoteNumber]);
const getDocumentIcon = (type: string) => { const getDocumentIcon = (type: string): ReactNode => {
const typeLower = type.toLowerCase(); const typeLower = type.toLowerCase();
const icons: Record<string, string> = { const cls = "h-6 w-6";
'application/pdf': '📄', const iconMap: Record<string, ReactNode> = {
'image/jpeg': '🖼️', 'application/pdf': <FileText className={`${cls} text-red-500`} />,
'image/png': '🖼️', 'image/jpeg': <ImageIcon className={`${cls} text-green-500`} />,
'image/jpg': '🖼️', 'image/png': <ImageIcon className={`${cls} text-green-500`} />,
pdf: '📄', 'image/jpg': <ImageIcon className={`${cls} text-green-500`} />,
jpeg: '🖼️', pdf: <FileText className={`${cls} text-red-500`} />,
jpg: '🖼️', jpeg: <ImageIcon className={`${cls} text-green-500`} />,
png: '🖼️', jpg: <ImageIcon className={`${cls} text-green-500`} />,
gif: '🖼️', png: <ImageIcon className={`${cls} text-green-500`} />,
image: '🖼️', gif: <ImageIcon className={`${cls} text-green-500`} />,
word: '📝', image: <ImageIcon className={`${cls} text-green-500`} />,
doc: '📝', word: <FileEdit className={`${cls} text-blue-500`} />,
docx: '📝', doc: <FileEdit className={`${cls} text-blue-500`} />,
excel: '📊', docx: <FileEdit className={`${cls} text-blue-500`} />,
xls: '📊', excel: <FileSpreadsheet className={`${cls} text-green-600`} />,
xlsx: '📊', xls: <FileSpreadsheet className={`${cls} text-green-600`} />,
csv: '📊', xlsx: <FileSpreadsheet className={`${cls} text-green-600`} />,
text: '📄', csv: <FileSpreadsheet className={`${cls} text-green-600`} />,
txt: '📄', text: <FileText className={`${cls} text-gray-500`} />,
txt: <FileText className={`${cls} text-gray-500`} />,
}; };
return icons[typeLower] || '📎'; return iconMap[typeLower] || <Paperclip className={`${cls} text-gray-400`} />;
}; };
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
@ -445,7 +448,7 @@ export default function AdminDocumentsPage() {
</td> </td>
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center"> <div className="flex items-center">
<span className="text-2xl mr-2">{getDocumentIcon(doc.fileType || doc.type)}</span> <span className="mr-2">{getDocumentIcon(doc.fileType || doc.type)}</span>
<div className="text-xs text-gray-500">{doc.fileType || doc.type}</div> <div className="text-xs text-gray-500">{doc.fileType || doc.type}</div>
</div> </div>
</td> </td>

View File

@ -8,6 +8,7 @@
import { useState, useEffect, Suspense } from 'react'; import { useState, useEffect, Suspense } from 'react';
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import { ClipboardList, Mail, AlertTriangle } from 'lucide-react';
import type { CsvRateSearchResult } from '@/types/rates'; import type { CsvRateSearchResult } from '@/types/rates';
import { createCsvBooking } from '@/lib/api/bookings'; import { createCsvBooking } from '@/lib/api/bookings';
@ -259,7 +260,7 @@ function NewBookingPageContent() {
{error && ( {error && (
<div className="mb-6 bg-red-50 border-2 border-red-200 rounded-lg p-4"> <div className="mb-6 bg-red-50 border-2 border-red-200 rounded-lg p-4">
<div className="flex items-start"> <div className="flex items-start">
<span className="text-2xl mr-3"></span> <AlertTriangle className="h-6 w-6 mr-3 text-red-500 flex-shrink-0" />
<div> <div>
<h4 className="font-semibold text-red-900 mb-1">Erreur</h4> <h4 className="font-semibold text-red-900 mb-1">Erreur</h4>
<p className="text-red-700 whitespace-pre-line">{error}</p> <p className="text-red-700 whitespace-pre-line">{error}</p>
@ -384,7 +385,7 @@ function NewBookingPageContent() {
<div className="mb-6 bg-yellow-50 border-2 border-yellow-200 rounded-lg p-4"> <div className="mb-6 bg-yellow-50 border-2 border-yellow-200 rounded-lg p-4">
<div className="flex items-start"> <div className="flex items-start">
<span className="text-2xl mr-3">📋</span> <ClipboardList className="h-6 w-6 mr-3 text-yellow-700 flex-shrink-0" />
<div> <div>
<h4 className="font-semibold text-yellow-900 mb-1">Information importante</h4> <h4 className="font-semibold text-yellow-900 mb-1">Information importante</h4>
<p className="text-sm text-yellow-800"> <p className="text-sm text-yellow-800">
@ -572,7 +573,7 @@ function NewBookingPageContent() {
{/* What happens next */} {/* What happens next */}
<div className="bg-yellow-50 border-2 border-yellow-200 rounded-lg p-6"> <div className="bg-yellow-50 border-2 border-yellow-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-3"> <h3 className="text-lg font-semibold text-gray-900 mb-3">
📧 Que se passe-t-il ensuite ? <Mail className="h-5 w-5 mr-2 inline-block" /> Que se passe-t-il ensuite ?
</h3> </h3>
<ul className="space-y-2 text-sm text-gray-700"> <ul className="space-y-2 text-sm text-gray-700">
<li className="flex items-start"> <li className="flex items-start">

View File

@ -10,6 +10,8 @@ import { useState } from 'react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { listBookings, listCsvBookings } from '@/lib/api'; import { listBookings, listCsvBookings } from '@/lib/api';
import Link from 'next/link'; import Link from 'next/link';
import { Plus } from 'lucide-react';
import ExportButton from '@/components/ExportButton';
type SearchType = 'pallets' | 'weight' | 'route' | 'status' | 'date' | 'quote'; type SearchType = 'pallets' | 'weight' | 'route' | 'status' | 'date' | 'quote';
@ -146,14 +148,38 @@ export default function BookingsListPage() {
<h1 className="text-2xl font-bold text-gray-900">Réservations</h1> <h1 className="text-2xl font-bold text-gray-900">Réservations</h1>
<p className="text-sm text-gray-500 mt-1">Gérez et suivez vos envois</p> <p className="text-sm text-gray-500 mt-1">Gérez et suivez vos envois</p>
</div> </div>
<div className="flex items-center space-x-3">
<ExportButton
data={filteredBookings}
filename="reservations"
columns={[
{ key: 'id', label: 'ID' },
{ key: 'palletCount', label: 'Palettes', format: (v) => `${v || 0}` },
{ key: 'weightKG', label: 'Poids (kg)', format: (v) => `${v || 0}` },
{ key: 'volumeCBM', label: 'Volume (CBM)', format: (v) => `${v || 0}` },
{ key: 'origin', label: 'Origine' },
{ key: 'destination', label: 'Destination' },
{ key: 'carrierName', label: 'Transporteur' },
{ key: 'status', label: 'Statut', format: (v) => {
const labels: Record<string, string> = {
PENDING: 'En attente',
ACCEPTED: 'Accepté',
REJECTED: 'Refusé',
};
return labels[v] || v;
}},
{ key: 'createdAt', label: 'Date de création', format: (v) => v ? new Date(v).toLocaleDateString('fr-FR') : '' },
]}
/>
<Link <Link
href="/dashboard/search-advanced" href="/dashboard/search-advanced"
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" 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> <Plus className="mr-2 h-4 w-4" />
Nouvelle Réservation Nouvelle Réservation
</Link> </Link>
</div> </div>
</div>
{/* Filters */} {/* Filters */}
<div className="bg-white rounded-lg shadow p-4"> <div className="bg-white rounded-lg shadow p-4">
@ -450,7 +476,7 @@ export default function BookingsListPage() {
href="/dashboard/search-advanced" 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" 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> <Plus className="mr-2 h-4 w-4" />
Nouvelle Réservation Nouvelle Réservation
</Link> </Link>
</div> </div>

View File

@ -2,6 +2,9 @@
import { useState, useEffect, useCallback, useRef } from 'react'; import { useState, useEffect, useCallback, useRef } from 'react';
import { listCsvBookings, CsvBookingResponse } from '@/lib/api/bookings'; import { listCsvBookings, CsvBookingResponse } from '@/lib/api/bookings';
import { FileText, Image as ImageIcon, FileEdit, FileSpreadsheet, Paperclip } from 'lucide-react';
import type { ReactNode } from 'react';
import ExportButton from '@/components/ExportButton';
interface Document { interface Document {
id: string; id: string;
@ -164,30 +167,31 @@ export default function UserDocumentsPage() {
setCurrentPage(1); setCurrentPage(1);
}, [searchTerm, filterStatus, filterQuoteNumber]); }, [searchTerm, filterStatus, filterQuoteNumber]);
const getDocumentIcon = (type: string) => { const getDocumentIcon = (type: string): ReactNode => {
const typeLower = type.toLowerCase(); const typeLower = type.toLowerCase();
const icons: Record<string, string> = { const cls = "h-6 w-6";
'application/pdf': '📄', const iconMap: Record<string, ReactNode> = {
'image/jpeg': '🖼️', 'application/pdf': <FileText className={`${cls} text-red-500`} />,
'image/png': '🖼️', 'image/jpeg': <ImageIcon className={`${cls} text-green-500`} />,
'image/jpg': '🖼️', 'image/png': <ImageIcon className={`${cls} text-green-500`} />,
pdf: '📄', 'image/jpg': <ImageIcon className={`${cls} text-green-500`} />,
jpeg: '🖼️', pdf: <FileText className={`${cls} text-red-500`} />,
jpg: '🖼️', jpeg: <ImageIcon className={`${cls} text-green-500`} />,
png: '🖼️', jpg: <ImageIcon className={`${cls} text-green-500`} />,
gif: '🖼️', png: <ImageIcon className={`${cls} text-green-500`} />,
image: '🖼️', gif: <ImageIcon className={`${cls} text-green-500`} />,
word: '📝', image: <ImageIcon className={`${cls} text-green-500`} />,
doc: '📝', word: <FileEdit className={`${cls} text-blue-500`} />,
docx: '📝', doc: <FileEdit className={`${cls} text-blue-500`} />,
excel: '📊', docx: <FileEdit className={`${cls} text-blue-500`} />,
xls: '📊', excel: <FileSpreadsheet className={`${cls} text-green-600`} />,
xlsx: '📊', xls: <FileSpreadsheet className={`${cls} text-green-600`} />,
csv: '📊', xlsx: <FileSpreadsheet className={`${cls} text-green-600`} />,
text: '📄', csv: <FileSpreadsheet className={`${cls} text-green-600`} />,
txt: '📄', text: <FileText className={`${cls} text-gray-500`} />,
txt: <FileText className={`${cls} text-gray-500`} />,
}; };
return icons[typeLower] || '📎'; return iconMap[typeLower] || <Paperclip className={`${cls} text-gray-400`} />;
}; };
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
@ -407,6 +411,28 @@ export default function UserDocumentsPage() {
Gérez tous les documents de vos réservations Gérez tous les documents de vos réservations
</p> </p>
</div> </div>
<div className="flex items-center space-x-3">
<ExportButton
data={filteredDocuments}
filename="documents"
columns={[
{ key: 'fileName', label: 'Nom du fichier' },
{ key: 'fileType', label: 'Type' },
{ key: 'quoteNumber', label: 'N° de Devis' },
{ key: 'route', label: 'Route' },
{ key: 'carrierName', label: 'Transporteur' },
{ key: 'status', label: 'Statut', format: (v) => {
const labels: Record<string, string> = {
PENDING: 'En attente',
ACCEPTED: 'Accepté',
REJECTED: 'Refusé',
CANCELLED: 'Annulé',
};
return labels[v] || v;
}},
{ key: 'uploadedAt', label: 'Date d\'ajout', format: (v) => v ? new Date(v).toLocaleDateString('fr-FR') : '' },
]}
/>
<button <button
onClick={handleAddDocumentClick} onClick={handleAddDocumentClick}
disabled={bookingsWithPendingStatus.length === 0} disabled={bookingsWithPendingStatus.length === 0}
@ -423,6 +449,7 @@ export default function UserDocumentsPage() {
Ajouter un document Ajouter un document
</button> </button>
</div> </div>
</div>
{/* Stats */} {/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
@ -533,7 +560,7 @@ export default function UserDocumentsPage() {
</td> </td>
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center"> <div className="flex items-center">
<span className="text-2xl mr-2"> <span className="mr-2">
{getDocumentIcon(doc.fileType || doc.type)} {getDocumentIcon(doc.fileType || doc.type)}
</span> </span>
<div className="text-xs text-gray-500">{doc.fileType || doc.type}</div> <div className="text-xs text-gray-500">{doc.fileType || doc.type}</div>

View File

@ -13,25 +13,34 @@ import { useState } from 'react';
import NotificationDropdown from '@/components/NotificationDropdown'; import NotificationDropdown from '@/components/NotificationDropdown';
import AdminPanelDropdown from '@/components/admin/AdminPanelDropdown'; import AdminPanelDropdown from '@/components/admin/AdminPanelDropdown';
import Image from 'next/image'; import Image from 'next/image';
import {
BarChart3,
Package,
FileText,
Search,
BookOpen,
User,
Building2,
Users,
LogOut,
} from 'lucide-react';
export default function DashboardLayout({ children }: { children: React.ReactNode }) { export default function DashboardLayout({ children }: { children: React.ReactNode }) {
const { user, logout } = useAuth(); const { user, logout } = useAuth();
const pathname = usePathname(); const pathname = usePathname();
const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false);
// { name: 'Search Rates', href: '/dashboard/search-advanced', icon: '🔎' },
const navigation = [ const navigation = [
{ name: 'Dashboard', href: '/dashboard', icon: '📊' }, { name: 'Tableau de bord', href: '/dashboard', icon: BarChart3 },
{ name: 'Bookings', href: '/dashboard/bookings', icon: '📦' }, { name: 'Réservations', href: '/dashboard/bookings', icon: Package },
{ name: 'Documents', href: '/dashboard/documents', icon: '📄' }, { name: 'Documents', href: '/dashboard/documents', icon: FileText },
{ name: 'Track & Trace', href: '/dashboard/track-trace', icon: '🔍' }, { name: 'Suivi', href: '/dashboard/track-trace', icon: Search },
{ name: 'Wiki', href: '/dashboard/wiki', icon: '📚' }, { name: 'Wiki Maritime', href: '/dashboard/wiki', icon: BookOpen },
{ name: 'My Profile', href: '/dashboard/profile', icon: '👤' }, { name: 'Mon Profil', href: '/dashboard/profile', icon: User },
{ name: 'Organization', href: '/dashboard/settings/organization', icon: '🏢' }, { name: 'Organisation', href: '/dashboard/settings/organization', icon: Building2 },
// ADMIN and MANAGER only navigation items // ADMIN and MANAGER only navigation items
...(user?.role === 'ADMIN' || user?.role === 'MANAGER' ? [ ...(user?.role === 'ADMIN' || user?.role === 'MANAGER' ? [
{ name: 'Users', href: '/dashboard/settings/users', icon: '👥' }, { name: 'Utilisateurs', href: '/dashboard/settings/users', icon: Users },
] : []), ] : []),
]; ];
@ -98,7 +107,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
: 'text-gray-700 hover:bg-gray-100' : 'text-gray-700 hover:bg-gray-100'
}`} }`}
> >
<span className="mr-3 text-xl">{item.icon}</span> <item.icon className="mr-3 h-5 w-5" />
{item.name} {item.name}
</Link> </Link>
))} ))}
@ -129,15 +138,8 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
onClick={logout} onClick={logout}
className="w-full flex items-center justify-center px-4 py-2 text-sm font-medium text-red-700 bg-red-50 rounded-lg hover:bg-red-100 transition-colors" className="w-full flex items-center justify-center px-4 py-2 text-sm font-medium text-red-700 bg-red-50 rounded-lg hover:bg-red-100 transition-colors"
> >
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <LogOut className="w-4 h-4 mr-2" />
<path Déconnexion
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
Logout
</button> </button>
</div> </div>
</div> </div>
@ -162,7 +164,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
</button> </button>
<div className="flex-1 lg:flex-none"> <div className="flex-1 lg:flex-none">
<h1 className="text-xl font-semibold text-gray-900"> <h1 className="text-xl font-semibold text-gray-900">
{navigation.find(item => isActive(item.href))?.name || 'Dashboard'} {navigation.find(item => isActive(item.href))?.name || 'Tableau de bord'}
</h1> </h1>
</div> </div>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
@ -170,9 +172,18 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
<NotificationDropdown /> <NotificationDropdown />
{/* User Role Badge */} {/* User Role Badge */}
{user?.role === 'ADMIN' ? (
<Link
href="/dashboard/admin/users"
className="px-3 py-1 text-xs font-medium text-blue-800 bg-blue-100 rounded-full hover:bg-blue-200 transition-colors cursor-pointer"
>
{user.role}
</Link>
) : (
<span className="px-3 py-1 text-xs font-medium text-blue-800 bg-blue-100 rounded-full"> <span className="px-3 py-1 text-xs font-medium text-blue-800 bg-blue-100 rounded-full">
{user?.role} {user?.role}
</span> </span>
)}
</div> </div>
</div> </div>

View File

@ -16,7 +16,8 @@ import {
deleteNotification, deleteNotification,
} from '@/lib/api'; } from '@/lib/api';
import type { NotificationResponse } from '@/types/api'; import type { NotificationResponse } from '@/types/api';
import { Trash2, CheckCheck, Filter, Bell, ChevronLeft, ChevronRight } from 'lucide-react'; import { Trash2, CheckCheck, Filter, Bell, ChevronLeft, ChevronRight, Package, RefreshCw, XCircle, CheckCircle, Mail, Clock, FileText, Megaphone, User, Building2 } from 'lucide-react';
import type { ReactNode } from 'react';
export default function NotificationsPage() { export default function NotificationsPage() {
const [selectedFilter, setSelectedFilter] = useState<'all' | 'unread' | 'read'>('all'); const [selectedFilter, setSelectedFilter] = useState<'all' | 'unread' | 'read'>('all');
@ -77,7 +78,7 @@ export default function NotificationsPage() {
const handleDelete = (e: React.MouseEvent, notificationId: string) => { const handleDelete = (e: React.MouseEvent, notificationId: string) => {
e.stopPropagation(); e.stopPropagation();
if (confirm('Are you sure you want to delete this notification?')) { if (confirm('Êtes-vous sûr de vouloir supprimer cette notification ?')) {
deleteNotificationMutation.mutate(notificationId); deleteNotificationMutation.mutate(notificationId);
} }
}; };
@ -92,22 +93,23 @@ export default function NotificationsPage() {
return colors[priority as keyof typeof colors] || 'border-l-4 border-gray-300 hover:bg-gray-100'; return colors[priority as keyof typeof colors] || 'border-l-4 border-gray-300 hover:bg-gray-100';
}; };
const getNotificationIcon = (type: string) => { const getNotificationIcon = (type: string): ReactNode => {
const icons: Record<string, string> = { const iconClass = "h-8 w-8";
booking_created: '📦', const icons: Record<string, ReactNode> = {
booking_updated: '🔄', booking_created: <Package className={`${iconClass} text-blue-600`} />,
booking_cancelled: '❌', booking_updated: <RefreshCw className={`${iconClass} text-orange-500`} />,
booking_confirmed: '✅', booking_cancelled: <XCircle className={`${iconClass} text-red-500`} />,
csv_booking_accepted: '✅', booking_confirmed: <CheckCircle className={`${iconClass} text-green-500`} />,
csv_booking_rejected: '❌', csv_booking_accepted: <CheckCircle className={`${iconClass} text-green-500`} />,
csv_booking_request_sent: '📧', csv_booking_rejected: <XCircle className={`${iconClass} text-red-500`} />,
rate_quote_expiring: '⏰', csv_booking_request_sent: <Mail className={`${iconClass} text-blue-500`} />,
document_uploaded: '📄', rate_quote_expiring: <Clock className={`${iconClass} text-yellow-500`} />,
system_announcement: '📢', document_uploaded: <FileText className={`${iconClass} text-gray-600`} />,
user_invited: '👤', system_announcement: <Megaphone className={`${iconClass} text-purple-500`} />,
organization_update: '🏢', user_invited: <User className={`${iconClass} text-teal-500`} />,
organization_update: <Building2 className={`${iconClass} text-indigo-500`} />,
}; };
return icons[type.toLowerCase()] || '🔔'; return icons[type.toLowerCase()] || <Bell className={`${iconClass} text-gray-500`} />;
}; };
const formatTime = (dateString: string) => { const formatTime = (dateString: string) => {
@ -118,11 +120,11 @@ export default function NotificationsPage() {
const diffHours = Math.floor(diffMs / 3600000); const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000); const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return 'Just now'; if (diffMins < 1) return 'A l\'instant';
if (diffMins < 60) return `${diffMins}m ago`; if (diffMins < 60) return `Il y a ${diffMins}min`;
if (diffHours < 24) return `${diffHours}h ago`; if (diffHours < 24) return `Il y a ${diffHours}h`;
if (diffDays < 7) return `${diffDays}d ago`; if (diffDays < 7) return `Il y a ${diffDays}j`;
return date.toLocaleDateString('en-US', { return date.toLocaleDateString('fr-FR', {
month: 'long', month: 'long',
day: 'numeric', day: 'numeric',
year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined, year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined,
@ -144,8 +146,8 @@ export default function NotificationsPage() {
<div> <div>
<h1 className="text-3xl font-bold text-gray-900">Notifications</h1> <h1 className="text-3xl font-bold text-gray-900">Notifications</h1>
<p className="text-sm text-gray-600 mt-1"> <p className="text-sm text-gray-600 mt-1">
{total} notification{total !== 1 ? 's' : ''} total {total} notification{total !== 1 ? 's' : ''} au total
{unreadCount > 0 && `${unreadCount} unread`} {unreadCount > 0 && `${unreadCount} non lue${unreadCount > 1 ? 's' : ''}`}
</p> </p>
</div> </div>
</div> </div>
@ -156,7 +158,7 @@ export default function NotificationsPage() {
className="flex items-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors" className="flex items-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors"
> >
<CheckCheck className="w-5 h-5" /> <CheckCheck className="w-5 h-5" />
<span>Mark all as read</span> <span>Tout marquer comme lu</span>
</button> </button>
)} )}
</div> </div>
@ -169,7 +171,7 @@ export default function NotificationsPage() {
<div className="bg-white rounded-lg shadow-sm border p-4 mb-6"> <div className="bg-white rounded-lg shadow-sm border p-4 mb-6">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<Filter className="w-5 h-5 text-gray-500" /> <Filter className="w-5 h-5 text-gray-500" />
<span className="text-sm font-medium text-gray-700">Filter:</span> <span className="text-sm font-medium text-gray-700">Filtrer :</span>
<div className="flex space-x-2"> <div className="flex space-x-2">
{(['all', 'unread', 'read'] as const).map((filter) => ( {(['all', 'unread', 'read'] as const).map((filter) => (
<button <button
@ -184,7 +186,7 @@ export default function NotificationsPage() {
: 'bg-gray-100 text-gray-700 hover:bg-gray-200' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`} }`}
> >
{filter.charAt(0).toUpperCase() + filter.slice(1)} {filter === 'all' ? 'Toutes' : filter === 'unread' ? 'Non lues' : 'Lues'}
{filter === 'unread' && unreadCount > 0 && ( {filter === 'unread' && unreadCount > 0 && (
<span className="ml-2 px-2 py-0.5 bg-white/20 rounded-full text-xs"> <span className="ml-2 px-2 py-0.5 bg-white/20 rounded-full text-xs">
{unreadCount} {unreadCount}
@ -202,18 +204,18 @@ export default function NotificationsPage() {
<div className="flex items-center justify-center py-20"> <div className="flex items-center justify-center py-20">
<div className="text-center"> <div className="text-center">
<div className="animate-spin rounded-full h-16 w-16 border-b-2 border-blue-600 mx-auto mb-4" /> <div className="animate-spin rounded-full h-16 w-16 border-b-2 border-blue-600 mx-auto mb-4" />
<p className="text-gray-500">Loading notifications...</p> <p className="text-gray-500">Chargement des notifications...</p>
</div> </div>
</div> </div>
) : notifications.length === 0 ? ( ) : notifications.length === 0 ? (
<div className="flex items-center justify-center py-20"> <div className="flex items-center justify-center py-20">
<div className="text-center"> <div className="text-center">
<div className="text-7xl mb-4">🔔</div> <div className="mb-4 flex justify-center"><Bell className="h-16 w-16 text-gray-300" /></div>
<h3 className="text-xl font-semibold text-gray-900 mb-2">No notifications</h3> <h3 className="text-xl font-semibold text-gray-900 mb-2">Aucune notification</h3>
<p className="text-gray-500"> <p className="text-gray-500">
{selectedFilter === 'unread' {selectedFilter === 'unread'
? "You're all caught up! Great job!" ? 'Vous êtes à jour !'
: 'No notifications to display'} : 'Aucune notification à afficher'}
</p> </p>
</div> </div>
</div> </div>
@ -229,7 +231,7 @@ export default function NotificationsPage() {
> >
<div className="flex items-start space-x-4"> <div className="flex items-start space-x-4">
{/* Icon */} {/* Icon */}
<div className="flex-shrink-0 text-4xl"> <div className="flex-shrink-0 flex items-center justify-center w-12 h-12">
{getNotificationIcon(notification.type)} {getNotificationIcon(notification.type)}
</div> </div>
@ -243,14 +245,14 @@ export default function NotificationsPage() {
{!notification.read && ( {!notification.read && (
<span className="flex items-center space-x-1 px-2 py-1 bg-blue-600 text-white text-xs font-medium rounded-full"> <span className="flex items-center space-x-1 px-2 py-1 bg-blue-600 text-white text-xs font-medium rounded-full">
<span className="w-2 h-2 bg-white rounded-full animate-pulse" /> <span className="w-2 h-2 bg-white rounded-full animate-pulse" />
<span>NEW</span> <span>NOUVEAU</span>
</span> </span>
)} )}
</div> </div>
<button <button
onClick={(e) => handleDelete(e, notification.id)} onClick={(e) => handleDelete(e, notification.id)}
className="opacity-0 group-hover:opacity-100 transition-opacity p-2 hover:bg-red-100 rounded-lg" className="opacity-0 group-hover:opacity-100 transition-opacity p-2 hover:bg-red-100 rounded-lg"
title="Delete notification" title="Supprimer la notification"
> >
<Trash2 className="w-5 h-5 text-red-600" /> <Trash2 className="w-5 h-5 text-red-600" />
</button> </button>
@ -300,7 +302,7 @@ export default function NotificationsPage() {
</div> </div>
{notification.actionUrl && ( {notification.actionUrl && (
<span className="text-sm text-blue-600 font-medium group-hover:underline flex items-center space-x-1"> <span className="text-sm text-blue-600 font-medium group-hover:underline flex items-center space-x-1">
<span>View details</span> <span>Voir les tails</span>
<svg <svg
className="w-4 h-4" className="w-4 h-4"
fill="none" fill="none"
@ -330,11 +332,10 @@ export default function NotificationsPage() {
<div className="mt-6 bg-white rounded-lg shadow-sm border p-4"> <div className="mt-6 bg-white rounded-lg shadow-sm border p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="text-sm text-gray-600"> <div className="text-sm text-gray-600">
Showing page <span className="font-semibold">{currentPage}</span> of{' '} Page <span className="font-semibold">{currentPage}</span> sur{' '}
<span className="font-semibold">{totalPages}</span> <span className="font-semibold">{totalPages}</span>
{' • '} {' • '}
<span className="font-semibold">{total}</span> total notification <span className="font-semibold">{total}</span> notification{total !== 1 ? 's' : ''} au total
{total !== 1 ? 's' : ''}
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<button <button
@ -343,7 +344,7 @@ export default function NotificationsPage() {
className="flex items-center space-x-1 px-4 py-2 text-sm font-medium bg-white border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" className="flex items-center space-x-1 px-4 py-2 text-sm font-medium bg-white border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
> >
<ChevronLeft className="w-4 h-4" /> <ChevronLeft className="w-4 h-4" />
<span>Previous</span> <span>Précédent</span>
</button> </button>
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => { {Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
@ -378,7 +379,7 @@ export default function NotificationsPage() {
disabled={currentPage === totalPages} disabled={currentPage === totalPages}
className="flex items-center space-x-1 px-4 py-2 text-sm font-medium bg-white border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" className="flex items-center space-x-1 px-4 py-2 text-sm font-medium bg-white border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
> >
<span>Next</span> <span>Suivant</span>
<ChevronRight className="w-4 h-4" /> <ChevronRight className="w-4 h-4" />
</button> </button>
</div> </div>

View File

@ -21,6 +21,7 @@ import {
Plus, Plus,
ArrowRight, ArrowRight,
} from 'lucide-react'; } from 'lucide-react';
import ExportButton from '@/components/ExportButton';
import { import {
PieChart, PieChart,
Pie, Pie,
@ -74,17 +75,33 @@ export default function DashboardPage() {
<div> <div>
<h1 className="text-3xl font-semibold text-gray-900">Tableau de Bord</h1> <h1 className="text-3xl font-semibold text-gray-900">Tableau de Bord</h1>
<p className="text-gray-600 mt-1 text-sm"> <p className="text-gray-600 mt-1 text-sm">
Vue d'ensemble de vos bookings et performances Vue d'ensemble de vos réservations et performances
</p> </p>
</div> </div>
<Link href="/dashboard/bookings"> <div className="flex items-center space-x-3">
<Button className="bg-blue-600 hover:bg-blue-700 text-white gap-2 shadow-sm"> <ExportButton
<Plus className="h-4 w-4" /> data={topCarriers || []}
Nouveau Booking filename="tableau-de-bord-transporteurs"
columns={[
{ key: 'carrierName', label: 'Transporteur' },
{ key: 'totalBookings', label: 'Total Réservations' },
{ key: 'acceptedBookings', label: 'Acceptées' },
{ key: 'rejectedBookings', label: 'Refusées' },
{ key: 'totalWeightKG', label: 'Poids Total (KG)', format: (v) => v?.toLocaleString('fr-FR') || '0' },
{ key: 'totalVolumeCBM', label: 'Volume Total (CBM)', format: (v) => v?.toFixed(2) || '0' },
{ key: 'acceptanceRate', label: 'Taux d\'acceptation (%)', format: (v) => v?.toFixed(1) || '0' },
{ key: 'avgPriceUSD', label: 'Prix moyen ($)', format: (v) => v?.toFixed(2) || '0' },
]}
/>
<Link href="/dashboard/search-advanced">
<Button className="bg-blue-600 hover:bg-blue-700 text-white gap-2 shadow-lg text-base px-6 py-5 font-semibold">
<Plus className="h-5 w-5" />
Nouvelle Réservation
</Button> </Button>
</Link> </Link>
</div> </div>
</div>
{/* KPI Cards - Compact with Color */} {/* KPI Cards - Compact with Color */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"> <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
@ -191,7 +208,7 @@ export default function DashboardPage() {
<Card className="border border-gray-200 shadow-sm bg-white"> <Card className="border border-gray-200 shadow-sm bg-white">
<CardHeader className="pb-4 border-b border-gray-100"> <CardHeader className="pb-4 border-b border-gray-100">
<CardTitle className="text-base font-semibold text-gray-900"> <CardTitle className="text-base font-semibold text-gray-900">
Distribution des Bookings Distribution des Réservations
</CardTitle> </CardTitle>
<CardDescription className="text-xs text-gray-600"> <CardDescription className="text-xs text-gray-600">
Répartition par statut Répartition par statut
@ -233,7 +250,7 @@ export default function DashboardPage() {
Poids par Transporteur Poids par Transporteur
</CardTitle> </CardTitle>
<CardDescription className="text-xs text-gray-600"> <CardDescription className="text-xs text-gray-600">
Top 5 carriers par poids (KG) Top 5 transporteurs par poids (KG)
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="pt-4"> <CardContent className="pt-4">
@ -282,7 +299,7 @@ export default function DashboardPage() {
<div className="h-10 w-10 rounded-lg bg-blue-100 flex items-center justify-center mb-2"> <div className="h-10 w-10 rounded-lg bg-blue-100 flex items-center justify-center mb-2">
<Package className="h-5 w-5 text-blue-600" /> <Package className="h-5 w-5 text-blue-600" />
</div> </div>
<p className="text-xs font-medium text-gray-600 mb-1">Total Bookings</p> <p className="text-xs font-medium text-gray-600 mb-1">Total Réservations</p>
<p className="text-2xl font-bold text-gray-900"> <p className="text-2xl font-bold text-gray-900">
{csvKpisLoading {csvKpisLoading
? '--' ? '--'
@ -360,7 +377,7 @@ export default function DashboardPage() {
{carrier.carrierName} {carrier.carrierName}
</h3> </h3>
<div className="flex items-center gap-3 text-xs text-gray-500 mt-0.5"> <div className="flex items-center gap-3 text-xs text-gray-500 mt-0.5">
<span>{carrier.totalBookings} bookings</span> <span>{carrier.totalBookings} réservations</span>
<span></span> <span></span>
<span>{carrier.totalWeightKG.toLocaleString()} KG</span> <span>{carrier.totalWeightKG.toLocaleString()} KG</span>
</div> </div>
@ -400,15 +417,15 @@ export default function DashboardPage() {
<Package className="h-6 w-6 text-gray-400" /> <Package className="h-6 w-6 text-gray-400" />
</div> </div>
<h3 className="text-sm font-semibold text-gray-900 mb-1"> <h3 className="text-sm font-semibold text-gray-900 mb-1">
Aucun booking Aucune réservation
</h3> </h3>
<p className="text-xs text-gray-500 mb-4 max-w-sm mx-auto"> <p className="text-xs text-gray-500 mb-4 max-w-sm mx-auto">
Créez votre premier booking pour voir vos statistiques Créez votre première réservation pour voir vos statistiques
</p> </p>
<Link href="/dashboard/bookings"> <Link href="/dashboard/bookings">
<Button size="sm" className="bg-blue-600 hover:bg-blue-700"> <Button size="sm" className="bg-blue-600 hover:bg-blue-700">
<Plus className="mr-1.5 h-3 w-3" /> <Plus className="mr-1.5 h-3 w-3" />
Créer un booking Créer une réservation
</Button> </Button>
</Link> </Link>
</div> </div>

View File

@ -17,18 +17,18 @@ import { updateUser, changePassword } from '@/lib/api';
// Password update schema // Password update schema
const passwordSchema = z const passwordSchema = z
.object({ .object({
currentPassword: z.string().min(1, 'Current password is required'), currentPassword: z.string().min(1, 'Le mot de passe actuel est requis'),
newPassword: z newPassword: z
.string() .string()
.min(12, 'Password must be at least 12 characters') .min(12, 'Le mot de passe doit contenir au moins 12 caractères')
.regex( .regex(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/, /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/,
'Password must contain uppercase, lowercase, number, and special character' 'Le mot de passe doit contenir une majuscule, une minuscule, un chiffre et un caractère spécial'
), ),
confirmPassword: z.string().min(1, 'Please confirm your password'), confirmPassword: z.string().min(1, 'Veuillez confirmer votre mot de passe'),
}) })
.refine(data => data.newPassword === data.confirmPassword, { .refine(data => data.newPassword === data.confirmPassword, {
message: "Passwords don't match", message: 'Les mots de passe ne correspondent pas',
path: ['confirmPassword'], path: ['confirmPassword'],
}); });
@ -36,9 +36,9 @@ type PasswordFormData = z.infer<typeof passwordSchema>;
// Profile update schema // Profile update schema
const profileSchema = z.object({ const profileSchema = z.object({
firstName: z.string().min(2, 'First name must be at least 2 characters'), firstName: z.string().min(2, 'Le prénom doit contenir au moins 2 caractères'),
lastName: z.string().min(2, 'Last name must be at least 2 characters'), lastName: z.string().min(2, 'Le nom doit contenir au moins 2 caractères'),
email: z.string().email('Invalid email address'), email: z.string().email('Adresse email invalide'),
}); });
type ProfileFormData = z.infer<typeof profileSchema>; type ProfileFormData = z.infer<typeof profileSchema>;
@ -101,14 +101,14 @@ export default function ProfilePage() {
return updateUser(user.id, data); return updateUser(user.id, data);
}, },
onSuccess: () => { onSuccess: () => {
setSuccessMessage('Profile updated successfully!'); setSuccessMessage('Profil mis à jour avec succès !');
setErrorMessage(''); setErrorMessage('');
refreshUser(); refreshUser();
queryClient.invalidateQueries({ queryKey: ['user'] }); queryClient.invalidateQueries({ queryKey: ['user'] });
setTimeout(() => setSuccessMessage(''), 3000); setTimeout(() => setSuccessMessage(''), 3000);
}, },
onError: (error: any) => { onError: (error: any) => {
setErrorMessage(error.message || 'Failed to update profile'); setErrorMessage(error.message || 'Échec de la mise à jour du profil');
setSuccessMessage(''); setSuccessMessage('');
}, },
}); });
@ -122,7 +122,7 @@ export default function ProfilePage() {
}); });
}, },
onSuccess: () => { onSuccess: () => {
setSuccessMessage('Password updated successfully!'); setSuccessMessage('Mot de passe mis à jour avec succès !');
setErrorMessage(''); setErrorMessage('');
passwordForm.reset({ passwordForm.reset({
currentPassword: '', currentPassword: '',
@ -132,7 +132,7 @@ export default function ProfilePage() {
setTimeout(() => setSuccessMessage(''), 3000); setTimeout(() => setSuccessMessage(''), 3000);
}, },
onError: (error: any) => { onError: (error: any) => {
setErrorMessage(error.message || 'Failed to update password'); setErrorMessage(error.message || 'Échec de la mise à jour du mot de passe');
setSuccessMessage(''); setSuccessMessage('');
}, },
}); });
@ -151,7 +151,7 @@ export default function ProfilePage() {
<div className="flex items-center justify-center min-h-screen"> <div className="flex items-center justify-center min-h-screen">
<div className="text-center"> <div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">Loading profile...</p> <p className="text-gray-600">Chargement du profil...</p>
</div> </div>
</div> </div>
); );
@ -162,12 +162,12 @@ export default function ProfilePage() {
return ( return (
<div className="flex items-center justify-center min-h-screen"> <div className="flex items-center justify-center min-h-screen">
<div className="text-center"> <div className="text-center">
<p className="text-red-600 mb-4">Unable to load user profile</p> <p className="text-red-600 mb-4">Impossible de charger le profil</p>
<button <button
onClick={() => window.location.reload()} onClick={() => window.location.reload()}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700" className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
> >
Retry Réessayer
</button> </button>
</div> </div>
</div> </div>
@ -178,8 +178,8 @@ export default function ProfilePage() {
<div className="max-w-4xl mx-auto space-y-6"> <div className="max-w-4xl mx-auto space-y-6">
{/* Header */} {/* Header */}
<div className="bg-gradient-to-r from-blue-600 to-blue-700 rounded-lg shadow-lg p-6 text-white"> <div className="bg-gradient-to-r from-blue-600 to-blue-700 rounded-lg shadow-lg p-6 text-white">
<h1 className="text-3xl font-bold mb-2">My Profile</h1> <h1 className="text-3xl font-bold mb-2">Mon Profil</h1>
<p className="text-blue-100">Manage your account settings and preferences</p> <p className="text-blue-100">Gérez vos paramètres de compte et préférences</p>
</div> </div>
{/* Success/Error Messages */} {/* Success/Error Messages */}
@ -230,7 +230,7 @@ export default function ProfilePage() {
{user?.role} {user?.role}
</span> </span>
<span className="px-3 py-1 text-xs font-medium text-green-800 bg-green-100 rounded-full"> <span className="px-3 py-1 text-xs font-medium text-green-800 bg-green-100 rounded-full">
Active Actif
</span> </span>
</div> </div>
</div> </div>
@ -249,7 +249,7 @@ export default function ProfilePage() {
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`} }`}
> >
Profile Information Informations personnelles
</button> </button>
<button <button
onClick={() => setActiveTab('password')} onClick={() => setActiveTab('password')}
@ -259,7 +259,7 @@ export default function ProfilePage() {
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`} }`}
> >
Change Password Modifier le mot de passe
</button> </button>
</nav> </nav>
</div> </div>
@ -274,7 +274,7 @@ export default function ProfilePage() {
htmlFor="firstName" htmlFor="firstName"
className="block text-sm font-medium text-gray-700 mb-2" className="block text-sm font-medium text-gray-700 mb-2"
> >
First Name Prénom
</label> </label>
<input <input
{...profileForm.register('firstName')} {...profileForm.register('firstName')}
@ -295,7 +295,7 @@ export default function ProfilePage() {
htmlFor="lastName" htmlFor="lastName"
className="block text-sm font-medium text-gray-700 mb-2" className="block text-sm font-medium text-gray-700 mb-2"
> >
Last Name Nom
</label> </label>
<input <input
{...profileForm.register('lastName')} {...profileForm.register('lastName')}
@ -314,7 +314,7 @@ export default function ProfilePage() {
{/* Email */} {/* Email */}
<div> <div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2"> <label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
Email Address Adresse email
</label> </label>
<input <input
{...profileForm.register('email')} {...profileForm.register('email')}
@ -323,7 +323,7 @@ export default function ProfilePage() {
disabled disabled
className="w-full px-4 py-2 border border-gray-300 rounded-lg bg-gray-50 text-gray-500 cursor-not-allowed" className="w-full px-4 py-2 border border-gray-300 rounded-lg bg-gray-50 text-gray-500 cursor-not-allowed"
/> />
<p className="mt-1 text-xs text-gray-500">Email cannot be changed</p> <p className="mt-1 text-xs text-gray-500">L&apos;adresse email ne peut pas être modifiée</p>
</div> </div>
{/* Submit Button */} {/* Submit Button */}
@ -333,7 +333,7 @@ export default function ProfilePage() {
disabled={updateProfileMutation.isPending} disabled={updateProfileMutation.isPending}
className="px-6 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed" className="px-6 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
> >
{updateProfileMutation.isPending ? 'Saving...' : 'Save Changes'} {updateProfileMutation.isPending ? 'Enregistrement...' : 'Enregistrer'}
</button> </button>
</div> </div>
</form> </form>
@ -345,7 +345,7 @@ export default function ProfilePage() {
htmlFor="currentPassword" htmlFor="currentPassword"
className="block text-sm font-medium text-gray-700 mb-2" className="block text-sm font-medium text-gray-700 mb-2"
> >
Current Password Mot de passe actuel
</label> </label>
<input <input
{...passwordForm.register('currentPassword')} {...passwordForm.register('currentPassword')}
@ -367,7 +367,7 @@ export default function ProfilePage() {
htmlFor="newPassword" htmlFor="newPassword"
className="block text-sm font-medium text-gray-700 mb-2" className="block text-sm font-medium text-gray-700 mb-2"
> >
New Password Nouveau mot de passe
</label> </label>
<input <input
{...passwordForm.register('newPassword')} {...passwordForm.register('newPassword')}
@ -382,8 +382,7 @@ export default function ProfilePage() {
</p> </p>
)} )}
<p className="mt-1 text-xs text-gray-500"> <p className="mt-1 text-xs text-gray-500">
Must be at least 12 characters with uppercase, lowercase, number, and special Au moins 12 caractères avec majuscule, minuscule, chiffre et caractère spécial
character
</p> </p>
</div> </div>
@ -393,7 +392,7 @@ export default function ProfilePage() {
htmlFor="confirmPassword" htmlFor="confirmPassword"
className="block text-sm font-medium text-gray-700 mb-2" className="block text-sm font-medium text-gray-700 mb-2"
> >
Confirm New Password Confirmer le nouveau mot de passe
</label> </label>
<input <input
{...passwordForm.register('confirmPassword')} {...passwordForm.register('confirmPassword')}
@ -416,7 +415,7 @@ export default function ProfilePage() {
disabled={updatePasswordMutation.isPending} disabled={updatePasswordMutation.isPending}
className="px-6 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed" className="px-6 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
> >
{updatePasswordMutation.isPending ? 'Updating...' : 'Update Password'} {updatePasswordMutation.isPending ? 'Mise à jour...' : 'Mettre à jour'}
</button> </button>
</div> </div>
</form> </form>

View File

@ -8,6 +8,7 @@
import { useState, useMemo } from 'react'; import { useState, useMemo } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { Search } from 'lucide-react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { searchPorts, Port } from '@/lib/api/ports'; import { searchPorts, Port } from '@/lib/api/ports';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
@ -641,7 +642,7 @@ export default function AdvancedSearchPage() {
disabled={!searchForm.origin || !searchForm.destination} disabled={!searchForm.origin || !searchForm.destination}
className="px-6 py-3 text-base font-medium text-white bg-green-600 rounded-md hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center" className="px-6 py-3 text-base font-medium text-white bg-green-600 rounded-md hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center"
> >
🔍 Rechercher les tarifs <Search className="h-5 w-5 mr-2" /> Rechercher les tarifs
</button> </button>
)} )}
</div> </div>

View File

@ -4,6 +4,7 @@ import { useEffect, useState, useCallback } from 'react';
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import { searchCsvRatesWithOffers } from '@/lib/api/rates'; import { searchCsvRatesWithOffers } from '@/lib/api/rates';
import type { CsvRateSearchResult } from '@/types/rates'; import type { CsvRateSearchResult } from '@/types/rates';
import { Search, Lightbulb, DollarSign, Scale, Zap, Trophy, XCircle, AlertTriangle } from 'lucide-react';
interface BestOptions { interface BestOptions {
eco: CsvRateSearchResult; eco: CsvRateSearchResult;
@ -121,7 +122,7 @@ export default function SearchResultsPage() {
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 py-12 px-4"> <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 py-12 px-4">
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto">
<div className="bg-red-50 border-2 border-red-200 rounded-lg p-8 text-center"> <div className="bg-red-50 border-2 border-red-200 rounded-lg p-8 text-center">
<div className="text-6xl mb-4"></div> <div className="mb-4 flex justify-center"><XCircle className="h-16 w-16 text-red-500" /></div>
<h3 className="text-xl font-bold text-red-900 mb-2">Erreur</h3> <h3 className="text-xl font-bold text-red-900 mb-2">Erreur</h3>
<p className="text-red-700 mb-4">{error}</p> <p className="text-red-700 mb-4">{error}</p>
<button <button
@ -148,13 +149,13 @@ export default function SearchResultsPage() {
</button> </button>
<div className="bg-yellow-50 border-2 border-yellow-200 rounded-lg p-8 text-center"> <div className="bg-yellow-50 border-2 border-yellow-200 rounded-lg p-8 text-center">
<div className="text-6xl mb-4">🔍</div> <div className="mb-4 flex justify-center"><Search className="h-16 w-16 text-yellow-500" /></div>
<h3 className="text-xl font-bold text-gray-900 mb-2">Aucun résultat trouvé</h3> <h3 className="text-xl font-bold text-gray-900 mb-2">Aucun résultat trouvé</h3>
<p className="text-gray-600 mb-4"> <p className="text-gray-600 mb-4">
Aucun tarif ne correspond à votre recherche pour le trajet {origin} {destination} Aucun tarif ne correspond à votre recherche pour le trajet {origin} {destination}
</p> </p>
<div className="bg-white border border-yellow-300 rounded-lg p-4 text-left max-w-2xl mx-auto mb-6"> <div className="bg-white border border-yellow-300 rounded-lg p-4 text-left max-w-2xl mx-auto mb-6">
<h4 className="font-semibold text-gray-900 mb-2">💡 Suggestions :</h4> <h4 className="font-semibold text-gray-900 mb-2 flex items-center"><Lightbulb className="h-5 w-5 mr-2 text-yellow-500" /> Suggestions :</h4>
<ul className="text-sm text-gray-700 space-y-2"> <ul className="text-sm text-gray-700 space-y-2">
<li> <li>
<strong>Ports disponibles :</strong> NLRTM, DEHAM, FRLEH, BEGNE (origine) USNYC, USLAX, <strong>Ports disponibles :</strong> NLRTM, DEHAM, FRLEH, BEGNE (origine) USNYC, USLAX,
@ -190,7 +191,7 @@ export default function SearchResultsPage() {
text: 'text-green-800', text: 'text-green-800',
button: 'bg-green-600 hover:bg-green-700', button: 'bg-green-600 hover:bg-green-700',
}, },
icon: '💰', icon: <DollarSign className="h-10 w-10 text-green-600" />,
badge: 'Le moins cher', badge: 'Le moins cher',
}, },
{ {
@ -202,7 +203,7 @@ export default function SearchResultsPage() {
text: 'text-blue-800', text: 'text-blue-800',
button: 'bg-blue-600 hover:bg-blue-700', button: 'bg-blue-600 hover:bg-blue-700',
}, },
icon: '⚖️', icon: <Scale className="h-10 w-10 text-blue-600" />,
badge: 'Équilibré', badge: 'Équilibré',
}, },
{ {
@ -214,7 +215,7 @@ export default function SearchResultsPage() {
text: 'text-purple-800', text: 'text-purple-800',
button: 'bg-purple-600 hover:bg-purple-700', button: 'bg-purple-600 hover:bg-purple-700',
}, },
icon: '⚡', icon: <Zap className="h-10 w-10 text-purple-600" />,
badge: 'Le plus rapide', badge: 'Le plus rapide',
}, },
]; ];
@ -253,7 +254,7 @@ export default function SearchResultsPage() {
{bestOptions && ( {bestOptions && (
<div className="mb-12"> <div className="mb-12">
<h2 className="text-2xl font-bold text-gray-900 mb-6 flex items-center"> <h2 className="text-2xl font-bold text-gray-900 mb-6 flex items-center">
<span className="text-3xl mr-3">🏆</span> <Trophy className="h-8 w-8 mr-3 text-yellow-500" />
Meilleurs choix pour votre recherche Meilleurs choix pour votre recherche
</h2> </h2>
@ -269,7 +270,7 @@ export default function SearchResultsPage() {
<div className={`p-6 ${card.colors.bg}`}> <div className={`p-6 ${card.colors.bg}`}>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<span className="text-4xl">{card.icon}</span> <span>{card.icon}</span>
<div> <div>
<h3 className={`text-xl font-bold ${card.colors.text}`}>{card.type}</h3> <h3 className={`text-xl font-bold ${card.colors.text}`}>{card.type}</h3>
<span className="text-xs bg-white px-2 py-1 rounded-full text-gray-600 font-medium"> <span className="text-xs bg-white px-2 py-1 rounded-full text-gray-600 font-medium">
@ -366,7 +367,7 @@ export default function SearchResultsPage() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center space-x-4 text-sm text-gray-600"> <div className="flex items-center space-x-4 text-sm text-gray-600">
<span> Valide jusqu'au {new Date(result.validUntil).toLocaleDateString('fr-FR')}</span> <span> Valide jusqu'au {new Date(result.validUntil).toLocaleDateString('fr-FR')}</span>
{result.hasSurcharges && <span className="text-orange-600"> Surcharges applicables</span>} {result.hasSurcharges && <span className="text-orange-600 flex items-center"><AlertTriangle className="h-4 w-4 mr-1" /> Surcharges applicables</span>}
</div> </div>
<button <button
onClick={() => { onClick={() => {

View File

@ -11,6 +11,7 @@ import { useQuery } from '@tanstack/react-query';
import Image from 'next/image'; import Image from 'next/image';
import { searchRates } from '@/lib/api'; import { searchRates } from '@/lib/api';
import { searchPorts, Port } from '@/lib/api/ports'; import { searchPorts, Port } from '@/lib/api/ports';
import { Search, Leaf, Package } from 'lucide-react';
type ContainerType = '20GP' | '40GP' | '40HC' | '45HC' | '20RF' | '40RF'; type ContainerType = '20GP' | '40GP' | '40HC' | '45HC' | '20RF' | '40RF';
type Mode = 'SEA' | 'AIR' | 'ROAD' | 'RAIL'; type Mode = 'SEA' | 'AIR' | 'ROAD' | 'RAIL';
@ -122,9 +123,9 @@ export default function RateSearchPage() {
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} {/* Header */}
<div> <div>
<h1 className="text-2xl font-bold text-gray-900">Search Shipping Rates</h1> <h1 className="text-2xl font-bold text-gray-900">Recherche de Tarifs Maritime</h1>
<p className="text-sm text-gray-500 mt-1"> <p className="text-sm text-gray-500 mt-1">
Compare rates from multiple carriers in real-time Comparez les tarifs de plusieurs transporteurs en temps réel
</p> </p>
</div> </div>
@ -135,7 +136,7 @@ export default function RateSearchPage() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Origin Port */} {/* Origin Port */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2">Origin Port *</label> <label className="block text-sm font-medium text-gray-700 mb-2">Port d'origine *</label>
<input <input
type="text" type="text"
required required
@ -146,7 +147,7 @@ export default function RateSearchPage() {
setSearchForm({ ...searchForm, originPort: '' }); setSearchForm({ ...searchForm, originPort: '' });
} }
}} }}
placeholder="e.g., Rotterdam, Shanghai" placeholder="ex : Rotterdam, Shanghai"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
/> />
{originPorts && originPorts.length > 0 && ( {originPorts && originPorts.length > 0 && (
@ -174,7 +175,7 @@ export default function RateSearchPage() {
{/* Destination Port */} {/* Destination Port */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Destination Port * Port de destination *
</label> </label>
<input <input
type="text" type="text"
@ -186,7 +187,7 @@ export default function RateSearchPage() {
setSearchForm({ ...searchForm, destinationPort: '' }); setSearchForm({ ...searchForm, destinationPort: '' });
} }
}} }}
placeholder="e.g., Los Angeles, Hamburg" placeholder="ex : Los Angeles, Hambourg"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
/> />
{destinationPorts && destinationPorts.length > 0 && ( {destinationPorts && destinationPorts.length > 0 && (
@ -216,7 +217,7 @@ export default function RateSearchPage() {
<div className="grid grid-cols-1 md:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Container Type * Type de conteneur *
</label> </label>
<select <select
value={searchForm.containerType} value={searchForm.containerType}
@ -235,7 +236,7 @@ export default function RateSearchPage() {
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2">Quantity *</label> <label className="block text-sm font-medium text-gray-700 mb-2">Quantité *</label>
<input <input
type="number" type="number"
min="1" min="1"
@ -250,7 +251,7 @@ export default function RateSearchPage() {
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Departure Date * Date de départ *
</label> </label>
<input <input
type="date" type="date"
@ -264,6 +265,7 @@ export default function RateSearchPage() {
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2">Mode *</label> <label className="block text-sm font-medium text-gray-700 mb-2">Mode *</label>
<select <select
value={searchForm.mode} value={searchForm.mode}
onChange={e => setSearchForm({ ...searchForm, mode: e.target.value as Mode })} onChange={e => setSearchForm({ ...searchForm, mode: e.target.value as Mode })}
@ -285,7 +287,7 @@ export default function RateSearchPage() {
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/> />
<label htmlFor="hazmat" className="ml-2 block text-sm text-gray-900"> <label htmlFor="hazmat" className="ml-2 block text-sm text-gray-900">
Hazardous Materials (requires special handling) Marchandises dangereuses (manutention spéciale requise)
</label> </label>
</div> </div>
@ -299,12 +301,12 @@ export default function RateSearchPage() {
{isSearching ? ( {isSearching ? (
<> <>
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div> <div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
Searching... Recherche en cours...
</> </>
) : ( ) : (
<> <>
<span className="mr-2">🔍</span> <Search className="h-5 w-5 mr-2" />
Search Rates Rechercher des tarifs
</> </>
)} )}
</button> </button>
@ -315,7 +317,7 @@ export default function RateSearchPage() {
{/* Error */} {/* Error */}
{searchError && ( {searchError && (
<div className="bg-red-50 border border-red-200 rounded-md p-4"> <div className="bg-red-50 border border-red-200 rounded-md p-4">
<div className="text-sm text-red-800">Failed to search rates. Please try again.</div> <div className="text-sm text-red-800">La recherche de tarifs a échoué. Veuillez réessayer.</div>
</div> </div>
)} )}
@ -326,20 +328,20 @@ export default function RateSearchPage() {
<div className="lg:col-span-1"> <div className="lg:col-span-1">
<div className="bg-white rounded-lg shadow p-4 space-y-6 sticky top-4"> <div className="bg-white rounded-lg shadow p-4 space-y-6 sticky top-4">
<div> <div>
<h3 className="text-sm font-semibold text-gray-900 mb-3">Sort By</h3> <h3 className="text-sm font-semibold text-gray-900 mb-3">Trier par</h3>
<select <select
value={sortBy} value={sortBy}
onChange={e => setSortBy(e.target.value as any)} onChange={e => setSortBy(e.target.value as any)}
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm"
> >
<option value="price">Price (Low to High)</option> <option value="price">Prix (croissant)</option>
<option value="transitTime">Transit Time</option> <option value="transitTime">Temps de transit</option>
<option value="co2">CO2 Emissions</option> <option value="co2">Émissions CO2</option>
</select> </select>
</div> </div>
<div> <div>
<h3 className="text-sm font-semibold text-gray-900 mb-3">Price Range (USD)</h3> <h3 className="text-sm font-semibold text-gray-900 mb-3">Fourchette de prix (USD)</h3>
<div className="space-y-2"> <div className="space-y-2">
<input <input
type="range" type="range"
@ -351,14 +353,14 @@ export default function RateSearchPage() {
className="w-full" className="w-full"
/> />
<div className="text-sm text-gray-600"> <div className="text-sm text-gray-600">
Up to ${priceRange[1].toLocaleString()} Jusqu'à {priceRange[1].toLocaleString()} $
</div> </div>
</div> </div>
</div> </div>
<div> <div>
<h3 className="text-sm font-semibold text-gray-900 mb-3"> <h3 className="text-sm font-semibold text-gray-900 mb-3">
Max Transit Time (days) Temps de transit max (jours)
</h3> </h3>
<div className="space-y-2"> <div className="space-y-2">
<input <input
@ -369,13 +371,13 @@ export default function RateSearchPage() {
onChange={e => setTransitTimeMax(parseInt(e.target.value))} onChange={e => setTransitTimeMax(parseInt(e.target.value))}
className="w-full" className="w-full"
/> />
<div className="text-sm text-gray-600">{transitTimeMax} days</div> <div className="text-sm text-gray-600">{transitTimeMax} jours</div>
</div> </div>
</div> </div>
{availableCarriers.length > 0 && ( {availableCarriers.length > 0 && (
<div> <div>
<h3 className="text-sm font-semibold text-gray-900 mb-3">Carriers</h3> <h3 className="text-sm font-semibold text-gray-900 mb-3">Transporteurs</h3>
<div className="space-y-2"> <div className="space-y-2">
{availableCarriers.map(carrier => ( {availableCarriers.map(carrier => (
<label key={carrier} className="flex items-center"> <label key={carrier} className="flex items-center">
@ -398,8 +400,7 @@ export default function RateSearchPage() {
<div className="lg:col-span-3 space-y-4"> <div className="lg:col-span-3 space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900"> <h2 className="text-lg font-semibold text-gray-900">
{filteredAndSortedQuotes.length} Rate {filteredAndSortedQuotes.length} tarif{filteredAndSortedQuotes.length !== 1 ? 's' : ''} trouvé{filteredAndSortedQuotes.length !== 1 ? 's' : ''}
{filteredAndSortedQuotes.length !== 1 ? 's' : ''} Found
</h2> </h2>
</div> </div>
@ -418,9 +419,9 @@ export default function RateSearchPage() {
d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M12 12h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M12 12h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/> />
</svg> </svg>
<h3 className="mt-2 text-sm font-medium text-gray-900">No rates found</h3> <h3 className="mt-2 text-sm font-medium text-gray-900">Aucun tarif trouvé</h3>
<p className="mt-1 text-sm text-gray-500"> <p className="mt-1 text-sm text-gray-500">
Try adjusting your filters or search criteria Essayez d'ajuster vos filtres ou vos critères de recherche
</p> </p>
</div> </div>
) : ( ) : (
@ -467,19 +468,19 @@ export default function RateSearchPage() {
{/* Route Info */} {/* Route Info */}
<div className="mt-4 grid grid-cols-3 gap-4"> <div className="mt-4 grid grid-cols-3 gap-4">
<div> <div>
<div className="text-xs text-gray-500 uppercase">Departure</div> <div className="text-xs text-gray-500 uppercase">Départ</div>
<div className="text-sm font-medium text-gray-900 mt-1"> <div className="text-sm font-medium text-gray-900 mt-1">
{new Date(quote.route.etd).toLocaleDateString()} {new Date(quote.route.etd).toLocaleDateString()}
</div> </div>
</div> </div>
<div> <div>
<div className="text-xs text-gray-500 uppercase">Transit Time</div> <div className="text-xs text-gray-500 uppercase">Temps de transit</div>
<div className="text-sm font-medium text-gray-900 mt-1"> <div className="text-sm font-medium text-gray-900 mt-1">
{quote.route.transitDays} days {quote.route.transitDays} jours
</div> </div>
</div> </div>
<div> <div>
<div className="text-xs text-gray-500 uppercase">Arrival</div> <div className="text-xs text-gray-500 uppercase">Arrivée</div>
<div className="text-sm font-medium text-gray-900 mt-1"> <div className="text-sm font-medium text-gray-900 mt-1">
{new Date(quote.route.eta).toLocaleDateString()} {new Date(quote.route.eta).toLocaleDateString()}
</div> </div>
@ -530,14 +531,14 @@ export default function RateSearchPage() {
<div className="mt-4 flex items-center space-x-4 text-sm text-gray-500"> <div className="mt-4 flex items-center space-x-4 text-sm text-gray-500">
{quote.co2Emissions && ( {quote.co2Emissions && (
<div className="flex items-center"> <div className="flex items-center">
<span className="mr-1">🌱</span> <Leaf className="h-4 w-4 mr-1 text-green-500" />
{quote.co2Emissions.value} kg CO2 {quote.co2Emissions.value} kg CO2
</div> </div>
)} )}
{quote.availability && ( {quote.availability && (
<div className="flex items-center"> <div className="flex items-center">
<span className="mr-1">📦</span> <Package className="h-4 w-4 mr-1 text-blue-500" />
{quote.availability} containers available {quote.availability} conteneurs disponibles
</div> </div>
)} )}
</div> </div>
@ -545,7 +546,7 @@ export default function RateSearchPage() {
{/* Surcharges */} {/* Surcharges */}
{quote.pricing.surcharges && quote.pricing.surcharges.length > 0 && ( {quote.pricing.surcharges && quote.pricing.surcharges.length > 0 && (
<div className="mt-4 text-sm"> <div className="mt-4 text-sm">
<div className="text-gray-500 mb-2">Includes surcharges:</div> <div className="text-gray-500 mb-2">Surcharges incluses :</div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{quote.pricing.surcharges.map((surcharge: any, idx: number) => ( {quote.pricing.surcharges.map((surcharge: any, idx: number) => (
<span <span
@ -565,7 +566,7 @@ export default function RateSearchPage() {
href={`/dashboard/bookings/new?quoteId=${quote.id}`} href={`/dashboard/bookings/new?quoteId=${quote.id}`}
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" 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"
> >
Book Now Réserver
</a> </a>
</div> </div>
</div> </div>
@ -591,10 +592,9 @@ export default function RateSearchPage() {
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/> />
</svg> </svg>
<h3 className="mt-4 text-lg font-medium text-gray-900">Search for Shipping Rates</h3> <h3 className="mt-4 text-lg font-medium text-gray-900">Rechercher des tarifs maritimes</h3>
<p className="mt-2 text-sm text-gray-500"> <p className="mt-2 text-sm text-gray-500">
Enter your origin, destination, and container details to compare rates from multiple Saisissez votre origine, destination et détails du conteneur pour comparer les tarifs de plusieurs transporteurs
carriers
</p> </p>
</div> </div>
)} )}

View File

@ -13,6 +13,7 @@ import { listUsers, updateUser, deleteUser, canInviteUser } from '@/lib/api';
import { createInvitation } from '@/lib/api/invitations'; import { createInvitation } from '@/lib/api/invitations';
import { useAuth } from '@/lib/context/auth-context'; import { useAuth } from '@/lib/context/auth-context';
import Link from 'next/link'; import Link from 'next/link';
import ExportButton from '@/components/ExportButton';
export default function UsersManagementPage() { export default function UsersManagementPage() {
const router = useRouter(); const router = useRouter();
@ -53,7 +54,7 @@ export default function UsersManagementPage() {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] }); queryClient.invalidateQueries({ queryKey: ['users'] });
queryClient.invalidateQueries({ queryKey: ['canInvite'] }); queryClient.invalidateQueries({ queryKey: ['canInvite'] });
setSuccess('Invitation sent successfully! The user will receive an email with a registration link.'); setSuccess('Invitation envoyée avec succès ! L\'utilisateur recevra un email avec un lien d\'inscription.');
setShowInviteModal(false); setShowInviteModal(false);
setInviteForm({ setInviteForm({
email: '', email: '',
@ -64,7 +65,7 @@ export default function UsersManagementPage() {
setTimeout(() => setSuccess(''), 5000); setTimeout(() => setSuccess(''), 5000);
}, },
onError: (err: any) => { onError: (err: any) => {
setError(err.response?.data?.message || 'Failed to send invitation'); setError(err.response?.data?.message || 'Échec de l\'envoi de l\'invitation');
setTimeout(() => setError(''), 5000); setTimeout(() => setError(''), 5000);
}, },
}); });
@ -75,11 +76,11 @@ export default function UsersManagementPage() {
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] }); queryClient.invalidateQueries({ queryKey: ['users'] });
setSuccess('Role updated successfully'); setSuccess('Rôle mis à jour avec succès');
setTimeout(() => setSuccess(''), 3000); setTimeout(() => setSuccess(''), 3000);
}, },
onError: (err: any) => { onError: (err: any) => {
setError(err.response?.data?.message || 'Failed to update role'); setError(err.response?.data?.message || 'Échec de la mise à jour du rôle');
setTimeout(() => setError(''), 5000); setTimeout(() => setError(''), 5000);
}, },
}); });
@ -91,11 +92,11 @@ export default function UsersManagementPage() {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] }); queryClient.invalidateQueries({ queryKey: ['users'] });
queryClient.invalidateQueries({ queryKey: ['canInvite'] }); queryClient.invalidateQueries({ queryKey: ['canInvite'] });
setSuccess('User status updated successfully'); setSuccess('Statut de l\'utilisateur mis à jour avec succès');
setTimeout(() => setSuccess(''), 3000); setTimeout(() => setSuccess(''), 3000);
}, },
onError: (err: any) => { onError: (err: any) => {
setError(err.response?.data?.message || 'Failed to update user status'); setError(err.response?.data?.message || 'Échec de la mise à jour du statut');
setTimeout(() => setError(''), 5000); setTimeout(() => setError(''), 5000);
}, },
}); });
@ -105,11 +106,11 @@ export default function UsersManagementPage() {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] }); queryClient.invalidateQueries({ queryKey: ['users'] });
queryClient.invalidateQueries({ queryKey: ['canInvite'] }); queryClient.invalidateQueries({ queryKey: ['canInvite'] });
setSuccess('User deleted successfully'); setSuccess('Utilisateur supprimé avec succès');
setTimeout(() => setSuccess(''), 3000); setTimeout(() => setSuccess(''), 3000);
}, },
onError: (err: any) => { onError: (err: any) => {
setError(err.response?.data?.message || 'Failed to delete user'); setError(err.response?.data?.message || 'Échec de la suppression de l\'utilisateur');
setTimeout(() => setError(''), 5000); setTimeout(() => setError(''), 5000);
}, },
}); });
@ -143,7 +144,7 @@ export default function UsersManagementPage() {
const handleToggleActive = (userId: string, isActive: boolean) => { const handleToggleActive = (userId: string, isActive: boolean) => {
if ( if (
window.confirm(`Are you sure you want to ${isActive ? 'deactivate' : 'activate'} this user?`) window.confirm(`Êtes-vous sûr de vouloir ${isActive ? 'désactiver' : 'activer'} cet utilisateur ?`)
) { ) {
toggleActiveMutation.mutate({ id: userId, isActive }); toggleActiveMutation.mutate({ id: userId, isActive });
} }
@ -151,7 +152,7 @@ export default function UsersManagementPage() {
const handleDelete = (userId: string) => { const handleDelete = (userId: string) => {
if ( if (
window.confirm('Are you sure you want to delete this user? This action cannot be undone.') window.confirm('Êtes-vous sûr de vouloir supprimer cet utilisateur ? Cette action est irréversible.')
) { ) {
deleteMutation.mutate(userId); deleteMutation.mutate(userId);
} }
@ -179,17 +180,17 @@ export default function UsersManagementPage() {
</svg> </svg>
</div> </div>
<div className="ml-3 flex-1"> <div className="ml-3 flex-1">
<h3 className="text-sm font-medium text-amber-800">License limit reached</h3> <h3 className="text-sm font-medium text-amber-800">Limite de licences atteinte</h3>
<p className="mt-1 text-sm text-amber-700"> <p className="mt-1 text-sm text-amber-700">
Your organization has used all available licenses ({licenseStatus.usedLicenses}/{licenseStatus.maxLicenses}). Votre organisation a utilisé toutes les licences disponibles ({licenseStatus.usedLicenses}/{licenseStatus.maxLicenses}).
Upgrade your subscription to invite more users. Mettez à niveau votre abonnement pour inviter plus d'utilisateurs.
</p> </p>
<div className="mt-3"> <div className="mt-3">
<Link <Link
href="/dashboard/settings/subscription" href="/dashboard/settings/subscription"
className="text-sm font-medium text-amber-800 hover:text-amber-900 underline" className="text-sm font-medium text-amber-800 hover:text-amber-900 underline"
> >
Upgrade Subscription Mettre à niveau l'abonnement
</Link> </Link>
</div> </div>
</div> </div>
@ -206,14 +207,14 @@ export default function UsersManagementPage() {
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" /> <path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
</svg> </svg>
<span className="text-sm text-blue-800"> <span className="text-sm text-blue-800">
{licenseStatus.availableLicenses} license{licenseStatus.availableLicenses !== 1 ? 's' : ''} remaining ({licenseStatus.usedLicenses}/{licenseStatus.maxLicenses} used) {licenseStatus.availableLicenses} licence{licenseStatus.availableLicenses !== 1 ? 's' : ''} restante{licenseStatus.availableLicenses !== 1 ? 's' : ''} ({licenseStatus.usedLicenses}/{licenseStatus.maxLicenses} utilisées)
</span> </span>
</div> </div>
<Link <Link
href="/dashboard/settings/subscription" href="/dashboard/settings/subscription"
className="text-sm font-medium text-blue-600 hover:text-blue-800" className="text-sm font-medium text-blue-600 hover:text-blue-800"
> >
Manage Subscription Gérer l'abonnement
</Link> </Link>
</div> </div>
</div> </div>
@ -222,16 +223,37 @@ export default function UsersManagementPage() {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-gray-900">User Management</h1> <h1 className="text-2xl font-bold text-gray-900">Gestion des Utilisateurs</h1>
<p className="text-sm text-gray-500 mt-1">Manage team members and their permissions</p> <p className="text-sm text-gray-500 mt-1">Gérez les membres de l'équipe et leurs permissions</p>
</div> </div>
<div className="flex items-center space-x-3">
<ExportButton
data={users?.users || []}
filename="utilisateurs"
columns={[
{ key: 'firstName', label: 'Prénom' },
{ key: 'lastName', label: 'Nom' },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Rôle', format: (v) => {
const labels: Record<string, string> = {
ADMIN: 'Administrateur',
MANAGER: 'Manager',
USER: 'Utilisateur',
VIEWER: 'Lecteur',
};
return labels[v] || v;
}},
{ key: 'isActive', label: 'Statut', format: (v) => v ? 'Actif' : 'Inactif' },
{ key: 'createdAt', label: 'Date de création', format: (v) => v ? new Date(v).toLocaleDateString('fr-FR') : '' },
]}
/>
{licenseStatus?.canInvite ? ( {licenseStatus?.canInvite ? (
<button <button
onClick={() => setShowInviteModal(true)} onClick={() => setShowInviteModal(true)}
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" 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> <span className="mr-2">+</span>
Invite User Inviter un utilisateur
</button> </button>
) : ( ) : (
<Link <Link
@ -239,10 +261,11 @@ export default function UsersManagementPage() {
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-amber-600 hover:bg-amber-700" className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-amber-600 hover:bg-amber-700"
> >
<span className="mr-2">+</span> <span className="mr-2">+</span>
Upgrade to Invite Mettre à niveau
</Link> </Link>
)} )}
</div> </div>
</div>
{success && ( {success && (
<div className="rounded-md bg-green-50 p-4"> <div className="rounded-md bg-green-50 p-4">
@ -261,7 +284,7 @@ export default function UsersManagementPage() {
{isLoading ? ( {isLoading ? (
<div className="px-6 py-12 text-center text-gray-500"> <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> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
Loading users... Chargement des utilisateurs...
</div> </div>
) : users?.users && users.users.length > 0 ? ( ) : users?.users && users.users.length > 0 ? (
<div className="overflow-x-auto overflow-y-visible"> <div className="overflow-x-auto overflow-y-visible">
@ -269,19 +292,19 @@ export default function UsersManagementPage() {
<thead className="bg-gray-50"> <thead className="bg-gray-50">
<tr> <tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
User Utilisateur
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Email Email
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Role Rôle
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status Statut
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Last Login Date de création
</th> </th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions Actions
@ -338,7 +361,7 @@ export default function UsersManagementPage() {
: 'bg-red-100 text-red-800' : 'bg-red-100 text-red-800'
}`} }`}
> >
{user.isActive ? 'Active' : 'Inactive'} {user.isActive ? 'Actif' : 'Inactif'}
</span> </span>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
@ -386,8 +409,8 @@ export default function UsersManagementPage() {
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
/> />
</svg> </svg>
<h3 className="mt-2 text-sm font-medium text-gray-900">No users</h3> <h3 className="mt-2 text-sm font-medium text-gray-900">Aucun utilisateur</h3>
<p className="mt-1 text-sm text-gray-500">Get started by inviting a team member</p> <p className="mt-1 text-sm text-gray-500">Commencez par inviter un membre de l'équipe</p>
<div className="mt-6"> <div className="mt-6">
{licenseStatus?.canInvite ? ( {licenseStatus?.canInvite ? (
<button <button
@ -490,7 +513,7 @@ export default function UsersManagementPage() {
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6"> <div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div> <div>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-gray-900">Invite User</h3> <h3 className="text-lg font-medium text-gray-900">Inviter un utilisateur</h3>
<button <button
onClick={() => setShowInviteModal(false)} onClick={() => setShowInviteModal(false)}
className="text-gray-400 hover:text-gray-500" className="text-gray-400 hover:text-gray-500"
@ -510,7 +533,7 @@ export default function UsersManagementPage() {
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700"> <label className="block text-sm font-medium text-gray-700">
First Name * Prénom *
</label> </label>
<input <input
type="text" type="text"
@ -521,7 +544,7 @@ export default function UsersManagementPage() {
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700">Last Name *</label> <label className="block text-sm font-medium text-gray-700">Nom *</label>
<input <input
type="text" type="text"
required required
@ -533,7 +556,7 @@ export default function UsersManagementPage() {
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700">Email *</label> <label className="block text-sm font-medium text-gray-700">Adresse email *</label>
<input <input
type="email" type="email"
required required
@ -544,20 +567,20 @@ export default function UsersManagementPage() {
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700">Role *</label> <label className="block text-sm font-medium text-gray-700">Rôle *</label>
<select <select
value={inviteForm.role} value={inviteForm.role}
onChange={e => setInviteForm({ ...inviteForm, role: e.target.value as any })} onChange={e => setInviteForm({ ...inviteForm, role: e.target.value as any })}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border"
> >
<option value="USER">User</option> <option value="USER">Utilisateur</option>
<option value="MANAGER">Manager</option> <option value="MANAGER">Manager</option>
{currentUser?.role === 'ADMIN' && <option value="ADMIN">Admin</option>} {currentUser?.role === 'ADMIN' && <option value="ADMIN">Administrateur</option>}
<option value="VIEWER">Viewer</option> <option value="VIEWER">Lecteur</option>
</select> </select>
{currentUser?.role !== 'ADMIN' && ( {currentUser?.role !== 'ADMIN' && (
<p className="mt-1 text-xs text-gray-500"> <p className="mt-1 text-xs text-gray-500">
Only platform administrators can assign the ADMIN role Seuls les administrateurs peuvent attribuer le rôle ADMIN
</p> </p>
)} )}
</div> </div>
@ -568,14 +591,14 @@ export default function UsersManagementPage() {
disabled={inviteMutation.isPending} disabled={inviteMutation.isPending}
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:col-start-2 sm:text-sm disabled:bg-gray-400" className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:col-start-2 sm:text-sm disabled:bg-gray-400"
> >
{inviteMutation.isPending ? 'Inviting...' : 'Send Invitation'} {inviteMutation.isPending ? 'Envoi en cours...' : 'Envoyer l\'invitation'}
</button> </button>
<button <button
type="button" type="button"
onClick={() => setShowInviteModal(false)} onClick={() => setShowInviteModal(false)}
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:mt-0 sm:col-start-1 sm:text-sm" className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:mt-0 sm:col-start-1 sm:text-sm"
> >
Cancel Annuler
</button> </button>
</div> </div>
</form> </form>

View File

@ -2,107 +2,181 @@
* Track & Trace Page * Track & Trace Page
* *
* Allows users to track their shipments by entering tracking numbers * Allows users to track their shipments by entering tracking numbers
* and selecting the carrier. Redirects to carrier's tracking page. * and selecting the carrier. Includes search history and vessel position map.
*/ */
'use client'; 'use client';
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card'; import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import {
Search,
Package,
FileText,
ClipboardList,
Lightbulb,
History,
MapPin,
X,
Clock,
Ship,
ExternalLink,
Maximize2,
Minimize2,
Globe,
Anchor,
} from 'lucide-react';
// Carrier tracking URLs - the tracking number will be appended // Search history item type
interface SearchHistoryItem {
id: string;
trackingNumber: string;
carrierId: string;
carrierName: string;
timestamp: Date;
}
// Carrier tracking URLs with official brand colors
const carriers = [ const carriers = [
{ {
id: 'maersk', id: 'maersk',
name: 'Maersk', name: 'Maersk',
logo: '🚢', color: '#00243D', // Maersk dark blue
textColor: 'text-white',
trackingUrl: 'https://www.maersk.com/tracking/', trackingUrl: 'https://www.maersk.com/tracking/',
placeholder: 'Ex: MSKU1234567', placeholder: 'Ex: MSKU1234567',
description: 'Container or B/L number', description: 'N° conteneur ou B/L',
logo: '/assets/logos/carriers/maersk.svg',
}, },
{ {
id: 'msc', id: 'msc',
name: 'MSC', name: 'MSC',
logo: '🛳️', color: '#002B5C', // MSC blue
textColor: 'text-white',
trackingUrl: 'https://www.msc.com/track-a-shipment?query=', trackingUrl: 'https://www.msc.com/track-a-shipment?query=',
placeholder: 'Ex: MSCU1234567', placeholder: 'Ex: MSCU1234567',
description: 'Container, B/L or Booking number', description: 'N° conteneur, B/L ou réservation',
logo: '/assets/logos/carriers/msc.svg',
}, },
{ {
id: 'cma-cgm', id: 'cma-cgm',
name: 'CMA CGM', name: 'CMA CGM',
logo: '⚓', color: '#E30613', // CMA CGM red
textColor: 'text-white',
trackingUrl: 'https://www.cma-cgm.com/ebusiness/tracking/search?SearchBy=Container&Reference=', trackingUrl: 'https://www.cma-cgm.com/ebusiness/tracking/search?SearchBy=Container&Reference=',
placeholder: 'Ex: CMAU1234567', placeholder: 'Ex: CMAU1234567',
description: 'Container or B/L number', description: 'N° conteneur ou B/L',
logo: '/assets/logos/carriers/cmacgm.svg',
}, },
{ {
id: 'hapag-lloyd', id: 'hapag-lloyd',
name: 'Hapag-Lloyd', name: 'Hapag-Lloyd',
logo: '🔷', color: '#FF6600', // Hapag orange
textColor: 'text-white',
trackingUrl: 'https://www.hapag-lloyd.com/en/online-business/track/track-by-container-solution.html?container=', trackingUrl: 'https://www.hapag-lloyd.com/en/online-business/track/track-by-container-solution.html?container=',
placeholder: 'Ex: HLCU1234567', placeholder: 'Ex: HLCU1234567',
description: 'Container number', description: 'N° conteneur',
logo: '/assets/logos/carriers/hapag.svg',
}, },
{ {
id: 'cosco', id: 'cosco',
name: 'COSCO', name: 'COSCO',
logo: '🌊', color: '#003A70', // COSCO blue
textColor: 'text-white',
trackingUrl: 'https://elines.coscoshipping.com/ebusiness/cargoTracking?trackingNumber=', trackingUrl: 'https://elines.coscoshipping.com/ebusiness/cargoTracking?trackingNumber=',
placeholder: 'Ex: COSU1234567', placeholder: 'Ex: COSU1234567',
description: 'Container or B/L number', description: 'N° conteneur ou B/L',
logo: '/assets/logos/carriers/cosco.svg',
}, },
{ {
id: 'one', id: 'one',
name: 'ONE (Ocean Network Express)', name: 'ONE',
logo: '🟣', color: '#FF00FF', // ONE magenta
textColor: 'text-white',
trackingUrl: 'https://ecomm.one-line.com/one-ecom/manage-shipment/cargo-tracking?trkNoParam=', trackingUrl: 'https://ecomm.one-line.com/one-ecom/manage-shipment/cargo-tracking?trkNoParam=',
placeholder: 'Ex: ONEU1234567', placeholder: 'Ex: ONEU1234567',
description: 'Container or B/L number', description: 'N° conteneur ou B/L',
logo: '/assets/logos/carriers/one.svg',
}, },
{ {
id: 'evergreen', id: 'evergreen',
name: 'Evergreen', name: 'Evergreen',
logo: '🌲', color: '#006633', // Evergreen green
textColor: 'text-white',
trackingUrl: 'https://www.shipmentlink.com/servlet/TDB1_CargoTracking.do?BL=', trackingUrl: 'https://www.shipmentlink.com/servlet/TDB1_CargoTracking.do?BL=',
placeholder: 'Ex: EGHU1234567', placeholder: 'Ex: EGHU1234567',
description: 'Container or B/L number', description: 'N° conteneur ou B/L',
logo: '/assets/logos/carriers/evergreen.svg',
}, },
{ {
id: 'yangming', id: 'yangming',
name: 'Yang Ming', name: 'Yang Ming',
logo: '🟡', color: '#FFD700', // Yang Ming yellow
textColor: 'text-gray-900',
trackingUrl: 'https://www.yangming.com/e-service/Track_Trace/track_trace_cargo_tracking.aspx?rdolType=CT&str=', trackingUrl: 'https://www.yangming.com/e-service/Track_Trace/track_trace_cargo_tracking.aspx?rdolType=CT&str=',
placeholder: 'Ex: YMLU1234567', placeholder: 'Ex: YMLU1234567',
description: 'Container number', description: 'N° conteneur',
logo: '/assets/logos/carriers/yangming.svg',
}, },
{ {
id: 'zim', id: 'zim',
name: 'ZIM', name: 'ZIM',
logo: '🔵', color: '#1E3A8A', // ZIM blue
textColor: 'text-white',
trackingUrl: 'https://www.zim.com/tools/track-a-shipment?consnumber=', trackingUrl: 'https://www.zim.com/tools/track-a-shipment?consnumber=',
placeholder: 'Ex: ZIMU1234567', placeholder: 'Ex: ZIMU1234567',
description: 'Container or B/L number', description: 'N° conteneur ou B/L',
logo: '/assets/logos/carriers/zim.svg',
}, },
{ {
id: 'hmm', id: 'hmm',
name: 'HMM (Hyundai)', name: 'HMM',
logo: '🟠', color: '#E65100', // HMM orange
textColor: 'text-white',
trackingUrl: 'https://www.hmm21.com/cms/business/ebiz/trackTrace/trackTrace/index.jsp?type=1&number=', trackingUrl: 'https://www.hmm21.com/cms/business/ebiz/trackTrace/trackTrace/index.jsp?type=1&number=',
placeholder: 'Ex: HDMU1234567', placeholder: 'Ex: HDMU1234567',
description: 'Container or B/L number', description: 'N° conteneur ou B/L',
logo: '/assets/logos/carriers/hmm.svg',
}, },
]; ];
// Local storage keys
const HISTORY_KEY = 'xpeditis_track_history';
export default function TrackTracePage() { export default function TrackTracePage() {
const [trackingNumber, setTrackingNumber] = useState(''); const [trackingNumber, setTrackingNumber] = useState('');
const [selectedCarrier, setSelectedCarrier] = useState(''); const [selectedCarrier, setSelectedCarrier] = useState('');
const [error, setError] = useState(''); const [error, setError] = useState('');
const [searchHistory, setSearchHistory] = useState<SearchHistoryItem[]>([]);
const [showMap, setShowMap] = useState(false);
const [isMapFullscreen, setIsMapFullscreen] = useState(false);
const [isMapLoading, setIsMapLoading] = useState(true);
// Load history from localStorage on mount
useEffect(() => {
const savedHistory = localStorage.getItem(HISTORY_KEY);
if (savedHistory) {
try {
const parsed = JSON.parse(savedHistory);
setSearchHistory(parsed.map((item: any) => ({
...item,
timestamp: new Date(item.timestamp)
})));
} catch (e) {
console.error('Failed to parse search history:', e);
}
}
}, []);
// Save to localStorage
const saveHistory = (history: SearchHistoryItem[]) => {
localStorage.setItem(HISTORY_KEY, JSON.stringify(history));
setSearchHistory(history);
};
const handleTrack = () => { const handleTrack = () => {
// Validation
if (!trackingNumber.trim()) { if (!trackingNumber.trim()) {
setError('Veuillez entrer un numéro de tracking'); setError('Veuillez entrer un numéro de tracking');
return; return;
@ -114,15 +188,43 @@ export default function TrackTracePage() {
setError(''); setError('');
// Find the carrier and build the tracking URL
const carrier = carriers.find(c => c.id === selectedCarrier); const carrier = carriers.find(c => c.id === selectedCarrier);
if (carrier) { if (carrier) {
// Add to history
const newHistoryItem: SearchHistoryItem = {
id: Date.now().toString(),
trackingNumber: trackingNumber.trim(),
carrierId: carrier.id,
carrierName: carrier.name,
timestamp: new Date(),
};
// Keep only last 10 unique searches
const updatedHistory = [newHistoryItem, ...searchHistory.filter(
h => !(h.trackingNumber === newHistoryItem.trackingNumber && h.carrierId === newHistoryItem.carrierId)
)].slice(0, 10);
saveHistory(updatedHistory);
const trackingUrl = carrier.trackingUrl + encodeURIComponent(trackingNumber.trim()); const trackingUrl = carrier.trackingUrl + encodeURIComponent(trackingNumber.trim());
// Open in new tab
window.open(trackingUrl, '_blank', 'noopener,noreferrer'); window.open(trackingUrl, '_blank', 'noopener,noreferrer');
} }
}; };
const handleHistoryClick = (item: SearchHistoryItem) => {
setTrackingNumber(item.trackingNumber);
setSelectedCarrier(item.carrierId);
};
const handleDeleteHistory = (id: string) => {
const updatedHistory = searchHistory.filter(h => h.id !== id);
saveHistory(updatedHistory);
};
const handleClearHistory = () => {
saveHistory([]);
};
const handleKeyPress = (e: React.KeyboardEvent) => { const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
handleTrack(); handleTrack();
@ -131,11 +233,25 @@ export default function TrackTracePage() {
const selectedCarrierData = carriers.find(c => c.id === selectedCarrier); const selectedCarrierData = carriers.find(c => c.id === selectedCarrier);
const formatTimeAgo = (date: Date) => {
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return 'À l\'instant';
if (diffMins < 60) return `Il y a ${diffMins}min`;
if (diffHours < 24) return `Il y a ${diffHours}h`;
if (diffDays < 7) return `Il y a ${diffDays}j`;
return date.toLocaleDateString('fr-FR');
};
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} {/* Header */}
<div className="mb-8"> <div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">Track & Trace</h1> <h1 className="text-3xl font-bold text-gray-900">Suivi des expéditions</h1>
<p className="mt-2 text-gray-600"> <p className="mt-2 text-gray-600">
Suivez vos expéditions en temps réel. Entrez votre numéro de tracking et sélectionnez le transporteur. Suivez vos expéditions en temps réel. Entrez votre numéro de tracking et sélectionnez le transporteur.
</p> </p>
@ -145,15 +261,15 @@ export default function TrackTracePage() {
<Card className="bg-white shadow-lg border-blue-100"> <Card className="bg-white shadow-lg border-blue-100">
<CardHeader> <CardHeader>
<CardTitle className="text-xl flex items-center gap-2"> <CardTitle className="text-xl flex items-center gap-2">
<span className="text-2xl">🔍</span> <Search className="h-5 w-5 text-blue-600" />
Rechercher une expédition Rechercher une expédition
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
Entrez votre numéro de conteneur, connaissement (B/L) ou référence de booking Entrez votre numéro de conteneur, connaissement (B/L) ou référence de réservation
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-6">
{/* Carrier Selection */} {/* Carrier Selection - US 5.1: Professional carrier cards with brand colors */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-3"> <label className="block text-sm font-medium text-gray-700 mb-3">
Sélectionnez le transporteur Sélectionnez le transporteur
@ -167,14 +283,20 @@ export default function TrackTracePage() {
setSelectedCarrier(carrier.id); setSelectedCarrier(carrier.id);
setError(''); setError('');
}} }}
className={`flex flex-col items-center justify-center p-3 rounded-lg border-2 transition-all ${ className={`flex flex-col items-center justify-center p-4 rounded-xl border-2 transition-all hover:scale-105 ${
selectedCarrier === carrier.id selectedCarrier === carrier.id
? 'border-blue-500 bg-blue-50 shadow-md' ? 'border-blue-500 shadow-lg ring-2 ring-blue-200'
: 'border-gray-200 hover:border-blue-300 hover:bg-gray-50' : 'border-gray-200 hover:border-gray-300 hover:shadow-md'
}`} }`}
> >
<span className="text-2xl mb-1">{carrier.logo}</span> {/* Carrier logo/badge with brand color */}
<span className="text-xs font-medium text-gray-700 text-center">{carrier.name}</span> <div
className={`w-12 h-12 rounded-lg flex items-center justify-center text-sm font-bold mb-2 shadow-sm ${carrier.textColor}`}
style={{ backgroundColor: carrier.color }}
>
{carrier.name.length <= 3 ? carrier.name : carrier.name.substring(0, 2).toUpperCase()}
</div>
<span className="text-xs font-semibold text-gray-800 text-center leading-tight">{carrier.name}</span>
</button> </button>
))} ))}
</div> </div>
@ -197,22 +319,42 @@ export default function TrackTracePage() {
}} }}
onKeyPress={handleKeyPress} onKeyPress={handleKeyPress}
placeholder={selectedCarrierData?.placeholder || 'Ex: MSKU1234567'} placeholder={selectedCarrierData?.placeholder || 'Ex: MSKU1234567'}
className="text-lg font-mono border-gray-300 focus:border-blue-500" className="text-lg font-mono border-gray-300 focus:border-blue-500 h-12"
/> />
{selectedCarrierData && ( {selectedCarrierData && (
<p className="mt-1 text-xs text-gray-500">{selectedCarrierData.description}</p> <p className="mt-1 text-xs text-gray-500">{selectedCarrierData.description}</p>
)} )}
</div> </div>
{/* US 5.2: Harmonized button color */}
<Button <Button
onClick={handleTrack} onClick={handleTrack}
className="bg-blue-600 hover:bg-blue-700 text-white px-6" size="lg"
className="bg-blue-600 hover:bg-blue-700 text-white px-8 h-12 font-semibold shadow-md"
> >
<span className="mr-2">🔍</span> <Search className="mr-2 h-5 w-5" />
Rechercher Rechercher
</Button> </Button>
</div> </div>
</div> </div>
{/* Action Button - Map */}
<div className="flex flex-wrap gap-3 pt-2">
<Button
variant={showMap ? "default" : "outline"}
onClick={() => {
setShowMap(!showMap);
if (!showMap) setIsMapLoading(true);
}}
className={showMap
? "bg-blue-600 hover:bg-blue-700 text-white"
: "text-gray-700 border-gray-300 hover:bg-blue-50 hover:border-blue-300 hover:text-blue-700"
}
>
<Globe className="mr-2 h-4 w-4" />
{showMap ? 'Masquer la carte maritime' : 'Afficher la carte maritime'}
</Button>
</div>
{/* Error Message */} {/* Error Message */}
{error && ( {error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-lg"> <div className="p-3 bg-red-50 border border-red-200 rounded-lg">
@ -222,12 +364,221 @@ export default function TrackTracePage() {
</CardContent> </CardContent>
</Card> </Card>
{/* Vessel Position Map - Large immersive display */}
{showMap && (
<div className={`${isMapFullscreen ? 'fixed inset-0 z-50 bg-gray-900' : ''}`}>
<Card className={`bg-white shadow-xl overflow-hidden ${isMapFullscreen ? 'h-full rounded-none' : ''}`}>
{/* Map Header */}
<div className={`flex items-center justify-between px-6 py-4 bg-gradient-to-r from-blue-600 to-blue-700 text-white ${isMapFullscreen ? '' : 'rounded-t-lg'}`}>
<div className="flex items-center gap-3">
<div className="p-2 bg-white/20 rounded-lg">
<Globe className="h-6 w-6" />
</div>
<div>
<h3 className="text-lg font-semibold">Carte Maritime Mondiale</h3>
<p className="text-blue-100 text-sm">Position des navires en temps réel</p>
</div>
</div>
<div className="flex items-center gap-2">
{/* Fullscreen Toggle */}
<Button
variant="ghost"
size="sm"
onClick={() => setIsMapFullscreen(!isMapFullscreen)}
className="text-white hover:bg-white/20"
>
{isMapFullscreen ? (
<>
<Minimize2 className="h-4 w-4 mr-2" />
Réduire
</>
) : (
<>
<Maximize2 className="h-4 w-4 mr-2" />
Plein écran
</>
)}
</Button>
{/* Close Button */}
<Button
variant="ghost"
size="sm"
onClick={() => {
setShowMap(false);
setIsMapFullscreen(false);
}}
className="text-white hover:bg-white/20"
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
{/* Map Container */}
<div className={`relative w-full ${isMapFullscreen ? 'h-[calc(100vh-80px)]' : 'h-[70vh] min-h-[500px] max-h-[800px]'}`}>
{/* Loading State */}
{isMapLoading && (
<div className="absolute inset-0 bg-gradient-to-br from-blue-50 to-blue-100 flex items-center justify-center z-10">
<div className="text-center">
<div className="relative">
<Ship className="h-16 w-16 text-blue-600 animate-pulse" />
<div className="absolute -bottom-1 left-1/2 transform -translate-x-1/2">
<div className="flex gap-1">
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
</div>
</div>
</div>
<p className="mt-4 text-blue-700 font-medium">Chargement de la carte...</p>
<p className="text-blue-500 text-sm">Connexion à MarineTraffic</p>
</div>
</div>
)}
{/* MarineTraffic Map */}
<iframe
src="https://www.marinetraffic.com/en/ais/embed/zoom:3/centery:25/centerx:0/maptype:4/shownames:true/mmsi:0/shipid:0/fleet:/fleet_id:/vtypes:/showmenu:true/remember:false"
className="w-full h-full border-0"
title="Carte maritime en temps réel"
loading="lazy"
onLoad={() => setIsMapLoading(false)}
/>
{/* Map Legend Overlay */}
<div className={`absolute bottom-4 left-4 bg-white/95 backdrop-blur-sm rounded-xl shadow-lg p-4 ${isMapFullscreen ? 'max-w-xs' : 'max-w-[280px]'}`}>
<h4 className="font-semibold text-gray-800 text-sm mb-3 flex items-center gap-2">
<Anchor className="h-4 w-4 text-blue-600" />
Légende
</h4>
<div className="space-y-2 text-xs">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-green-500" />
<span className="text-gray-600">Cargos</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-red-500" />
<span className="text-gray-600">Tankers</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-blue-500" />
<span className="text-gray-600">Passagers</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-yellow-500" />
<span className="text-gray-600">High Speed</span>
</div>
</div>
</div>
{/* Quick Stats Overlay */}
<div className={`absolute top-4 right-4 bg-white/95 backdrop-blur-sm rounded-xl shadow-lg p-4 ${isMapFullscreen ? '' : 'hidden lg:block'}`}>
<div className="flex items-center gap-4 text-sm">
<div className="text-center">
<p className="text-2xl font-bold text-blue-600">90K+</p>
<p className="text-gray-500 text-xs">Navires actifs</p>
</div>
<div className="w-px h-10 bg-gray-200" />
<div className="text-center">
<p className="text-2xl font-bold text-green-600">3,500+</p>
<p className="text-gray-500 text-xs">Ports mondiaux</p>
</div>
</div>
</div>
</div>
{/* Map Footer */}
<div className="px-6 py-3 bg-gray-50 border-t border-gray-200 flex items-center justify-between">
<p className="text-xs text-gray-500 flex items-center gap-1">
<ExternalLink className="h-3 w-3" />
Données fournies par MarineTraffic - Mise à jour en temps réel
</p>
<a
href="https://www.marinetraffic.com"
target="_blank"
rel="noopener noreferrer"
className="text-xs text-blue-600 hover:text-blue-800 font-medium flex items-center gap-1"
>
Ouvrir sur MarineTraffic
<ExternalLink className="h-3 w-3" />
</a>
</div>
</Card>
</div>
)}
{/* Search History */}
<Card className="bg-white shadow">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-lg flex items-center gap-2">
<History className="h-5 w-5 text-gray-600" />
Historique des recherches
</CardTitle>
{searchHistory.length > 0 && (
<Button
variant="ghost"
size="sm"
onClick={handleClearHistory}
className="text-gray-500 hover:text-red-600 text-xs"
>
Effacer tout
</Button>
)}
</div>
</CardHeader>
<CardContent>
{searchHistory.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<Clock className="h-10 w-10 mx-auto mb-3 text-gray-300" />
<p className="text-sm">Aucune recherche récente</p>
<p className="text-xs text-gray-400 mt-1">Vos recherches apparaîtront ici</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{searchHistory.map(item => {
const carrier = carriers.find(c => c.id === item.carrierId);
return (
<div
key={item.id}
className="flex items-center justify-between p-3 rounded-lg border border-gray-100 hover:bg-gray-50 group cursor-pointer transition-colors"
onClick={() => handleHistoryClick(item)}
>
<div className="flex items-center gap-3">
<div
className={`w-8 h-8 rounded flex items-center justify-center text-xs font-bold ${carrier?.textColor || 'text-white'}`}
style={{ backgroundColor: carrier?.color || '#666' }}
>
{item.carrierName.substring(0, 2).toUpperCase()}
</div>
<div>
<p className="font-mono text-sm font-medium text-gray-900">{item.trackingNumber}</p>
<p className="text-xs text-gray-500">{item.carrierName} {formatTimeAgo(item.timestamp)}</p>
</div>
</div>
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteHistory(item.id);
}}
className="opacity-0 group-hover:opacity-100 p-1 hover:bg-red-50 rounded transition-opacity"
>
<X className="h-4 w-4 text-gray-400 hover:text-red-500" />
</button>
</div>
);
})}
</div>
)}
</CardContent>
</Card>
{/* Help Section */} {/* Help Section */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card className="bg-white"> <Card className="bg-white">
<CardHeader> <CardHeader>
<CardTitle className="text-lg flex items-center gap-2"> <CardTitle className="text-lg flex items-center gap-2">
<span>📦</span> <Package className="h-5 w-5 text-blue-600" />
Numéro de conteneur Numéro de conteneur
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@ -242,14 +593,14 @@ export default function TrackTracePage() {
<Card className="bg-white"> <Card className="bg-white">
<CardHeader> <CardHeader>
<CardTitle className="text-lg flex items-center gap-2"> <CardTitle className="text-lg flex items-center gap-2">
<span>📋</span> <FileText className="h-5 w-5 text-blue-600" />
Connaissement (B/L) Connaissement (B/L)
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">
Le numéro de Bill of Lading est fourni par le transporteur lors de la confirmation de booking. Le numéro de connaissement (Bill of Lading) est fourni par le transporteur lors de la confirmation de réservation.
Format variable selon le carrier. Le format varie selon le transporteur.
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
@ -257,8 +608,8 @@ export default function TrackTracePage() {
<Card className="bg-white"> <Card className="bg-white">
<CardHeader> <CardHeader>
<CardTitle className="text-lg flex items-center gap-2"> <CardTitle className="text-lg flex items-center gap-2">
<span>📝</span> <ClipboardList className="h-5 w-5 text-blue-600" />
Référence de booking Référence de réservation
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@ -272,7 +623,7 @@ export default function TrackTracePage() {
{/* Info Box */} {/* Info Box */}
<div className="p-4 bg-blue-50 rounded-lg border border-blue-100"> <div className="p-4 bg-blue-50 rounded-lg border border-blue-100">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<span className="text-xl">💡</span> <Lightbulb className="h-5 w-5 text-blue-600 flex-shrink-0" />
<div> <div>
<p className="text-sm font-medium text-blue-800">Comment fonctionne le suivi ?</p> <p className="text-sm font-medium text-blue-800">Comment fonctionne le suivi ?</p>
<p className="text-sm text-blue-700 mt-1"> <p className="text-sm text-blue-700 mt-1">

View File

@ -6,6 +6,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Shield, ClipboardList, DollarSign, Pencil, Lightbulb, Plus } from 'lucide-react';
const clausesICC = [ const clausesICC = [
{ {
@ -64,7 +65,7 @@ export default function AssurancePage() {
{/* Title */} {/* Title */}
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-4xl">🛡</span> <Shield className="w-10 h-10 text-blue-600" />
<h1 className="text-3xl font-bold text-gray-900">Assurance Maritime (Cargo Insurance)</h1> <h1 className="text-3xl font-bold text-gray-900">Assurance Maritime (Cargo Insurance)</h1>
</div> </div>
<p className="mt-3 text-gray-600 max-w-3xl"> <p className="mt-3 text-gray-600 max-w-3xl">
@ -88,7 +89,7 @@ export default function AssurancePage() {
{/* ICC Clauses */} {/* ICC Clauses */}
<div className="mt-8"> <div className="mt-8">
<h2 className="text-xl font-bold text-gray-900 mb-4">📋 Clauses ICC (Institute Cargo Clauses)</h2> <h2 className="text-xl font-bold text-gray-900 mb-4 flex items-center gap-2"><ClipboardList className="w-5 h-5" /> Clauses ICC (Institute Cargo Clauses)</h2>
<div className="space-y-4"> <div className="space-y-4">
{clausesICC.map((clause) => ( {clausesICC.map((clause) => (
<Card key={clause.code} className={`bg-white ${clause.recommended ? 'border-green-300 border-2' : ''}`}> <Card key={clause.code} className={`bg-white ${clause.recommended ? 'border-green-300 border-2' : ''}`}>
@ -138,7 +139,7 @@ export default function AssurancePage() {
{/* Valeur assurée */} {/* Valeur assurée */}
<Card className="mt-8 bg-gray-50"> <Card className="mt-8 bg-gray-50">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-gray-900 mb-3">💰 Calcul de la Valeur Assurée</h3> <h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2"><DollarSign className="w-5 h-5" /> Calcul de la Valeur Assurée</h3>
<div className="bg-white p-4 rounded-lg border"> <div className="bg-white p-4 rounded-lg border">
<div className="text-center mb-4"> <div className="text-center mb-4">
<p className="text-lg font-mono bg-blue-100 text-blue-800 p-3 rounded inline-block"> <p className="text-lg font-mono bg-blue-100 text-blue-800 p-3 rounded inline-block">
@ -165,7 +166,7 @@ export default function AssurancePage() {
{/* Extensions */} {/* Extensions */}
<div className="mt-8"> <div className="mt-8">
<h2 className="text-xl font-bold text-gray-900 mb-4"> Extensions de Garantie</h2> <h2 className="text-xl font-bold text-gray-900 mb-4 flex items-center gap-2"><Plus className="w-5 h-5" /> Extensions de Garantie</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{extensionsGaranties.map((ext) => ( {extensionsGaranties.map((ext) => (
<Card key={ext.name} className="bg-white"> <Card key={ext.name} className="bg-white">
@ -181,7 +182,7 @@ export default function AssurancePage() {
{/* Process */} {/* Process */}
<Card className="mt-8 bg-gray-50"> <Card className="mt-8 bg-gray-50">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-gray-900 mb-3">📝 En Cas de Sinistre</h3> <h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2"><Pencil className="w-5 h-5" /> En Cas de Sinistre</h3>
<ol className="list-decimal list-inside space-y-3 text-gray-700"> <ol className="list-decimal list-inside space-y-3 text-gray-700">
<li><strong>Constater</strong> : Émettre des réserves précises sur le bon de livraison</li> <li><strong>Constater</strong> : Émettre des réserves précises sur le bon de livraison</li>
<li><strong>Préserver</strong> : Ne pas modifier l&apos;état des marchandises (photos, témoins)</li> <li><strong>Préserver</strong> : Ne pas modifier l&apos;état des marchandises (photos, témoins)</li>
@ -195,7 +196,7 @@ export default function AssurancePage() {
{/* Tips */} {/* Tips */}
<Card className="mt-8 bg-amber-50 border-amber-200"> <Card className="mt-8 bg-amber-50 border-amber-200">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-amber-900 mb-3">💡 Conseils Pratiques</h3> <h3 className="font-semibold text-amber-900 mb-3 flex items-center gap-2"><Lightbulb className="w-5 h-5" /> Conseils Pratiques</h3>
<ul className="list-disc list-inside space-y-2 text-amber-800"> <ul className="list-disc list-inside space-y-2 text-amber-800">
<li>Toujours opter pour ICC A (All Risks) sauf marchandises très résistantes</li> <li>Toujours opter pour ICC A (All Risks) sauf marchandises très résistantes</li>
<li>Vérifier les exclusions et souscrire les extensions nécessaires</li> <li>Vérifier les exclusions et souscrire les extensions nécessaires</li>

View File

@ -6,6 +6,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Calculator, Ruler, ClipboardList, Banknote, BarChart3, Lightbulb, Scale } from 'lucide-react';
const surcharges = [ const surcharges = [
{ {
@ -86,7 +87,7 @@ export default function CalculFretPage() {
{/* Title */} {/* Title */}
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-4xl">🧮</span> <Calculator className="w-10 h-10 text-blue-600" />
<h1 className="text-3xl font-bold text-gray-900">Calcul du Fret Maritime</h1> <h1 className="text-3xl font-bold text-gray-900">Calcul du Fret Maritime</h1>
</div> </div>
<p className="mt-3 text-gray-600 max-w-3xl"> <p className="mt-3 text-gray-600 max-w-3xl">
@ -98,7 +99,7 @@ export default function CalculFretPage() {
{/* Base Calculation */} {/* Base Calculation */}
<Card className="bg-blue-50 border-blue-200"> <Card className="bg-blue-50 border-blue-200">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-blue-900 mb-3">📐 Principes de Base</h3> <h3 className="font-semibold text-blue-900 mb-3 flex items-center gap-2"><Ruler className="w-5 h-5" /> Principes de Base</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div> <div>
<h4 className="font-medium text-blue-800">FCL (Conteneur Complet)</h4> <h4 className="font-medium text-blue-800">FCL (Conteneur Complet)</h4>
@ -121,7 +122,7 @@ export default function CalculFretPage() {
{/* Weight Calculation */} {/* Weight Calculation */}
<Card className="mt-8 bg-gray-50"> <Card className="mt-8 bg-gray-50">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-gray-900 mb-3"> Poids Taxable (LCL)</h3> <h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2"><Scale className="w-5 h-5" /> Poids Taxable (LCL)</h3>
<div className="bg-white p-4 rounded-lg border mb-4"> <div className="bg-white p-4 rounded-lg border mb-4">
<div className="text-center"> <div className="text-center">
<p className="text-lg font-mono bg-blue-100 text-blue-800 p-3 rounded inline-block"> <p className="text-lg font-mono bg-blue-100 text-blue-800 p-3 rounded inline-block">
@ -157,7 +158,7 @@ export default function CalculFretPage() {
{/* Surcharges */} {/* Surcharges */}
<div className="mt-8"> <div className="mt-8">
<h2 className="text-xl font-bold text-gray-900 mb-4">📋 Surcharges Courantes</h2> <h2 className="text-xl font-bold text-gray-900 mb-4 flex items-center gap-2"><ClipboardList className="w-5 h-5" /> Surcharges Courantes</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{surcharges.map((sur) => ( {surcharges.map((sur) => (
<Card key={sur.code} className="bg-white"> <Card key={sur.code} className="bg-white">
@ -180,7 +181,7 @@ export default function CalculFretPage() {
{/* Additional fees */} {/* Additional fees */}
<div className="mt-8"> <div className="mt-8">
<h2 className="text-xl font-bold text-gray-900 mb-4">💵 Frais Additionnels</h2> <h2 className="text-xl font-bold text-gray-900 mb-4 flex items-center gap-2"><Banknote className="w-5 h-5" /> Frais Additionnels</h2>
<Card className="bg-white"> <Card className="bg-white">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
@ -210,7 +211,7 @@ export default function CalculFretPage() {
{/* Example calculation */} {/* Example calculation */}
<Card className="mt-8 bg-green-50 border-green-200"> <Card className="mt-8 bg-green-50 border-green-200">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-green-900 mb-3">📊 Exemple de Devis FCL</h3> <h3 className="font-semibold text-green-900 mb-3 flex items-center gap-2"><BarChart3 className="w-5 h-5" /> Exemple de Devis FCL</h3>
<div className="bg-white p-4 rounded-lg border"> <div className="bg-white p-4 rounded-lg border">
<p className="text-sm text-gray-600 mb-3">Conteneur 40&apos; Shanghai Le Havre</p> <p className="text-sm text-gray-600 mb-3">Conteneur 40&apos; Shanghai Le Havre</p>
<div className="space-y-2 font-mono text-sm"> <div className="space-y-2 font-mono text-sm">
@ -254,7 +255,7 @@ export default function CalculFretPage() {
{/* Tips */} {/* Tips */}
<Card className="mt-8 bg-amber-50 border-amber-200"> <Card className="mt-8 bg-amber-50 border-amber-200">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-amber-900 mb-3">💡 Conseils pour Optimiser</h3> <h3 className="font-semibold text-amber-900 mb-3 flex items-center gap-2"><Lightbulb className="w-5 h-5" /> Conseils pour Optimiser</h3>
<ul className="list-disc list-inside space-y-2 text-amber-800"> <ul className="list-disc list-inside space-y-2 text-amber-800">
<li>Demandez des devis &quot;All-in&quot; pour éviter les surprises de surcharges</li> <li>Demandez des devis &quot;All-in&quot; pour éviter les surprises de surcharges</li>
<li>Comparez les transitaires sur le total, pas seulement le fret de base</li> <li>Comparez les transitaires sur le total, pas seulement le fret de base</li>

View File

@ -6,6 +6,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Package, PackageOpen, Truck, Cylinder, Snowflake, type LucideIcon } from 'lucide-react';
const containers = [ const containers = [
{ {
@ -22,7 +23,7 @@ const containers = [
tare: '2,300 kg', tare: '2,300 kg',
}, },
usage: 'Marchandises générales sèches', usage: 'Marchandises générales sèches',
icon: '📦', icon: Package,
}, },
{ {
type: '40\' Standard (40\' DRY)', type: '40\' Standard (40\' DRY)',
@ -38,7 +39,7 @@ const containers = [
tare: '3,800 kg', tare: '3,800 kg',
}, },
usage: 'Marchandises générales, cargo volumineux', usage: 'Marchandises générales, cargo volumineux',
icon: '📦', icon: Package,
}, },
{ {
type: '40\' High Cube (40\' HC)', type: '40\' High Cube (40\' HC)',
@ -54,7 +55,7 @@ const containers = [
tare: '4,020 kg', tare: '4,020 kg',
}, },
usage: 'Cargo léger mais volumineux', usage: 'Cargo léger mais volumineux',
icon: '📦', icon: Package,
}, },
{ {
type: 'Reefer (Réfrigéré)', type: 'Reefer (Réfrigéré)',
@ -70,7 +71,7 @@ const containers = [
temperature: '-30°C à +30°C', temperature: '-30°C à +30°C',
}, },
usage: 'Produits périssables, pharmaceutiques', usage: 'Produits périssables, pharmaceutiques',
icon: '❄️', icon: Snowflake,
}, },
{ {
type: 'Open Top', type: 'Open Top',
@ -86,7 +87,7 @@ const containers = [
tare: '2,400 kg / 4,100 kg', tare: '2,400 kg / 4,100 kg',
}, },
usage: 'Cargo hors gabarit en hauteur, machinerie', usage: 'Cargo hors gabarit en hauteur, machinerie',
icon: '📭', icon: PackageOpen,
}, },
{ {
type: 'Flat Rack', type: 'Flat Rack',
@ -102,7 +103,7 @@ const containers = [
tare: '2,700 kg / 4,700 kg', tare: '2,700 kg / 4,700 kg',
}, },
usage: 'Cargo très lourd ou surdimensionné', usage: 'Cargo très lourd ou surdimensionné',
icon: '🚛', icon: Truck,
}, },
{ {
type: 'Tank Container', type: 'Tank Container',
@ -118,7 +119,7 @@ const containers = [
tare: '3,500 kg', tare: '3,500 kg',
}, },
usage: 'Liquides, gaz, produits chimiques', usage: 'Liquides, gaz, produits chimiques',
icon: '🛢️', icon: Cylinder,
}, },
]; ];
@ -148,7 +149,7 @@ export default function ConteneursPage() {
{/* Title */} {/* Title */}
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-4xl">📦</span> <Package className="w-10 h-10 text-blue-600" />
<h1 className="text-3xl font-bold text-gray-900">Conteneurs et Types de Cargo</h1> <h1 className="text-3xl font-bold text-gray-900">Conteneurs et Types de Cargo</h1>
</div> </div>
<p className="mt-3 text-gray-600 max-w-3xl"> <p className="mt-3 text-gray-600 max-w-3xl">
@ -189,7 +190,7 @@ export default function ConteneursPage() {
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="flex items-center justify-between"> <CardTitle className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-2xl">{container.icon}</span> <container.icon className="w-6 h-6 text-blue-600" />
<div> <div>
<span className="text-lg">{container.type}</span> <span className="text-lg">{container.type}</span>
<span className="ml-2 px-2 py-1 bg-gray-100 text-gray-700 text-xs font-mono rounded"> <span className="ml-2 px-2 py-1 bg-gray-100 text-gray-700 text-xs font-mono rounded">
@ -260,35 +261,35 @@ export default function ConteneursPage() {
<CardContent> <CardContent>
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<span className="text-xl">📦</span> <Package className="w-5 h-5 text-green-700 mt-0.5" />
<div> <div>
<p className="font-medium text-green-900">Marchandises générales</p> <p className="font-medium text-green-900">Marchandises générales</p>
<p className="text-sm text-green-800"> 20&apos; ou 40&apos; Standard (DRY)</p> <p className="text-sm text-green-800"> 20&apos; ou 40&apos; Standard (DRY)</p>
</div> </div>
</div> </div>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<span className="text-xl"></span> <Snowflake className="w-5 h-5 text-green-700 mt-0.5" />
<div> <div>
<p className="font-medium text-green-900">Produits réfrigérés/congelés</p> <p className="font-medium text-green-900">Produits réfrigérés/congelés</p>
<p className="text-sm text-green-800"> Reefer 20&apos; ou 40&apos;</p> <p className="text-sm text-green-800"> Reefer 20&apos; ou 40&apos;</p>
</div> </div>
</div> </div>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<span className="text-xl">📭</span> <PackageOpen className="w-5 h-5 text-green-700 mt-0.5" />
<div> <div>
<p className="font-medium text-green-900">Cargo hors gabarit (hauteur)</p> <p className="font-medium text-green-900">Cargo hors gabarit (hauteur)</p>
<p className="text-sm text-green-800"> Open Top</p> <p className="text-sm text-green-800"> Open Top</p>
</div> </div>
</div> </div>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<span className="text-xl">🚛</span> <Truck className="w-5 h-5 text-green-700 mt-0.5" />
<div> <div>
<p className="font-medium text-green-900">Machinerie lourde/surdimensionnée</p> <p className="font-medium text-green-900">Machinerie lourde/surdimensionnée</p>
<p className="text-sm text-green-800"> Flat Rack</p> <p className="text-sm text-green-800"> Flat Rack</p>
</div> </div>
</div> </div>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<span className="text-xl">🛢</span> <Cylinder className="w-5 h-5 text-green-700 mt-0.5" />
<div> <div>
<p className="font-medium text-green-900">Liquides en vrac</p> <p className="font-medium text-green-900">Liquides en vrac</p>
<p className="text-sm text-green-800"> Tank Container ou Flexitank</p> <p className="text-sm text-green-800"> Tank Container ou Flexitank</p>

View File

@ -6,6 +6,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { FileText, ClipboardList, FileStack, Package, Receipt, Factory, type LucideIcon } from 'lucide-react';
const documents = [ const documents = [
{ {
@ -18,7 +19,7 @@ const documents = [
{ name: 'B/L au porteur', desc: 'Propriété à celui qui le détient' }, { name: 'B/L au porteur', desc: 'Propriété à celui qui le détient' },
], ],
importance: 'Critique', importance: 'Critique',
icon: '📄', icon: FileText,
}, },
{ {
name: 'Sea Waybill', name: 'Sea Waybill',
@ -29,7 +30,7 @@ const documents = [
{ name: 'Express', desc: 'Libération rapide sans documents originaux' }, { name: 'Express', desc: 'Libération rapide sans documents originaux' },
], ],
importance: 'Important', importance: 'Important',
icon: '📋', icon: ClipboardList,
}, },
{ {
name: 'Manifest', name: 'Manifest',
@ -40,7 +41,7 @@ const documents = [
{ name: 'Freight Manifest', desc: 'Inclut les informations de fret' }, { name: 'Freight Manifest', desc: 'Inclut les informations de fret' },
], ],
importance: 'Obligatoire', importance: 'Obligatoire',
icon: '📑', icon: FileStack,
}, },
{ {
name: 'Packing List', name: 'Packing List',
@ -51,7 +52,7 @@ const documents = [
{ name: 'Détaillée', desc: 'Avec poids, dimensions, marquage' }, { name: 'Détaillée', desc: 'Avec poids, dimensions, marquage' },
], ],
importance: 'Important', importance: 'Important',
icon: '📦', icon: Package,
}, },
{ {
name: 'Commercial Invoice', name: 'Commercial Invoice',
@ -62,7 +63,7 @@ const documents = [
{ name: 'Définitive', desc: 'Document final de facturation' }, { name: 'Définitive', desc: 'Document final de facturation' },
], ],
importance: 'Critique', importance: 'Critique',
icon: '🧾', icon: Receipt,
}, },
{ {
name: 'Certificate of Origin', name: 'Certificate of Origin',
@ -74,7 +75,7 @@ const documents = [
{ name: 'Non préférentiel', desc: 'Attestation simple d\'origine' }, { name: 'Non préférentiel', desc: 'Attestation simple d\'origine' },
], ],
importance: 'Selon destination', importance: 'Selon destination',
icon: '🏭', icon: Factory,
}, },
]; ];
@ -120,7 +121,7 @@ export default function DocumentsTransportPage() {
{/* Title */} {/* Title */}
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-4xl">📋</span> <ClipboardList className="w-10 h-10 text-blue-600" />
<h1 className="text-3xl font-bold text-gray-900">Documents de Transport Maritime</h1> <h1 className="text-3xl font-bold text-gray-900">Documents de Transport Maritime</h1>
</div> </div>
<p className="mt-3 text-gray-600 max-w-3xl"> <p className="mt-3 text-gray-600 max-w-3xl">
@ -149,7 +150,7 @@ export default function DocumentsTransportPage() {
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="flex items-center justify-between"> <CardTitle className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-2xl">{doc.icon}</span> <doc.icon className="w-6 h-6 text-blue-600" />
<div> <div>
<span className="text-lg">{doc.name}</span> <span className="text-lg">{doc.name}</span>
<span className="text-gray-500 text-sm ml-2">({doc.french})</span> <span className="text-gray-500 text-sm ml-2">({doc.french})</span>

View File

@ -6,6 +6,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { ShieldCheck, ClipboardList, FileText, DollarSign, AlertTriangle } from 'lucide-react';
const regimesDouaniers = [ const regimesDouaniers = [
{ {
@ -97,7 +98,7 @@ export default function DouanesPage() {
{/* Title */} {/* Title */}
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-4xl">🛃</span> <ShieldCheck className="w-10 h-10 text-blue-600" />
<h1 className="text-3xl font-bold text-gray-900">Procédures Douanières</h1> <h1 className="text-3xl font-bold text-gray-900">Procédures Douanières</h1>
</div> </div>
<p className="mt-3 text-gray-600 max-w-3xl"> <p className="mt-3 text-gray-600 max-w-3xl">
@ -134,7 +135,7 @@ export default function DouanesPage() {
{/* Régimes douaniers */} {/* Régimes douaniers */}
<div className="mt-8"> <div className="mt-8">
<h2 className="text-xl font-bold text-gray-900 mb-4">📋 Régimes Douaniers</h2> <h2 className="text-xl font-bold text-gray-900 mb-4 flex items-center gap-2"><ClipboardList className="w-5 h-5" /> Régimes Douaniers</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{regimesDouaniers.map((regime) => ( {regimesDouaniers.map((regime) => (
<Card key={regime.code} className="bg-white"> <Card key={regime.code} className="bg-white">
@ -156,7 +157,7 @@ export default function DouanesPage() {
{/* Documents requis */} {/* Documents requis */}
<div className="mt-8"> <div className="mt-8">
<h2 className="text-xl font-bold text-gray-900 mb-4">📄 Documents Requis</h2> <h2 className="text-xl font-bold text-gray-900 mb-4 flex items-center gap-2"><FileText className="w-5 h-5" /> Documents Requis</h2>
<Card className="bg-white"> <Card className="bg-white">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="space-y-4"> <div className="space-y-4">
@ -179,7 +180,7 @@ export default function DouanesPage() {
{/* Droits et taxes */} {/* Droits et taxes */}
<Card className="mt-8 bg-gray-50"> <Card className="mt-8 bg-gray-50">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-gray-900 mb-3">💰 Droits et Taxes</h3> <h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2"><DollarSign className="w-5 h-5" /> Droits et Taxes</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-white p-4 rounded-lg border"> <div className="bg-white p-4 rounded-lg border">
<h4 className="font-medium text-gray-900">Droits de douane</h4> <h4 className="font-medium text-gray-900">Droits de douane</h4>
@ -203,7 +204,7 @@ export default function DouanesPage() {
{/* Tips */} {/* Tips */}
<Card className="mt-8 bg-amber-50 border-amber-200"> <Card className="mt-8 bg-amber-50 border-amber-200">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-amber-900 mb-3"> Points d&apos;Attention</h3> <h3 className="font-semibold text-amber-900 mb-3 flex items-center gap-2"><AlertTriangle className="w-5 h-5" /> Points d&apos;Attention</h3>
<ul className="list-disc list-inside space-y-2 text-amber-800"> <ul className="list-disc list-inside space-y-2 text-amber-800">
<li>Toujours vérifier le classement tarifaire avant l&apos;importation</li> <li>Toujours vérifier le classement tarifaire avant l&apos;importation</li>
<li>Conserver tous les documents 3 ans minimum (contrôle a posteriori)</li> <li>Conserver tous les documents 3 ans minimum (contrôle a posteriori)</li>

View File

@ -6,6 +6,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { ClipboardList, Hash, Package, FileText, Tag, Shuffle, Lightbulb, AlertTriangle } from 'lucide-react';
const classesIMDG = [ const classesIMDG = [
{ {
@ -110,7 +111,7 @@ export default function IMDGPage() {
{/* Title */} {/* Title */}
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-4xl"></span> <AlertTriangle className="w-10 h-10 text-orange-500" />
<h1 className="text-3xl font-bold text-gray-900">Marchandises Dangereuses (Code IMDG)</h1> <h1 className="text-3xl font-bold text-gray-900">Marchandises Dangereuses (Code IMDG)</h1>
</div> </div>
<p className="mt-3 text-gray-600 max-w-3xl"> <p className="mt-3 text-gray-600 max-w-3xl">
@ -123,7 +124,7 @@ export default function IMDGPage() {
{/* Key Info */} {/* Key Info */}
<Card className="bg-red-50 border-red-200"> <Card className="bg-red-50 border-red-200">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-red-900 mb-3"> Responsabilités de l&apos;Expéditeur</h3> <h3 className="font-semibold text-red-900 mb-3 flex items-center gap-2"><AlertTriangle className="w-5 h-5" /> Responsabilités de l&apos;Expéditeur</h3>
<ul className="list-disc list-inside space-y-2 text-red-800"> <ul className="list-disc list-inside space-y-2 text-red-800">
<li>Classer correctement la marchandise selon le Code IMDG</li> <li>Classer correctement la marchandise selon le Code IMDG</li>
<li>Utiliser des emballages homologués UN</li> <li>Utiliser des emballages homologués UN</li>
@ -136,7 +137,7 @@ export default function IMDGPage() {
{/* Classes */} {/* Classes */}
<div className="mt-8"> <div className="mt-8">
<h2 className="text-xl font-bold text-gray-900 mb-4">📋 Les 9 Classes de Danger</h2> <h2 className="text-xl font-bold text-gray-900 mb-4 flex items-center gap-2"><ClipboardList className="w-5 h-5" /> Les 9 Classes de Danger</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{classesIMDG.map((cls) => ( {classesIMDG.map((cls) => (
<Card key={cls.class} className="bg-white overflow-hidden"> <Card key={cls.class} className="bg-white overflow-hidden">
@ -171,7 +172,7 @@ export default function IMDGPage() {
{/* UN Number */} {/* UN Number */}
<Card className="mt-8 bg-gray-50"> <Card className="mt-8 bg-gray-50">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-gray-900 mb-3">🔢 Numéro ONU (UN Number)</h3> <h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2"><Hash className="w-5 h-5" /> Numéro ONU (UN Number)</h3>
<p className="text-gray-600 mb-4"> <p className="text-gray-600 mb-4">
Chaque matière dangereuse est identifiée par un numéro ONU à 4 chiffres. Chaque matière dangereuse est identifiée par un numéro ONU à 4 chiffres.
Ce numéro permet de retrouver toutes les informations dans le Code IMDG. Ce numéro permet de retrouver toutes les informations dans le Code IMDG.
@ -195,7 +196,7 @@ export default function IMDGPage() {
{/* Packaging Groups */} {/* Packaging Groups */}
<div className="mt-8"> <div className="mt-8">
<h2 className="text-xl font-bold text-gray-900 mb-4">📦 Groupes d&apos;Emballage</h2> <h2 className="text-xl font-bold text-gray-900 mb-4 flex items-center gap-2"><Package className="w-5 h-5" /> Groupes d&apos;Emballage</h2>
<Card className="bg-white"> <Card className="bg-white">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="space-y-4"> <div className="space-y-4">
@ -219,7 +220,7 @@ export default function IMDGPage() {
{/* Documents */} {/* Documents */}
<div className="mt-8"> <div className="mt-8">
<h2 className="text-xl font-bold text-gray-900 mb-4">📄 Documents Requis</h2> <h2 className="text-xl font-bold text-gray-900 mb-4 flex items-center gap-2"><FileText className="w-5 h-5" /> Documents Requis</h2>
<Card className="bg-white"> <Card className="bg-white">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="space-y-3"> <div className="space-y-3">
@ -240,7 +241,7 @@ export default function IMDGPage() {
{/* Labeling */} {/* Labeling */}
<Card className="mt-8 bg-gray-50"> <Card className="mt-8 bg-gray-50">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-gray-900 mb-3">🏷 Marquage et Étiquetage</h3> <h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2"><Tag className="w-5 h-5" /> Marquage et Étiquetage</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-white p-4 rounded-lg border"> <div className="bg-white p-4 rounded-lg border">
<h4 className="font-medium text-gray-900">Colis</h4> <h4 className="font-medium text-gray-900">Colis</h4>
@ -267,7 +268,7 @@ export default function IMDGPage() {
{/* Segregation */} {/* Segregation */}
<Card className="mt-8 bg-orange-50 border-orange-200"> <Card className="mt-8 bg-orange-50 border-orange-200">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-orange-900 mb-3">🔀 Ségrégation</h3> <h3 className="font-semibold text-orange-900 mb-3 flex items-center gap-2"><Shuffle className="w-5 h-5" /> Ségrégation</h3>
<p className="text-orange-800 mb-3"> <p className="text-orange-800 mb-3">
Certaines classes de marchandises dangereuses ne peuvent pas être chargées ensemble. Certaines classes de marchandises dangereuses ne peuvent pas être chargées ensemble.
Le Code IMDG définit des règles strictes de ségrégation : Le Code IMDG définit des règles strictes de ségrégation :
@ -296,7 +297,7 @@ export default function IMDGPage() {
{/* Tips */} {/* Tips */}
<Card className="mt-8 bg-amber-50 border-amber-200"> <Card className="mt-8 bg-amber-50 border-amber-200">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-amber-900 mb-3">💡 Conseils Pratiques</h3> <h3 className="font-semibold text-amber-900 mb-3 flex items-center gap-2"><Lightbulb className="w-5 h-5" /> Conseils Pratiques</h3>
<ul className="list-disc list-inside space-y-2 text-amber-800"> <ul className="list-disc list-inside space-y-2 text-amber-800">
<li>Vérifier l&apos;acceptation par la compagnie maritime (certaines refusent certaines classes)</li> <li>Vérifier l&apos;acceptation par la compagnie maritime (certaines refusent certaines classes)</li>
<li>Anticiper les surcharges DG (dangerous goods) qui peuvent être significatives</li> <li>Anticiper les surcharges DG (dangerous goods) qui peuvent être significatives</li>

View File

@ -6,6 +6,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { ScrollText, Ship, ArrowUpFromLine, ArrowDownToLine } from 'lucide-react';
const incoterms = [ const incoterms = [
{ {
@ -119,7 +120,7 @@ export default function IncotermsPage() {
{/* Title */} {/* Title */}
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-4xl">📜</span> <ScrollText className="w-10 h-10 text-blue-600" />
<h1 className="text-3xl font-bold text-gray-900">Incoterms 2020</h1> <h1 className="text-3xl font-bold text-gray-900">Incoterms 2020</h1>
</div> </div>
<p className="mt-3 text-gray-600 max-w-3xl"> <p className="mt-3 text-gray-600 max-w-3xl">
@ -146,8 +147,11 @@ export default function IncotermsPage() {
{categories.map((category) => ( {categories.map((category) => (
<div key={category} className="mt-8"> <div key={category} className="mt-8">
<h2 className="text-xl font-bold text-gray-900 mb-4"> <h2 className="text-xl font-bold text-gray-900 mb-4">
{category === 'Maritime' ? '🚢 Incoterms Maritimes' : <span className="flex items-center gap-2">
category === 'Départ' ? '📤 Incoterms de Départ' : '📥 Incoterms d\'Arrivée'} {category === 'Maritime' ? <><Ship className="w-5 h-5" /> Incoterms Maritimes</> :
category === 'Départ' ? <><ArrowUpFromLine className="w-5 h-5" /> Incoterms de Départ</> :
<><ArrowDownToLine className="w-5 h-5" /> Incoterms d&apos;Arrivée</>}
</span>
</h2> </h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{incoterms {incoterms

View File

@ -6,6 +6,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Package, Truck, Scale } from 'lucide-react';
export default function LclVsFclPage() { export default function LclVsFclPage() {
return ( return (
@ -26,7 +27,7 @@ export default function LclVsFclPage() {
{/* Title */} {/* Title */}
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-4xl"></span> <Scale className="w-10 h-10 text-blue-600" />
<h1 className="text-3xl font-bold text-gray-900">LCL vs FCL</h1> <h1 className="text-3xl font-bold text-gray-900">LCL vs FCL</h1>
</div> </div>
<p className="mt-3 text-gray-600 max-w-3xl"> <p className="mt-3 text-gray-600 max-w-3xl">
@ -40,7 +41,7 @@ export default function LclVsFclPage() {
<Card className="bg-blue-50 border-blue-200"> <Card className="bg-blue-50 border-blue-200">
<CardHeader> <CardHeader>
<CardTitle className="text-blue-900 flex items-center gap-2"> <CardTitle className="text-blue-900 flex items-center gap-2">
<span className="text-2xl">📦</span> <Package className="w-6 h-6" />
LCL - Groupage LCL - Groupage
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@ -64,7 +65,7 @@ export default function LclVsFclPage() {
<Card className="bg-green-50 border-green-200"> <Card className="bg-green-50 border-green-200">
<CardHeader> <CardHeader>
<CardTitle className="text-green-900 flex items-center gap-2"> <CardTitle className="text-green-900 flex items-center gap-2">
<span className="text-2xl">🚛</span> <Truck className="w-6 h-6" />
FCL - Complet FCL - Complet
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>

View File

@ -6,6 +6,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { CreditCard, RefreshCw, Users, ClipboardList, FileText, Calendar, DollarSign, ScrollText, Lightbulb, AlertTriangle } from 'lucide-react';
const typesLC = [ const typesLC = [
{ {
@ -93,7 +94,7 @@ export default function LettreCreditPage() {
{/* Title */} {/* Title */}
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-4xl">💳</span> <CreditCard className="w-10 h-10 text-blue-600" />
<h1 className="text-3xl font-bold text-gray-900">Lettre de Crédit (L/C)</h1> <h1 className="text-3xl font-bold text-gray-900">Lettre de Crédit (L/C)</h1>
</div> </div>
<p className="mt-3 text-gray-600 max-w-3xl"> <p className="mt-3 text-gray-600 max-w-3xl">
@ -106,7 +107,7 @@ export default function LettreCreditPage() {
{/* How it works */} {/* How it works */}
<Card className="bg-blue-50 border-blue-200"> <Card className="bg-blue-50 border-blue-200">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-blue-900 mb-3">🔄 Fonctionnement Simplifié</h3> <h3 className="font-semibold text-blue-900 mb-3 flex items-center gap-2"><RefreshCw className="w-5 h-5" /> Fonctionnement Simplifié</h3>
<div className="grid grid-cols-1 md:grid-cols-5 gap-2"> <div className="grid grid-cols-1 md:grid-cols-5 gap-2">
{[ {[
{ step: '1', title: 'Demande', desc: 'Acheteur demande l\'ouverture à sa banque' }, { step: '1', title: 'Demande', desc: 'Acheteur demande l\'ouverture à sa banque' },
@ -129,7 +130,7 @@ export default function LettreCreditPage() {
{/* Parties */} {/* Parties */}
<div className="mt-8"> <div className="mt-8">
<h2 className="text-xl font-bold text-gray-900 mb-4">👥 Parties Impliquées</h2> <h2 className="text-xl font-bold text-gray-900 mb-4 flex items-center gap-2"><Users className="w-5 h-5" /> Parties Impliquées</h2>
<Card className="bg-white"> <Card className="bg-white">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="space-y-4"> <div className="space-y-4">
@ -149,7 +150,7 @@ export default function LettreCreditPage() {
{/* Types */} {/* Types */}
<div className="mt-8"> <div className="mt-8">
<h2 className="text-xl font-bold text-gray-900 mb-4">📋 Types de Lettres de Crédit</h2> <h2 className="text-xl font-bold text-gray-900 mb-4 flex items-center gap-2"><ClipboardList className="w-5 h-5" /> Types de Lettres de Crédit</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{typesLC.map((lc) => ( {typesLC.map((lc) => (
<Card key={lc.type} className={`bg-white ${lc.recommended ? 'border-green-300 border-2' : ''}`}> <Card key={lc.type} className={`bg-white ${lc.recommended ? 'border-green-300 border-2' : ''}`}>
@ -172,7 +173,7 @@ export default function LettreCreditPage() {
{/* Documents */} {/* Documents */}
<div className="mt-8"> <div className="mt-8">
<h2 className="text-xl font-bold text-gray-900 mb-4">📄 Documents Typiquement Requis</h2> <h2 className="text-xl font-bold text-gray-900 mb-4 flex items-center gap-2"><FileText className="w-5 h-5" /> Documents Typiquement Requis</h2>
<Card className="bg-white"> <Card className="bg-white">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
@ -192,7 +193,7 @@ export default function LettreCreditPage() {
{/* Key Dates */} {/* Key Dates */}
<Card className="mt-8 bg-gray-50"> <Card className="mt-8 bg-gray-50">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-gray-900 mb-3">📅 Dates Clés à Surveiller</h3> <h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2"><Calendar className="w-5 h-5" /> Dates Clés à Surveiller</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-white p-4 rounded-lg border"> <div className="bg-white p-4 rounded-lg border">
<h4 className="font-medium text-gray-900">Date d&apos;expédition</h4> <h4 className="font-medium text-gray-900">Date d&apos;expédition</h4>
@ -219,7 +220,7 @@ export default function LettreCreditPage() {
{/* Costs */} {/* Costs */}
<Card className="mt-8 bg-gray-50"> <Card className="mt-8 bg-gray-50">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-gray-900 mb-3">💰 Coûts Typiques</h3> <h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2"><DollarSign className="w-5 h-5" /> Coûts Typiques</h3>
<div className="bg-white p-4 rounded-lg border"> <div className="bg-white p-4 rounded-lg border">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<div> <div>
@ -249,7 +250,7 @@ export default function LettreCreditPage() {
{/* Common Errors */} {/* Common Errors */}
<Card className="mt-8 bg-red-50 border-red-200"> <Card className="mt-8 bg-red-50 border-red-200">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-red-900 mb-3"> Erreurs Fréquentes (Réserves)</h3> <h3 className="font-semibold text-red-900 mb-3 flex items-center gap-2"><AlertTriangle className="w-5 h-5" /> Erreurs Fréquentes (Réserves)</h3>
<p className="text-red-800 mb-3"> <p className="text-red-800 mb-3">
Ces erreurs entraînent des &quot;réserves&quot; de la banque et peuvent bloquer le paiement : Ces erreurs entraînent des &quot;réserves&quot; de la banque et peuvent bloquer le paiement :
</p> </p>
@ -264,7 +265,7 @@ export default function LettreCreditPage() {
{/* UCP 600 */} {/* UCP 600 */}
<Card className="mt-8 bg-gray-50"> <Card className="mt-8 bg-gray-50">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-gray-900 mb-3">📜 Règles UCP 600</h3> <h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2"><ScrollText className="w-5 h-5" /> Règles UCP 600</h3>
<p className="text-gray-600"> <p className="text-gray-600">
Les Règles et Usances Uniformes (UCP 600) de la Chambre de Commerce Internationale (CCI) Les Règles et Usances Uniformes (UCP 600) de la Chambre de Commerce Internationale (CCI)
régissent les lettres de crédit documentaires depuis 2007. Points clés : régissent les lettres de crédit documentaires depuis 2007. Points clés :
@ -281,7 +282,7 @@ export default function LettreCreditPage() {
{/* Tips */} {/* Tips */}
<Card className="mt-8 bg-amber-50 border-amber-200"> <Card className="mt-8 bg-amber-50 border-amber-200">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-amber-900 mb-3">💡 Conseils Pratiques</h3> <h3 className="font-semibold text-amber-900 mb-3 flex items-center gap-2"><Lightbulb className="w-5 h-5" /> Conseils Pratiques</h3>
<ul className="list-disc list-inside space-y-2 text-amber-800"> <ul className="list-disc list-inside space-y-2 text-amber-800">
<li>Vérifier minutieusement les termes de la L/C dès réception</li> <li>Vérifier minutieusement les termes de la L/C dès réception</li>
<li>Demander des modifications AVANT expédition si nécessaire</li> <li>Demander des modifications AVANT expédition si nécessaire</li>

View File

@ -6,89 +6,112 @@
import Link from 'next/link'; import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card'; import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card';
import {
ScrollText,
ClipboardList,
Package,
Scale,
ShieldCheck,
Shield,
Calculator,
Globe,
Anchor,
AlertTriangle,
CreditCard,
Timer,
type LucideIcon,
} from 'lucide-react';
const wikiTopics = [ interface WikiTopic {
title: string;
description: string;
icon: LucideIcon;
href: string;
tags: string[];
}
const wikiTopics: WikiTopic[] = [
{ {
title: 'Incoterms 2020', title: 'Incoterms 2020',
description: 'Les règles internationales pour l\'interprétation des termes commerciaux', description: 'Les règles internationales pour l\'interprétation des termes commerciaux',
icon: '📜', icon: ScrollText,
href: '/dashboard/wiki/incoterms', href: '/dashboard/wiki/incoterms',
tags: ['FOB', 'CIF', 'EXW', 'DDP'], tags: ['FOB', 'CIF', 'EXW', 'DDP'],
}, },
{ {
title: 'Documents de Transport', title: 'Documents de Transport',
description: 'Les documents essentiels pour le transport maritime', description: 'Les documents essentiels pour le transport maritime',
icon: '📋', icon: ClipboardList,
href: '/dashboard/wiki/documents-transport', href: '/dashboard/wiki/documents-transport',
tags: ['B/L', 'Sea Waybill', 'Manifest'], tags: ['B/L', 'Sea Waybill', 'Manifest'],
}, },
{ {
title: 'Conteneurs et Types de Cargo', title: 'Conteneurs et Types de Cargo',
description: 'Guide complet des types de conteneurs maritimes', description: 'Guide complet des types de conteneurs maritimes',
icon: '📦', icon: Package,
href: '/dashboard/wiki/conteneurs', href: '/dashboard/wiki/conteneurs',
tags: ['20\'', '40\'', 'Reefer', 'Open Top'], tags: ['20\'', '40\'', 'Reefer', 'Open Top'],
}, },
{ {
title: 'LCL vs FCL', title: 'LCL vs FCL',
description: 'Différences entre groupage et conteneur complet', description: 'Différences entre groupage et conteneur complet',
icon: '⚖️', icon: Scale,
href: '/dashboard/wiki/lcl-vs-fcl', href: '/dashboard/wiki/lcl-vs-fcl',
tags: ['Groupage', 'Complet', 'Coûts'], tags: ['Groupage', 'Complet', 'Coûts'],
}, },
{ {
title: 'Procédures Douanières', title: 'Procédures Douanières',
description: 'Guide des formalités douanières import/export', description: 'Guide des formalités douanières import/export',
icon: '🛃', icon: ShieldCheck,
href: '/dashboard/wiki/douanes', href: '/dashboard/wiki/douanes',
tags: ['Déclaration', 'Tarifs', 'Régimes'], tags: ['Déclaration', 'Tarifs', 'Régimes'],
}, },
{ {
title: 'Assurance Maritime', title: 'Assurance Maritime',
description: 'Protection des marchandises en transit', description: 'Protection des marchandises en transit',
icon: '🛡️', icon: Shield,
href: '/dashboard/wiki/assurance', href: '/dashboard/wiki/assurance',
tags: ['ICC A', 'ICC B', 'ICC C'], tags: ['ICC A', 'ICC B', 'ICC C'],
}, },
{ {
title: 'Calcul du Fret Maritime', title: 'Calcul du Fret Maritime',
description: 'Comment sont calculés les coûts de transport', description: 'Comment sont calculés les coûts de transport',
icon: '🧮', icon: Calculator,
href: '/dashboard/wiki/calcul-fret', href: '/dashboard/wiki/calcul-fret',
tags: ['CBM', 'THC', 'BAF', 'CAF'], tags: ['CBM', 'THC', 'BAF', 'CAF'],
}, },
{ {
title: 'Ports et Routes Maritimes', title: 'Ports et Routes Maritimes',
description: 'Les principales routes commerciales mondiales', description: 'Les principales routes commerciales mondiales',
icon: '🌍', icon: Globe,
href: '/dashboard/wiki/ports-routes', href: '/dashboard/wiki/ports-routes',
tags: ['Hub', 'Détroits', 'Canaux'], tags: ['Hub', 'Détroits', 'Canaux'],
}, },
{ {
title: 'VGM (Verified Gross Mass)', title: 'VGM (Verified Gross Mass)',
description: 'Obligation de pesée des conteneurs (SOLAS)', description: 'Obligation de pesée des conteneurs (SOLAS)',
icon: '⚓', icon: Anchor,
href: '/dashboard/wiki/vgm', href: '/dashboard/wiki/vgm',
tags: ['SOLAS', 'Pesée', 'Certification'], tags: ['SOLAS', 'Pesée', 'Certification'],
}, },
{ {
title: 'Marchandises Dangereuses (IMDG)', title: 'Marchandises Dangereuses (IMDG)',
description: 'Transport de matières dangereuses par mer', description: 'Transport de matières dangereuses par mer',
icon: '⚠️', icon: AlertTriangle,
href: '/dashboard/wiki/imdg', href: '/dashboard/wiki/imdg',
tags: ['Classes', 'Étiquetage', 'Sécurité'], tags: ['Classes', 'Étiquetage', 'Sécurité'],
}, },
{ {
title: 'Lettre de Crédit (L/C)', title: 'Lettre de Crédit (L/C)',
description: 'Instrument de paiement international sécurisé', description: 'Instrument de paiement international sécurisé',
icon: '💳', icon: CreditCard,
href: '/dashboard/wiki/lettre-credit', href: '/dashboard/wiki/lettre-credit',
tags: ['Banque', 'Paiement', 'Sécurité'], tags: ['Banque', 'Paiement', 'Sécurité'],
}, },
{ {
title: 'Transit Time et Délais', title: 'Transit Time et Délais',
description: 'Comprendre les délais en transport maritime', description: 'Comprendre les délais en transport maritime',
icon: '⏱️', icon: Timer,
href: '/dashboard/wiki/transit-time', href: '/dashboard/wiki/transit-time',
tags: ['Cut-off', 'Free time', 'Demurrage'], tags: ['Cut-off', 'Free time', 'Demurrage'],
}, },
@ -107,12 +130,16 @@ export default function WikiPage() {
{/* Cards Grid */} {/* Cards Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{wikiTopics.map((topic) => ( {wikiTopics.map((topic) => {
const IconComponent = topic.icon;
return (
<Link key={topic.href} href={topic.href} className="block group"> <Link key={topic.href} href={topic.href} className="block group">
<Card className="h-full transition-all duration-200 hover:shadow-lg hover:border-blue-300 bg-white"> <Card className="h-full transition-all duration-200 hover:shadow-lg hover:border-blue-300 bg-white">
<CardHeader> <CardHeader>
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<span className="text-4xl">{topic.icon}</span> <div className="h-10 w-10 rounded-lg bg-blue-50 flex items-center justify-center">
<IconComponent className="h-5 w-5 text-blue-600" />
</div>
</div> </div>
<CardTitle className="text-xl mt-3 group-hover:text-blue-600 transition-colors"> <CardTitle className="text-xl mt-3 group-hover:text-blue-600 transition-colors">
{topic.title} {topic.title}
@ -135,7 +162,8 @@ export default function WikiPage() {
</CardContent> </CardContent>
</Card> </Card>
</Link> </Link>
))} );
})}
</div> </div>
{/* Footer info */} {/* Footer info */}

View File

@ -6,6 +6,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Globe, BarChart3, Ship, Trophy, RefreshCw, Lightbulb, Anchor } from 'lucide-react';
const majorRoutes = [ const majorRoutes = [
{ {
@ -113,7 +114,7 @@ export default function PortsRoutesPage() {
{/* Title */} {/* Title */}
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-4xl">🌍</span> <Globe className="w-10 h-10 text-blue-600" />
<h1 className="text-3xl font-bold text-gray-900">Ports et Routes Maritimes</h1> <h1 className="text-3xl font-bold text-gray-900">Ports et Routes Maritimes</h1>
</div> </div>
<p className="mt-3 text-gray-600 max-w-3xl"> <p className="mt-3 text-gray-600 max-w-3xl">
@ -125,7 +126,7 @@ export default function PortsRoutesPage() {
{/* Key Stats */} {/* Key Stats */}
<Card className="bg-blue-50 border-blue-200"> <Card className="bg-blue-50 border-blue-200">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-blue-900 mb-3">📊 Chiffres Clés du Maritime Mondial</h3> <h3 className="font-semibold text-blue-900 mb-3 flex items-center gap-2"><BarChart3 className="w-5 h-5" /> Chiffres Clés du Maritime Mondial</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center"> <div className="text-center">
<p className="text-3xl font-bold text-blue-700">80%</p> <p className="text-3xl font-bold text-blue-700">80%</p>
@ -149,7 +150,7 @@ export default function PortsRoutesPage() {
{/* Major Routes */} {/* Major Routes */}
<div className="mt-8"> <div className="mt-8">
<h2 className="text-xl font-bold text-gray-900 mb-4">🛳 Routes Commerciales Majeures</h2> <h2 className="text-xl font-bold text-gray-900 mb-4 flex items-center gap-2"><Ship className="w-5 h-5" /> Routes Commerciales Majeures</h2>
<div className="space-y-4"> <div className="space-y-4">
{majorRoutes.map((route) => ( {majorRoutes.map((route) => (
<Card key={route.name} className="bg-white"> <Card key={route.name} className="bg-white">
@ -184,7 +185,7 @@ export default function PortsRoutesPage() {
{/* Strategic Passages */} {/* Strategic Passages */}
<div className="mt-8"> <div className="mt-8">
<h2 className="text-xl font-bold text-gray-900 mb-4"> Passages Stratégiques</h2> <h2 className="text-xl font-bold text-gray-900 mb-4 flex items-center gap-2"><Anchor className="w-5 h-5" /> Passages Stratégiques</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{strategicPassages.map((passage) => ( {strategicPassages.map((passage) => (
<Card key={passage.name} className="bg-white"> <Card key={passage.name} className="bg-white">
@ -222,7 +223,7 @@ export default function PortsRoutesPage() {
{/* Top Ports */} {/* Top Ports */}
<div className="mt-8"> <div className="mt-8">
<h2 className="text-xl font-bold text-gray-900 mb-4">🏆 Top 10 Ports Mondiaux (TEU)</h2> <h2 className="text-xl font-bold text-gray-900 mb-4 flex items-center gap-2"><Trophy className="w-5 h-5" /> Top 10 Ports Mondiaux (TEU)</h2>
<Card className="bg-white"> <Card className="bg-white">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
@ -261,7 +262,7 @@ export default function PortsRoutesPage() {
{/* Hub Ports Info */} {/* Hub Ports Info */}
<Card className="mt-8 bg-gray-50"> <Card className="mt-8 bg-gray-50">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-gray-900 mb-3">🔄 Ports Hub vs Ports Régionaux</h3> <h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2"><RefreshCw className="w-5 h-5" /> Ports Hub vs Ports Régionaux</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-white p-4 rounded-lg border"> <div className="bg-white p-4 rounded-lg border">
<h4 className="font-medium text-gray-900">Ports Hub (Transbordement)</h4> <h4 className="font-medium text-gray-900">Ports Hub (Transbordement)</h4>
@ -286,7 +287,7 @@ export default function PortsRoutesPage() {
{/* Tips */} {/* Tips */}
<Card className="mt-8 bg-amber-50 border-amber-200"> <Card className="mt-8 bg-amber-50 border-amber-200">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-amber-900 mb-3">💡 Conseils Pratiques</h3> <h3 className="font-semibold text-amber-900 mb-3 flex items-center gap-2"><Lightbulb className="w-5 h-5" /> Conseils Pratiques</h3>
<ul className="list-disc list-inside space-y-2 text-amber-800"> <ul className="list-disc list-inside space-y-2 text-amber-800">
<li>Privilégiez les routes directes pour réduire les délais et risques</li> <li>Privilégiez les routes directes pour réduire les délais et risques</li>
<li>Anticipez les congestions portuaires (Los Angeles, Rotterdam en haute saison)</li> <li>Anticipez les congestions portuaires (Los Angeles, Rotterdam en haute saison)</li>

View File

@ -6,6 +6,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { BookOpen, Calendar, Ship, DollarSign, RefreshCw, Lightbulb, Clock, AlertTriangle } from 'lucide-react';
const etapesTimeline = [ const etapesTimeline = [
{ {
@ -132,7 +133,7 @@ export default function TransitTimePage() {
{/* Title */} {/* Title */}
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-4xl"></span> <Clock className="w-10 h-10 text-blue-600" />
<h1 className="text-3xl font-bold text-gray-900">Transit Time et Délais</h1> <h1 className="text-3xl font-bold text-gray-900">Transit Time et Délais</h1>
</div> </div>
<p className="mt-3 text-gray-600 max-w-3xl"> <p className="mt-3 text-gray-600 max-w-3xl">
@ -145,7 +146,7 @@ export default function TransitTimePage() {
{/* Key Terms */} {/* Key Terms */}
<Card className="bg-blue-50 border-blue-200"> <Card className="bg-blue-50 border-blue-200">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-blue-900 mb-3">📖 Termes Clés</h3> <h3 className="font-semibold text-blue-900 mb-3 flex items-center gap-2"><BookOpen className="w-5 h-5" /> Termes Clés</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div> <div>
<h4 className="font-medium text-blue-800">ETD</h4> <h4 className="font-medium text-blue-800">ETD</h4>
@ -169,7 +170,7 @@ export default function TransitTimePage() {
{/* Timeline */} {/* Timeline */}
<div className="mt-8"> <div className="mt-8">
<h2 className="text-xl font-bold text-gray-900 mb-4">📅 Timeline d&apos;une Expédition FCL</h2> <h2 className="text-xl font-bold text-gray-900 mb-4 flex items-center gap-2"><Calendar className="w-5 h-5" /> Timeline d&apos;une Expédition FCL</h2>
<div className="space-y-3"> <div className="space-y-3">
{etapesTimeline.map((item, index) => ( {etapesTimeline.map((item, index) => (
<Card key={item.etape} className={`bg-white ${index === 5 || index === 7 ? 'border-blue-300 border-2' : ''}`}> <Card key={item.etape} className={`bg-white ${index === 5 || index === 7 ? 'border-blue-300 border-2' : ''}`}>
@ -195,7 +196,7 @@ export default function TransitTimePage() {
{/* Transit Times */} {/* Transit Times */}
<div className="mt-8"> <div className="mt-8">
<h2 className="text-xl font-bold text-gray-900 mb-4">🚢 Transit Times Indicatifs</h2> <h2 className="text-xl font-bold text-gray-900 mb-4 flex items-center gap-2"><Ship className="w-5 h-5" /> Transit Times Indicatifs</h2>
<Card className="bg-white"> <Card className="bg-white">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
@ -228,7 +229,7 @@ export default function TransitTimePage() {
{/* Free Time */} {/* Free Time */}
<Card className="mt-8 bg-gray-50"> <Card className="mt-8 bg-gray-50">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-gray-900 mb-3"> Free Time (Jours Gratuits)</h3> <h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2"><Clock className="w-5 h-5" /> Free Time (Jours Gratuits)</h3>
<p className="text-gray-600 mb-4"> <p className="text-gray-600 mb-4">
Période pendant laquelle le conteneur peut rester au terminal ou chez l&apos;importateur Période pendant laquelle le conteneur peut rester au terminal ou chez l&apos;importateur
sans frais supplémentaires. sans frais supplémentaires.
@ -257,7 +258,7 @@ export default function TransitTimePage() {
{/* Late Fees */} {/* Late Fees */}
<div className="mt-8"> <div className="mt-8">
<h2 className="text-xl font-bold text-gray-900 mb-4">💸 Frais de Retard</h2> <h2 className="text-xl font-bold text-gray-900 mb-4 flex items-center gap-2"><DollarSign className="w-5 h-5" /> Frais de Retard</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{fraisRetard.map((frais) => ( {fraisRetard.map((frais) => (
<Card key={frais.nom} className="bg-white border-red-200"> <Card key={frais.nom} className="bg-white border-red-200">
@ -283,7 +284,7 @@ export default function TransitTimePage() {
{/* Factors affecting transit */} {/* Factors affecting transit */}
<Card className="mt-8 bg-orange-50 border-orange-200"> <Card className="mt-8 bg-orange-50 border-orange-200">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-orange-900 mb-3"> Facteurs Impactant les Délais</h3> <h3 className="font-semibold text-orange-900 mb-3 flex items-center gap-2"><AlertTriangle className="w-5 h-5" /> Facteurs Impactant les Délais</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<h4 className="font-medium text-orange-800">Retards potentiels</h4> <h4 className="font-medium text-orange-800">Retards potentiels</h4>
@ -312,7 +313,7 @@ export default function TransitTimePage() {
{/* Roll-over */} {/* Roll-over */}
<Card className="mt-8 bg-gray-50"> <Card className="mt-8 bg-gray-50">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-gray-900 mb-3">🔄 Roll-over (Report)</h3> <h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2"><RefreshCw className="w-5 h-5" /> Roll-over (Report)</h3>
<p className="text-gray-600 mb-3"> <p className="text-gray-600 mb-3">
Situation un conteneur n&apos;est pas chargé sur le navire prévu et est reporté Situation un conteneur n&apos;est pas chargé sur le navire prévu et est reporté
sur le prochain départ. sur le prochain départ.
@ -336,7 +337,7 @@ export default function TransitTimePage() {
{/* Tips */} {/* Tips */}
<Card className="mt-8 bg-amber-50 border-amber-200"> <Card className="mt-8 bg-amber-50 border-amber-200">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-amber-900 mb-3">💡 Conseils pour Optimiser les Délais</h3> <h3 className="font-semibold text-amber-900 mb-3 flex items-center gap-2"><Lightbulb className="w-5 h-5" /> Conseils pour Optimiser les Délais</h3>
<ul className="list-disc list-inside space-y-2 text-amber-800"> <ul className="list-disc list-inside space-y-2 text-amber-800">
<li>Réserver tôt, surtout en haute saison (2-3 semaines d&apos;avance)</li> <li>Réserver tôt, surtout en haute saison (2-3 semaines d&apos;avance)</li>
<li>Respecter les cut-off avec une marge de sécurité (24h minimum)</li> <li>Respecter les cut-off avec une marge de sécurité (24h minimum)</li>

View File

@ -6,6 +6,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Shield, Construction, Truck, ClipboardList, Microscope, User, Ruler, Lightbulb, Anchor, Scale, AlertTriangle } from 'lucide-react';
const methodesPesee = [ const methodesPesee = [
{ {
@ -69,7 +70,7 @@ export default function VGMPage() {
{/* Title */} {/* Title */}
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-4xl"></span> <Anchor className="w-10 h-10 text-blue-600" />
<h1 className="text-3xl font-bold text-gray-900">VGM (Verified Gross Mass)</h1> <h1 className="text-3xl font-bold text-gray-900">VGM (Verified Gross Mass)</h1>
</div> </div>
<p className="mt-3 text-gray-600 max-w-3xl"> <p className="mt-3 text-gray-600 max-w-3xl">
@ -85,19 +86,19 @@ export default function VGMPage() {
<h3 className="font-semibold text-blue-900 mb-3">Pourquoi le VGM ?</h3> <h3 className="font-semibold text-blue-900 mb-3">Pourquoi le VGM ?</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-blue-800"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-blue-800">
<div> <div>
<h4 className="font-medium">🛡 Sécurité</h4> <h4 className="font-medium flex items-center gap-1"><Shield className="w-4 h-4" /> Sécurité</h4>
<p className="text-sm">Les conteneurs mal déclarés causent des accidents graves (chute de conteneurs, navires instables).</p> <p className="text-sm">Les conteneurs mal déclarés causent des accidents graves (chute de conteneurs, navires instables).</p>
</div> </div>
<div> <div>
<h4 className="font-medium"> Stabilité du navire</h4> <h4 className="font-medium flex items-center gap-1"><Scale className="w-4 h-4" /> Stabilité du navire</h4>
<p className="text-sm">Le capitaine doit connaître le poids exact pour calculer le plan de chargement.</p> <p className="text-sm">Le capitaine doit connaître le poids exact pour calculer le plan de chargement.</p>
</div> </div>
<div> <div>
<h4 className="font-medium">🏗 Équipements portuaires</h4> <h4 className="font-medium flex items-center gap-1"><Construction className="w-4 h-4" /> Équipements portuaires</h4>
<p className="text-sm">Les grues et portiques sont dimensionnés pour des charges maximales.</p> <p className="text-sm">Les grues et portiques sont dimensionnés pour des charges maximales.</p>
</div> </div>
<div> <div>
<h4 className="font-medium">🚛 Transport terrestre</h4> <h4 className="font-medium flex items-center gap-1"><Truck className="w-4 h-4" /> Transport terrestre</h4>
<p className="text-sm">Évite les surcharges sur les camions et wagons de pré/post-acheminement.</p> <p className="text-sm">Évite les surcharges sur les camions et wagons de pré/post-acheminement.</p>
</div> </div>
</div> </div>
@ -106,7 +107,7 @@ export default function VGMPage() {
{/* VGM Components */} {/* VGM Components */}
<div className="mt-8"> <div className="mt-8">
<h2 className="text-xl font-bold text-gray-900 mb-4">📋 Composants du VGM</h2> <h2 className="text-xl font-bold text-gray-900 mb-4 flex items-center gap-2"><ClipboardList className="w-5 h-5" /> Composants du VGM</h2>
<Card className="bg-white"> <Card className="bg-white">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="bg-gray-50 p-4 rounded-lg border mb-4"> <div className="bg-gray-50 p-4 rounded-lg border mb-4">
@ -131,7 +132,7 @@ export default function VGMPage() {
{/* Methods */} {/* Methods */}
<div className="mt-8"> <div className="mt-8">
<h2 className="text-xl font-bold text-gray-900 mb-4">🔬 Méthodes de Détermination</h2> <h2 className="text-xl font-bold text-gray-900 mb-4 flex items-center gap-2"><Microscope className="w-5 h-5" /> Méthodes de Détermination</h2>
<div className="space-y-4"> <div className="space-y-4">
{methodesPesee.map((method) => ( {methodesPesee.map((method) => (
<Card key={method.method} className="bg-white"> <Card key={method.method} className="bg-white">
@ -186,7 +187,7 @@ export default function VGMPage() {
{/* Responsibility */} {/* Responsibility */}
<Card className="mt-8 bg-gray-50"> <Card className="mt-8 bg-gray-50">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-gray-900 mb-3">👤 Responsabilités</h3> <h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2"><User className="w-5 h-5" /> Responsabilités</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-white p-4 rounded-lg border"> <div className="bg-white p-4 rounded-lg border">
<h4 className="font-medium text-gray-900">Expéditeur (Shipper)</h4> <h4 className="font-medium text-gray-900">Expéditeur (Shipper)</h4>
@ -213,7 +214,7 @@ export default function VGMPage() {
{/* Tolerances */} {/* Tolerances */}
<Card className="mt-8 bg-gray-50"> <Card className="mt-8 bg-gray-50">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-gray-900 mb-3">📏 Tolérances</h3> <h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2"><Ruler className="w-5 h-5" /> Tolérances</h3>
<div className="bg-white p-4 rounded-lg border"> <div className="bg-white p-4 rounded-lg border">
<p className="text-gray-600 mb-3"> <p className="text-gray-600 mb-3">
Les tolérances varient selon les compagnies maritimes et les ports, mais généralement : Les tolérances varient selon les compagnies maritimes et les ports, mais généralement :
@ -234,7 +235,7 @@ export default function VGMPage() {
{/* Sanctions */} {/* Sanctions */}
<div className="mt-8"> <div className="mt-8">
<h2 className="text-xl font-bold text-gray-900 mb-4"> Sanctions par Région</h2> <h2 className="text-xl font-bold text-gray-900 mb-4 flex items-center gap-2"><AlertTriangle className="w-5 h-5" /> Sanctions par Région</h2>
<Card className="bg-white"> <Card className="bg-white">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="space-y-3"> <div className="space-y-3">
@ -252,7 +253,7 @@ export default function VGMPage() {
{/* Tips */} {/* Tips */}
<Card className="mt-8 bg-amber-50 border-amber-200"> <Card className="mt-8 bg-amber-50 border-amber-200">
<CardContent className="pt-6"> <CardContent className="pt-6">
<h3 className="font-semibold text-amber-900 mb-3">💡 Bonnes Pratiques</h3> <h3 className="font-semibold text-amber-900 mb-3 flex items-center gap-2"><Lightbulb className="w-5 h-5" /> Bonnes Pratiques</h3>
<ul className="list-disc list-inside space-y-2 text-amber-800"> <ul className="list-disc list-inside space-y-2 text-amber-800">
<li>Transmettre le VGM au moins 24-48h avant le cut-off</li> <li>Transmettre le VGM au moins 24-48h avant le cut-off</li>
<li>Utiliser des balances étalonnées et certifiées</li> <li>Utiliser des balances étalonnées et certifiées</li>

View File

@ -52,7 +52,7 @@ export default function LandingPage() {
const features = [ const features = [
{ {
icon: BarChart3, icon: BarChart3,
title: 'Dashboard Analytics', title: 'Tableau de bord',
description: description:
'Suivez tous vos KPIs en temps réel : bookings, volumes, revenus et alertes personnalisées.', 'Suivez tous vos KPIs en temps réel : bookings, volumes, revenus et alertes personnalisées.',
color: 'from-blue-500 to-cyan-500', color: 'from-blue-500 to-cyan-500',
@ -60,7 +60,7 @@ export default function LandingPage() {
}, },
{ {
icon: Package, icon: Package,
title: 'Gestion des Bookings', title: 'Gestion des Réservations',
description: 'Créez, gérez et suivez vos réservations maritimes LCL/FCL avec un historique complet.', description: 'Créez, gérez et suivez vos réservations maritimes LCL/FCL avec un historique complet.',
color: 'from-purple-500 to-pink-500', color: 'from-purple-500 to-pink-500',
link: '/dashboard/bookings', link: '/dashboard/bookings',
@ -74,7 +74,7 @@ export default function LandingPage() {
}, },
{ {
icon: Search, icon: Search,
title: 'Track & Trace', title: 'Suivi des expéditions',
description: description:
'Suivez vos conteneurs en temps réel auprès de 10+ transporteurs majeurs (Maersk, MSC, CMA CGM...).', 'Suivez vos conteneurs en temps réel auprès de 10+ transporteurs majeurs (Maersk, MSC, CMA CGM...).',
color: 'from-green-500 to-emerald-500', color: 'from-green-500 to-emerald-500',
@ -101,13 +101,13 @@ export default function LandingPage() {
const tools = [ const tools = [
{ {
icon: LayoutDashboard, icon: LayoutDashboard,
title: 'Dashboard', title: 'Tableau de bord',
description: 'Vue d\'ensemble de votre activité maritime', description: 'Vue d\'ensemble de votre activité maritime',
link: '/dashboard', link: '/dashboard',
}, },
{ {
icon: Package, icon: Package,
title: 'Mes Bookings', title: 'Mes Réservations',
description: 'Gérez toutes vos réservations en un seul endroit', description: 'Gérez toutes vos réservations en un seul endroit',
link: '/dashboard/bookings', link: '/dashboard/bookings',
}, },
@ -119,7 +119,7 @@ export default function LandingPage() {
}, },
{ {
icon: Search, icon: Search,
title: 'Track & Trace', title: 'Suivi des expéditions',
description: 'Suivez vos conteneurs en temps réel', description: 'Suivez vos conteneurs en temps réel',
link: '/dashboard/track-trace', link: '/dashboard/track-trace',
}, },
@ -158,7 +158,7 @@ export default function LandingPage() {
{ text: 'Support par email', included: true }, { text: 'Support par email', included: true },
{ text: 'Gestion des documents', included: false }, { text: 'Gestion des documents', included: false },
{ text: 'Notifications temps réel', included: false }, { text: 'Notifications temps réel', included: false },
{ text: 'API access', included: false }, { text: 'Accès API', included: false },
], ],
cta: 'Commencer gratuitement', cta: 'Commencer gratuitement',
highlighted: false, highlighted: false,
@ -176,7 +176,7 @@ export default function LandingPage() {
{ text: 'Support prioritaire', included: true }, { text: 'Support prioritaire', included: true },
{ text: 'Gestion des documents', included: true }, { text: 'Gestion des documents', included: true },
{ text: 'Notifications temps réel', included: true }, { text: 'Notifications temps réel', included: true },
{ text: 'API access', included: false }, { text: 'Accès API', included: false },
], ],
cta: 'Essai gratuit 14 jours', cta: 'Essai gratuit 14 jours',
highlighted: true, highlighted: true,
@ -187,10 +187,10 @@ export default function LandingPage() {
period: '', period: '',
description: 'Pour les grandes entreprises', description: 'Pour les grandes entreprises',
features: [ features: [
{ text: 'Tout Professional +', included: true }, { text: 'Tout Professionnel +', included: true },
{ text: 'API access complet', included: true }, { text: 'Accès API complet', included: true },
{ text: 'Intégrations personnalisées', included: true }, { text: 'Intégrations personnalisées', included: true },
{ text: 'Account manager dédié', included: true }, { text: 'Responsable de compte dédié', included: true },
{ text: 'SLA garanti 99.9%', included: true }, { text: 'SLA garanti 99.9%', included: true },
{ text: 'Formation sur site', included: true }, { text: 'Formation sur site', included: true },
{ text: 'Multi-organisations', included: true }, { text: 'Multi-organisations', included: true },
@ -323,7 +323,7 @@ export default function LandingPage() {
href="/dashboard" href="/dashboard"
className="group px-8 py-4 bg-brand-turquoise text-white rounded-lg hover:bg-brand-turquoise/90 transition-all hover:shadow-2xl hover:scale-105 font-semibold text-lg w-full sm:w-auto flex items-center justify-center space-x-2" className="group px-8 py-4 bg-brand-turquoise text-white rounded-lg hover:bg-brand-turquoise/90 transition-all hover:shadow-2xl hover:scale-105 font-semibold text-lg w-full sm:w-auto flex items-center justify-center space-x-2"
> >
<span>Accéder au Dashboard</span> <span>Accéder au tableau de bord</span>
<LayoutDashboard className="w-5 h-5 group-hover:translate-x-1 transition-transform" /> <LayoutDashboard className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
</Link> </Link>
) : ( ) : (
@ -709,13 +709,13 @@ export default function LandingPage() {
{ {
step: '03', step: '03',
title: 'Réservez', title: 'Réservez',
description: 'Confirmez votre booking en un clic', description: 'Confirmez votre réservation en un clic',
icon: CheckCircle2, icon: CheckCircle2,
}, },
{ {
step: '04', step: '04',
title: 'Suivez', title: 'Suivez',
description: 'Trackez votre envoi en temps réel', description: 'Suivez votre envoi en temps réel',
icon: Container, icon: Container,
}, },
].map((step, index) => { ].map((step, index) => {
@ -833,7 +833,7 @@ export default function LandingPage() {
href="/dashboard" href="/dashboard"
className="group px-8 py-4 bg-brand-turquoise text-white rounded-lg hover:bg-brand-turquoise/90 transition-all hover:shadow-2xl hover:scale-105 font-semibold text-lg w-full sm:w-auto flex items-center justify-center space-x-2" className="group px-8 py-4 bg-brand-turquoise text-white rounded-lg hover:bg-brand-turquoise/90 transition-all hover:shadow-2xl hover:scale-105 font-semibold text-lg w-full sm:w-auto flex items-center justify-center space-x-2"
> >
<span>Accéder au Dashboard</span> <span>Accéder au tableau de bord</span>
<LayoutDashboard className="w-5 h-5 group-hover:translate-x-1 transition-transform" /> <LayoutDashboard className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
</Link> </Link>
) : ( ) : (

View File

@ -0,0 +1,244 @@
/**
* Export Button Component
*
* Reusable component for exporting data to CSV or Excel
*/
'use client';
import { useState, useRef, useEffect } from 'react';
import { Download, FileSpreadsheet, FileText, ChevronDown } from 'lucide-react';
interface ExportButtonProps<T> {
data: T[];
filename: string;
columns: {
key: keyof T | string;
label: string;
format?: (value: any, row: T) => string;
}[];
disabled?: boolean;
}
export default function ExportButton<T extends Record<string, any>>({
data,
filename,
columns,
disabled = false,
}: ExportButtonProps<T>) {
const [isOpen, setIsOpen] = useState(false);
const [isExporting, setIsExporting] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const formatValue = (value: any): string => {
if (value === null || value === undefined) return '';
if (typeof value === 'boolean') return value ? 'Oui' : 'Non';
if (value instanceof Date) return value.toLocaleDateString('fr-FR');
if (typeof value === 'object') return JSON.stringify(value);
return String(value);
};
const getNestedValue = (obj: T, path: string): any => {
return path.split('.').reduce((acc, part) => acc?.[part], obj as any);
};
const generateCSV = (): string => {
// Headers
const headers = columns.map(col => `"${col.label.replace(/"/g, '""')}"`).join(';');
// Rows
const rows = data.map(row => {
return columns
.map(col => {
const value = getNestedValue(row, col.key as string);
const formattedValue = col.format ? col.format(value, row) : formatValue(value);
// Escape quotes and wrap in quotes
return `"${formattedValue.replace(/"/g, '""')}"`;
})
.join(';');
});
return [headers, ...rows].join('\n');
};
const downloadFile = (content: string, mimeType: string, extension: string) => {
const BOM = '\uFEFF'; // UTF-8 BOM for Excel compatibility
const blob = new Blob([BOM + content], { type: `${mimeType};charset=utf-8` });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${filename}_${new Date().toISOString().split('T')[0]}.${extension}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
const handleExportCSV = async () => {
setIsExporting(true);
try {
const csv = generateCSV();
downloadFile(csv, 'text/csv', 'csv');
} catch (error) {
console.error('Export CSV error:', error);
alert('Erreur lors de l\'export CSV');
} finally {
setIsExporting(false);
setIsOpen(false);
}
};
const handleExportExcel = async () => {
setIsExporting(true);
try {
// Generate Excel-compatible XML (SpreadsheetML)
const xmlHeader = `<?xml version="1.0" encoding="UTF-8"?>
<?mso-application progid="Excel.Sheet"?>
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">
<Styles>
<Style ss:ID="Header">
<Font ss:Bold="1" ss:Color="#FFFFFF"/>
<Interior ss:Color="#3B82F6" ss:Pattern="Solid"/>
<Alignment ss:Horizontal="Center" ss:Vertical="Center"/>
</Style>
<Style ss:ID="Default">
<Alignment ss:Vertical="Center"/>
</Style>
</Styles>
<Worksheet ss:Name="Export">
<Table>`;
// Column widths
const columnWidths = columns.map(() => '<Column ss:AutoFitWidth="1" ss:Width="120"/>').join('');
// Header row
const headerRow = `<Row ss:StyleID="Header">
${columns.map(col => `<Cell><Data ss:Type="String">${escapeXml(col.label)}</Data></Cell>`).join('')}
</Row>`;
// Data rows
const dataRows = data
.map(row => {
const cells = columns
.map(col => {
const value = getNestedValue(row, col.key as string);
const formattedValue = col.format ? col.format(value, row) : formatValue(value);
const type = typeof value === 'number' ? 'Number' : 'String';
return `<Cell ss:StyleID="Default"><Data ss:Type="${type}">${escapeXml(formattedValue)}</Data></Cell>`;
})
.join('');
return `<Row>${cells}</Row>`;
})
.join('');
const xmlFooter = `</Table>
</Worksheet>
</Workbook>`;
const xmlContent = xmlHeader + columnWidths + headerRow + dataRows + xmlFooter;
downloadFile(xmlContent, 'application/vnd.ms-excel', 'xls');
} catch (error) {
console.error('Export Excel error:', error);
alert('Erreur lors de l\'export Excel');
} finally {
setIsExporting(false);
setIsOpen(false);
}
};
const escapeXml = (str: string): string => {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
};
return (
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
disabled={disabled || data.length === 0 || isExporting}
className="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 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isExporting ? (
<>
<svg
className="animate-spin -ml-1 mr-2 h-4 w-4 text-gray-600"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
Export en cours...
</>
) : (
<>
<Download className="mr-2 h-4 w-4" />
Exporter
<ChevronDown className="ml-2 h-4 w-4" />
</>
)}
</button>
{/* Dropdown Menu */}
{isOpen && !isExporting && (
<div className="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 z-50">
<div className="py-1">
<button
onClick={handleExportCSV}
className="flex items-center w-full px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
>
<FileText className="mr-3 h-4 w-4 text-green-600" />
<div className="text-left">
<div className="font-medium">Export CSV</div>
<div className="text-xs text-gray-500">Compatible Excel, Google Sheets</div>
</div>
</button>
<button
onClick={handleExportExcel}
className="flex items-center w-full px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
>
<FileSpreadsheet className="mr-3 h-4 w-4 text-blue-600" />
<div className="text-left">
<div className="font-medium">Export Excel</div>
<div className="text-xs text-gray-500">Format .xls natif</div>
</div>
</button>
</div>
<div className="border-t border-gray-100 px-4 py-2">
<p className="text-xs text-gray-500">
{data.length} ligne{data.length > 1 ? 's' : ''} à exporter
</p>
</div>
</div>
)}
</div>
);
}

View File

@ -11,6 +11,18 @@ import { useState, useRef, useEffect } from 'react';
import { listNotifications, markNotificationAsRead, markAllNotificationsAsRead } from '@/lib/api'; import { listNotifications, markNotificationAsRead, markAllNotificationsAsRead } from '@/lib/api';
import type { NotificationResponse } from '@/types/api'; import type { NotificationResponse } from '@/types/api';
import NotificationPanel from './NotificationPanel'; import NotificationPanel from './NotificationPanel';
import {
CheckCircle,
RefreshCw,
XCircle,
DollarSign,
Ship,
Settings,
AlertTriangle,
Bell,
Megaphone,
type LucideIcon,
} from 'lucide-react';
export default function NotificationDropdown() { export default function NotificationDropdown() {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@ -83,17 +95,17 @@ export default function NotificationDropdown() {
return colors[priority as keyof typeof colors] || colors.low; return colors[priority as keyof typeof colors] || colors.low;
}; };
const getNotificationIcon = (type: string) => { const getNotificationIcon = (type: string): LucideIcon => {
const icons: Record<string, string> = { const icons: Record<string, LucideIcon> = {
BOOKING_CONFIRMED: '✅', BOOKING_CONFIRMED: CheckCircle,
BOOKING_UPDATED: '🔄', BOOKING_UPDATED: RefreshCw,
BOOKING_CANCELLED: '❌', BOOKING_CANCELLED: XCircle,
RATE_ALERT: '💰', RATE_ALERT: DollarSign,
CARRIER_UPDATE: '🚢', CARRIER_UPDATE: Ship,
SYSTEM: '⚙️', SYSTEM: Settings,
WARNING: '⚠️', WARNING: AlertTriangle,
}; };
return icons[type] || '📢'; return icons[type] || Megaphone;
}; };
const formatTime = (dateString: string) => { const formatTime = (dateString: string) => {
@ -104,10 +116,10 @@ export default function NotificationDropdown() {
const diffHours = Math.floor(diffMs / 3600000); const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000); const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return 'Just now'; if (diffMins < 1) return 'À l\'instant';
if (diffMins < 60) return `${diffMins}m ago`; if (diffMins < 60) return `Il y a ${diffMins}min`;
if (diffHours < 24) return `${diffHours}h ago`; if (diffHours < 24) return `Il y a ${diffHours}h`;
if (diffDays < 7) return `${diffDays}d ago`; if (diffDays < 7) return `Il y a ${diffDays}j`;
return date.toLocaleDateString(); return date.toLocaleDateString();
}; };
@ -146,7 +158,7 @@ export default function NotificationDropdown() {
disabled={markAllAsReadMutation.isPending} disabled={markAllAsReadMutation.isPending}
className="text-xs text-blue-600 hover:text-blue-800 font-medium disabled:opacity-50" className="text-xs text-blue-600 hover:text-blue-800 font-medium disabled:opacity-50"
> >
Mark all as read Tout marquer comme lu
</button> </button>
)} )}
</div> </div>
@ -154,11 +166,11 @@ export default function NotificationDropdown() {
{/* Notifications List */} {/* Notifications List */}
<div className="max-h-96 overflow-y-auto"> <div className="max-h-96 overflow-y-auto">
{isLoading ? ( {isLoading ? (
<div className="p-4 text-center text-sm text-gray-500">Loading notifications...</div> <div className="p-4 text-center text-sm text-gray-500">Chargement des notifications...</div>
) : notifications.length === 0 ? ( ) : notifications.length === 0 ? (
<div className="p-8 text-center"> <div className="p-8 text-center">
<div className="text-4xl mb-2">🔔</div> <Bell className="h-10 w-10 text-gray-300 mx-auto mb-2" />
<p className="text-sm text-gray-500">No new notifications</p> <p className="text-sm text-gray-500">Aucune nouvelle notification</p>
</div> </div>
) : ( ) : (
<div className="divide-y"> <div className="divide-y">
@ -172,8 +184,8 @@ export default function NotificationDropdown() {
}`} }`}
> >
<div className="flex items-start space-x-3"> <div className="flex items-start space-x-3">
<div className="flex-shrink-0 text-2xl"> <div className="flex-shrink-0">
{getNotificationIcon(notification.type)} {(() => { const Icon = getNotificationIcon(notification.type); return <Icon className="h-5 w-5 text-gray-500" />; })()}
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
@ -210,7 +222,7 @@ export default function NotificationDropdown() {
}} }}
className="w-full text-center text-sm text-blue-600 hover:text-blue-800 font-medium py-2 hover:bg-blue-50 rounded transition-colors" className="w-full text-center text-sm text-blue-600 hover:text-blue-800 font-medium py-2 hover:bg-blue-50 rounded transition-colors"
> >
View all notifications Voir toutes les notifications
</button> </button>
</div> </div>
</div> </div>

View File

@ -16,7 +16,7 @@ import {
deleteNotification deleteNotification
} from '@/lib/api'; } from '@/lib/api';
import type { NotificationResponse } from '@/types/api'; import type { NotificationResponse } from '@/types/api';
import { X, Trash2, CheckCheck, Filter } from 'lucide-react'; import { X, Trash2, CheckCheck, Filter, Bell, Package, RefreshCw, XCircle, CheckCircle, Mail, Timer, FileText, Megaphone, User, Building2 } from 'lucide-react';
interface NotificationPanelProps { interface NotificationPanelProps {
isOpen: boolean; isOpen: boolean;
@ -83,7 +83,7 @@ export default function NotificationPanel({ isOpen, onClose }: NotificationPanel
const handleDelete = (e: React.MouseEvent, notificationId: string) => { const handleDelete = (e: React.MouseEvent, notificationId: string) => {
e.stopPropagation(); e.stopPropagation();
if (confirm('Are you sure you want to delete this notification?')) { if (confirm('Voulez-vous vraiment supprimer cette notification ?')) {
deleteNotificationMutation.mutate(notificationId); deleteNotificationMutation.mutate(notificationId);
} }
}; };
@ -98,22 +98,22 @@ export default function NotificationPanel({ isOpen, onClose }: NotificationPanel
return colors[priority as keyof typeof colors] || 'border-l-4 border-gray-300'; return colors[priority as keyof typeof colors] || 'border-l-4 border-gray-300';
}; };
const getNotificationIcon = (type: string) => { const getNotificationIconComponent = (type: string) => {
const icons: Record<string, string> = { const icons: Record<string, typeof Bell> = {
booking_created: '📦', booking_created: Package,
booking_updated: '🔄', booking_updated: RefreshCw,
booking_cancelled: '❌', booking_cancelled: XCircle,
booking_confirmed: '✅', booking_confirmed: CheckCircle,
csv_booking_accepted: '✅', csv_booking_accepted: CheckCircle,
csv_booking_rejected: '❌', csv_booking_rejected: XCircle,
csv_booking_request_sent: '📧', csv_booking_request_sent: Mail,
rate_quote_expiring: '⏰', rate_quote_expiring: Timer,
document_uploaded: '📄', document_uploaded: FileText,
system_announcement: '📢', system_announcement: Megaphone,
user_invited: '👤', user_invited: User,
organization_update: '🏢', organization_update: Building2,
}; };
return icons[type.toLowerCase()] || '🔔'; return icons[type.toLowerCase()] || Bell;
}; };
const formatTime = (dateString: string) => { const formatTime = (dateString: string) => {
@ -124,13 +124,13 @@ export default function NotificationPanel({ isOpen, onClose }: NotificationPanel
const diffHours = Math.floor(diffMs / 3600000); const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000); const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return 'Just now'; if (diffMins < 1) return 'À l\'instant';
if (diffMins < 60) return `${diffMins}m ago`; if (diffMins < 60) return `Il y a ${diffMins}min`;
if (diffHours < 24) return `${diffHours}h ago`; if (diffHours < 24) return `Il y a ${diffHours}h`;
if (diffDays < 7) return `${diffDays}d ago`; if (diffDays < 7) return `Il y a ${diffDays}j`;
return date.toLocaleDateString('en-US', { return date.toLocaleDateString('fr-FR', {
month: 'short',
day: 'numeric', day: 'numeric',
month: 'short',
year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined, year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined,
}); });
}; };
@ -158,7 +158,7 @@ export default function NotificationPanel({ isOpen, onClose }: NotificationPanel
<button <button
onClick={onClose} onClick={onClose}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors text-gray-600" className="p-2 hover:bg-gray-100 rounded-lg transition-colors text-gray-600"
aria-label="Close panel" aria-label="Fermer le panneau"
> >
<X className="w-6 h-6" /> <X className="w-6 h-6" />
</button> </button>
@ -182,7 +182,7 @@ export default function NotificationPanel({ isOpen, onClose }: NotificationPanel
: 'bg-white text-gray-700 hover:bg-gray-100 border border-gray-300' : 'bg-white text-gray-700 hover:bg-gray-100 border border-gray-300'
}`} }`}
> >
{filter.charAt(0).toUpperCase() + filter.slice(1)} {filter === 'all' ? 'Toutes' : filter === 'unread' ? 'Non lues' : 'Lues'}
</button> </button>
))} ))}
</div> </div>
@ -194,7 +194,7 @@ export default function NotificationPanel({ isOpen, onClose }: NotificationPanel
className="flex items-center space-x-1 text-sm text-blue-600 hover:text-blue-800 font-medium disabled:opacity-50" className="flex items-center space-x-1 text-sm text-blue-600 hover:text-blue-800 font-medium disabled:opacity-50"
> >
<CheckCheck className="w-4 h-4" /> <CheckCheck className="w-4 h-4" />
<span>Mark all as read</span> <span>Tout marquer comme lu</span>
</button> </button>
)} )}
</div> </div>
@ -205,18 +205,18 @@ export default function NotificationPanel({ isOpen, onClose }: NotificationPanel
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center h-64">
<div className="text-center"> <div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4" /> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4" />
<p className="text-sm text-gray-500">Loading notifications...</p> <p className="text-sm text-gray-500">Chargement des notifications...</p>
</div> </div>
</div> </div>
) : notifications.length === 0 ? ( ) : notifications.length === 0 ? (
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center h-64">
<div className="text-center"> <div className="text-center">
<div className="text-6xl mb-4">🔔</div> <Bell className="h-16 w-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">No notifications</h3> <h3 className="text-lg font-semibold text-gray-900 mb-2">Aucune notification</h3>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
{selectedFilter === 'unread' {selectedFilter === 'unread'
? "You're all caught up!" ? 'Vous êtes à jour !'
: 'No notifications to display'} : 'Aucune notification à afficher'}
</p> </p>
</div> </div>
</div> </div>
@ -232,8 +232,8 @@ export default function NotificationPanel({ isOpen, onClose }: NotificationPanel
> >
<div className="flex items-start space-x-4"> <div className="flex items-start space-x-4">
{/* Icon */} {/* Icon */}
<div className="flex-shrink-0 text-3xl"> <div className="flex-shrink-0">
{getNotificationIcon(notification.type)} {(() => { const Icon = getNotificationIconComponent(notification.type); return <Icon className="h-6 w-6 text-gray-500" />; })()}
</div> </div>
{/* Content */} {/* Content */}
@ -250,7 +250,7 @@ export default function NotificationPanel({ isOpen, onClose }: NotificationPanel
<button <button
onClick={(e) => handleDelete(e, notification.id)} onClick={(e) => handleDelete(e, notification.id)}
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-red-100 rounded" className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-red-100 rounded"
title="Delete notification" title="Supprimer la notification"
> >
<Trash2 className="w-4 h-4 text-red-600" /> <Trash2 className="w-4 h-4 text-red-600" />
</button> </button>
@ -287,7 +287,7 @@ export default function NotificationPanel({ isOpen, onClose }: NotificationPanel
</div> </div>
{notification.actionUrl && ( {notification.actionUrl && (
<span className="text-xs text-blue-600 font-medium group-hover:underline"> <span className="text-xs text-blue-600 font-medium group-hover:underline">
View details Voir les tails
</span> </span>
)} )}
</div> </div>
@ -303,7 +303,7 @@ export default function NotificationPanel({ isOpen, onClose }: NotificationPanel
{totalPages > 1 && ( {totalPages > 1 && (
<div className="flex items-center justify-between px-6 py-4 border-t bg-gray-50"> <div className="flex items-center justify-between px-6 py-4 border-t bg-gray-50">
<div className="text-sm text-gray-600"> <div className="text-sm text-gray-600">
Page {currentPage} of {totalPages} Page {currentPage} sur {totalPages}
</div> </div>
<div className="flex space-x-2"> <div className="flex space-x-2">
<button <button
@ -311,14 +311,14 @@ export default function NotificationPanel({ isOpen, onClose }: NotificationPanel
disabled={currentPage === 1} disabled={currentPage === 1}
className="px-3 py-1.5 text-sm font-medium bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed" className="px-3 py-1.5 text-sm font-medium bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
> >
Previous Précédent
</button> </button>
<button <button
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages} disabled={currentPage === totalPages}
className="px-3 py-1.5 text-sm font-medium bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed" className="px-3 py-1.5 text-sm font-medium bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
> >
Next Suivant
</button> </button>
</div> </div>
</div> </div>

View File

@ -3,44 +3,45 @@
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { Users, Building2, Package, FileText, BarChart3, Settings, type LucideIcon } from 'lucide-react';
interface AdminMenuItem { interface AdminMenuItem {
name: string; name: string;
href: string; href: string;
icon: string; icon: LucideIcon;
description: string; description: string;
} }
const adminMenuItems: AdminMenuItem[] = [ const adminMenuItems: AdminMenuItem[] = [
{ {
name: 'Users', name: 'Utilisateurs',
href: '/dashboard/admin/users', href: '/dashboard/admin/users',
icon: '👥', icon: Users,
description: 'Manage users and permissions', description: 'Gérer les utilisateurs et les permissions',
}, },
{ {
name: 'Organizations', name: 'Organisations',
href: '/dashboard/admin/organizations', href: '/dashboard/admin/organizations',
icon: '🏢', icon: Building2,
description: 'Manage organizations and companies', description: 'Gérer les organisations et entreprises',
}, },
{ {
name: 'Bookings', name: 'Réservations',
href: '/dashboard/admin/bookings', href: '/dashboard/admin/bookings',
icon: '📦', icon: Package,
description: 'View and manage all bookings', description: 'Consulter et gérer toutes les réservations',
}, },
{ {
name: 'Documents', name: 'Documents',
href: '/dashboard/admin/documents', href: '/dashboard/admin/documents',
icon: '📄', icon: FileText,
description: 'Manage organization documents', description: 'Gérer les documents des organisations',
}, },
{ {
name: 'CSV Rates', name: 'Tarifs CSV',
href: '/dashboard/admin/csv-rates', href: '/dashboard/admin/csv-rates',
icon: '📊', icon: BarChart3,
description: 'Upload and manage CSV rates', description: 'Importer et gérer les tarifs CSV',
}, },
]; ];
@ -84,8 +85,8 @@ export default function AdminPanelDropdown() {
: 'text-gray-700 hover:bg-gray-100' : 'text-gray-700 hover:bg-gray-100'
}`} }`}
> >
<span className="mr-3 text-xl"></span> <Settings className="mr-3 h-5 w-5" />
<span className="flex-1 text-left">Admin Panel</span> <span className="flex-1 text-left">Administration</span>
<svg <svg
className={`w-4 h-4 transition-transform ${isOpen ? 'rotate-180' : ''}`} className={`w-4 h-4 transition-transform ${isOpen ? 'rotate-180' : ''}`}
fill="none" fill="none"
@ -107,6 +108,7 @@ export default function AdminPanelDropdown() {
<div className="py-2"> <div className="py-2">
{adminMenuItems.map(item => { {adminMenuItems.map(item => {
const isActive = pathname.startsWith(item.href); const isActive = pathname.startsWith(item.href);
const IconComponent = item.icon;
return ( return (
<Link <Link
key={item.name} key={item.name}
@ -115,7 +117,7 @@ export default function AdminPanelDropdown() {
isActive ? 'bg-blue-50' : '' isActive ? 'bg-blue-50' : ''
}`} }`}
> >
<span className="text-2xl mr-3 mt-0.5">{item.icon}</span> <IconComponent className="h-5 w-5 mr-3 mt-0.5 text-gray-500" />
<div className="flex-1"> <div className="flex-1">
<div className={`text-sm font-medium ${isActive ? 'text-blue-700' : 'text-gray-900'}`}> <div className={`text-sm font-medium ${isActive ? 'text-blue-700' : 'text-gray-900'}`}>
{item.name} {item.name}