Add comprehensive analytics system with: - Analytics data layer (aggregator, metrics, trends, cache) - 6 API endpoints (overview, plants, transport, farms, sustainability, export) - 6 chart components (LineChart, BarChart, PieChart, AreaChart, Gauge, Heatmap) - 5 dashboard widgets (KPICard, TrendIndicator, DataTable, DateRangePicker, FilterPanel) - 5 dashboard pages (overview, plants, transport, farms, sustainability) - Export functionality (CSV, JSON) Dependencies added: recharts, d3, date-fns Also includes minor fixes: - Fix EnvironmentalForm spread type error - Fix AgentOrchestrator Map iteration issues - Fix next.config.js image domains undefined error - Add downlevelIteration to tsconfig
232 lines
7.3 KiB
TypeScript
232 lines
7.3 KiB
TypeScript
/**
|
|
* Transport Analytics Page
|
|
* Carbon footprint and food miles analysis
|
|
*/
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import Link from 'next/link';
|
|
import {
|
|
KPICard,
|
|
DateRangePicker,
|
|
LineChart,
|
|
BarChart,
|
|
PieChart,
|
|
AreaChart,
|
|
Gauge,
|
|
DataTable,
|
|
} from '../../components/analytics';
|
|
import { TimeRange, TransportAnalytics } from '../../lib/analytics/types';
|
|
|
|
export default function TransportAnalyticsPage() {
|
|
const [timeRange, setTimeRange] = useState<TimeRange>('30d');
|
|
const [loading, setLoading] = useState(true);
|
|
const [data, setData] = useState<TransportAnalytics | null>(null);
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, [timeRange]);
|
|
|
|
const fetchData = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const response = await fetch(`/api/analytics/transport?timeRange=${timeRange}`);
|
|
const result = await response.json();
|
|
setData(result.data);
|
|
} catch (error) {
|
|
console.error('Failed to fetch transport analytics:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const methodColumns = [
|
|
{ key: 'method', header: 'Method' },
|
|
{ key: 'count', header: 'Events', align: 'right' as const },
|
|
{ key: 'distanceKm', header: 'Distance (km)', align: 'right' as const, render: (v: number) => v.toLocaleString() },
|
|
{ key: 'carbonKg', header: 'Carbon (kg)', align: 'right' as const, render: (v: number) => v.toFixed(2) },
|
|
{
|
|
key: 'efficiency',
|
|
header: 'Efficiency',
|
|
align: 'right' as const,
|
|
render: (v: number) => (
|
|
<span className={`font-medium ${v >= 80 ? 'text-green-600' : v >= 50 ? 'text-yellow-600' : 'text-red-600'}`}>
|
|
{v}%
|
|
</span>
|
|
),
|
|
},
|
|
];
|
|
|
|
const routeColumns = [
|
|
{ key: 'from', header: 'From' },
|
|
{ key: 'to', header: 'To' },
|
|
{ key: 'method', header: 'Method' },
|
|
{ key: 'distanceKm', header: 'Distance', align: 'right' as const, render: (v: number) => `${v} km` },
|
|
{ key: 'frequency', header: 'Frequency', align: 'right' as const },
|
|
];
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100">
|
|
{/* Header */}
|
|
<div className="bg-gradient-to-r from-teal-600 to-cyan-600 text-white">
|
|
<div className="max-w-7xl mx-auto px-4 py-8">
|
|
<h1 className="text-3xl font-bold">Transport Analytics</h1>
|
|
<p className="text-teal-200 mt-1">Carbon footprint and food miles analysis</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="max-w-7xl mx-auto px-4 py-8">
|
|
{/* Time Range Selector */}
|
|
<div className="mb-8">
|
|
<DateRangePicker value={timeRange} onChange={setTimeRange} />
|
|
</div>
|
|
|
|
{/* Navigation Tabs */}
|
|
<div className="flex space-x-4 mb-8">
|
|
<Link href="/analytics">
|
|
<a className="px-4 py-2 bg-white hover:bg-gray-50 text-gray-700 rounded-lg font-medium shadow">Overview</a>
|
|
</Link>
|
|
<Link href="/analytics/plants">
|
|
<a className="px-4 py-2 bg-white hover:bg-gray-50 text-gray-700 rounded-lg font-medium shadow">Plants</a>
|
|
</Link>
|
|
<Link href="/analytics/transport">
|
|
<a className="px-4 py-2 bg-teal-500 text-white rounded-lg font-medium">Transport</a>
|
|
</Link>
|
|
<Link href="/analytics/farms">
|
|
<a className="px-4 py-2 bg-white hover:bg-gray-50 text-gray-700 rounded-lg font-medium shadow">Farms</a>
|
|
</Link>
|
|
<Link href="/analytics/sustainability">
|
|
<a className="px-4 py-2 bg-white hover:bg-gray-50 text-gray-700 rounded-lg font-medium shadow">Sustainability</a>
|
|
</Link>
|
|
</div>
|
|
|
|
{/* KPI Cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
|
<KPICard
|
|
title="Total Events"
|
|
value={data?.totalEvents || 0}
|
|
trend="up"
|
|
color="blue"
|
|
loading={loading}
|
|
/>
|
|
<KPICard
|
|
title="Total Distance"
|
|
value={data?.totalDistanceKm?.toLocaleString() || '0'}
|
|
unit="km"
|
|
trend="stable"
|
|
color="purple"
|
|
loading={loading}
|
|
/>
|
|
<KPICard
|
|
title="Carbon Emitted"
|
|
value={data?.totalCarbonKg?.toFixed(1) || '0'}
|
|
unit="kg CO2"
|
|
trend="down"
|
|
color="orange"
|
|
loading={loading}
|
|
/>
|
|
<KPICard
|
|
title="Carbon Saved"
|
|
value={data?.carbonSavedKg?.toFixed(1) || '0'}
|
|
unit="kg CO2"
|
|
trend="up"
|
|
color="green"
|
|
loading={loading}
|
|
/>
|
|
</div>
|
|
|
|
{/* Gauges */}
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 mb-8">
|
|
<Gauge
|
|
value={data?.totalCarbonKg ? (data.carbonSavedKg / (data.totalCarbonKg + data.carbonSavedKg)) * 100 : 0}
|
|
title="Carbon Reduction"
|
|
unit="%"
|
|
/>
|
|
<Gauge
|
|
value={100 - ((data?.averageDistancePerEvent || 0) / 50) * 100}
|
|
title="Distance Efficiency"
|
|
unit="%"
|
|
/>
|
|
<Gauge
|
|
value={data?.eventsByMethod?.filter(m => m.efficiency >= 80).length
|
|
? (data.eventsByMethod.filter(m => m.efficiency >= 80).reduce((s, m) => s + m.count, 0) / data.totalEvents) * 100
|
|
: 0}
|
|
title="Green Transport"
|
|
unit="%"
|
|
/>
|
|
<Gauge
|
|
value={75}
|
|
title="Local Sourcing"
|
|
unit="%"
|
|
/>
|
|
</div>
|
|
|
|
{/* Charts */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
|
{data?.carbonTrend && (
|
|
<AreaChart
|
|
data={data.carbonTrend}
|
|
xKey="label"
|
|
yKey="value"
|
|
title="Carbon Emissions Trend"
|
|
colors={['#f59e0b']}
|
|
height={300}
|
|
/>
|
|
)}
|
|
|
|
{data?.eventsByMethod && (
|
|
<BarChart
|
|
data={data.eventsByMethod}
|
|
xKey="method"
|
|
yKey="carbonKg"
|
|
title="Carbon by Transport Method"
|
|
height={300}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* Event Types Pie Chart */}
|
|
{data?.eventsByType && (
|
|
<div className="mb-8">
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<PieChart
|
|
data={data.eventsByType}
|
|
dataKey="count"
|
|
nameKey="eventType"
|
|
title="Events by Type"
|
|
height={300}
|
|
/>
|
|
<PieChart
|
|
data={data.eventsByMethod}
|
|
dataKey="distanceKm"
|
|
nameKey="method"
|
|
title="Distance by Method"
|
|
height={300}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Tables */}
|
|
<div className="grid grid-cols-1 gap-6">
|
|
{data?.eventsByMethod && (
|
|
<DataTable
|
|
data={data.eventsByMethod}
|
|
columns={methodColumns}
|
|
title="Transport Method Breakdown"
|
|
pageSize={8}
|
|
/>
|
|
)}
|
|
|
|
{data?.mostEfficientRoutes && (
|
|
<DataTable
|
|
data={data.mostEfficientRoutes}
|
|
columns={routeColumns}
|
|
title="Most Efficient Routes"
|
|
pageSize={5}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|