xpeditis2.0/apps/frontend/app/dashboard/page.tsx
David-Henri ARNAUD 07258e5adb feature phase 3
2025-10-13 13:58:39 +02:00

363 lines
15 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Dashboard Home Page
*
* Main dashboard with KPIs, charts, and alerts
*/
'use client';
import { useQuery } from '@tanstack/react-query';
import { dashboardApi, bookingsApi } from '@/lib/api';
import Link from 'next/link';
import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
export default function DashboardPage() {
// Fetch dashboard data
const { data: kpis, isLoading: kpisLoading } = useQuery({
queryKey: ['dashboard', 'kpis'],
queryFn: () => dashboardApi.getKPIs(),
});
const { data: chartData, isLoading: chartLoading } = useQuery({
queryKey: ['dashboard', 'bookings-chart'],
queryFn: () => dashboardApi.getBookingsChart(),
});
const { data: tradeLanes, isLoading: tradeLanesLoading } = useQuery({
queryKey: ['dashboard', 'top-trade-lanes'],
queryFn: () => dashboardApi.getTopTradeLanes(),
});
const { data: alerts, isLoading: alertsLoading } = useQuery({
queryKey: ['dashboard', 'alerts'],
queryFn: () => dashboardApi.getAlerts(),
});
const { data: recentBookings, isLoading: bookingsLoading } = useQuery({
queryKey: ['bookings', 'recent'],
queryFn: () => bookingsApi.list({ limit: 5 }),
});
// Format chart data for Recharts
const formattedChartData = chartData
? chartData.labels.map((label, index) => ({
month: label,
bookings: chartData.data[index],
}))
: [];
// Format change percentage
const formatChange = (value: number) => {
const sign = value >= 0 ? '+' : '';
return `${sign}${value.toFixed(1)}%`;
};
// Get change color
const getChangeColor = (value: number) => {
if (value > 0) return 'text-green-600';
if (value < 0) return 'text-red-600';
return 'text-gray-600';
};
// Get alert color
const getAlertColor = (severity: string) => {
const colors = {
critical: 'bg-red-100 border-red-500 text-red-800',
high: 'bg-orange-100 border-orange-500 text-orange-800',
medium: 'bg-yellow-100 border-yellow-500 text-yellow-800',
low: 'bg-blue-100 border-blue-500 text-blue-800',
};
return colors[severity as keyof typeof colors] || colors.low;
};
return (
<div className="space-y-6">
{/* Welcome Section */}
<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">Welcome back!</h1>
<p className="text-blue-100">Here's what's happening with your shipments today.</p>
</div>
{/* KPI Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{kpisLoading ? (
// Loading skeletons
Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="bg-white rounded-lg shadow p-6 animate-pulse">
<div className="h-4 bg-gray-200 rounded w-1/2 mb-4"></div>
<div className="h-8 bg-gray-200 rounded w-3/4"></div>
</div>
))
) : (
<>
<div className="bg-white rounded-lg shadow p-6 hover:shadow-md transition-shadow">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Bookings This Month</p>
<p className="text-3xl font-bold text-gray-900 mt-2">{kpis?.bookingsThisMonth || 0}</p>
</div>
<div className="text-4xl">📦</div>
</div>
<div className="mt-4">
<span className={`text-sm font-medium ${getChangeColor(kpis?.bookingsThisMonthChange || 0)}`}>
{formatChange(kpis?.bookingsThisMonthChange || 0)}
</span>
<span className="text-sm text-gray-500 ml-2">vs last month</span>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6 hover:shadow-md transition-shadow">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Total TEUs</p>
<p className="text-3xl font-bold text-gray-900 mt-2">{kpis?.totalTEUs || 0}</p>
</div>
<div className="text-4xl">📊</div>
</div>
<div className="mt-4">
<span className={`text-sm font-medium ${getChangeColor(kpis?.totalTEUsChange || 0)}`}>
{formatChange(kpis?.totalTEUsChange || 0)}
</span>
<span className="text-sm text-gray-500 ml-2">vs last month</span>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6 hover:shadow-md transition-shadow">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Estimated Revenue</p>
<p className="text-3xl font-bold text-gray-900 mt-2">
${(kpis?.estimatedRevenue || 0).toLocaleString()}
</p>
</div>
<div className="text-4xl">💰</div>
</div>
<div className="mt-4">
<span className={`text-sm font-medium ${getChangeColor(kpis?.estimatedRevenueChange || 0)}`}>
{formatChange(kpis?.estimatedRevenueChange || 0)}
</span>
<span className="text-sm text-gray-500 ml-2">vs last month</span>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6 hover:shadow-md transition-shadow">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Pending Confirmations</p>
<p className="text-3xl font-bold text-gray-900 mt-2">{kpis?.pendingConfirmations || 0}</p>
</div>
<div className="text-4xl"></div>
</div>
<div className="mt-4">
<span className={`text-sm font-medium ${getChangeColor(kpis?.pendingConfirmationsChange || 0)}`}>
{formatChange(kpis?.pendingConfirmationsChange || 0)}
</span>
<span className="text-sm text-gray-500 ml-2">vs last month</span>
</div>
</div>
</>
)}
</div>
{/* Alerts Section */}
{!alertsLoading && alerts && alerts.length > 0 && (
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4"> Alerts & Notifications</h2>
<div className="space-y-3">
{alerts.slice(0, 5).map((alert) => (
<div
key={alert.id}
className={`border-l-4 p-4 rounded ${getAlertColor(alert.severity)}`}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<h3 className="font-semibold">{alert.title}</h3>
<p className="text-sm mt-1">{alert.message}</p>
{alert.bookingNumber && (
<Link
href={`/dashboard/bookings/${alert.bookingId}`}
className="text-sm font-medium underline mt-2 inline-block"
>
View Booking {alert.bookingNumber}
</Link>
)}
</div>
<span className="text-xs font-medium uppercase ml-4">{alert.severity}</span>
</div>
</div>
))}
</div>
</div>
)}
{/* Charts Section */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Bookings Trend Chart */}
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Bookings Trend (6 Months)</h2>
{chartLoading ? (
<div className="h-64 bg-gray-100 animate-pulse rounded"></div>
) : (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={formattedChartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="bookings" stroke="#3b82f6" strokeWidth={2} />
</LineChart>
</ResponsiveContainer>
)}
</div>
{/* Top Trade Lanes Chart */}
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Top 5 Trade Lanes</h2>
{tradeLanesLoading ? (
<div className="h-64 bg-gray-100 animate-pulse rounded"></div>
) : (
<ResponsiveContainer width="100%" height={300}>
<BarChart data={tradeLanes}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="route" angle={-45} textAnchor="end" height={100} />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="bookingCount" fill="#3b82f6" name="Bookings" />
</BarChart>
</ResponsiveContainer>
)}
</div>
</div>
{/* Quick Actions */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Link
href="/dashboard/search"
className="bg-white rounded-lg shadow p-6 hover:shadow-md transition-shadow group"
>
<div className="flex items-center space-x-4">
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center text-2xl group-hover:bg-blue-200 transition-colors">
🔍
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900">Search Rates</h3>
<p className="text-sm text-gray-500">Find the best shipping rates</p>
</div>
</div>
</Link>
<Link
href="/dashboard/bookings/new"
className="bg-white rounded-lg shadow p-6 hover:shadow-md transition-shadow group"
>
<div className="flex items-center space-x-4">
<div className="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center text-2xl group-hover:bg-green-200 transition-colors">
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900">New Booking</h3>
<p className="text-sm text-gray-500">Create a new shipment</p>
</div>
</div>
</Link>
<Link
href="/dashboard/bookings"
className="bg-white rounded-lg shadow p-6 hover:shadow-md transition-shadow group"
>
<div className="flex items-center space-x-4">
<div className="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center text-2xl group-hover:bg-purple-200 transition-colors">
📋
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900">My Bookings</h3>
<p className="text-sm text-gray-500">View all your shipments</p>
</div>
</div>
</Link>
</div>
{/* Recent Bookings */}
<div className="bg-white rounded-lg shadow">
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900">Recent Bookings</h2>
<Link href="/dashboard/bookings" className="text-sm text-blue-600 hover:text-blue-800">
View All
</Link>
</div>
<div className="p-6">
{bookingsLoading ? (
<div className="space-y-4">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="h-16 bg-gray-100 animate-pulse rounded"></div>
))}
</div>
) : recentBookings && recentBookings.bookings.length > 0 ? (
<div className="space-y-4">
{recentBookings.bookings.map((booking: any) => (
<Link
key={booking.id}
href={`/dashboard/bookings/${booking.id}`}
className="flex items-center justify-between p-4 hover:bg-gray-50 rounded-lg transition-colors"
>
<div className="flex items-center space-x-4">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
📦
</div>
<div>
<div className="font-medium text-gray-900">{booking.bookingNumber}</div>
<div className="text-sm text-gray-500">
{new Date(booking.createdAt).toLocaleDateString()}
</div>
</div>
</div>
<div className="flex items-center space-x-4">
<span
className={`px-3 py-1 rounded-full text-xs font-medium ${
booking.status === 'confirmed'
? 'bg-green-100 text-green-800'
: booking.status === 'pending'
? 'bg-yellow-100 text-yellow-800'
: 'bg-gray-100 text-gray-800'
}`}
>
{booking.status}
</span>
<svg
className="w-5 h-5 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</div>
</Link>
))}
</div>
) : (
<div className="text-center py-12">
<div className="text-6xl mb-4">📦</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">No bookings yet</h3>
<p className="text-gray-500 mb-6">Create your first booking to get started</p>
<Link
href="/dashboard/bookings/new"
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
>
Create Booking
</Link>
</div>
)}
</div>
</div>
</div>
);
}