mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
Some checks failed
CI/CD Pipeline / backend-build (push) Has been skipped
CI/CD Pipeline / backend-lint (push) Failing after 31s
CI/CD Pipeline / frontend-lint (push) Failing after 1m26s
CI/CD Pipeline / frontend-build (push) Has been skipped
CI/CD Pipeline / e2e-test (push) Has been skipped
- Fix prettier formatting issues in multiple components - Fix React Compiler memoization issues in ProductServiceMarkers.tsx - Replace literal strings with i18n keys across components - Address i18n issues in heritage, network graph, and match components - Fix dependency arrays in useMemo hooks to match React Compiler expectations
188 lines
6.0 KiB
TypeScript
188 lines
6.0 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">
|
|
{t('matchesMap.distanceValue', { distance: match.DistanceKm.toFixed(1) })}
|
|
</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);
|