infor annotations

This commit is contained in:
2025-12-13 14:09:20 +05:00
parent 4b0722787e
commit 378a8727e2
6 changed files with 87 additions and 13 deletions

View File

@@ -21,7 +21,7 @@ export function DrawingHandler({ mapId, onItemCreated }: DrawingHandlerProps) {
const [endDeviceId, setEndDeviceId] = useState<string | null>(null); const [endDeviceId, setEndDeviceId] = useState<string | null>(null);
const isCableTool = ['fiber', 'cat6', 'cat6_poe'].includes(activeTool); const isCableTool = ['fiber', 'cat6', 'cat6_poe'].includes(activeTool);
const isDeviceTool = ['switch', 'indoor_ap', 'outdoor_ap'].includes(activeTool); const isDeviceTool = ['switch', 'indoor_ap', 'outdoor_ap', 'info'].includes(activeTool);
const isWirelessTool = activeTool === 'wireless_mesh'; const isWirelessTool = activeTool === 'wireless_mesh';
// Load all map items for snapping // Load all map items for snapping
@@ -37,7 +37,7 @@ export function DrawingHandler({ mapId, onItemCreated }: DrawingHandlerProps) {
loadItems(); loadItems();
}, [mapId]); }, [mapId]);
// Find nearby device for snapping // Find nearby device for snapping (exclude info markers)
const findNearbyDevice = (lat: number, lng: number, radiusMeters = 5): any | null => { const findNearbyDevice = (lat: number, lng: number, radiusMeters = 5): any | null => {
const devices = allItems.filter(item => const devices = allItems.filter(item =>
['switch', 'indoor_ap', 'outdoor_ap'].includes(item.type) && ['switch', 'indoor_ap', 'outdoor_ap'].includes(item.type) &&
@@ -109,17 +109,25 @@ export function DrawingHandler({ mapId, onItemCreated }: DrawingHandlerProps) {
// Device placement - single click // Device placement - single click
if (isDeviceTool) { if (isDeviceTool) {
try { try {
const properties: any = {
name: activeTool === 'info'
? `Info at ${lat.toFixed(4)}, ${lng.toFixed(4)}`
: `${activeTool} at ${lat.toFixed(4)}, ${lng.toFixed(4)}`,
};
// Only add port_count and connections if it's not an info marker
if (activeTool !== 'info') {
properties.port_count = activeTool === 'switch' ? 5 : activeTool === 'outdoor_ap' ? 1 : 4;
properties.connections = [];
}
await mapItemService.createMapItem(mapId, { await mapItemService.createMapItem(mapId, {
type: activeTool as any, type: activeTool as any,
geometry: { geometry: {
type: 'Point', type: 'Point',
coordinates: [lng, lat], coordinates: [lng, lat],
}, },
properties: { properties,
name: `${activeTool} at ${lat.toFixed(4)}, ${lng.toFixed(4)}`,
port_count: activeTool === 'switch' ? 5 : activeTool === 'outdoor_ap' ? 1 : 4,
connections: [],
},
}); });
onItemCreated(); onItemCreated();
// Reload items for snapping // Reload items for snapping

View File

@@ -86,6 +86,19 @@ const outdoorApIcon = new L.DivIcon({
iconAnchor: [20, 40], iconAnchor: [20, 40],
}); });
const infoIcon = new L.DivIcon({
html: `<div class="info-marker-icon">
<svg viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" strokeWidth="2"/>
<path d="M12 16v-4" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
<circle cx="12" cy="8" r="1.2"/>
</svg>
</div>`,
className: 'custom-info-marker',
iconSize: [20, 20],
iconAnchor: [10, 20],
});
export function MapItemsLayer({ mapId, refreshTrigger, readOnly = false }: MapItemsLayerProps) { export function MapItemsLayer({ mapId, refreshTrigger, readOnly = false }: MapItemsLayerProps) {
const [items, setItems] = useState<MapItem[]>([]); const [items, setItems] = useState<MapItem[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -137,6 +150,8 @@ export function MapItemsLayer({ mapId, refreshTrigger, readOnly = false }: MapIt
return indoorApIcon; return indoorApIcon;
case 'outdoor_ap': case 'outdoor_ap':
return outdoorApIcon; return outdoorApIcon;
case 'info':
return infoIcon;
default: default:
return undefined; return undefined;
} }
@@ -343,9 +358,9 @@ export function MapItemsLayer({ mapId, refreshTrigger, readOnly = false }: MapIt
); );
} }
// Render devices // Render devices and info markers
if ( if (
['switch', 'indoor_ap', 'outdoor_ap'].includes(item.type) && ['switch', 'indoor_ap', 'outdoor_ap', 'info'].includes(item.type) &&
item.geometry.type === 'Point' item.geometry.type === 'Point'
) { ) {
const [lng, lat] = item.geometry.coordinates; const [lng, lat] = item.geometry.coordinates;
@@ -373,7 +388,9 @@ export function MapItemsLayer({ mapId, refreshTrigger, readOnly = false }: MapIt
<Popup> <Popup>
<div className="text-sm dark:bg-gray-800 dark:text-white" style={{ minWidth: '200px' }}> <div className="text-sm dark:bg-gray-800 dark:text-white" style={{ minWidth: '200px' }}>
<div className="font-semibold text-gray-900 dark:text-white">{item.properties.name || item.type}</div> <div className="font-semibold text-gray-900 dark:text-white">{item.properties.name || item.type}</div>
{item.type !== 'info' && (
<div className="text-gray-600 dark:text-gray-400">Type: {item.type}</div> <div className="text-gray-600 dark:text-gray-400">Type: {item.type}</div>
)}
{item.properties.port_count && ( {item.properties.port_count && (
<div className="text-gray-600 dark:text-gray-400"> <div className="text-gray-600 dark:text-gray-400">
Ports: {item.properties.connections?.length || 0} / {item.properties.port_count} Ports: {item.properties.connections?.length || 0} / {item.properties.port_count}

View File

@@ -74,6 +74,13 @@ const DEVICE_TOOLS: ToolButton[] = [
}, },
]; ];
const INFO_TOOL: ToolButton = {
id: 'info',
label: 'Info',
icon: 'info',
description: 'Information Marker',
};
export function Toolbar({ mapId, readOnly = false }: ToolbarProps) { export function Toolbar({ mapId, readOnly = false }: ToolbarProps) {
const { activeTool, setActiveTool } = useDrawingStore(); const { activeTool, setActiveTool } = useDrawingStore();
@@ -122,6 +129,17 @@ export function Toolbar({ mapId, readOnly = false }: ToolbarProps) {
); );
} }
if (tool.icon === 'info') {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" style={{ color: '#6366F1' }}>
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" strokeWidth="2"/>
<path d="M12 16v-4"/>
<circle cx="12" cy="8" r="1"/>
<line x1="12" y1="12" x2="12" y2="16" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
</svg>
);
}
return ( return (
<span <span
className="text-lg" className="text-lg"
@@ -180,6 +198,12 @@ export function Toolbar({ mapId, readOnly = false }: ToolbarProps) {
{/* Devices section */} {/* Devices section */}
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 px-1 mb-2">Devices</h3> <h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 px-1 mb-2">Devices</h3>
{DEVICE_TOOLS.map(renderToolButton)} {DEVICE_TOOLS.map(renderToolButton)}
<div className="border-t border-gray-200 dark:border-gray-700 my-2"></div>
{/* Info section */}
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 px-1 mb-2">Annotations</h3>
{renderToolButton(INFO_TOOL)}
</div> </div>
); );
} }

View File

@@ -125,6 +125,30 @@
color: #F59E0B; color: #F59E0B;
} }
/* Info marker icon */
.custom-info-marker {
background: transparent !important;
border: none !important;
}
.info-marker-icon {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
background: white;
border-radius: 50%;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
border: 2px solid #6366F1;
color: #6366F1;
}
.dark .info-marker-icon {
background: rgb(31, 41, 55);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.6);
}
/* Distance labels on map */ /* Distance labels on map */
.distance-label { .distance-label {
background-color: rgba(255, 255, 255, 0.95) !important; background-color: rgba(255, 255, 255, 0.95) !important;

View File

@@ -63,7 +63,7 @@ export function Dashboard() {
<div <div
className={` className={`
flex flex-col bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 transition-all duration-300 ease-in-out flex flex-col bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 transition-all duration-300 ease-in-out
fixed md:relative h-full w-80 fixed md:relative h-full w-80 overflow-y-auto
${isSidebarOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0'} ${isSidebarOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0'}
`} `}
style={{ zIndex: 9999 }} style={{ zIndex: 9999 }}

View File

@@ -1,4 +1,4 @@
export type MapItemType = 'cable' | 'switch' | 'indoor_ap' | 'outdoor_ap' | 'wireless_mesh'; export type MapItemType = 'cable' | 'switch' | 'indoor_ap' | 'outdoor_ap' | 'wireless_mesh' | 'info';
export type CableType = 'fiber' | 'cat6' | 'cat6_poe'; export type CableType = 'fiber' | 'cat6' | 'cat6_poe';
@@ -60,7 +60,8 @@ export type DrawingTool =
| 'switch' | 'switch'
| 'indoor_ap' | 'indoor_ap'
| 'outdoor_ap' | 'outdoor_ap'
| 'wireless_mesh'; | 'wireless_mesh'
| 'info';
export const CABLE_COLORS: Record<CableType, string> = { export const CABLE_COLORS: Record<CableType, string> = {
fiber: '#3B82F6', // Blue fiber: '#3B82F6', // Blue