turash/bugulma/frontend/pages/MatchingDashboard.tsx
Damir Mukimov 673e8d4361
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
fix: resolve all frontend lint errors (85 issues fixed)
- 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
2025-12-25 14:14:58 +01:00

350 lines
14 KiB
TypeScript

import { MainLayout } from '@/components/layout/MainLayout.tsx';
import PageHeader from '@/components/layout/PageHeader.tsx';
import { useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
// Imports trimmed: unused components removed
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 Input from '@/components/ui/Input.tsx';
import { Container, Flex, Grid, Stack } from '@/components/ui/layout';
import Select from '@/components/ui/Select.tsx';
import { useFindMatches, useTopMatches } from '@/hooks/api/useMatchingAPI.ts';
import { useTranslation } from '@/hooks/useI18n.tsx';
import { useNavigation } from '@/hooks/useNavigation.tsx';
import { useOrganizations } from '@/hooks/useOrganizations.ts';
import { Plus, Target, TrendingUp } from 'lucide-react';
const MatchingDashboard = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const { handleBackNavigation, handleFooterNavigate } = useNavigation();
const { organizations } = useOrganizations();
// Search state
const [searchQuery, setSearchQuery] = useState('');
const [selectedOrgId, setSelectedOrgId] = useState('');
const [maxDistance, setMaxDistance] = useState(50);
const [minScore, setMinScore] = useState(0.6);
// Get top matches for overview
const { data: topMatches, isLoading: isLoadingTop } = useTopMatches(20);
// Get matches based on search criteria
const searchResourceId = searchQuery ? searchQuery : undefined;
const { data: searchMatches, isLoading: isLoadingSearch } = useFindMatches(
searchResourceId,
searchResourceId
? {
max_distance_km: maxDistance,
min_score: minScore,
limit: 20,
}
: undefined
);
// Calculate statistics
const stats = useMemo(() => {
if (!topMatches?.matches) return { total: 0, highScore: 0, avgDistance: 0 };
const matches = topMatches.matches;
const total = matches.length;
const highScore = matches.filter((m) => m.overall_score >= 0.8).length;
const avgDistance =
matches.length > 0 ? matches.reduce((sum, m) => sum + m.distance_km, 0) / matches.length : 0;
return { total, highScore, avgDistance: Math.round(avgDistance * 10) / 10 };
}, [topMatches]);
const handleViewMatch = (matchId: string) => {
navigate(`/matching/${matchId}`);
};
const handleCreateResourceFlow = () => {
navigate('/resources');
};
const organizationOptions = useMemo(() => {
return (
organizations?.map((org) => ({
value: org.ID,
label: org.Name,
})) || []
);
}, [organizations]);
const currentMatches = searchQuery ? searchMatches : topMatches;
return (
<MainLayout onNavigate={handleFooterNavigate} className="bg-muted/30">
<Container size="2xl" className="py-8 sm:py-12">
<PageHeader
title={t('matchingDashboard.title')}
subtitle={t('matchingDashboard.subtitle')}
onBack={handleBackNavigation}
/>
<Stack spacing="2xl">
{/* Statistics Cards */}
<Grid cols={{ md: 3 }} gap="md">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<Target className="h-4 text-current w-4" />
{t('matchingDashboard.totalMatches')}
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.total}</div>
<p className="text-xs text-muted-foreground mt-1">
{t('matchingDashboard.activeMatches')}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<TrendingUp className="h-4 text-current w-4" />
{t('matchingDashboard.highQuality')}
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.highScore}</div>
<p className="text-xs text-muted-foreground mt-1">
{t('matchingDashboard.scoreAbove80')}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium text-muted-foreground">
📍 {t('matchingDashboard.avgDistance')}
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{stats.avgDistance} {t('common.km')}
</div>
<p className="text-xs text-muted-foreground mt-1">
{t('matchingDashboard.withinRange')}
</p>
</CardContent>
</Card>
</Grid>
{/* Search and Filters */}
<Card>
<CardHeader>
<CardTitle>{t('matchingDashboard.findMatches')}</CardTitle>
</CardHeader>
<CardContent>
<Stack spacing="md">
<Grid cols={{ md: 2 }} gap="md">
<div>
<label className="block text-sm font-medium mb-2">
{t('matchingDashboard.resourceId')}
</label>
<Input
type="text"
placeholder={t('matchingDashboard.enterResourceId')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
{t('matchingDashboard.organization')}
</label>
<Select
value={selectedOrgId}
onValueChange={setSelectedOrgId}
options={organizationOptions}
placeholder={t('matchingDashboard.selectOrganization')}
/>
</div>
</Grid>
<Grid cols={{ sm: 2, md: 4 }} gap="md">
<div>
<label className="block text-sm font-medium mb-2">
{t('matchingDashboard.maxDistance')}
</label>
<Input
type="number"
min="1"
max="500"
value={maxDistance}
onChange={(e) => setMaxDistance(Number(e.target.value))}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
{t('matchingDashboard.minScore')}
</label>
<Input
type="number"
min="0"
max="1"
step="0.1"
value={minScore}
onChange={(e) => setMinScore(Number(e.target.value))}
/>
</div>
<div className="flex items-end">
<Button
variant="outline"
onClick={() => {
setSearchQuery('');
setSelectedOrgId('');
setMaxDistance(50);
setMinScore(0.6);
}}
className="w-full"
>
{t('common.reset')}
</Button>
</div>
<div className="flex items-end">
<Button onClick={handleCreateResourceFlow} className="w-full">
<Plus className="h-4 mr-2 text-current w-4" />
{t('matchingDashboard.createResourceFlow')}
</Button>
</div>
</Grid>
</Stack>
</CardContent>
</Card>
{/* Results */}
{currentMatches && (
<Card>
<CardHeader>
<Flex align="center" justify="between">
<CardTitle>
{searchQuery
? t('matchingDashboard.searchResults')
: t('matchingDashboard.topMatches')}
</CardTitle>
{currentMatches.matches.length > 0 && (
<Badge variant="outline">
{currentMatches.matches.length} {t('matchingDashboard.matches')}
</Badge>
)}
</Flex>
</CardHeader>
<CardContent>
{isLoadingTop || isLoadingSearch ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
) : currentMatches.matches.length > 0 ? (
<div className="space-y-4">
{currentMatches.matches.slice(0, 10).map((match) => (
<div
key={match.ID}
className="border rounded-lg p-4 hover:bg-muted/50 transition-colors cursor-pointer"
onClick={() => handleViewMatch(match.ID)}
>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<h4 className="font-medium text-sm text-muted-foreground mb-1">
{t('matchingDashboard.sourceFlow')}
</h4>
<p className="text-sm">
{match.source_flow?.type} ({match.source_flow?.direction})
</p>
</div>
<div>
<h4 className="font-medium text-sm text-muted-foreground mb-1">
{t('matchingDashboard.targetFlow')}
</h4>
<p className="text-sm">
{match.target_flow?.type} ({match.target_flow?.direction})
</p>
</div>
<div className="text-right">
<div className="flex items-center justify-end gap-2">
<Badge variant={match.overall_score >= 0.8 ? 'default' : 'secondary'}>
{Math.round(match.overall_score * 100)}%
</Badge>
<span className="text-sm text-muted-foreground">
{match.distance_km?.toFixed(1)} {t('common.km')}
</span>
</div>
</div>
</div>
</div>
))}
{currentMatches.matches.length > 10 && (
<div className="text-center pt-4">
<Button variant="outline">{t('matchingDashboard.viewAllMatches')}</Button>
</div>
)}
</div>
) : (
<div className="text-center py-8">
<Target className="h-12 h-4 mb-4 mx-auto text-current text-muted-foreground w-12 w-4" />
<p className="text-muted-foreground mb-4">
{searchQuery
? t('matchingDashboard.noSearchResults')
: t('matchingDashboard.noMatchesFound')}
</p>
{!searchQuery && (
<Button onClick={handleCreateResourceFlow}>
<Plus className="h-4 mr-2 text-current w-4" />
{t('matchingDashboard.createFirstResourceFlow')}
</Button>
)}
</div>
)}
</CardContent>
</Card>
)}
{/* Quick Actions */}
<Card>
<CardHeader>
<CardTitle>{t('matchingDashboard.quickActions')}</CardTitle>
</CardHeader>
<CardContent>
<Grid cols={{ sm: 2, md: 3 }} gap="md">
<Button
variant="outline"
onClick={() => navigate('/resources')}
className="h-20 flex flex-col items-center justify-center gap-2"
>
<Plus className="h-4 h-6 text-current w-4 w-6" />
<span className="text-sm">{t('matchingDashboard.manageResourceFlows')}</span>
</Button>
<Button
variant="outline"
onClick={() => navigate('/map')}
className="h-20 flex flex-col items-center justify-center gap-2"
>
<Target className="h-4 h-6 text-current w-4 w-6" />
<span className="text-sm">{t('matchingDashboard.exploreMap')}</span>
</Button>
<Button
variant="outline"
onClick={() => navigate('/matching/map')}
className="h-20 flex flex-col items-center justify-center gap-2"
>
<TrendingUp className="h-4 h-6 text-current w-4 w-6" />
<span className="text-sm">{t('matchingDashboard.viewMatchesMap')}</span>
</Button>
</Grid>
</CardContent>
</Card>
</Stack>
</Container>
</MainLayout>
);
};
export default MatchingDashboard;