mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
- Remove nested git repository from bugulma/frontend/.git - Add all frontend files to main repository tracking - Convert from separate frontend/backend repos to unified monorepo - Preserve all frontend code and development history as tracked files - Eliminate nested repository complexity for simpler development workflow This creates a proper monorepo structure with frontend and backend coexisting in the same repository for easier development and deployment.
535 lines
23 KiB
TypeScript
535 lines
23 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', 'Analytics Dashboard')}
|
|
subtitle={t('analyticsDashboard.subtitle', 'Deep insights into platform performance and impact')}
|
|
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', 'Total Organizations')}
|
|
value={formatNumber(analytics.totalOrganizations)}
|
|
/>
|
|
<MetricItem
|
|
icon={<Target className="h-4 h-5 text-current text-success w-4 w-5" />}
|
|
label={t('analyticsDashboard.totalResourceFlows', 'Resource Flows')}
|
|
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', 'Total Matches')}
|
|
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', 'CO₂ Saved (t)')}
|
|
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', 'Matching Performance')}
|
|
</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', 'Success Rate')}
|
|
</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', 'Avg Days')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="text-sm font-medium mb-2">
|
|
{t('analyticsDashboard.totalMatchValue', 'Total Match Value')}
|
|
</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', 'Top Resource Types')}
|
|
/>
|
|
)}
|
|
</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', 'Connection Analytics')}
|
|
</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', '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', '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', 'Potential')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="text-sm font-medium mb-2">
|
|
{t('analyticsDashboard.connectionRate', 'Connection Rate')}
|
|
</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', 'Resource Flow Distribution')}
|
|
</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', 'Total Flow Volume')}
|
|
</div>
|
|
<div className="text-lg font-bold">
|
|
{formatNumber(analytics.totalFlowVolume)}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="text-sm font-medium mb-2">
|
|
{t('analyticsDashboard.avgFlowValue', 'Average Flow Value')}
|
|
</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', 'Flows by Type')}
|
|
/>
|
|
)}
|
|
</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', 'Environmental Impact')}
|
|
</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', 'Tonnes CO₂ Saved')}
|
|
</p>
|
|
<Badge variant="outline" className="mt-2">
|
|
<ArrowUp className="h-3 h-4 mr-1 text-current w-3 w-4" />
|
|
{t('analyticsDashboard.perYear', 'per year')}
|
|
</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', 'Economic Value Created')}
|
|
</p>
|
|
<Badge variant="outline" className="mt-2">
|
|
<ArrowUp className="h-3 h-4 mr-1 text-current w-3 w-4" />
|
|
{t('analyticsDashboard.annual', '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', 'Active Matches')}
|
|
</p>
|
|
<Badge variant="outline" className="mt-2">
|
|
<Clock className="h-3 h-4 mr-1 text-current w-3 w-4" />
|
|
{t('analyticsDashboard.operational', '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', 'Impact Categories')}
|
|
</p>
|
|
<Badge variant="outline" className="mt-2">
|
|
<BarChart3 className="h-3 h-4 mr-1 text-current w-3 w-4" />
|
|
{t('analyticsDashboard.tracked', 'tracked')}
|
|
</Badge>
|
|
</div>
|
|
</Grid>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Supply & Demand Analysis */}
|
|
<Grid cols={{ md: 2 }} gap="lg">
|
|
{/* Top Needs */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{t('analyticsDashboard.topNeeds', 'Top Resource Needs')}</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', 'requests')}
|
|
</Badge>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
<p>{t('analyticsDashboard.noNeedsData', 'No needs data available')}</p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Top Offers */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{t('analyticsDashboard.topOffers', 'Top Resource Offers')}</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', 'offers')}
|
|
</Badge>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
<p>{t('analyticsDashboard.noOffersData', 'No offers data available')}</p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
|
|
{/* Market Gaps */}
|
|
{analytics.marketGaps.length > 0 && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{t('analyticsDashboard.marketGaps', 'Market Gaps & Opportunities')}</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', 'gap')}
|
|
</Badge>
|
|
<span className="text-xs text-muted-foreground">
|
|
{gap.potential_matches} {t('analyticsDashboard.potentialMatches', 'potential matches')}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Export Actions */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{t('analyticsDashboard.exportData', 'Export Analytics Data')}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Flex gap="md" wrap>
|
|
<Button variant="outline">
|
|
{t('analyticsDashboard.exportPDF', 'Export PDF Report')}
|
|
</Button>
|
|
<Button variant="outline">
|
|
{t('analyticsDashboard.exportCSV', 'Export CSV Data')}
|
|
</Button>
|
|
<Button variant="outline">
|
|
{t('analyticsDashboard.scheduleReports', 'Schedule Reports')}
|
|
</Button>
|
|
</Flex>
|
|
</CardContent>
|
|
</Card>
|
|
</Stack>
|
|
</Container>
|
|
</MainLayout>
|
|
);
|
|
};
|
|
|
|
export default AnalyticsDashboard;
|