mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
535 lines
22 KiB
TypeScript
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;
|