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
315 lines
12 KiB
TypeScript
315 lines
12 KiB
TypeScript
/**
|
|
* Sustainability Analytics Page
|
|
* Environmental impact and sustainability metrics
|
|
*/
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import Link from 'next/link';
|
|
import {
|
|
KPICard,
|
|
DateRangePicker,
|
|
LineChart,
|
|
AreaChart,
|
|
Gauge,
|
|
DataTable,
|
|
TrendIndicator,
|
|
} from '../../components/analytics';
|
|
import { TimeRange, SustainabilityAnalytics } from '../../lib/analytics/types';
|
|
|
|
export default function SustainabilityAnalyticsPage() {
|
|
const [timeRange, setTimeRange] = useState<TimeRange>('30d');
|
|
const [loading, setLoading] = useState(true);
|
|
const [data, setData] = useState<SustainabilityAnalytics | null>(null);
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, [timeRange]);
|
|
|
|
const fetchData = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const response = await fetch(`/api/analytics/sustainability?timeRange=${timeRange}`);
|
|
const result = await response.json();
|
|
setData(result.data);
|
|
} catch (error) {
|
|
console.error('Failed to fetch sustainability analytics:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const goalColumns = [
|
|
{ key: 'name', header: 'Goal' },
|
|
{ key: 'current', header: 'Current', align: 'right' as const, render: (v: number, row: any) => `${v} ${row.unit}` },
|
|
{ key: 'target', header: 'Target', align: 'right' as const, render: (v: number, row: any) => `${v} ${row.unit}` },
|
|
{
|
|
key: 'progress',
|
|
header: 'Progress',
|
|
align: 'right' as const,
|
|
render: (v: number) => (
|
|
<div className="flex items-center space-x-2">
|
|
<div className="w-20 bg-gray-200 rounded-full h-2">
|
|
<div
|
|
className={`h-2 rounded-full ${v >= 90 ? 'bg-green-500' : v >= 70 ? 'bg-yellow-500' : 'bg-red-500'}`}
|
|
style={{ width: `${Math.min(v, 100)}%` }}
|
|
/>
|
|
</div>
|
|
<span className="text-sm">{v}%</span>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: 'status',
|
|
header: 'Status',
|
|
render: (v: string) => (
|
|
<span
|
|
className={`px-2 py-1 text-xs rounded-full ${
|
|
v === 'on_track'
|
|
? 'bg-green-100 text-green-700'
|
|
: v === 'at_risk'
|
|
? 'bg-yellow-100 text-yellow-700'
|
|
: 'bg-red-100 text-red-700'
|
|
}`}
|
|
>
|
|
{v.replace('_', ' ')}
|
|
</span>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100">
|
|
{/* Header */}
|
|
<div className="bg-gradient-to-r from-green-700 to-emerald-700 text-white">
|
|
<div className="max-w-7xl mx-auto px-4 py-8">
|
|
<h1 className="text-3xl font-bold">Sustainability Analytics</h1>
|
|
<p className="text-green-200 mt-1">Environmental impact and sustainability metrics</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-white hover:bg-gray-50 text-gray-700 rounded-lg font-medium shadow">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-green-700 text-white rounded-lg font-medium">Sustainability</a>
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Overall Score */}
|
|
<div className="bg-gradient-to-r from-green-500 to-emerald-500 rounded-lg p-8 mb-8 text-white">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h2 className="text-2xl font-bold">Overall Sustainability Score</h2>
|
|
<p className="text-green-100 mt-1">Based on carbon, local production, water, and waste metrics</p>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-6xl font-bold">{data?.overallScore?.toFixed(0) || 0}</div>
|
|
<div className="text-green-100">out of 100</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* KPI Cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
|
<KPICard
|
|
title="Carbon Saved"
|
|
value={data?.carbonFootprint?.totalSavedKg?.toFixed(0) || '0'}
|
|
unit="kg CO2"
|
|
trend="up"
|
|
color="green"
|
|
loading={loading}
|
|
/>
|
|
<KPICard
|
|
title="Reduction Rate"
|
|
value={data?.carbonFootprint?.reductionPercentage?.toFixed(0) || '0'}
|
|
unit="%"
|
|
trend="up"
|
|
color="teal"
|
|
loading={loading}
|
|
/>
|
|
<KPICard
|
|
title="Local Production"
|
|
value={data?.localProduction?.percentage?.toFixed(0) || '0'}
|
|
unit="%"
|
|
trend="up"
|
|
color="blue"
|
|
loading={loading}
|
|
/>
|
|
<KPICard
|
|
title="Water Efficiency"
|
|
value={data?.waterUsage?.efficiencyScore?.toFixed(0) || '0'}
|
|
unit="%"
|
|
trend="up"
|
|
color="purple"
|
|
loading={loading}
|
|
/>
|
|
</div>
|
|
|
|
{/* Gauges */}
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 mb-8">
|
|
<Gauge
|
|
value={data?.carbonFootprint?.reductionPercentage || 0}
|
|
title="Carbon Reduction"
|
|
unit="%"
|
|
/>
|
|
<Gauge
|
|
value={data?.foodMiles?.localPercentage || 0}
|
|
title="Local Sourcing"
|
|
unit="%"
|
|
/>
|
|
<Gauge
|
|
value={data?.waterUsage?.efficiencyScore || 0}
|
|
title="Water Efficiency"
|
|
unit="%"
|
|
/>
|
|
<Gauge
|
|
value={data?.overallScore || 0}
|
|
title="Sustainability Score"
|
|
unit="pts"
|
|
/>
|
|
</div>
|
|
|
|
{/* Impact Metrics */}
|
|
<div className="bg-white rounded-lg shadow-lg p-6 mb-8">
|
|
<h3 className="text-lg font-bold text-gray-900 mb-4">Environmental Impact</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
|
<div className="text-center p-4 bg-green-50 rounded-lg">
|
|
<p className="text-3xl font-bold text-green-600">
|
|
{data?.carbonFootprint?.equivalentTrees?.toFixed(1) || 0}
|
|
</p>
|
|
<p className="text-sm text-gray-500">Trees Equivalent</p>
|
|
<p className="text-xs text-gray-400 mt-1">CO2 absorption per year</p>
|
|
</div>
|
|
<div className="text-center p-4 bg-blue-50 rounded-lg">
|
|
<p className="text-3xl font-bold text-blue-600">
|
|
{data?.foodMiles?.savedMiles?.toLocaleString() || 0}
|
|
</p>
|
|
<p className="text-sm text-gray-500">Miles Saved</p>
|
|
<p className="text-xs text-gray-400 mt-1">vs conventional transport</p>
|
|
</div>
|
|
<div className="text-center p-4 bg-cyan-50 rounded-lg">
|
|
<p className="text-3xl font-bold text-cyan-600">
|
|
{data?.waterUsage?.savedLiters?.toLocaleString() || 0}
|
|
</p>
|
|
<p className="text-sm text-gray-500">Liters Saved</p>
|
|
<p className="text-xs text-gray-400 mt-1">Water conservation</p>
|
|
</div>
|
|
<div className="text-center p-4 bg-purple-50 rounded-lg">
|
|
<p className="text-3xl font-bold text-purple-600">
|
|
{data?.localProduction?.localCount || 0}
|
|
</p>
|
|
<p className="text-sm text-gray-500">Local Plants</p>
|
|
<p className="text-xs text-gray-400 mt-1">Within 50km radius</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Charts */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
|
{data?.carbonFootprint?.monthlyTrend && (
|
|
<AreaChart
|
|
data={data.carbonFootprint.monthlyTrend}
|
|
xKey="label"
|
|
yKey="value"
|
|
title="Carbon Emissions Trend"
|
|
colors={['#10b981']}
|
|
height={300}
|
|
/>
|
|
)}
|
|
|
|
{data?.foodMiles?.monthlyTrend && (
|
|
<AreaChart
|
|
data={data.foodMiles.monthlyTrend}
|
|
xKey="label"
|
|
yKey="value"
|
|
title="Food Miles Trend"
|
|
colors={['#3b82f6']}
|
|
height={300}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* Sustainability Trends */}
|
|
{data?.trends && data.trends.length > 0 && (
|
|
<div className="mb-8">
|
|
<LineChart
|
|
data={data.trends[0].values}
|
|
xKey="label"
|
|
yKey="value"
|
|
title="Sustainability Trend Over Time"
|
|
colors={['#10b981', '#3b82f6']}
|
|
height={250}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Goals Table */}
|
|
{data?.goals && (
|
|
<DataTable
|
|
data={data.goals}
|
|
columns={goalColumns}
|
|
title="Sustainability Goals Progress"
|
|
pageSize={10}
|
|
showSearch={false}
|
|
/>
|
|
)}
|
|
|
|
{/* Tips Section */}
|
|
<div className="mt-8 bg-gradient-to-r from-green-50 to-emerald-50 rounded-lg border border-green-200 p-6">
|
|
<h3 className="text-lg font-bold text-gray-900 mb-4">Sustainability Recommendations</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="flex items-start space-x-3">
|
|
<span className="text-green-500 mt-1">
|
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
</span>
|
|
<p className="text-sm text-gray-600">Increase local sourcing to reduce transport emissions</p>
|
|
</div>
|
|
<div className="flex items-start space-x-3">
|
|
<span className="text-green-500 mt-1">
|
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
</span>
|
|
<p className="text-sm text-gray-600">Use electric or bicycle delivery for short distances</p>
|
|
</div>
|
|
<div className="flex items-start space-x-3">
|
|
<span className="text-green-500 mt-1">
|
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
</span>
|
|
<p className="text-sm text-gray-600">Optimize water usage in vertical farming operations</p>
|
|
</div>
|
|
<div className="flex items-start space-x-3">
|
|
<span className="text-green-500 mt-1">
|
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
</span>
|
|
<p className="text-sm text-gray-600">Consolidate deliveries to reduce carbon footprint</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|