mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
Some checks failed
CI/CD Pipeline / backend-lint (push) Failing after 31s
CI/CD Pipeline / backend-build (push) Has been skipped
CI/CD Pipeline / frontend-lint (push) Failing after 1m22s
CI/CD Pipeline / frontend-build (push) Has been skipped
CI/CD Pipeline / e2e-test (push) Has been skipped
- Fix React Compiler memoization issues in useOrganizationPage.ts - Replace useCallback with useRef pattern in useKeyboard.ts - Remove unnecessary dependencies from useMemo hooks - Fix prettier formatting in api-client.ts and api-config.ts - Replace any types with proper types in error-handling, http-client, security - Remove unused imports and variables - Move ImpactBreakdownChart component outside render in ImpactMetrics.tsx - Fix setState in effect by using useMemo in HeritageBuildingPage.tsx - Memoize getHistoryTitle with useCallback in MatchDetailPage and MatchNegotiationPage - Add i18n for literal strings in community pages and LoginPage - Fix missing dependencies in DashboardPage and DiscoveryPage
222 lines
7.7 KiB
TypeScript
222 lines
7.7 KiB
TypeScript
import {
|
|
ActiveProposalsSection,
|
|
ImpactMetricsSection,
|
|
MyOrganizationsSection,
|
|
PlatformHealthSection,
|
|
PlatformOverviewSection,
|
|
QuickActionsSection,
|
|
RecentActivitySection,
|
|
YourContributionSection,
|
|
} from '@/components/dashboard';
|
|
import { MainLayout } from '@/components/layout/MainLayout.tsx';
|
|
import PageHeader from '@/components/layout/PageHeader.tsx';
|
|
import DateRangePicker, { type DateRange } from '@/components/ui/DateRangePicker.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 { useUserOrganizations } from '@/hooks/api/useOrganizationsAPI.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';
|
|
import type { Proposal } from '@/types.ts';
|
|
import { useMemo, useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
|
type ActivityFilter = 'all' | 'match' | 'proposal' | 'organization';
|
|
|
|
const DashboardPage = () => {
|
|
const { t } = useTranslation();
|
|
const navigate = useNavigate();
|
|
const { handleBackNavigation, handleFooterNavigate } = useNavigation();
|
|
const { user } = useAuth();
|
|
const [activityFilter, setActivityFilter] = useState<ActivityFilter>('all');
|
|
const [dateRange, setDateRange] = useState<DateRange>({
|
|
startDate: null,
|
|
endDate: null,
|
|
preset: '30d',
|
|
});
|
|
|
|
// Analytics data - Zod validates these, so if data exists, it's guaranteed to be valid
|
|
const {
|
|
data: dashboardStats,
|
|
isLoading: isLoadingDashboard,
|
|
error: dashboardError,
|
|
} = useDashboardStatistics();
|
|
const {
|
|
data: platformStats,
|
|
isLoading: isLoadingPlatform,
|
|
error: platformError,
|
|
} = usePlatformStatistics();
|
|
const {
|
|
data: matchingStats,
|
|
isLoading: isLoadingMatching,
|
|
error: matchingError,
|
|
} = useMatchingStatistics();
|
|
const {
|
|
data: impactMetrics,
|
|
isLoading: isLoadingImpact,
|
|
error: impactError,
|
|
} = useImpactMetrics();
|
|
|
|
// User-specific data
|
|
const { data: proposalsData } = useProposals();
|
|
const { organizations } = useOrganizations();
|
|
const { data: userOrganizations } = useUserOrganizations();
|
|
|
|
// Proposals data - validated by API
|
|
const proposals: Proposal[] = proposalsData?.proposals ?? [];
|
|
const pendingProposals = proposals.filter((p: Proposal) => p.status === 'pending');
|
|
|
|
// Calculate derived statistics - data is validated by Zod, so we can trust it
|
|
const stats = useMemo(() => {
|
|
// If data exists, it's validated by Zod - use it directly
|
|
const dashboard = dashboardStats;
|
|
const platform = platformStats;
|
|
const matching = matchingStats;
|
|
const impact = impactMetrics;
|
|
|
|
// User-specific metrics
|
|
const myOrganizationsCount = Array.isArray(userOrganizations) ? userOrganizations.length : 0;
|
|
const myProposalsCount = proposals.length;
|
|
const myPendingProposalsCount = pendingProposals.length;
|
|
|
|
return {
|
|
// Platform-wide metrics - prefer dashboard stats, fallback to platform stats
|
|
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: pendingProposals.length,
|
|
recentActivity: dashboard?.recent_activity ?? [],
|
|
|
|
// User-specific metrics
|
|
myOrganizations: myOrganizationsCount,
|
|
myProposals: myProposalsCount,
|
|
myPendingProposals: myPendingProposalsCount,
|
|
|
|
// Matching statistics - optional fields use nullish coalescing
|
|
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 ?? impact?.total_co2_savings_tonnes ?? 0,
|
|
totalEconomicValue: impact?.total_economic_value ?? impact?.total_economic_value_eur ?? 0,
|
|
activeMatches: impact?.active_matches_count ?? 0,
|
|
};
|
|
}, [
|
|
dashboardStats,
|
|
platformStats,
|
|
matchingStats,
|
|
impactMetrics,
|
|
proposalsData,
|
|
userOrganizations,
|
|
pendingProposals,
|
|
proposals.length,
|
|
]);
|
|
|
|
// Filter activities based on selected filter
|
|
// Activity type is validated by Zod, so it's guaranteed to be a string
|
|
const filteredActivities = useMemo(() => {
|
|
if (activityFilter === 'all') {
|
|
return stats.recentActivity;
|
|
}
|
|
return stats.recentActivity.filter((activity) => {
|
|
const activityType = activity.type.toLowerCase();
|
|
switch (activityFilter) {
|
|
case 'match':
|
|
return activityType.includes('match');
|
|
case 'proposal':
|
|
return activityType.includes('proposal');
|
|
case 'organization':
|
|
return activityType.includes('organization') || activityType.includes('org');
|
|
default:
|
|
return true;
|
|
}
|
|
});
|
|
}, [stats.recentActivity, activityFilter]);
|
|
|
|
// Number formatting handled by shared utilities inside components
|
|
|
|
const isLoading = isLoadingDashboard || isLoadingPlatform || isLoadingMatching || isLoadingImpact;
|
|
|
|
return (
|
|
<MainLayout onNavigate={handleFooterNavigate} className="bg-muted/30">
|
|
<Container size="2xl" className="py-8 sm:py-12">
|
|
<Flex align="center" justify="between" className="mb-6">
|
|
<PageHeader
|
|
title={t('dashboard.title')}
|
|
subtitle={t('dashboard.subtitle', { name: user?.name || user?.email || 'User' })}
|
|
onBack={handleBackNavigation}
|
|
/>
|
|
<DateRangePicker value={dateRange} onChange={setDateRange} />
|
|
</Flex>
|
|
|
|
<Stack spacing="2xl">
|
|
<YourContributionSection
|
|
myOrganizations={stats.myOrganizations}
|
|
myProposals={stats.myProposals}
|
|
myPendingProposals={stats.myPendingProposals}
|
|
t={t}
|
|
/>
|
|
|
|
<PlatformOverviewSection
|
|
totalOrganizations={stats.totalOrganizations}
|
|
totalSites={stats.totalSites}
|
|
totalResourceFlows={stats.totalResourceFlows}
|
|
totalMatches={stats.totalMatches}
|
|
t={t}
|
|
/>
|
|
|
|
<ImpactMetricsSection
|
|
totalCo2Saved={stats.totalCo2Saved}
|
|
totalEconomicValue={stats.totalEconomicValue}
|
|
activeMatches={stats.activeMatches}
|
|
t={t}
|
|
/>
|
|
|
|
<QuickActionsSection onNavigate={(path) => navigate(path)} t={t} />
|
|
|
|
{/* Recent Activity & Proposals */}
|
|
<Grid cols={{ md: 2 }} gap="lg">
|
|
<RecentActivitySection
|
|
filteredActivities={filteredActivities}
|
|
activityFilter={activityFilter}
|
|
setActivityFilter={setActivityFilter}
|
|
isLoading={isLoading}
|
|
t={t}
|
|
/>
|
|
|
|
<ActiveProposalsSection
|
|
pendingProposals={pendingProposals}
|
|
onNavigate={(path) => navigate(path)}
|
|
t={t}
|
|
/>
|
|
</Grid>
|
|
|
|
<MyOrganizationsSection
|
|
organizations={organizations}
|
|
onNavigate={(path) => navigate(path)}
|
|
t={t}
|
|
/>
|
|
|
|
<PlatformHealthSection
|
|
matchSuccessRate={stats.matchSuccessRate}
|
|
avgMatchTime={stats.avgMatchTime}
|
|
topResourceTypes={stats.topResourceTypes}
|
|
t={t}
|
|
/>
|
|
</Stack>
|
|
</Container>
|
|
</MainLayout>
|
|
);
|
|
};
|
|
|
|
export default DashboardPage;
|