turash/bugulma/frontend/pages/ImpactMetrics.tsx

482 lines
20 KiB
TypeScript

import { useMemo } from 'react';
import { Award, Briefcase, DollarSign, Globe, Target, TrendingUp, Zap } from 'lucide-react';
import { MainLayout } from '@/components/layout/MainLayout.tsx';
import PageHeader from '@/components/layout/PageHeader.tsx';
import Badge from '@/components/ui/Badge.tsx';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card.tsx';
import Spinner from '@/components/ui/Spinner.tsx';
import { Container, Flex, Grid, Stack } from '@/components/ui/layout';
import { useImpactMetrics, usePlatformStatistics } from '@/hooks/api/useAnalyticsAPI.ts';
import { useTranslation } from '@/hooks/useI18n.tsx';
import { useNavigation } from '@/hooks/useNavigation.tsx';
const ImpactMetrics = () => {
const { t } = useTranslation();
const { handleBackNavigation, handleFooterNavigate } = useNavigation();
const { data: impactMetrics, isLoading: isLoadingImpact } = useImpactMetrics();
const { data: platformStats, isLoading: isLoadingPlatform } = usePlatformStatistics();
const isLoading = isLoadingImpact || isLoadingPlatform;
// Process impact data
const impact = useMemo(() => {
const data = impactMetrics || {};
const platform = platformStats || {};
return {
// Core impact metrics
totalCo2Saved: data.total_co2_saved_tonnes || 0,
totalEconomicValue: data.total_economic_value || 0,
activeMatchesCount: data.active_matches_count || 0,
totalOrganizations: platform.total_organizations || 0,
// Environmental breakdown
environmentalBreakdown: data.environmental_breakdown || {},
co2BySector: data.co2_by_sector || {},
co2ByResourceType: data.co2_by_resource_type || {},
// Economic metrics
economicBreakdown: data.economic_breakdown || {},
avgValuePerMatch: data.total_economic_value && data.active_matches_count
? data.total_economic_value / data.active_matches_count
: 0,
// Impact over time
monthlyImpact: data.monthly_impact || [],
yearlyProjections: data.yearly_projections || {},
// Resource-specific impacts
resourceImpacts: data.resource_impacts || [],
topImpactingMatches: data.top_impacting_matches || [],
};
}, [impactMetrics, platformStats]);
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value);
};
const formatNumber = (value: number) => {
return new Intl.NumberFormat('en-US').format(value);
};
// Simple visualization component for impact breakdown
const ImpactBreakdownChart = ({
data,
title,
color = 'hsl(var(--primary))'
}: {
data: Record<string, number>;
title: string;
color?: string;
}) => {
const entries = Object.entries(data);
const maxValue = Math.max(...entries.map(([_, value]) => value));
return (
<div className="space-y-3">
<h4 className="text-sm font-medium text-muted-foreground">{title}</h4>
<div className="space-y-2">
{entries.map(([key, value]) => (
<div key={key} className="flex items-center gap-3">
<span className="text-sm min-w-20 truncate capitalize">{key.replace('_', ' ')}</span>
<div className="flex-1 bg-muted rounded-full h-3">
<div
className="h-3 rounded-full transition-all duration-500"
style={{
width: `${(value / maxValue) * 100}%`,
backgroundColor: color,
}}
/>
</div>
<span className="text-sm font-medium min-w-16 text-right">
{formatNumber(value)}
</span>
</div>
))}
</div>
</div>
);
};
// Impact category icons
const getCategoryIcon = (category: string) => {
switch (category.toLowerCase()) {
case 'energy':
case 'electricity':
case 'heat':
return <Zap className="h-4 h-5 text-current text-yellow-500 w-4 w-5" />;
case 'transport':
case 'logistics':
return <Target className="h-4 h-5 text-blue-500 text-current w-4 w-5" />;
case 'industrial':
case 'manufacturing':
return <Briefcase className="h-4 h-5 text-current text-gray-600 w-4 w-5" />;
case 'buildings':
case 'construction':
return <Briefcase className="h-4 h-5 text-current text-green-600 w-4 w-5" />;
case 'waste':
case 'biowaste':
return <Globe className="h-4 h-5 text-brown-500 text-current w-4 w-5" />;
default:
return <Globe className="h-4 h-5 text-current text-primary w-4 w-5" />;
}
};
const getCategoryColor = (category: string) => {
switch (category.toLowerCase()) {
case 'energy':
case 'electricity':
case 'heat':
return 'text-yellow-600 bg-yellow-50 border-yellow-200';
case 'transport':
case 'logistics':
return 'text-blue-600 bg-blue-50 border-blue-200';
case 'industrial':
case 'manufacturing':
return 'text-gray-600 bg-gray-50 border-gray-200';
case 'buildings':
case 'construction':
return 'text-green-600 bg-green-50 border-green-200';
case 'waste':
case 'biowaste':
return 'text-amber-600 bg-amber-50 border-amber-200';
default:
return 'text-primary bg-primary/5 border-primary/20';
}
};
return (
<MainLayout onNavigate={handleFooterNavigate} className="bg-muted/30">
<Container size="2xl" className="py-8 sm:py-12">
<PageHeader
title={t('impactMetrics.title')}
subtitle={t('impactMetrics.subtitle')}
onBack={handleBackNavigation}
/>
<Stack spacing="2xl">
{/* Key Impact Indicators */}
<Grid cols={{ md: 2, lg: 4 }} gap="md">
<Card className="border-green-200 bg-green-50/50">
<CardContent className="p-6">
<Flex align="center" gap="md" className="mb-4">
<div className="p-3 rounded-full bg-green-100">
<TrendingUp className="h-4 h-6 text-current text-green-600 w-4 w-6" />
</div>
<div>
<p className="text-sm font-medium text-green-700">
{t('impactMetrics.co2Saved')}
</p>
<p className="text-2xl font-bold text-green-800">
{formatNumber(impact.totalCo2Saved)}
</p>
</div>
</Flex>
<Badge variant="outline" className="border-green-300 text-green-700">
{t('impactMetrics.tonnesPerYear')}
</Badge>
</CardContent>
</Card>
<Card className="border-blue-200 bg-blue-50/50">
<CardContent className="p-6">
<Flex align="center" gap="md" className="mb-4">
<div className="p-3 rounded-full bg-blue-100">
<DollarSign className="h-4 h-6 text-blue-600 text-current w-4 w-6" />
</div>
<div>
<p className="text-sm font-medium text-blue-700">
{t('impactMetrics.economicValue')}
</p>
<p className="text-2xl font-bold text-blue-800">
{formatCurrency(impact.totalEconomicValue)}
</p>
</div>
</Flex>
<Badge variant="outline" className="border-blue-300 text-blue-700">
{t('impactMetrics.createdAnnually')}
</Badge>
</CardContent>
</Card>
<Card className="border-purple-200 bg-purple-50/50">
<CardContent className="p-6">
<Flex align="center" gap="md" className="mb-4">
<div className="p-3 rounded-full bg-purple-100">
<Target className="h-4 h-6 text-current text-purple-600 w-4 w-6" />
</div>
<div>
<p className="text-sm font-medium text-purple-700">
{t('impactMetrics.activeMatches')}
</p>
<p className="text-2xl font-bold text-purple-800">
{formatNumber(impact.activeMatchesCount)}
</p>
</div>
</Flex>
<Badge variant="outline" className="border-purple-300 text-purple-700">
{t('impactMetrics.operational')}
</Badge>
</CardContent>
</Card>
<Card className="border-amber-200 bg-amber-50/50">
<CardContent className="p-6">
<Flex align="center" gap="md" className="mb-4">
<div className="p-3 rounded-full bg-amber-100">
<Award className="h-4 h-6 text-amber-600 text-current w-4 w-6" />
</div>
<div>
<p className="text-sm font-medium text-amber-700">
{t('impactMetrics.avgValuePerMatch')}
</p>
<p className="text-2xl font-bold text-amber-800">
{formatCurrency(impact.avgValuePerMatch)}
</p>
</div>
</Flex>
<Badge variant="outline" className="border-amber-300 text-amber-700">
{t('impactMetrics.perMatch')}
</Badge>
</CardContent>
</Card>
</Grid>
{/* Environmental Impact Breakdown */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<TrendingUp className="h-4 h-5 text-current w-4 w-5" />
{t('impactMetrics.environmentalBreakdown')}
</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="flex items-center justify-center py-8">
<Spinner className="h-6 w-6" />
</div>
) : Object.keys(impact.environmentalBreakdown).length > 0 ? (
<Grid cols={{ md: 2 }} gap="lg">
<div>
<ImpactBreakdownChart
data={impact.environmentalBreakdown}
title={t('impactMetrics.byCategory')}
color="hsl(142, 71%, 45%)"
/>
</div>
<div>
<ImpactBreakdownChart
data={impact.co2BySector}
title={t('impactMetrics.bySector')}
color="hsl(221, 83%, 53%)"
/>
</div>
</Grid>
) : (
<div className="text-center py-8 text-muted-foreground">
<TrendingUp className="h-12 h-4 mb-4 mx-auto opacity-50 text-current w-12 w-4" />
<p>{t('impactMetrics.noEnvironmentalData')}</p>
</div>
)}
</CardContent>
</Card>
{/* Economic Impact Breakdown */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<DollarSign className="h-4 h-5 text-current w-4 w-5" />
{t('impactMetrics.economicBreakdown')}
</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="flex items-center justify-center py-8">
<Spinner className="h-6 w-6" />
</div>
) : Object.keys(impact.economicBreakdown).length > 0 ? (
<ImpactBreakdownChart
data={impact.economicBreakdown}
title={t('impactMetrics.byEconomicCategory')}
color="hsl(217, 91%, 60%)"
/>
) : (
<div className="text-center py-8 text-muted-foreground">
<DollarSign className="h-12 h-4 mb-4 mx-auto opacity-50 text-current w-12 w-4" />
<p>{t('impactMetrics.noEconomicData')}</p>
</div>
)}
</CardContent>
</Card>
{/* Top Impacting Matches */}
{impact.topImpactingMatches.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Award className="h-4 h-5 text-current w-4 w-5" />
{t('impactMetrics.topImpactingMatches')}
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{impact.topImpactingMatches.slice(0, 5).map((match: any, index: number) => (
<div key={index} className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center gap-4">
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary/10 text-primary font-bold text-sm">
{index + 1}
</div>
<div>
<p className="font-medium">{match.description || `Match ${match.id}`}</p>
<p className="text-sm text-muted-foreground">{match.resource_type}</p>
</div>
</div>
<div className="text-right">
<p className="font-semibold text-green-600">
{formatNumber(match.co2_impact || 0)} t CO
</p>
<p className="text-sm text-muted-foreground">
{formatCurrency(match.economic_impact || 0)}
</p>
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* Impact Categories Overview */}
<Card>
<CardHeader>
<CardTitle>{t('impactMetrics.impactCategoriesOverview')}</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Object.entries(impact.environmentalBreakdown).map(([category, value]) => (
<div
key={category}
className={`p-4 rounded-lg border ${getCategoryColor(category)}`}
>
<Flex align="center" gap="sm" className="mb-3">
{getCategoryIcon(category)}
<span className="font-medium capitalize">{category.replace('_', ' ')}</span>
</Flex>
<div className="text-2xl font-bold mb-1">
{formatNumber(value)}
</div>
<p className="text-sm opacity-75">
{t('impactMetrics.tonnesCo2Reduced')}
</p>
</div>
))}
</div>
</CardContent>
</Card>
{/* Projections and Goals */}
<Grid cols={{ md: 2 }} gap="lg">
{/* Yearly Projections */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<TrendingUp className="h-4 h-5 text-current w-4 w-5" />
{t('impactMetrics.yearlyProjections')}
</CardTitle>
</CardHeader>
<CardContent>
{Object.keys(impact.yearlyProjections).length > 0 ? (
<div className="space-y-3">
{Object.entries(impact.yearlyProjections).map(([year, projection]: [string, any]) => (
<div key={year} className="flex justify-between items-center p-3 bg-muted/50 rounded">
<span className="font-medium">{year}</span>
<div className="text-right">
<div className="font-semibold text-green-600">
{formatNumber(projection.co2_projected || 0)} t CO
</div>
<div className="text-sm text-muted-foreground">
{formatCurrency(projection.economic_projected || 0)}
</div>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8 text-muted-foreground">
<TrendingUp className="h-12 h-4 mb-4 mx-auto opacity-50 text-current w-12 w-4" />
<p>{t('impactMetrics.noProjectionData')}</p>
</div>
)}
</CardContent>
</Card>
{/* Impact Achievements */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Award className="h-4 h-5 text-current w-4 w-5" />
{t('impactMetrics.achievements')}
</CardTitle>
</CardHeader>
<CardContent>
<Stack spacing="md">
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
<div className="flex items-center gap-3 mb-2">
<TrendingUp className="h-4 h-5 text-current text-green-600 w-4 w-5" />
<span className="font-medium text-green-800">
{t('impactMetrics.carbonReduction')}
</span>
</div>
<p className="text-sm text-green-700">
{impact.totalCo2Saved >= 1000
? t('impactMetrics.majorContributor')
: t('impactMetrics.growingImpact')
}
</p>
</div>
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-center gap-3 mb-2">
<DollarSign className="h-4 h-5 text-blue-600 text-current w-4 w-5" />
<span className="font-medium text-blue-800">
{t('impactMetrics.economicGrowth')}
</span>
</div>
<p className="text-sm text-blue-700">
{impact.totalEconomicValue >= 1000000
? t('impactMetrics.significantEconomicValue')
: t('impactMetrics.economicBenefits')
}
</p>
</div>
<div className="p-4 bg-purple-50 border border-purple-200 rounded-lg">
<div className="flex items-center gap-3 mb-2">
<Target className="h-4 h-5 text-current text-purple-600 w-4 w-5" />
<span className="font-medium text-purple-800">
{t('impactMetrics.networkGrowth')}
</span>
</div>
<p className="text-sm text-purple-700">
{t('impactMetrics.activeConnections')}
.replace('{{count}}', impact.activeMatchesCount.toString())
</p>
</div>
</Stack>
</CardContent>
</Card>
</Grid>
</Stack>
</Container>
</MainLayout>
);
};
export default ImpactMetrics;