363 lines
15 KiB
TypeScript
363 lines
15 KiB
TypeScript
/**
|
||
* 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>
|
||
);
|
||
}
|