From cf19c36586829dd3ffdc128b0a9608d0371347ca Mon Sep 17 00:00:00 2001 From: David Date: Tue, 3 Feb 2026 16:08:00 +0100 Subject: [PATCH] fix ui --- .../app/dashboard/admin/documents/page.tsx | 49 +- .../app/dashboard/booking/new/page.tsx | 7 +- apps/frontend/app/dashboard/bookings/page.tsx | 42 +- .../frontend/app/dashboard/documents/page.tsx | 103 ++-- apps/frontend/app/dashboard/layout.tsx | 59 ++- .../app/dashboard/notifications/page.tsx | 83 ++-- apps/frontend/app/dashboard/page.tsx | 45 +- apps/frontend/app/dashboard/profile/page.tsx | 61 ++- .../app/dashboard/search-advanced/page.tsx | 3 +- .../search-advanced/results/page.tsx | 19 +- apps/frontend/app/dashboard/search/page.tsx | 78 +-- .../app/dashboard/settings/users/page.tsx | 131 +++--- .../app/dashboard/track-trace/page.tsx | 445 ++++++++++++++++-- .../app/dashboard/wiki/assurance/page.tsx | 13 +- .../app/dashboard/wiki/calcul-fret/page.tsx | 15 +- .../app/dashboard/wiki/conteneurs/page.tsx | 29 +- .../wiki/documents-transport/page.tsx | 17 +- .../app/dashboard/wiki/douanes/page.tsx | 11 +- .../frontend/app/dashboard/wiki/imdg/page.tsx | 19 +- .../app/dashboard/wiki/incoterms/page.tsx | 10 +- .../app/dashboard/wiki/lcl-vs-fcl/page.tsx | 7 +- .../app/dashboard/wiki/lettre-credit/page.tsx | 21 +- apps/frontend/app/dashboard/wiki/page.tsx | 112 +++-- .../app/dashboard/wiki/ports-routes/page.tsx | 15 +- .../app/dashboard/wiki/transit-time/page.tsx | 19 +- apps/frontend/app/dashboard/wiki/vgm/page.tsx | 23 +- apps/frontend/app/page.tsx | 30 +- apps/frontend/src/components/ExportButton.tsx | 244 ++++++++++ .../src/components/NotificationDropdown.tsx | 54 ++- .../src/components/NotificationPanel.tsx | 76 +-- .../components/admin/AdminPanelDropdown.tsx | 38 +- 31 files changed, 1320 insertions(+), 558 deletions(-) create mode 100644 apps/frontend/src/components/ExportButton.tsx diff --git a/apps/frontend/app/dashboard/admin/documents/page.tsx b/apps/frontend/app/dashboard/admin/documents/page.tsx index edfc288..84aff4f 100644 --- a/apps/frontend/app/dashboard/admin/documents/page.tsx +++ b/apps/frontend/app/dashboard/admin/documents/page.tsx @@ -2,6 +2,8 @@ import { useState, useEffect, useCallback } from 'react'; 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 { id: string; @@ -226,30 +228,31 @@ export default function AdminDocumentsPage() { setCurrentPage(1); }, [searchTerm, filterUserId, filterQuoteNumber]); - const getDocumentIcon = (type: string) => { + const getDocumentIcon = (type: string): ReactNode => { const typeLower = type.toLowerCase(); - const icons: Record = { - 'application/pdf': '📄', - 'image/jpeg': '🖼️', - 'image/png': '🖼️', - 'image/jpg': '🖼️', - pdf: '📄', - jpeg: '🖼️', - jpg: '🖼️', - png: '🖼️', - gif: '🖼️', - image: '🖼️', - word: '📝', - doc: '📝', - docx: '📝', - excel: '📊', - xls: '📊', - xlsx: '📊', - csv: '📊', - text: '📄', - txt: '📄', + const cls = "h-6 w-6"; + const iconMap: Record = { + 'application/pdf': , + 'image/jpeg': , + 'image/png': , + 'image/jpg': , + pdf: , + jpeg: , + jpg: , + png: , + gif: , + image: , + word: , + doc: , + docx: , + excel: , + xls: , + xlsx: , + csv: , + text: , + txt: , }; - return icons[typeLower] || '📎'; + return iconMap[typeLower] || ; }; const getStatusColor = (status: string) => { @@ -445,7 +448,7 @@ export default function AdminDocumentsPage() {
- {getDocumentIcon(doc.fileType || doc.type)} + {getDocumentIcon(doc.fileType || doc.type)}
{doc.fileType || doc.type}
diff --git a/apps/frontend/app/dashboard/booking/new/page.tsx b/apps/frontend/app/dashboard/booking/new/page.tsx index 9ca8d6d..c590cd6 100644 --- a/apps/frontend/app/dashboard/booking/new/page.tsx +++ b/apps/frontend/app/dashboard/booking/new/page.tsx @@ -8,6 +8,7 @@ import { useState, useEffect, Suspense } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; +import { ClipboardList, Mail, AlertTriangle } from 'lucide-react'; import type { CsvRateSearchResult } from '@/types/rates'; import { createCsvBooking } from '@/lib/api/bookings'; @@ -259,7 +260,7 @@ function NewBookingPageContent() { {error && (
- ⚠️ +

Erreur

{error}

@@ -384,7 +385,7 @@ function NewBookingPageContent() {
- 📋 +

Information importante

@@ -572,7 +573,7 @@ function NewBookingPageContent() { {/* What happens next */}

- 📧 Que se passe-t-il ensuite ? + Que se passe-t-il ensuite ?

  • diff --git a/apps/frontend/app/dashboard/bookings/page.tsx b/apps/frontend/app/dashboard/bookings/page.tsx index a59096a..19043cd 100644 --- a/apps/frontend/app/dashboard/bookings/page.tsx +++ b/apps/frontend/app/dashboard/bookings/page.tsx @@ -10,6 +10,8 @@ import { useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { listBookings, listCsvBookings } from '@/lib/api'; import Link from 'next/link'; +import { Plus } from 'lucide-react'; +import ExportButton from '@/components/ExportButton'; type SearchType = 'pallets' | 'weight' | 'route' | 'status' | 'date' | 'quote'; @@ -146,13 +148,37 @@ export default function BookingsListPage() {

    Réservations

    Gérez et suivez vos envois

- - - Nouvelle Réservation - +
+ `${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 = { + 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') : '' }, + ]} + /> + + + Nouvelle Réservation + +
{/* Filters */} @@ -450,7 +476,7 @@ export default function BookingsListPage() { 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" > - + Nouvelle Réservation
diff --git a/apps/frontend/app/dashboard/documents/page.tsx b/apps/frontend/app/dashboard/documents/page.tsx index d9b90e8..7a59945 100644 --- a/apps/frontend/app/dashboard/documents/page.tsx +++ b/apps/frontend/app/dashboard/documents/page.tsx @@ -2,6 +2,9 @@ import { useState, useEffect, useCallback, useRef } from 'react'; 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 { id: string; @@ -164,30 +167,31 @@ export default function UserDocumentsPage() { setCurrentPage(1); }, [searchTerm, filterStatus, filterQuoteNumber]); - const getDocumentIcon = (type: string) => { + const getDocumentIcon = (type: string): ReactNode => { const typeLower = type.toLowerCase(); - const icons: Record = { - 'application/pdf': '📄', - 'image/jpeg': '🖼️', - 'image/png': '🖼️', - 'image/jpg': '🖼️', - pdf: '📄', - jpeg: '🖼️', - jpg: '🖼️', - png: '🖼️', - gif: '🖼️', - image: '🖼️', - word: '📝', - doc: '📝', - docx: '📝', - excel: '📊', - xls: '📊', - xlsx: '📊', - csv: '📊', - text: '📄', - txt: '📄', + const cls = "h-6 w-6"; + const iconMap: Record = { + 'application/pdf': , + 'image/jpeg': , + 'image/png': , + 'image/jpg': , + pdf: , + jpeg: , + jpg: , + png: , + gif: , + image: , + word: , + doc: , + docx: , + excel: , + xls: , + xlsx: , + csv: , + text: , + txt: , }; - return icons[typeLower] || '📎'; + return iconMap[typeLower] || ; }; const getStatusColor = (status: string) => { @@ -407,21 +411,44 @@ export default function UserDocumentsPage() { Gérez tous les documents de vos réservations

- +
+ { + const labels: Record = { + 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') : '' }, + ]} + /> + +
{/* Stats */} @@ -533,7 +560,7 @@ export default function UserDocumentsPage() {
- + {getDocumentIcon(doc.fileType || doc.type)}
{doc.fileType || doc.type}
diff --git a/apps/frontend/app/dashboard/layout.tsx b/apps/frontend/app/dashboard/layout.tsx index 3c6779e..919a31e 100644 --- a/apps/frontend/app/dashboard/layout.tsx +++ b/apps/frontend/app/dashboard/layout.tsx @@ -13,25 +13,34 @@ import { useState } from 'react'; import NotificationDropdown from '@/components/NotificationDropdown'; import AdminPanelDropdown from '@/components/admin/AdminPanelDropdown'; 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 }) { const { user, logout } = useAuth(); const pathname = usePathname(); const [sidebarOpen, setSidebarOpen] = useState(false); - // { name: 'Search Rates', href: '/dashboard/search-advanced', icon: '🔎' }, - const navigation = [ - { name: 'Dashboard', href: '/dashboard', icon: '📊' }, - { name: 'Bookings', href: '/dashboard/bookings', icon: '📦' }, - { name: 'Documents', href: '/dashboard/documents', icon: '📄' }, - { name: 'Track & Trace', href: '/dashboard/track-trace', icon: '🔍' }, - { name: 'Wiki', href: '/dashboard/wiki', icon: '📚' }, - { name: 'My Profile', href: '/dashboard/profile', icon: '👤' }, - { name: 'Organization', href: '/dashboard/settings/organization', icon: '🏢' }, + { name: 'Tableau de bord', href: '/dashboard', icon: BarChart3 }, + { name: 'Réservations', href: '/dashboard/bookings', icon: Package }, + { name: 'Documents', href: '/dashboard/documents', icon: FileText }, + { name: 'Suivi', href: '/dashboard/track-trace', icon: Search }, + { name: 'Wiki Maritime', href: '/dashboard/wiki', icon: BookOpen }, + { name: 'Mon Profil', href: '/dashboard/profile', icon: User }, + { name: 'Organisation', href: '/dashboard/settings/organization', icon: Building2 }, // ADMIN and MANAGER only navigation items ...(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' }`} > - {item.icon} + {item.name} ))} @@ -129,15 +138,8 @@ export default function DashboardLayout({ children }: { children: React.ReactNod 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" > - - - - Logout + + Déconnexion
@@ -162,7 +164,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod

- {navigation.find(item => isActive(item.href))?.name || 'Dashboard'} + {navigation.find(item => isActive(item.href))?.name || 'Tableau de bord'}

@@ -170,9 +172,18 @@ export default function DashboardLayout({ children }: { children: React.ReactNod {/* User Role Badge */} - - {user?.role} - + {user?.role === 'ADMIN' ? ( + + {user.role} + + ) : ( + + {user?.role} + + )}
diff --git a/apps/frontend/app/dashboard/notifications/page.tsx b/apps/frontend/app/dashboard/notifications/page.tsx index 8faa910..f63a5e4 100644 --- a/apps/frontend/app/dashboard/notifications/page.tsx +++ b/apps/frontend/app/dashboard/notifications/page.tsx @@ -16,7 +16,8 @@ import { deleteNotification, } from '@/lib/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() { const [selectedFilter, setSelectedFilter] = useState<'all' | 'unread' | 'read'>('all'); @@ -77,7 +78,7 @@ export default function NotificationsPage() { const handleDelete = (e: React.MouseEvent, notificationId: string) => { 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); } }; @@ -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'; }; - const getNotificationIcon = (type: string) => { - const icons: Record = { - booking_created: '📦', - booking_updated: '🔄', - booking_cancelled: '❌', - booking_confirmed: '✅', - csv_booking_accepted: '✅', - csv_booking_rejected: '❌', - csv_booking_request_sent: '📧', - rate_quote_expiring: '⏰', - document_uploaded: '📄', - system_announcement: '📢', - user_invited: '👤', - organization_update: '🏢', + const getNotificationIcon = (type: string): ReactNode => { + const iconClass = "h-8 w-8"; + const icons: Record = { + booking_created: , + booking_updated: , + booking_cancelled: , + booking_confirmed: , + csv_booking_accepted: , + csv_booking_rejected: , + csv_booking_request_sent: , + rate_quote_expiring: , + document_uploaded: , + system_announcement: , + user_invited: , + organization_update: , }; - return icons[type.toLowerCase()] || '🔔'; + return icons[type.toLowerCase()] || ; }; const formatTime = (dateString: string) => { @@ -118,11 +120,11 @@ export default function NotificationsPage() { const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); - if (diffMins < 1) return 'Just now'; - if (diffMins < 60) return `${diffMins}m ago`; - if (diffHours < 24) return `${diffHours}h ago`; - if (diffDays < 7) return `${diffDays}d ago`; - return date.toLocaleDateString('en-US', { + if (diffMins < 1) return 'A 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', { month: 'long', day: 'numeric', year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined, @@ -144,8 +146,8 @@ export default function NotificationsPage() {

Notifications

- {total} notification{total !== 1 ? 's' : ''} total - {unreadCount > 0 && ` • ${unreadCount} unread`} + {total} notification{total !== 1 ? 's' : ''} au total + {unreadCount > 0 && ` • ${unreadCount} non lue${unreadCount > 1 ? 's' : ''}`}

@@ -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" > - Mark all as read + Tout marquer comme lu )} @@ -169,7 +171,7 @@ export default function NotificationsPage() {
- Filter: + Filtrer :
{(['all', 'unread', 'read'] as const).map((filter) => ( @@ -300,7 +302,7 @@ export default function NotificationsPage() {
{notification.actionUrl && ( - View details + Voir les détails
- Showing page {currentPage} of{' '} + Page {currentPage} sur{' '} {totalPages} {' • '} - {total} total notification - {total !== 1 ? 's' : ''} + {total} notification{total !== 1 ? 's' : ''} au total
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => { @@ -378,7 +379,7 @@ export default function NotificationsPage() { 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" > - Next + Suivant
diff --git a/apps/frontend/app/dashboard/page.tsx b/apps/frontend/app/dashboard/page.tsx index caf812c..9804c26 100644 --- a/apps/frontend/app/dashboard/page.tsx +++ b/apps/frontend/app/dashboard/page.tsx @@ -21,6 +21,7 @@ import { Plus, ArrowRight, } from 'lucide-react'; +import ExportButton from '@/components/ExportButton'; import { PieChart, Pie, @@ -74,16 +75,32 @@ export default function DashboardPage() {

Tableau de Bord

- Vue d'ensemble de vos bookings et performances + Vue d'ensemble de vos réservations et performances

- - - +
+ 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' }, + ]} + /> + + + +
{/* KPI Cards - Compact with Color */} @@ -191,7 +208,7 @@ export default function DashboardPage() { - Distribution des Bookings + Distribution des Réservations Répartition par statut @@ -233,7 +250,7 @@ export default function DashboardPage() { Poids par Transporteur - Top 5 carriers par poids (KG) + Top 5 transporteurs par poids (KG) @@ -282,7 +299,7 @@ export default function DashboardPage() {
-

Total Bookings

+

Total Réservations

{csvKpisLoading ? '--' @@ -360,7 +377,7 @@ export default function DashboardPage() { {carrier.carrierName}

- {carrier.totalBookings} bookings + {carrier.totalBookings} réservations {carrier.totalWeightKG.toLocaleString()} KG
@@ -400,15 +417,15 @@ export default function DashboardPage() {

- Aucun booking + Aucune réservation

- Créez votre premier booking pour voir vos statistiques + Créez votre première réservation pour voir vos statistiques

diff --git a/apps/frontend/app/dashboard/profile/page.tsx b/apps/frontend/app/dashboard/profile/page.tsx index 1dee34b..416e319 100644 --- a/apps/frontend/app/dashboard/profile/page.tsx +++ b/apps/frontend/app/dashboard/profile/page.tsx @@ -17,18 +17,18 @@ import { updateUser, changePassword } from '@/lib/api'; // Password update schema const passwordSchema = z .object({ - currentPassword: z.string().min(1, 'Current password is required'), + currentPassword: z.string().min(1, 'Le mot de passe actuel est requis'), newPassword: z .string() - .min(12, 'Password must be at least 12 characters') + .min(12, 'Le mot de passe doit contenir au moins 12 caractères') .regex( /^(?=.*[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, { - message: "Passwords don't match", + message: 'Les mots de passe ne correspondent pas', path: ['confirmPassword'], }); @@ -36,9 +36,9 @@ type PasswordFormData = z.infer; // Profile update schema const profileSchema = z.object({ - firstName: z.string().min(2, 'First name must be at least 2 characters'), - lastName: z.string().min(2, 'Last name must be at least 2 characters'), - email: z.string().email('Invalid email address'), + firstName: z.string().min(2, 'Le prénom doit contenir au moins 2 caractères'), + lastName: z.string().min(2, 'Le nom doit contenir au moins 2 caractères'), + email: z.string().email('Adresse email invalide'), }); type ProfileFormData = z.infer; @@ -101,14 +101,14 @@ export default function ProfilePage() { return updateUser(user.id, data); }, onSuccess: () => { - setSuccessMessage('Profile updated successfully!'); + setSuccessMessage('Profil mis à jour avec succès !'); setErrorMessage(''); refreshUser(); queryClient.invalidateQueries({ queryKey: ['user'] }); setTimeout(() => setSuccessMessage(''), 3000); }, onError: (error: any) => { - setErrorMessage(error.message || 'Failed to update profile'); + setErrorMessage(error.message || 'Échec de la mise à jour du profil'); setSuccessMessage(''); }, }); @@ -122,7 +122,7 @@ export default function ProfilePage() { }); }, onSuccess: () => { - setSuccessMessage('Password updated successfully!'); + setSuccessMessage('Mot de passe mis à jour avec succès !'); setErrorMessage(''); passwordForm.reset({ currentPassword: '', @@ -132,7 +132,7 @@ export default function ProfilePage() { setTimeout(() => setSuccessMessage(''), 3000); }, onError: (error: any) => { - setErrorMessage(error.message || 'Failed to update password'); + setErrorMessage(error.message || 'Échec de la mise à jour du mot de passe'); setSuccessMessage(''); }, }); @@ -151,7 +151,7 @@ export default function ProfilePage() {
-

Loading profile...

+

Chargement du profil...

); @@ -162,12 +162,12 @@ export default function ProfilePage() { return (
-

Unable to load user profile

+

Impossible de charger le profil

@@ -178,8 +178,8 @@ export default function ProfilePage() {
{/* Header */}
-

My Profile

-

Manage your account settings and preferences

+

Mon Profil

+

Gérez vos paramètres de compte et préférences

{/* Success/Error Messages */} @@ -230,7 +230,7 @@ export default function ProfilePage() { {user?.role} - Active + Actif
@@ -249,7 +249,7 @@ export default function ProfilePage() { : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }`} > - Profile Information + Informations personnelles @@ -274,7 +274,7 @@ export default function ProfilePage() { htmlFor="firstName" className="block text-sm font-medium text-gray-700 mb-2" > - First Name + Prénom - Last Name + Nom -

Email cannot be changed

+

L'adresse email ne peut pas être modifiée

{/* Submit Button */} @@ -333,7 +333,7 @@ export default function ProfilePage() { 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" > - {updateProfileMutation.isPending ? 'Saving...' : 'Save Changes'} + {updateProfileMutation.isPending ? 'Enregistrement...' : 'Enregistrer'} @@ -345,7 +345,7 @@ export default function ProfilePage() { htmlFor="currentPassword" className="block text-sm font-medium text-gray-700 mb-2" > - Current Password + Mot de passe actuel - New Password + Nouveau mot de passe )}

- Must be at least 12 characters with uppercase, lowercase, number, and special - character + Au moins 12 caractères avec majuscule, minuscule, chiffre et caractère spécial

@@ -393,7 +392,7 @@ export default function ProfilePage() { htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 mb-2" > - Confirm New Password + Confirmer le nouveau mot de passe - {updatePasswordMutation.isPending ? 'Updating...' : 'Update Password'} + {updatePasswordMutation.isPending ? 'Mise à jour...' : 'Mettre à jour'} diff --git a/apps/frontend/app/dashboard/search-advanced/page.tsx b/apps/frontend/app/dashboard/search-advanced/page.tsx index 53be855..e5f8ae2 100644 --- a/apps/frontend/app/dashboard/search-advanced/page.tsx +++ b/apps/frontend/app/dashboard/search-advanced/page.tsx @@ -8,6 +8,7 @@ import { useState, useMemo } from 'react'; import { useRouter } from 'next/navigation'; +import { Search } from 'lucide-react'; import { useQuery } from '@tanstack/react-query'; import { searchPorts, Port } from '@/lib/api/ports'; import dynamic from 'next/dynamic'; @@ -641,7 +642,7 @@ export default function AdvancedSearchPage() { 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" > - 🔍 Rechercher les tarifs + Rechercher les tarifs )} diff --git a/apps/frontend/app/dashboard/search-advanced/results/page.tsx b/apps/frontend/app/dashboard/search-advanced/results/page.tsx index c950e28..44a1b1c 100644 --- a/apps/frontend/app/dashboard/search-advanced/results/page.tsx +++ b/apps/frontend/app/dashboard/search-advanced/results/page.tsx @@ -4,6 +4,7 @@ import { useEffect, useState, useCallback } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import { searchCsvRatesWithOffers } from '@/lib/api/rates'; import type { CsvRateSearchResult } from '@/types/rates'; +import { Search, Lightbulb, DollarSign, Scale, Zap, Trophy, XCircle, AlertTriangle } from 'lucide-react'; interface BestOptions { eco: CsvRateSearchResult; @@ -121,7 +122,7 @@ export default function SearchResultsPage() {
-
+

Erreur

{error}

+ ) : ( + + + + Mettre à niveau + + )}
- {licenseStatus?.canInvite ? ( - - ) : ( - - + - Upgrade to Invite - - )}
{success && ( @@ -261,7 +284,7 @@ export default function UsersManagementPage() { {isLoading ? (
- Loading users... + Chargement des utilisateurs...
) : users?.users && users.users.length > 0 ? (
@@ -269,19 +292,19 @@ export default function UsersManagementPage() { - User + Utilisateur Email - Role + Rôle - Status + Statut - Last Login + Date de création Actions @@ -338,7 +361,7 @@ export default function UsersManagementPage() { : 'bg-red-100 text-red-800' }`} > - {user.isActive ? 'Active' : 'Inactive'} + {user.isActive ? 'Actif' : 'Inactif'} @@ -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" /> -

No users

-

Get started by inviting a team member

+

Aucun utilisateur

+

Commencez par inviter un membre de l'équipe

{licenseStatus?.canInvite ? (
diff --git a/apps/frontend/app/dashboard/track-trace/page.tsx b/apps/frontend/app/dashboard/track-trace/page.tsx index 143d759..4e22cd9 100644 --- a/apps/frontend/app/dashboard/track-trace/page.tsx +++ b/apps/frontend/app/dashboard/track-trace/page.tsx @@ -2,107 +2,181 @@ * Track & Trace Page * * 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'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; 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 = [ { id: 'maersk', name: 'Maersk', - logo: '🚢', + color: '#00243D', // Maersk dark blue + textColor: 'text-white', trackingUrl: 'https://www.maersk.com/tracking/', placeholder: 'Ex: MSKU1234567', - description: 'Container or B/L number', + description: 'N° conteneur ou B/L', + logo: '/assets/logos/carriers/maersk.svg', }, { id: 'msc', name: 'MSC', - logo: '🛳️', + color: '#002B5C', // MSC blue + textColor: 'text-white', trackingUrl: 'https://www.msc.com/track-a-shipment?query=', 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', name: 'CMA CGM', - logo: '⚓', + color: '#E30613', // CMA CGM red + textColor: 'text-white', trackingUrl: 'https://www.cma-cgm.com/ebusiness/tracking/search?SearchBy=Container&Reference=', placeholder: 'Ex: CMAU1234567', - description: 'Container or B/L number', + description: 'N° conteneur ou B/L', + logo: '/assets/logos/carriers/cmacgm.svg', }, { id: '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=', placeholder: 'Ex: HLCU1234567', - description: 'Container number', + description: 'N° conteneur', + logo: '/assets/logos/carriers/hapag.svg', }, { id: 'cosco', name: 'COSCO', - logo: '🌊', + color: '#003A70', // COSCO blue + textColor: 'text-white', trackingUrl: 'https://elines.coscoshipping.com/ebusiness/cargoTracking?trackingNumber=', placeholder: 'Ex: COSU1234567', - description: 'Container or B/L number', + description: 'N° conteneur ou B/L', + logo: '/assets/logos/carriers/cosco.svg', }, { id: 'one', - name: 'ONE (Ocean Network Express)', - logo: '🟣', + name: 'ONE', + color: '#FF00FF', // ONE magenta + textColor: 'text-white', trackingUrl: 'https://ecomm.one-line.com/one-ecom/manage-shipment/cargo-tracking?trkNoParam=', placeholder: 'Ex: ONEU1234567', - description: 'Container or B/L number', + description: 'N° conteneur ou B/L', + logo: '/assets/logos/carriers/one.svg', }, { id: 'evergreen', name: 'Evergreen', - logo: '🌲', + color: '#006633', // Evergreen green + textColor: 'text-white', trackingUrl: 'https://www.shipmentlink.com/servlet/TDB1_CargoTracking.do?BL=', placeholder: 'Ex: EGHU1234567', - description: 'Container or B/L number', + description: 'N° conteneur ou B/L', + logo: '/assets/logos/carriers/evergreen.svg', }, { id: 'yangming', 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=', placeholder: 'Ex: YMLU1234567', - description: 'Container number', + description: 'N° conteneur', + logo: '/assets/logos/carriers/yangming.svg', }, { id: 'zim', name: 'ZIM', - logo: '🔵', + color: '#1E3A8A', // ZIM blue + textColor: 'text-white', trackingUrl: 'https://www.zim.com/tools/track-a-shipment?consnumber=', placeholder: 'Ex: ZIMU1234567', - description: 'Container or B/L number', + description: 'N° conteneur ou B/L', + logo: '/assets/logos/carriers/zim.svg', }, { id: 'hmm', - name: 'HMM (Hyundai)', - logo: '🟠', + name: 'HMM', + color: '#E65100', // HMM orange + textColor: 'text-white', trackingUrl: 'https://www.hmm21.com/cms/business/ebiz/trackTrace/trackTrace/index.jsp?type=1&number=', 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() { const [trackingNumber, setTrackingNumber] = useState(''); const [selectedCarrier, setSelectedCarrier] = useState(''); const [error, setError] = useState(''); + const [searchHistory, setSearchHistory] = useState([]); + 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 = () => { - // Validation if (!trackingNumber.trim()) { setError('Veuillez entrer un numéro de tracking'); return; @@ -114,15 +188,43 @@ export default function TrackTracePage() { setError(''); - // Find the carrier and build the tracking URL const carrier = carriers.find(c => c.id === selectedCarrier); 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()); - // Open in new tab 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) => { if (e.key === 'Enter') { handleTrack(); @@ -131,11 +233,25 @@ export default function TrackTracePage() { 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 (
{/* Header */}
-

Track & Trace

+

Suivi des expéditions

Suivez vos expéditions en temps réel. Entrez votre numéro de tracking et sélectionnez le transporteur.

@@ -145,15 +261,15 @@ export default function TrackTracePage() { - 🔍 + Rechercher une expédition - 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 - {/* Carrier Selection */} + {/* Carrier Selection - US 5.1: Professional carrier cards with brand colors */}
@@ -197,22 +319,42 @@ export default function TrackTracePage() { }} onKeyPress={handleKeyPress} 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.description}

)}
+ {/* US 5.2: Harmonized button color */}
+ {/* Action Button - Map */} +
+ +
+ {/* Error Message */} {error && (
@@ -222,12 +364,221 @@ export default function TrackTracePage() { + {/* Vessel Position Map - Large immersive display */} + {showMap && ( +
+ + {/* Map Header */} +
+
+
+ +
+
+

Carte Maritime Mondiale

+

Position des navires en temps réel

+
+
+
+ {/* Fullscreen Toggle */} + + {/* Close Button */} + +
+
+ + {/* Map Container */} +
+ {/* Loading State */} + {isMapLoading && ( +
+
+
+ +
+
+
+
+
+
+
+
+

Chargement de la carte...

+

Connexion à MarineTraffic

+
+
+ )} + + {/* MarineTraffic Map */} +