turash/bugulma/frontend/pages/MatchingDashboard.tsx
2025-12-15 10:06:41 +01:00

352 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 handleViewResourceFlow = (resourceId: string) => {
navigate(`/resources/${resourceId}`);
};
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} 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)} 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;