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.
398 lines
17 KiB
TypeScript
398 lines
17 KiB
TypeScript
import { useMemo } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { BarChart3, Briefcase, DollarSign, MapPin, Plus, Search, Target, TrendingUp, Users, Leaf } 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 { useAuth } from '@/contexts/AuthContext.tsx';
|
|
import {
|
|
useDashboardStatistics,
|
|
useImpactMetrics,
|
|
useMatchingStatistics,
|
|
usePlatformStatistics
|
|
} from '@/hooks/api/useAnalyticsAPI.ts';
|
|
import { useProposals } from '@/hooks/api/useProposalsAPI.ts';
|
|
import { useTranslation } from '@/hooks/useI18n.tsx';
|
|
import { useNavigation } from '@/hooks/useNavigation.tsx';
|
|
import { useOrganizations } from '@/hooks/useOrganizations.ts';
|
|
|
|
const DashboardPage = () => {
|
|
const { t } = useTranslation();
|
|
const navigate = useNavigate();
|
|
const { handleBackNavigation, handleFooterNavigate } = useNavigation();
|
|
const { user } = useAuth();
|
|
|
|
// Analytics data
|
|
const { data: dashboardStats, isLoading: isLoadingDashboard } = useDashboardStatistics();
|
|
const { data: platformStats, isLoading: isLoadingPlatform } = usePlatformStatistics();
|
|
const { data: matchingStats, isLoading: isLoadingMatching } = useMatchingStatistics();
|
|
const { data: impactMetrics, isLoading: isLoadingImpact } = useImpactMetrics();
|
|
|
|
// User-specific data
|
|
const { data: proposalsData } = useProposals();
|
|
const { organizations } = useOrganizations();
|
|
|
|
// Calculate derived statistics
|
|
const stats = useMemo(() => {
|
|
const dashboard = dashboardStats || {};
|
|
const platform = platformStats || {};
|
|
const matching = matchingStats || {};
|
|
const impact = impactMetrics || {};
|
|
|
|
// Safely handle proposals data
|
|
const proposals: any[] = Array.isArray(proposalsData?.proposals)
|
|
? proposalsData.proposals
|
|
: [];
|
|
const pendingProposals = proposals.filter((p: any) => p?.status === 'pending');
|
|
|
|
return {
|
|
totalOrganizations: dashboard.total_organizations || platform.total_organizations || 0,
|
|
totalSites: dashboard.total_sites || platform.total_sites || 0,
|
|
totalResourceFlows: dashboard.total_resource_flows || platform.total_resource_flows || 0,
|
|
totalMatches: dashboard.total_matches || platform.total_matches || 0,
|
|
activeProposals: dashboard.active_proposals || pendingProposals.length || 0,
|
|
recentActivity: dashboard.recent_activity || [],
|
|
|
|
// Matching statistics
|
|
matchSuccessRate: matching.match_success_rate || 0,
|
|
avgMatchTime: matching.avg_match_time_days || 0,
|
|
topResourceTypes: matching.top_resource_types || [],
|
|
|
|
// Impact metrics
|
|
totalCo2Saved: impact.total_co2_saved_tonnes || 0,
|
|
totalEconomicValue: impact.total_economic_value || 0,
|
|
activeMatches: impact.active_matches_count || 0,
|
|
};
|
|
}, [dashboardStats, platformStats, matchingStats, impactMetrics, proposalsData]);
|
|
|
|
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 isLoading = isLoadingDashboard || isLoadingPlatform || isLoadingMatching || isLoadingImpact;
|
|
|
|
return (
|
|
<MainLayout onNavigate={handleFooterNavigate} className="bg-muted/30">
|
|
<Container size="2xl" className="py-8 sm:py-12">
|
|
<PageHeader
|
|
title={t('dashboard.title', 'Dashboard')}
|
|
subtitle={t('dashboard.subtitle', `Welcome back, ${user?.name || user?.email || 'User'}!`)}
|
|
onBack={handleBackNavigation}
|
|
/>
|
|
|
|
<Stack spacing="2xl">
|
|
{/* Key Metrics */}
|
|
<Grid cols={{ md: 2, lg: 4 }} gap="md">
|
|
<MetricItem
|
|
icon={<Briefcase className="h-4 h-5 text-current text-primary w-4 w-5" />}
|
|
label={t('dashboard.organizations', 'Organizations')}
|
|
value={formatNumber(stats.totalOrganizations)}
|
|
/>
|
|
<MetricItem
|
|
icon={<MapPin className="h-4 h-5 text-current text-warning w-4 w-5" />}
|
|
label={t('dashboard.sites', 'Sites')}
|
|
value={formatNumber(stats.totalSites)}
|
|
/>
|
|
<MetricItem
|
|
icon={<Target className="h-4 h-5 text-current text-success w-4 w-5" />}
|
|
label={t('dashboard.resourceFlows', 'Resource Flows')}
|
|
value={formatNumber(stats.totalResourceFlows)}
|
|
/>
|
|
<MetricItem
|
|
icon={<Target className="h-4 h-5 text-current text-purple-500 w-4 w-5" />}
|
|
label={t('dashboard.matches', 'Matches')}
|
|
value={formatNumber(stats.totalMatches)}
|
|
/>
|
|
</Grid>
|
|
|
|
{/* Impact Metrics */}
|
|
{(stats.totalCo2Saved > 0 || stats.totalEconomicValue > 0) && (
|
|
<Grid cols={{ md: 3 }} gap="md">
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-center gap-2">
|
|
<Leaf className="h-4 w-4 text-green-600" />
|
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
{t('dashboard.co2Saved', 'CO₂ Saved')}
|
|
</CardTitle>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold text-green-600">
|
|
{formatNumber(stats.totalCo2Saved)} t
|
|
</div>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
{t('dashboard.perYear', 'per year')}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-center gap-2">
|
|
<DollarSign className="h-4 text-current text-success w-4" />
|
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
{t('dashboard.economicValue', 'Economic Value')}
|
|
</CardTitle>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold text-success">
|
|
{formatCurrency(stats.totalEconomicValue)}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
{t('dashboard.created', 'created annually')}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-center gap-2">
|
|
<TrendingUp className="h-4 text-blue-600 text-current w-4" />
|
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
{t('dashboard.activeMatches', 'Active Matches')}
|
|
</CardTitle>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold text-blue-600">
|
|
{formatNumber(stats.activeMatches)}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
{t('dashboard.operational', 'currently operational')}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
)}
|
|
|
|
{/* Quick Actions */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{t('dashboard.quickActions', 'Quick Actions')}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Grid cols={{ sm: 2, md: 4 }} gap="md">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => navigate('/resources')}
|
|
className="h-20 flex flex-col items-center justify-center gap-2 hover:bg-primary/5"
|
|
>
|
|
<Plus className="h-4 h-6 text-current w-4 w-6" />
|
|
<span className="text-sm">{t('dashboard.createResourceFlow', 'Create Resource Flow')}</span>
|
|
</Button>
|
|
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => navigate('/matching')}
|
|
className="h-20 flex flex-col items-center justify-center gap-2 hover:bg-primary/5"
|
|
>
|
|
<Search className="h-4 h-6 text-current w-4 w-6" />
|
|
<span className="text-sm">{t('dashboard.findMatches', 'Find Matches')}</span>
|
|
</Button>
|
|
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => navigate('/map')}
|
|
className="h-20 flex flex-col items-center justify-center gap-2 hover:bg-primary/5"
|
|
>
|
|
<MapPin className="h-4 h-6 text-current w-4 w-6" />
|
|
<span className="text-sm">{t('dashboard.exploreMap', 'Explore Map')}</span>
|
|
</Button>
|
|
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => navigate('/analytics')}
|
|
className="h-20 flex flex-col items-center justify-center gap-2 hover:bg-primary/5"
|
|
>
|
|
<BarChart3 className="h-4 h-6 text-current w-4 w-6" />
|
|
<span className="text-sm">{t('dashboard.viewAnalytics', 'View Analytics')}</span>
|
|
</Button>
|
|
</Grid>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Recent Activity & Proposals */}
|
|
<Grid cols={{ md: 2 }} gap="lg">
|
|
{/* Recent Activity */}
|
|
<Card>
|
|
<CardHeader>
|
|
<Flex align="center" justify="between">
|
|
<CardTitle>{t('dashboard.recentActivity', 'Recent Activity')}</CardTitle>
|
|
<Badge variant="outline">
|
|
{stats.recentActivity.length}
|
|
</Badge>
|
|
</Flex>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center py-8">
|
|
<Spinner className="h-6 w-6" />
|
|
</div>
|
|
) : stats.recentActivity.length > 0 ? (
|
|
<div className="space-y-3">
|
|
{stats.recentActivity.slice(0, 5).map((activity: any, index: number) => (
|
|
<div
|
|
key={index}
|
|
className="flex items-start gap-3 p-3 rounded-lg border bg-card/50 hover:bg-card transition-colors"
|
|
>
|
|
<div className="w-2 h-2 rounded-full bg-primary mt-2 shrink-0" />
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium">{activity.description}</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{new Date(activity.timestamp).toLocaleDateString()}
|
|
</p>
|
|
</div>
|
|
<Badge variant="outline" className="text-xs">
|
|
{activity.type}
|
|
</Badge>
|
|
</div>
|
|
))}
|
|
{stats.recentActivity.length > 5 && (
|
|
<div className="text-center pt-2">
|
|
<Button variant="outline" size="sm">
|
|
{t('dashboard.viewAllActivity', 'View All Activity')}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
<Target className="h-12 h-4 mb-4 mx-auto opacity-50 text-current w-12 w-4" />
|
|
<p>{t('dashboard.noRecentActivity', 'No recent activity')}</p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Active Proposals */}
|
|
<Card>
|
|
<CardHeader>
|
|
<Flex align="center" justify="between">
|
|
<CardTitle>{t('dashboard.activeProposals', 'Active Proposals')}</CardTitle>
|
|
<Badge variant="outline">
|
|
{stats.activeProposals}
|
|
</Badge>
|
|
</Flex>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{stats.activeProposals > 0 ? (
|
|
<div className="space-y-3">
|
|
{/* This would show actual proposals - placeholder for now */}
|
|
<div className="p-3 rounded-lg border bg-card/50">
|
|
<p className="text-sm font-medium">
|
|
{t('dashboard.pendingReviews', 'Pending proposal reviews')}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
{stats.activeProposals} {t('dashboard.requireAttention', 'require your attention')}
|
|
</p>
|
|
</div>
|
|
<div className="text-center pt-2">
|
|
<Button variant="outline" size="sm" onClick={() => navigate('/map')}>
|
|
{t('dashboard.manageProposals', 'Manage Proposals')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
<Users className="h-12 h-4 mb-4 mx-auto opacity-50 text-current w-12 w-4" />
|
|
<p>{t('dashboard.noActiveProposals', 'No active proposals')}</p>
|
|
<Button variant="outline" size="sm" className="mt-2" onClick={() => navigate('/map')}>
|
|
{t('dashboard.createProposal', 'Create Proposal')}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
|
|
{/* My Organizations Summary */}
|
|
{organizations && organizations.length > 0 && (
|
|
<Card>
|
|
<CardHeader>
|
|
<Flex align="center" justify="between">
|
|
<CardTitle>{t('dashboard.myOrganizations', 'My Organizations')}</CardTitle>
|
|
<Button variant="outline" size="sm" onClick={() => navigate('/organizations')}>
|
|
{t('dashboard.viewAll', 'View All')}
|
|
</Button>
|
|
</Flex>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Grid cols={{ sm: 2, md: 3 }} gap="md">
|
|
{organizations.slice(0, 3).map((org: any) => (
|
|
<div
|
|
key={org.ID}
|
|
className="p-4 border rounded-lg hover:bg-muted/50 cursor-pointer transition-colors"
|
|
onClick={() => navigate(`/organization/${org.ID}`)}
|
|
>
|
|
<h4 className="font-medium mb-1">{org.Name}</h4>
|
|
<p className="text-sm text-muted-foreground mb-2">{org.sector}</p>
|
|
<Badge variant="outline" className="text-xs">
|
|
{org.subtype}
|
|
</Badge>
|
|
</div>
|
|
))}
|
|
</Grid>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Platform Health Indicators */}
|
|
{stats.matchSuccessRate > 0 && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{t('dashboard.platformHealth', 'Platform Health')}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Grid cols={{ md: 3 }} gap="md">
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-success mb-1">
|
|
{Math.round(stats.matchSuccessRate * 100)}%
|
|
</div>
|
|
<p className="text-sm text-muted-foreground">
|
|
{t('dashboard.matchSuccessRate', 'Match Success Rate')}
|
|
</p>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-primary mb-1">
|
|
{stats.avgMatchTime.toFixed(1)}
|
|
</div>
|
|
<p className="text-sm text-muted-foreground">
|
|
{t('dashboard.avgMatchTime', 'Avg Match Time (days)')}
|
|
</p>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-warning mb-1">
|
|
{stats.topResourceTypes.length}
|
|
</div>
|
|
<p className="text-sm text-muted-foreground">
|
|
{t('dashboard.activeResourceTypes', 'Active Resource Types')}
|
|
</p>
|
|
</div>
|
|
</Grid>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</Stack>
|
|
</Container>
|
|
</MainLayout>
|
|
);
|
|
};
|
|
|
|
export default DashboardPage;
|