turash/bugulma/frontend/components/map/MatchLines.tsx
2025-12-15 10:06:41 +01:00

186 lines
5.9 KiB
TypeScript

import { useTranslation } from '@/hooks/useI18n';
import type { BackendMatch } from '@/schemas/backend/match';
import { LatLngTuple } from 'leaflet';
import React, { useMemo } from 'react';
import { Polyline, Popup } from 'react-leaflet';
import { formatCurrency } from '../../lib/fin';
interface MatchLinesProps {
matches: BackendMatch[];
selectedMatchId: string | null;
onMatchSelect: (matchId: string) => void;
}
/**
* Individual match line component memoized to prevent unnecessary re-renders
*/
const MatchLine = React.memo<{
match: BackendMatch;
positions: LatLngTuple[];
isSelected: boolean;
onClick: () => void;
}>(({ match, positions, isSelected, onClick }) => {
const { t } = useTranslation();
const getLineColor = () => {
switch (match.Status) {
case 'live':
return 'hsl(var(--primary))';
case 'contracted':
return 'hsl(var(--success))';
case 'negotiating':
return 'hsl(var(--warning))';
case 'reserved':
return 'hsl(var(--secondary))';
default:
return 'hsl(var(--muted-foreground))';
}
};
const formatScore = (score: number) => {
return `${(score * 100).toFixed(1)}%`;
};
// use central fin.formatCurrency
return (
<Polyline
positions={positions}
pathOptions={{
color: getLineColor(),
weight: isSelected ? 5 : 3,
opacity: isSelected ? 1 : 0.7,
dashArray: match.Status === 'suggested' ? '5, 5' : undefined,
}}
eventHandlers={{
click: onClick,
mouseover: (e) => {
const layer = e.target;
layer.setStyle({
weight: isSelected ? 6 : 4,
opacity: 1,
});
},
mouseout: (e) => {
const layer = e.target;
layer.setStyle({
weight: isSelected ? 5 : 3,
opacity: isSelected ? 1 : 0.7,
});
},
}}
>
<Popup>
<div className="min-w-64">
<div className="flex items-center justify-between mb-2">
<h4 className="font-semibold text-sm">
{t('matchesMap.matchConnection', 'Match Connection')}
</h4>
<span
className={`text-xs px-2 py-1 rounded ${
match.Status === 'live'
? 'bg-success/20 text-success'
: match.Status === 'contracted'
? 'bg-success/20 text-success'
: match.Status === 'negotiating'
? 'bg-warning/20 text-warning'
: 'bg-muted text-muted-foreground'
}`}
>
{t(`matchStatus.${match.Status}`, match.Status)}
</span>
</div>
<div className="space-y-2 text-sm">
<div className="grid grid-cols-2 gap-2">
<div>
<span className="text-muted-foreground">
{t('matchesMap.compatibility', 'Compatibility')}:
</span>
<div className="font-medium">{formatScore(match.CompatibilityScore)}</div>
</div>
<div>
<span className="text-muted-foreground">
{t('matchesMap.distance', 'Distance')}:
</span>
<div className="font-medium">{match.DistanceKm.toFixed(1)} km</div>
</div>
</div>
<div>
<span className="text-muted-foreground">
{t('matchesMap.economicValue', 'Economic Value')}:
</span>
<div className="font-medium text-success">{formatCurrency(match.EconomicValue)}</div>
</div>
{match.EconomicImpact?.annual_savings && (
<div>
<span className="text-muted-foreground">
{t('matchesMap.annualSavings', 'Annual Savings')}:
</span>
<div className="font-medium text-success">
{formatCurrency(match.EconomicImpact.annual_savings)}
</div>
</div>
)}
</div>
<button
onClick={onClick}
className="mt-3 w-full px-3 py-2 bg-primary text-primary-foreground rounded text-sm font-medium hover:bg-primary/90 transition-colors"
>
{t('matchesMap.viewDetails', 'View Details')}
</button>
</div>
</Popup>
</Polyline>
);
});
MatchLine.displayName = 'MatchLine';
const MatchLines: React.FC<MatchLinesProps> = ({ matches, selectedMatchId, onMatchSelect }) => {
// Transform matches into line data with positions
const matchLines = useMemo(() => {
return matches
.map((match) => {
// For now, we'll need to get site coordinates from the match data
// In a real implementation, we'd join with site data
// For now, creating mock positions based on match ID for demonstration
const baseLat = 55.1644; // Bugulma center
const baseLng = 50.205;
// Generate pseudo-random but consistent positions based on match ID
const hash = match.ID.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
const lat1 = baseLat + ((hash % 100) - 50) * 0.01;
const lng1 = baseLng + (((hash * 7) % 100) - 50) * 0.01;
const lat2 = baseLat + (((hash * 13) % 100) - 50) * 0.01;
const lng2 = baseLng + (((hash * 17) % 100) - 50) * 0.01;
return {
match,
positions: [[lat1, lng1] as LatLngTuple, [lat2, lng2] as LatLngTuple],
isSelected: match.ID === selectedMatchId,
};
})
.filter((line) => line.positions.length === 2);
}, [matches, selectedMatchId]);
return (
<>
{matchLines.map(({ match, positions, isSelected }) => (
<MatchLine
key={match.ID}
match={match}
positions={positions}
isSelected={isSelected}
onClick={() => onMatchSelect(match.ID)}
/>
))}
</>
);
};
export default React.memo(MatchLines);