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
350 lines
14 KiB
TypeScript
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;
|