diff --git a/frontend/package/pages/Analytics/AnalyticsDashboard.tsx b/frontend/package/pages/Analytics/AnalyticsDashboard.tsx new file mode 100644 index 00000000..709ba6ef --- /dev/null +++ b/frontend/package/pages/Analytics/AnalyticsDashboard.tsx @@ -0,0 +1,322 @@ +'use client'; + +import { useEffect, useMemo, useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { + LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, + PieChart, Pie, Cell, Legend, + BarChart, Bar, +} from 'recharts'; +import { format, subWeeks, startOfWeek, subMonths, startOfMonth } from 'date-fns'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Skeleton } from '@/components/ui/skeleton'; +import { shipmentApi } from '@/lib/api/shipment.api'; +import { apiClient } from '@/lib/api/client'; +import type { Shipment, ShipmentStatus } from '@/types/shipment.types'; + +const STATUS_COLORS: Record = { + pending: '#f59e0b', + accepted: '#3b82f6', + in_transit: '#8b5cf6', + delivered: '#10b981', + completed: '#22c55e', + cancelled: '#ef4444', + disputed: '#f97316', +}; + +const BAR_COLOR = '#6366f1'; +const LINE_COLOR = '#6366f1'; + +interface AnalyticsData { + totalShipments: number; + totalRevenue: number; + averageDeliveryTime: number; + onTimeRate: number; + activeShipments: number; + weeklyData: { week: string; count: number }[]; + statusData: { name: string; value: number; status: string }[]; + monthlySpend: { month: string; spend: number }[]; +} + +function buildWeeklyData(shipments: Shipment[]) { + const now = new Date(); + return Array.from({ length: 12 }, (_, i) => { + const weekStart = startOfWeek(subWeeks(now, 11 - i)); + const weekEnd = new Date(weekStart); + weekEnd.setDate(weekEnd.getDate() + 7); + const count = shipments.filter((s) => { + const d = new Date(s.createdAt); + return d >= weekStart && d < weekEnd; + }).length; + return { week: format(weekStart, 'MMM d'), count }; + }); +} + +function buildStatusData(shipments: Shipment[]) { + const counts: Record = {}; + for (const s of shipments) { + counts[s.status] = (counts[s.status] ?? 0) + 1; + } + return Object.entries(counts).map(([status, value]) => ({ + name: status.replace('_', ' ').replace(/\b\w/g, (l) => l.toUpperCase()), + value, + status, + })); +} + +function buildMonthlySpend(shipments: Shipment[]) { + const now = new Date(); + return Array.from({ length: 6 }, (_, i) => { + const monthStart = startOfMonth(subMonths(now, 5 - i)); + const monthEnd = startOfMonth(subMonths(now, 4 - i)); + const spend = shipments + .filter((s) => { + const d = new Date(s.createdAt); + return d >= monthStart && d < monthEnd; + }) + .reduce((sum, s) => sum + s.price, 0); + return { month: format(monthStart, 'MMM yy'), spend }; + }); +} + +export function AnalyticsDashboard() { + const [shipments, setShipments] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + const [dateRange, setDateRange] = useState<'12w' | '6m' | 'all'>('12w'); + + const { data: analyticsData } = useQuery({ + queryKey: ['analytics-summary'], + queryFn: () => apiClient('/analytics/shipments'), + enabled: false, // fallback to local calculation + }); + + useEffect(() => { + setLoading(true); + shipmentApi + .list({ limit: 200 }) + .then((res) => setShipments(res.data)) + .catch(() => setError(true)) + .finally(() => setLoading(false)); + }, []); + + const weeklyData = useMemo(() => buildWeeklyData(shipments), [shipments]); + const statusData = useMemo(() => buildStatusData(shipments), [shipments]); + const monthlySpend = useMemo(() => buildMonthlySpend(shipments), [shipments]); + + const kpis = useMemo(() => { + const completed = shipments.filter((s) => s.status === 'completed'); + const totalRevenue = completed.reduce((sum, s) => sum + s.price, 0); + const active = shipments.filter( + (s) => s.status === 'in_transit' || s.status === 'accepted', + ).length; + return { totalShipments: shipments.length, totalRevenue, activeShipments: active }; + }, [shipments]); + + if (loading) { + return ( +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+ ))} +
+ ); + } + + if (error) { + return ( +
+

Failed to load analytics data. Please refresh.

+
+ ); + } + + const isEmpty = shipments.length === 0; + + return ( +
+
+
+

Analytics Dashboard

+

+ Shipment activity and spending overview. +

+
+
+ {(['12w', '6m', 'all'] as const).map((range) => ( + + ))} +
+
+ + {/* KPI Cards */} +
+ + + + Total Shipments + + + +

{kpis.totalShipments}

+
+
+ + + + Total Revenue + + + +

+ ${kpis.totalRevenue.toLocaleString()} +

+
+
+ + + + Active Shipments + + + +

{kpis.activeShipments}

+
+
+ + + + Avg Delivery Time + + + +

+
+
+
+ + {isEmpty ? ( +
+

+ No shipment data yet. Create your first shipment to see analytics. +

+
+ ) : ( +
+ {/* Shipment Volume Line Chart */} + + + Shipments Created — Last 12 Weeks + + + + + + + + [v, 'Shipments']} + /> + + + + + + + {/* Status Breakdown Pie Chart */} + + + Shipments by Status + + + {statusData.length === 0 ? ( +

No data

+ ) : ( + + + + {statusData.map((entry) => ( + + ))} + + [v, name]} + /> + + + + )} +
+
+ + {/* Monthly Spend Bar Chart */} + + + Monthly Spend — Last 6 Months + + + + + + + `$${v}`} + /> + [`$${v.toLocaleString()}`, 'Spend']} + /> + + + + + + + {/* Top Carriers (Admin) */} + + + Top Performing Carriers + + +

+ Carrier performance data will appear once shipments are completed with carrier ratings. +

+
+
+
+ )} +
+ ); +} diff --git a/frontend/package/pages/Analytics/index.ts b/frontend/package/pages/Analytics/index.ts new file mode 100644 index 00000000..2cf78a9f --- /dev/null +++ b/frontend/package/pages/Analytics/index.ts @@ -0,0 +1 @@ +export { AnalyticsDashboard } from './AnalyticsDashboard';