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 1m37s
CI/CD Pipeline / frontend-build (push) Has been skipped
CI/CD Pipeline / e2e-test (push) Has been skipped
- Replace all 'any' types with proper TypeScript interfaces - Fix React hooks setState in useEffect issues with lazy initialization - Remove unused variables and imports across all files - Fix React Compiler memoization dependency issues - Add comprehensive i18n translation keys for admin interfaces - Apply consistent prettier formatting throughout codebase - Clean up unused bulk editing functionality - Improve type safety and code quality across frontend Files changed: 39 - ImpactMetrics.tsx: Fixed any types and interfaces - AdminVerificationQueuePage.tsx: Added i18n keys, removed unused vars - LocalizationUIPage.tsx: Fixed memoization, added translations - LocalizationDataPage.tsx: Added type safety and translations - And 35+ other files with various lint fixes
288 lines
12 KiB
TypeScript
288 lines
12 KiB
TypeScript
import { MainLayout } from '@/components/layout/MainLayout.tsx';
|
|
import PageHeader from '@/components/layout/PageHeader.tsx';
|
|
import OrganizationStatistics from '@/components/organization/OrganizationStatistics.tsx';
|
|
import ResourceFlowList from '@/components/resource-flow/ResourceFlowList.tsx';
|
|
import Button from '@/components/ui/Button.tsx';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card.tsx';
|
|
import DateRangePicker, { type DateRange } from '@/components/ui/DateRangePicker.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 { useOrganizationStatistics } from '@/hooks/api/useAnalyticsAPI.ts';
|
|
import { useOrganization } from '@/hooks/api/useOrganizationsAPI.ts';
|
|
import { useProposalsForOrganization } from '@/hooks/api/useProposalsAPI.ts';
|
|
import { useTranslation } from '@/hooks/useI18n.tsx';
|
|
import { useNavigation } from '@/hooks/useNavigation.tsx';
|
|
import {
|
|
Activity,
|
|
BarChart3,
|
|
Briefcase,
|
|
DollarSign,
|
|
Leaf,
|
|
MapPin,
|
|
Target,
|
|
TrendingUp,
|
|
} from 'lucide-react';
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
import { useNavigate, useParams } from 'react-router-dom';
|
|
import { formatCurrency, formatNumber } from '../lib/fin';
|
|
|
|
const OrganizationDashboardPage = () => {
|
|
const { id } = useParams<{ id: string }>();
|
|
const navigate = useNavigate();
|
|
const { t } = useTranslation();
|
|
const { handleFooterNavigate } = useNavigation();
|
|
|
|
// Fetch organization data
|
|
const { data: organization, isLoading: isLoadingOrg } = useOrganization(id);
|
|
const { data: stats, isLoading: isLoadingStats } = useOrganizationStatistics(id);
|
|
const { data: proposalsData } = useProposalsForOrganization(id);
|
|
const [dateRange, setDateRange] = useState<DateRange>({
|
|
startDate: null,
|
|
endDate: null,
|
|
preset: '30d',
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (!isLoadingOrg && !organization) {
|
|
navigate('/map');
|
|
}
|
|
}, [organization, isLoadingOrg, navigate]);
|
|
|
|
// use central fin utilities
|
|
|
|
// Calculate proposals summary
|
|
const proposalsSummary = useMemo(() => {
|
|
const incoming = Array.isArray(proposalsData?.incoming) ? proposalsData.incoming : [];
|
|
const outgoing = Array.isArray(proposalsData?.outgoing) ? proposalsData.outgoing : [];
|
|
const pendingIncoming = incoming.filter((p) => p?.status === 'pending');
|
|
const pendingOutgoing = outgoing.filter((p) => p?.status === 'pending');
|
|
|
|
return {
|
|
total: incoming.length + outgoing.length,
|
|
incoming: incoming.length,
|
|
outgoing: outgoing.length,
|
|
pending: pendingIncoming.length + pendingOutgoing.length,
|
|
pendingIncoming: pendingIncoming.length,
|
|
pendingOutgoing: pendingOutgoing.length,
|
|
};
|
|
}, [proposalsData]);
|
|
|
|
const isLoading = isLoadingOrg || isLoadingStats;
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<MainLayout onNavigate={handleFooterNavigate} className="bg-muted/30">
|
|
<Container size="2xl" className="py-8 sm:py-12">
|
|
<div className="flex items-center justify-center py-20">
|
|
<Spinner className="h-8 w-8 text-primary" />
|
|
</div>
|
|
</Container>
|
|
</MainLayout>
|
|
);
|
|
}
|
|
|
|
if (!organization) {
|
|
return null;
|
|
}
|
|
|
|
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('organizationDashboard.title', `${organization.Name} - Dashboard`)}
|
|
subtitle={t(
|
|
'organizationDashboard.subtitle',
|
|
"Overview of your organization's performance and activity"
|
|
)}
|
|
onBack={() => navigate(`/organization/${id}`)}
|
|
/>
|
|
<DateRangePicker value={dateRange} onChange={setDateRange} />
|
|
</Flex>
|
|
|
|
<Stack spacing="2xl">
|
|
{/* Organization Statistics */}
|
|
<OrganizationStatistics organizationId={id!} />
|
|
|
|
{/* Key Metrics Overview */}
|
|
{stats && (
|
|
<Grid cols={{ md: 2, lg: 4 }} gap="md">
|
|
<MetricItem
|
|
icon={<MapPin className="h-4 h-5 text-current text-warning w-4 w-5" />}
|
|
label={t('organizationDashboard.totalSites')}
|
|
value={formatNumber(stats.total_sites)}
|
|
/>
|
|
<MetricItem
|
|
icon={<Target className="h-4 h-5 text-current text-success w-4 w-5" />}
|
|
label={t('organizationDashboard.resourceFlows')}
|
|
value={formatNumber(stats.total_resource_flows)}
|
|
/>
|
|
<MetricItem
|
|
icon={<Activity className="h-4 h-5 text-current text-primary w-4 w-5" />}
|
|
label={t('organizationDashboard.activeMatches')}
|
|
value={formatNumber(stats.active_matches)}
|
|
/>
|
|
<MetricItem
|
|
icon={<TrendingUp className="h-4 h-5 text-current text-purple-500 w-4 w-5" />}
|
|
label={t('organizationDashboard.totalMatches')}
|
|
value={formatNumber(stats.total_matches)}
|
|
/>
|
|
</Grid>
|
|
)}
|
|
|
|
{/* Impact Metrics */}
|
|
{stats && (stats.co2_savings_tonnes > 0 || stats.economic_value_eur > 0) && (
|
|
<Grid cols={{ md: 2 }} 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('organizationDashboard.co2Saved')}
|
|
</CardTitle>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold text-green-600">
|
|
{formatNumber(stats.co2_savings_tonnes)} {t('common.tonnes')}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
{t('organizationDashboard.totalSavings')}
|
|
</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('organizationDashboard.economicValue')}
|
|
</CardTitle>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold text-success">
|
|
{formatCurrency(stats.economic_value_eur)}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
{t('organizationDashboard.totalValue')}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
)}
|
|
|
|
{/* Proposals Summary */}
|
|
{proposalsSummary.total > 0 && (
|
|
<Card>
|
|
<CardHeader>
|
|
<Flex align="center" justify="between">
|
|
<CardTitle>{t('organizationDashboard.proposals')}</CardTitle>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => navigate(`/organization/${id}`)}
|
|
>
|
|
{t('organizationDashboard.viewAll')}
|
|
</Button>
|
|
</Flex>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Grid cols={{ md: 3 }} gap="md">
|
|
<div className="text-center p-4 border rounded-lg">
|
|
<div className="text-2xl font-bold mb-1">{proposalsSummary.total}</div>
|
|
<p className="text-sm text-muted-foreground">
|
|
{t('organizationDashboard.totalProposals')}
|
|
</p>
|
|
</div>
|
|
<div className="text-center p-4 border rounded-lg">
|
|
<div className="text-2xl font-bold text-warning mb-1">
|
|
{proposalsSummary.pending}
|
|
</div>
|
|
<p className="text-sm text-muted-foreground">
|
|
{t('organizationDashboard.pendingProposals')}
|
|
</p>
|
|
</div>
|
|
<div className="text-center p-4 border rounded-lg">
|
|
<div className="text-2xl font-bold text-success mb-1">
|
|
{proposalsSummary.incoming}
|
|
</div>
|
|
<p className="text-sm text-muted-foreground">
|
|
{t('organizationDashboard.incomingProposals')}
|
|
</p>
|
|
</div>
|
|
</Grid>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Resource Flows */}
|
|
<Card>
|
|
<CardHeader>
|
|
<Flex align="center" justify="between">
|
|
<CardTitle>{t('organizationDashboard.resourceFlows')}</CardTitle>
|
|
<Button variant="outline" size="sm" onClick={() => navigate(`/organization/${id}`)}>
|
|
{t('organizationDashboard.viewDetails')}
|
|
</Button>
|
|
</Flex>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ResourceFlowList organizationId={id!} />
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Quick Actions */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{t('organizationDashboard.quickActions')}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Grid cols={{ sm: 2, md: 4 }} gap="md">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => navigate(`/organization/${id}`)}
|
|
className="h-20 flex flex-col items-center justify-center gap-2 hover:bg-primary/5"
|
|
>
|
|
<Briefcase className="h-4 h-6 text-current w-4 w-6" />
|
|
<span className="text-sm">{t('organizationDashboard.viewOrganization')}</span>
|
|
</Button>
|
|
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => navigate('/resources')}
|
|
className="h-20 flex flex-col items-center justify-center gap-2 hover:bg-primary/5"
|
|
>
|
|
<Target className="h-4 h-6 text-current w-4 w-6" />
|
|
<span className="text-sm">{t('organizationDashboard.manageResources')}</span>
|
|
</Button>
|
|
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => navigate('/matching')}
|
|
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('organizationDashboard.findMatches')}</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('organizationDashboard.exploreMap')}</span>
|
|
</Button>
|
|
</Grid>
|
|
</CardContent>
|
|
</Card>
|
|
</Stack>
|
|
</Container>
|
|
</MainLayout>
|
|
);
|
|
};
|
|
|
|
export default OrganizationDashboardPage;
|