import { useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; import { Polyline, Marker, Popup, Circle, useMapEvents } from 'react-leaflet'; import L from 'leaflet'; import { mapItemService } from '../../services/mapItemService'; import { CABLE_COLORS, type MapItem, type CableType } from '../../types/mapItem'; import { ItemContextMenu } from './ItemContextMenu'; import { useDrawingStore } from '../../stores/drawingStore'; interface MapItemsLayerProps { mapId: string; refreshTrigger: number; readOnly?: boolean; } // Custom marker icons for devices using CSS const switchIcon = new L.DivIcon({ html: `
`, className: 'custom-device-marker', iconSize: [40, 40], iconAnchor: [20, 40], }); const indoorApIcon = new L.DivIcon({ html: `
`, className: 'custom-device-marker', iconSize: [40, 40], iconAnchor: [20, 40], }); const outdoorApIcon = new L.DivIcon({ html: `
`, className: 'custom-device-marker', iconSize: [40, 40], iconAnchor: [20, 40], }); export function MapItemsLayer({ mapId, refreshTrigger, readOnly = false }: MapItemsLayerProps) { const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [contextMenu, setContextMenu] = useState<{ item: MapItem; position: { x: number; y: number }; } | null>(null); const { activeTool } = useDrawingStore(); // Check if we're in drawing mode (should suppress popups) const isCableTool = ['fiber', 'cat6', 'cat6_poe'].includes(activeTool); const isWirelessTool = activeTool === 'wireless_mesh'; const shouldSuppressPopups = isCableTool || isWirelessTool; useEffect(() => { loadItems(); }, [mapId, refreshTrigger]); const loadItems = async () => { try { setLoading(true); const data = await mapItemService.getMapItems(mapId); console.log('Loaded items:', data); // Log devices with their connections data.forEach(item => { if (['switch', 'indoor_ap', 'outdoor_ap'].includes(item.type)) { console.log(`Device ${item.type} (${item.id}): ${item.properties.connections?.length || 0} / ${item.properties.port_count} ports`); } }); setItems(data); } catch (error) { console.error('Failed to load map items:', error); } finally { setLoading(false); } }; // Close context menu on any map click useMapEvents({ click: () => setContextMenu(null), contextmenu: () => {}, // Prevent default map context menu }); const getDeviceIcon = (type: string) => { switch (type) { case 'switch': return switchIcon; case 'indoor_ap': return indoorApIcon; case 'outdoor_ap': return outdoorApIcon; default: return undefined; } }; if (loading) return null; return ( <> {items.map((item) => { // Render cables if (item.type === 'cable' && item.geometry.type === 'LineString') { const positions = item.geometry.coordinates.map( ([lng, lat]) => [lat, lng] as [number, number] ); const cableType = item.properties.cable_type as CableType; const color = CABLE_COLORS[cableType] || '#6B7280'; // Find connected devices const startDevice = item.properties.start_device_id ? items.find(i => i.id === item.properties.start_device_id) : null; const endDevice = item.properties.end_device_id ? items.find(i => i.id === item.properties.end_device_id) : null; return (
{ L.DomEvent.stopPropagation(e); if (!readOnly) { setContextMenu({ item, position: { x: e.originalEvent.clientX, y: e.originalEvent.clientY } }); } }, }} > {!shouldSuppressPopups && (
{item.properties.name || 'Cable'}
Type: {cableType}
{startDevice && (
From: {startDevice.properties.name || startDevice.type}
)} {endDevice && (
To: {endDevice.properties.name || endDevice.type}
)} {item.properties.notes && (
{item.properties.notes}
)} {item.properties.image && (
Attachment
)}
)}
{/* Show circles at cable bend points (not first/last) */} {positions.slice(1, -1).map((pos, idx) => ( ))}
); } // Render wireless mesh if (item.type === 'wireless_mesh' && item.geometry.type === 'LineString') { const positions = item.geometry.coordinates.map( ([lng, lat]) => [lat, lng] as [number, number] ); // Find connected APs const startAp = item.properties.start_ap_id ? items.find(i => i.id === item.properties.start_ap_id) : null; const endAp = item.properties.end_ap_id ? items.find(i => i.id === item.properties.end_ap_id) : null; return ( { L.DomEvent.stopPropagation(e); if (!readOnly) { setContextMenu({ item, position: { x: e.originalEvent.clientX, y: e.originalEvent.clientY } }); } }, }} > {!shouldSuppressPopups && (
{item.properties.name || 'Wireless Mesh'}
{startAp && (
From: {startAp.properties.name || startAp.type}
)} {endAp && (
To: {endAp.properties.name || endAp.type}
)} {item.properties.notes && (
{item.properties.notes}
)} {item.properties.image && (
Attachment
)}
)}
); } // Render devices if ( ['switch', 'indoor_ap', 'outdoor_ap'].includes(item.type) && item.geometry.type === 'Point' ) { const [lng, lat] = item.geometry.coordinates; const position: [number, number] = [lat, lng]; const icon = getDeviceIcon(item.type); return ( { L.DomEvent.stopPropagation(e); if (!readOnly) { setContextMenu({ item, position: { x: e.originalEvent.clientX, y: e.originalEvent.clientY } }); } }, }} > {!shouldSuppressPopups && (
{item.properties.name || item.type}
Type: {item.type}
{item.properties.port_count && (
Ports: {item.properties.connections?.length || 0} / {item.properties.port_count}
)} {/* Show port connections details */} {item.properties.connections && item.properties.connections.length > 0 && (
Port Connections:
{item.properties.connections .sort((a: any, b: any) => a.port_number - b.port_number) .map((conn: any) => { const cable = items.find(i => i.id === conn.cable_id); if (!cable) return null; // Find the other device connected to this cable const otherDeviceId = cable.properties.start_device_id === item.id ? cable.properties.end_device_id : cable.properties.start_device_id; const otherDevice = otherDeviceId ? items.find(i => i.id === otherDeviceId) : null; const cableType = cable.properties.cable_type; return (
Port {conn.port_number} → {otherDevice ? `${otherDevice.properties.name || otherDevice.type} (${cableType})` : `${cableType} cable`}
); })}
)} {/* Show wireless mesh count for APs */} {['indoor_ap', 'outdoor_ap'].includes(item.type) && (
Wireless mesh: {items.filter(i => i.type === 'wireless_mesh' && (i.properties.start_ap_id === item.id || i.properties.end_ap_id === item.id) ).length} / 4
)} {/* Show wireless mesh connections details for APs */} {['indoor_ap', 'outdoor_ap'].includes(item.type) && (() => { const meshLinks = items.filter(i => i.type === 'wireless_mesh' && (i.properties.start_ap_id === item.id || i.properties.end_ap_id === item.id) ); if (meshLinks.length > 0) { return (
Mesh Connections:
{meshLinks.map((mesh) => { const otherApId = mesh.properties.start_ap_id === item.id ? mesh.properties.end_ap_id : mesh.properties.start_ap_id; const otherAp = otherApId ? items.find(i => i.id === otherApId) : null; return (
→ {otherAp ? (otherAp.properties.name || otherAp.type) : 'Unknown AP'}
); })}
); } return null; })()} {item.properties.notes && (
{item.properties.notes}
)} {item.properties.image && (
Attachment
)}
)}
); } return null; })} {/* Context menu rendered outside map using portal */} {contextMenu && createPortal( setContextMenu(null)} onUpdate={loadItems} />, document.body )} ); }