turash/bugulma/frontend/pages/DashboardPage.tsx
Damir Mukimov 6347f42e20
Consolidate repositories: Remove nested frontend .git and merge into main repository
- 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.
2025-11-25 06:02:57 +01:00

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;