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.
266 lines
8.2 KiB
TypeScript
266 lines
8.2 KiB
TypeScript
import L from 'leaflet';
|
|
import React, { useMemo } from 'react';
|
|
import { Marker, Popup } from 'react-leaflet';
|
|
import MarkerClusterGroup from 'react-leaflet-markercluster';
|
|
import { useTranslation } from '@/hooks/useI18n';
|
|
import type { BackendMatch } from '@/schemas/backend/match';
|
|
|
|
// Resource type color mapping
|
|
const resourceTypeColors: Record<string, string> = {
|
|
heat: '#FF6B6B',
|
|
water: '#4ECDC4',
|
|
steam: '#95E1D3',
|
|
CO2: '#45B7D1',
|
|
biowaste: '#96CEB4',
|
|
cooling: '#FECA57',
|
|
logistics: '#FF9FF3',
|
|
materials: '#54A0FF',
|
|
service: '#5F27CD',
|
|
};
|
|
|
|
interface ResourceFlowMarkersProps {
|
|
matches: BackendMatch[];
|
|
selectedMatchId: string | null;
|
|
onMatchSelect: (matchId: string) => void;
|
|
}
|
|
|
|
/**
|
|
* Individual resource flow marker component
|
|
*/
|
|
const ResourceFlowMarker = React.memo<{
|
|
position: [number, number];
|
|
resourceType: string;
|
|
direction: string;
|
|
match: BackendMatch;
|
|
isInSelectedMatch: boolean;
|
|
onClick: () => void;
|
|
}>(({ position, resourceType, direction, match, isInSelectedMatch, onClick }) => {
|
|
const { t } = useTranslation();
|
|
|
|
const color = resourceTypeColors[resourceType] || '#6C757D';
|
|
|
|
// Create custom icon
|
|
const icon = useMemo(() => {
|
|
const markerHtml = `
|
|
<div style="
|
|
background-color: ${color};
|
|
border: 3px solid white;
|
|
border-radius: 50%;
|
|
width: 20px;
|
|
height: 20px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
|
${isInSelectedMatch ? 'transform: scale(1.2);' : ''}
|
|
transition: transform 0.2s ease;
|
|
">
|
|
<div style="
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
width: 8px;
|
|
height: 8px;
|
|
background-color: white;
|
|
border-radius: 50%;
|
|
"></div>
|
|
</div>
|
|
`;
|
|
|
|
return L.divIcon({
|
|
html: markerHtml,
|
|
className: 'custom-resource-flow-marker',
|
|
iconSize: [26, 26],
|
|
iconAnchor: [13, 13],
|
|
});
|
|
}, [color, isInSelectedMatch]);
|
|
|
|
const formatScore = (score: number) => {
|
|
return `${(score * 100).toFixed(1)}%`;
|
|
};
|
|
|
|
const formatCurrency = (value: number) => {
|
|
return new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: 'EUR',
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 0,
|
|
}).format(value);
|
|
};
|
|
|
|
return (
|
|
<Marker
|
|
position={position}
|
|
icon={icon}
|
|
eventHandlers={{
|
|
click: onClick,
|
|
mouseover: (e) => {
|
|
// Could add hover effects here
|
|
},
|
|
mouseout: (e) => {
|
|
// Reset hover effects here
|
|
},
|
|
}}
|
|
>
|
|
<Popup>
|
|
<div className="min-w-64">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<div
|
|
className="w-4 h-4 rounded-full border-2 border-white shadow-sm"
|
|
style={{ backgroundColor: color }}
|
|
/>
|
|
<h4 className="font-semibold text-sm">
|
|
{t(`resourceTypes.${resourceType}`, { defaultValue: resourceType })} {t('matchesMap.flow')}
|
|
</h4>
|
|
<span className={`text-xs px-2 py-1 rounded ${
|
|
direction === 'input' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800'
|
|
}`}>
|
|
{direction === 'input' ? t('matchesMap.input') : t('matchesMap.output')}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="space-y-2 text-sm">
|
|
<div>
|
|
<span className="text-muted-foreground">{t('matchesMap.matchId')}:</span>
|
|
<div className="font-mono text-xs bg-muted px-2 py-1 rounded mt-1">
|
|
{match.ID}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<div>
|
|
<span className="text-muted-foreground">{t('matchesMap.compatibility')}:</span>
|
|
<div className="font-medium">{formatScore(match.CompatibilityScore)}</div>
|
|
</div>
|
|
<div>
|
|
<span className="text-muted-foreground">{t('matchesMap.distance')}:</span>
|
|
<div className="font-medium">{match.DistanceKm.toFixed(1)} km</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<span className="text-muted-foreground">{t('matchesMap.economicValue')}:</span>
|
|
<div className="font-medium text-success">{formatCurrency(match.EconomicValue)}</div>
|
|
</div>
|
|
|
|
<div>
|
|
<span className="text-muted-foreground">{t('matchDetail.updateStatus')}:</span>
|
|
<span className={`ml-2 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}`, { defaultValue: match.Status })}
|
|
</span>
|
|
</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.viewMatchDetails')}
|
|
</button>
|
|
</div>
|
|
</Popup>
|
|
</Marker>
|
|
);
|
|
});
|
|
|
|
ResourceFlowMarker.displayName = 'ResourceFlowMarker';
|
|
|
|
const ResourceFlowMarkers: React.FC<ResourceFlowMarkersProps> = ({
|
|
matches,
|
|
selectedMatchId,
|
|
onMatchSelect
|
|
}) => {
|
|
// Transform matches into resource flow markers
|
|
const resourceFlowMarkers = useMemo(() => {
|
|
const markers: Array<{
|
|
id: string;
|
|
position: [number, number];
|
|
resourceType: string;
|
|
direction: string;
|
|
match: BackendMatch;
|
|
}> = [];
|
|
|
|
matches.forEach(match => {
|
|
// For now, we'll generate mock positions based on match ID
|
|
// In a real implementation, we'd get coordinates from site data
|
|
const baseLat = 55.1644; // Bugulma center
|
|
const baseLng = 50.2050;
|
|
|
|
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;
|
|
|
|
// Add source flow marker (assuming it's an output)
|
|
markers.push({
|
|
id: `${match.ID}-source`,
|
|
position: [lat1, lng1],
|
|
resourceType: 'heat', // Would come from match data
|
|
direction: 'output',
|
|
match,
|
|
});
|
|
|
|
// Add target flow marker (assuming it's an input)
|
|
markers.push({
|
|
id: `${match.ID}-target`,
|
|
position: [lat2, lng2],
|
|
resourceType: 'heat', // Would come from match data
|
|
direction: 'input',
|
|
match,
|
|
});
|
|
});
|
|
|
|
return markers;
|
|
}, [matches]);
|
|
|
|
return (
|
|
<MarkerClusterGroup
|
|
chunkedLoading={true}
|
|
chunkDelay={50}
|
|
chunkInterval={100}
|
|
maxClusterRadius={60}
|
|
spiderfyOnMaxZoom={true}
|
|
showCoverageOnHover={false}
|
|
zoomToBoundsOnClick={true}
|
|
disableClusteringAtZoom={16}
|
|
removeOutsideVisibleBounds={false}
|
|
animate={true}
|
|
animateAddingMarkers={true}
|
|
spiderfyDistanceMultiplier={2}
|
|
spiderLegPolylineOptions={{
|
|
weight: 1.5,
|
|
color: 'hsl(var(--muted-foreground))',
|
|
opacity: 0.5,
|
|
}}
|
|
iconCreateFunction={(cluster) => {
|
|
const count = cluster.getChildCount();
|
|
const size = count < 10 ? 'small' : count < 100 ? 'medium' : 'large';
|
|
const className = `marker-cluster marker-cluster-${size}`;
|
|
return L.divIcon({
|
|
html: `<div><span>${count}</span></div>`,
|
|
className,
|
|
iconSize: [40, 40],
|
|
});
|
|
}}
|
|
>
|
|
{resourceFlowMarkers.map(marker => (
|
|
<ResourceFlowMarker
|
|
key={marker.id}
|
|
position={marker.position}
|
|
resourceType={marker.resourceType}
|
|
direction={marker.direction}
|
|
match={marker.match}
|
|
isInSelectedMatch={marker.match.ID === selectedMatchId}
|
|
onClick={() => onMatchSelect(marker.match.ID)}
|
|
/>
|
|
))}
|
|
</MarkerClusterGroup>
|
|
);
|
|
};
|
|
|
|
export default React.memo(ResourceFlowMarkers);
|