mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
- Remove nested git repository from bugulma/frontend/.git - Add all frontend files to main repository tracking - Convert from separate frontend/backend repos to unified monorepo - Preserve all frontend code and development history as tracked files - Eliminate nested repository complexity for simpler development workflow This creates a proper monorepo structure with frontend and backend coexisting in the same repository for easier development and deployment.
354 lines
14 KiB
TypeScript
354 lines
14 KiB
TypeScript
import React, { useState, useMemo } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { MainLayout } from '@/components/layout/MainLayout.tsx';
|
|
import PageHeader from '@/components/layout/PageHeader.tsx';
|
|
import MatchesList from '@/components/matches/MatchesList.tsx';
|
|
import ResourceFlowCard from '@/components/resource-flow/ResourceFlowCard.tsx';
|
|
import { Container, Stack, Grid, Flex } from '@/components/ui/layout';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card.tsx';
|
|
import Button from '@/components/ui/Button.tsx';
|
|
import Input from '@/components/ui/Input.tsx';
|
|
import Select from '@/components/ui/Select.tsx';
|
|
import Badge from '@/components/ui/Badge.tsx';
|
|
import { useTranslation } from '@/hooks/useI18n.tsx';
|
|
import { useTopMatches, useFindMatches } from '@/hooks/api/useMatchingAPI.ts';
|
|
import { useNavigation } from '@/hooks/useNavigation.tsx';
|
|
import { useOrganizations } from '@/hooks/useOrganizations.ts';
|
|
import { Plus, Search, 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;
|