turash/bugulma/frontend/pages/AnalyticsDashboard.tsx

535 lines
22 KiB
TypeScript

import { useMemo } from 'react';
import { ArrowUp, BarChart3, Clock, Target, TrendingUp, Users, Building2 } 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 Button from '@/components/ui/Button.tsx';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card.tsx';
import MetricItem from '@/components/ui/MetricItem.tsx';
import Spinner from '@/components/ui/Spinner.tsx';
import { Container, Flex, Grid, Stack } from '@/components/ui/layout';
import {
useConnectionStatistics,
useImpactMetrics,
useMatchingStatistics,
usePlatformStatistics,
useResourceFlowStatistics,
useSupplyDemandAnalysis,
} from '@/hooks/api/useAnalyticsAPI.ts';
import { useTranslation } from '@/hooks/useI18n.tsx';
import { useNavigation } from '@/hooks/useNavigation.tsx';
const AnalyticsDashboard = () => {
const { t } = useTranslation();
const { handleBackNavigation, handleFooterNavigate } = useNavigation();
// Analytics data
const { data: platformStats, isLoading: isLoadingPlatform } = usePlatformStatistics();
const { data: matchingStats, isLoading: isLoadingMatching } = useMatchingStatistics();
const { data: resourceFlowStats, isLoading: isLoadingResourceFlow } = useResourceFlowStatistics();
const { data: impactMetrics, isLoading: isLoadingImpact } = useImpactMetrics();
const { data: connectionStats, isLoading: isLoadingConnections } = useConnectionStatistics();
const { data: supplyDemand, isLoading: isLoadingSupplyDemand } = useSupplyDemandAnalysis();
const isLoading = isLoadingPlatform || isLoadingMatching || isLoadingResourceFlow ||
isLoadingImpact || isLoadingConnections || isLoadingSupplyDemand;
// Calculate derived metrics
const analytics = useMemo(() => {
const platform = platformStats || {};
const matching = matchingStats || {};
const resourceFlow = resourceFlowStats || {};
const impact = impactMetrics || {};
const connections = connectionStats || {};
const supplyDemandData = supplyDemand || {};
return {
// Platform overview
totalOrganizations: platform.total_organizations || 0,
totalSites: platform.total_sites || 0,
totalResourceFlows: platform.total_resource_flows || 0,
totalMatches: platform.total_matches || 0,
// Matching performance
matchSuccessRate: matching.match_success_rate || 0,
avgMatchTime: matching.avg_match_time_days || 0,
totalMatchValue: matching.total_match_value || 0,
topResourceTypes: matching.top_resource_types || [],
matchTrends: matching.match_trends || [],
// Resource flow analytics
flowsByType: resourceFlow.flows_by_type || {},
flowsBySector: resourceFlow.flows_by_sector || {},
avgFlowValue: resourceFlow.avg_flow_value || 0,
totalFlowVolume: resourceFlow.total_flow_volume || 0,
// Impact metrics
totalCo2Saved: impact.total_co2_saved_tonnes || 0,
totalEconomicValue: impact.total_economic_value || 0,
activeMatchesCount: impact.active_matches_count || 0,
environmentalBreakdown: impact.environmental_breakdown || {},
// Connection analytics
totalConnections: connections.total_connections || 0,
activeConnections: connections.active_connections || 0,
potentialConnections: connections.potential_connections || 0,
connectionRate: connections.connection_rate || 0,
// Supply/demand
topNeeds: supplyDemandData.top_needs || [],
topOffers: supplyDemandData.top_offers || [],
marketGaps: supplyDemandData.market_gaps || [],
};
}, [platformStats, matchingStats, resourceFlowStats, impactMetrics, connectionStats, supplyDemand]);
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);
};
const formatPercentage = (value: number) => {
return `${(value * 100).toFixed(1)}%`;
};
// Simple bar chart component using CSS
const SimpleBarChart = ({ data, title }: { data: Array<{ label: string; value: number; color?: string }>; title: string }) => {
const maxValue = Math.max(...data.map(d => d.value));
return (
<div className="space-y-2">
<h4 className="text-sm font-medium text-muted-foreground">{title}</h4>
<div className="space-y-2">
{data.map((item, index) => (
<div key={index} className="flex items-center gap-3">
<span className="text-sm min-w-16 truncate">{item.label}</span>
<div className="flex-1 bg-muted rounded-full h-2">
<div
className="h-2 rounded-full transition-all duration-500"
style={{
width: `${(item.value / maxValue) * 100}%`,
backgroundColor: item.color || 'hsl(var(--primary))',
}}
/>
</div>
<span className="text-sm font-medium min-w-12 text-right">
{typeof item.value === 'number' && item.value < 1
? formatPercentage(item.value)
: formatNumber(item.value)
}
</span>
</div>
))}
</div>
</div>
);
};
return (
<MainLayout onNavigate={handleFooterNavigate} className="bg-muted/30">
<Container size="2xl" className="py-8 sm:py-12">
<PageHeader
title={t('analyticsDashboard.title')}
subtitle={t('analyticsDashboard.subtitle')}
onBack={handleBackNavigation}
/>
<Stack spacing="2xl">
{/* Key Performance Indicators */}
<Grid cols={{ md: 2, lg: 4 }} gap="md">
<MetricItem
icon={<Building2 className="h-5 w-5 text-primary" />}
label={t('analyticsDashboard.totalOrganizations')}
value={formatNumber(analytics.totalOrganizations)}
/>
<MetricItem
icon={<Target className="h-4 h-5 text-current text-success w-4 w-5" />}
label={t('analyticsDashboard.totalResourceFlows')}
value={formatNumber(analytics.totalResourceFlows)}
/>
<MetricItem
icon={<Target className="h-4 h-5 text-current text-purple-500 w-4 w-5" />}
label={t('analyticsDashboard.totalMatches')}
value={formatNumber(analytics.totalMatches)}
/>
<MetricItem
icon={<TrendingUp className="h-4 h-5 text-current text-green-600 w-4 w-5" />}
label={t('analyticsDashboard.co2Saved')}
value={formatNumber(analytics.totalCo2Saved)}
/>
</Grid>
{/* Platform Overview */}
<Grid cols={{ md: 2, lg: 3 }} gap="lg">
{/* Matching Performance */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Target className="h-4 h-5 text-current w-4 w-5" />
{t('analyticsDashboard.matchingPerformance')}
</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="flex items-center justify-center py-8">
<Spinner className="h-6 w-6" />
</div>
) : (
<Stack spacing="md">
<div className="grid grid-cols-2 gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-success mb-1">
{formatPercentage(analytics.matchSuccessRate)}
</div>
<p className="text-xs text-muted-foreground">
{t('analyticsDashboard.successRate')}
</p>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-primary mb-1">
{analytics.avgMatchTime.toFixed(1)}
</div>
<p className="text-xs text-muted-foreground">
{t('analyticsDashboard.avgDays')}
</p>
</div>
</div>
<div>
<div className="text-sm font-medium mb-2">
{t('analyticsDashboard.totalMatchValue')}
</div>
<div className="text-lg font-bold text-success">
{formatCurrency(analytics.totalMatchValue)}
</div>
</div>
{analytics.topResourceTypes.length > 0 && (
<SimpleBarChart
data={analytics.topResourceTypes.slice(0, 5).map(item => ({
label: item.type,
value: item.count,
}))}
title={t('analyticsDashboard.topResourceTypes')}
/>
)}
</Stack>
)}
</CardContent>
</Card>
{/* Connection Analytics */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Users className="h-4 h-5 text-current w-4 w-5" />
{t('analyticsDashboard.connectionAnalytics')}
</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="flex items-center justify-center py-8">
<Spinner className="h-6 w-6" />
</div>
) : (
<Stack spacing="md">
<div className="grid grid-cols-3 gap-2 text-center">
<div>
<div className="text-lg font-bold text-primary">
{formatNumber(analytics.totalConnections)}
</div>
<p className="text-xs text-muted-foreground">
{t('analyticsDashboard.total')}
</p>
</div>
<div>
<div className="text-lg font-bold text-success">
{formatNumber(analytics.activeConnections)}
</div>
<p className="text-xs text-muted-foreground">
{t('analyticsDashboard.active')}
</p>
</div>
<div>
<div className="text-lg font-bold text-warning">
{formatNumber(analytics.potentialConnections)}
</div>
<p className="text-xs text-muted-foreground">
{t('analyticsDashboard.potential')}
</p>
</div>
</div>
<div>
<div className="text-sm font-medium mb-2">
{t('analyticsDashboard.connectionRate')}
</div>
<div className="text-lg font-bold">
{formatPercentage(analytics.connectionRate)}
</div>
</div>
</Stack>
)}
</CardContent>
</Card>
{/* Resource Flow Distribution */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Target className="h-4 h-5 text-current w-4 w-5" />
{t('analyticsDashboard.resourceFlowDistribution')}
</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="flex items-center justify-center py-8">
<Spinner className="h-6 w-6" />
</div>
) : (
<Stack spacing="md">
<div>
<div className="text-sm font-medium mb-2">
{t('analyticsDashboard.totalFlowVolume')}
</div>
<div className="text-lg font-bold">
{formatNumber(analytics.totalFlowVolume)}
</div>
</div>
<div>
<div className="text-sm font-medium mb-2">
{t('analyticsDashboard.avgFlowValue')}
</div>
<div className="text-lg font-bold text-success">
{formatCurrency(analytics.avgFlowValue)}
</div>
</div>
{Object.keys(analytics.flowsByType).length > 0 && (
<SimpleBarChart
data={Object.entries(analytics.flowsByType).slice(0, 5).map(([type, count]) => ({
label: type,
value: count as number,
}))}
title={t('analyticsDashboard.flowsByType')}
/>
)}
</Stack>
)}
</CardContent>
</Card>
</Grid>
{/* Environmental Impact */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<TrendingUp className="h-4 h-5 text-current w-4 w-5" />
{t('analyticsDashboard.environmentalImpact')}
</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="flex items-center justify-center py-8">
<Spinner className="h-6 w-6" />
</div>
) : (
<Grid cols={{ md: 2, lg: 4 }} gap="md">
<div className="text-center">
<div className="text-3xl font-bold text-green-600 mb-2">
{formatNumber(analytics.totalCo2Saved)}
</div>
<p className="text-sm text-muted-foreground">
{t('analyticsDashboard.tonnesCo2Saved')}
</p>
<Badge variant="outline" className="mt-2">
<ArrowUp className="h-3 h-4 mr-1 text-current w-3 w-4" />
{t('analyticsDashboard.perYear')}
</Badge>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-success mb-2">
{formatCurrency(analytics.totalEconomicValue)}
</div>
<p className="text-sm text-muted-foreground">
{t('analyticsDashboard.economicValueCreated')}
</p>
<Badge variant="outline" className="mt-2">
<ArrowUp className="h-3 h-4 mr-1 text-current w-3 w-4" />
{t('analyticsDashboard.annual')}
</Badge>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-blue-600 mb-2">
{formatNumber(analytics.activeMatchesCount)}
</div>
<p className="text-sm text-muted-foreground">
{t('analyticsDashboard.activeMatches')}
</p>
<Badge variant="outline" className="mt-2">
<Clock className="h-3 h-4 mr-1 text-current w-3 w-4" />
{t('analyticsDashboard.operational')}
</Badge>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-purple-600 mb-2">
{Object.keys(analytics.environmentalBreakdown).length}
</div>
<p className="text-sm text-muted-foreground">
{t('analyticsDashboard.impactCategories')}
</p>
<Badge variant="outline" className="mt-2">
<BarChart3 className="h-3 h-4 mr-1 text-current w-3 w-4" />
{t('analyticsDashboard.tracked')}
</Badge>
</div>
</Grid>
)}
</CardContent>
</Card>
{/* Supply & Demand Analysis */}
<Grid cols={{ md: 2 }} gap="lg">
{/* Top Needs */}
<Card>
<CardHeader>
<CardTitle>{t('analyticsDashboard.topNeeds')}</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="flex items-center justify-center py-8">
<Spinner className="h-6 w-6" />
</div>
) : analytics.topNeeds.length > 0 ? (
<div className="space-y-3">
{analytics.topNeeds.slice(0, 8).map((need: any, index: number) => (
<div key={index} className="flex items-center justify-between p-3 bg-muted/50 rounded-lg">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-red-100 flex items-center justify-center">
<span className="text-xs font-bold text-red-600">{index + 1}</span>
</div>
<div>
<p className="font-medium">{need.item}</p>
<p className="text-xs text-muted-foreground">{need.sector}</p>
</div>
</div>
<Badge variant="secondary">
{need.count} {t('analyticsDashboard.requests')}
</Badge>
</div>
))}
</div>
) : (
<div className="text-center py-8 text-muted-foreground">
<p>{t('analyticsDashboard.noNeedsData')}</p>
</div>
)}
</CardContent>
</Card>
{/* Top Offers */}
<Card>
<CardHeader>
<CardTitle>{t('analyticsDashboard.topOffers')}</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="flex items-center justify-center py-8">
<Spinner className="h-6 w-6" />
</div>
) : analytics.topOffers.length > 0 ? (
<div className="space-y-3">
{analytics.topOffers.slice(0, 8).map((offer: any, index: number) => (
<div key={index} className="flex items-center justify-between p-3 bg-muted/50 rounded-lg">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-green-100 flex items-center justify-center">
<span className="text-xs font-bold text-green-600">{index + 1}</span>
</div>
<div>
<p className="font-medium">{offer.item}</p>
<p className="text-xs text-muted-foreground">{offer.sector}</p>
</div>
</div>
<Badge variant="default">
{offer.count} {t('analyticsDashboard.offers')}
</Badge>
</div>
))}
</div>
) : (
<div className="text-center py-8 text-muted-foreground">
<p>{t('analyticsDashboard.noOffersData')}</p>
</div>
)}
</CardContent>
</Card>
</Grid>
{/* Market Gaps */}
{analytics.marketGaps.length > 0 && (
<Card>
<CardHeader>
<CardTitle>{t('analyticsDashboard.marketGaps')}</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{analytics.marketGaps.slice(0, 6).map((gap: any, index: number) => (
<div key={index} className="p-4 border rounded-lg">
<div className="flex items-center gap-2 mb-2">
<TrendingUp className="h-4 text-current text-warning w-4" />
<span className="font-medium">{gap.resource_type}</span>
</div>
<p className="text-sm text-muted-foreground mb-2">{gap.description}</p>
<div className="flex items-center justify-between">
<Badge variant="outline">
{gap.severity} {t('analyticsDashboard.gap')}
</Badge>
<span className="text-xs text-muted-foreground">
{gap.potential_matches} {t('analyticsDashboard.potentialMatches')}
</span>
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* Export Actions */}
<Card>
<CardHeader>
<CardTitle>{t('analyticsDashboard.exportData')}</CardTitle>
</CardHeader>
<CardContent>
<Flex gap="md" wrap>
<Button variant="outline">
{t('analyticsDashboard.exportPDF')}
</Button>
<Button variant="outline">
{t('analyticsDashboard.exportCSV')}
</Button>
<Button variant="outline">
{t('analyticsDashboard.scheduleReports')}
</Button>
</Flex>
</CardContent>
</Card>
</Stack>
</Container>
</MainLayout>
);
};
export default AnalyticsDashboard;