diff --git a/app/models/map_item.py b/app/models/map_item.py index e6e2ae6..7e36e01 100644 --- a/app/models/map_item.py +++ b/app/models/map_item.py @@ -16,15 +16,18 @@ class MapItem(Base): - switch: Network switch - indoor_ap: Indoor access point - outdoor_ap: Outdoor access point + - other_device: Other device (1 ethernet port) + - info: Information marker Geometry: - - Point for devices (switches, APs) + - Point for devices (switches, APs, other devices, info markers) - LineString for cables and wireless mesh Properties (JSONB): - For cables: cable_type, name, notes, length_meters, start_device_id, end_device_id - For devices: name, notes, port_count, connections (array of {cable_id, port_number}) - For wireless_mesh: name, notes, start_ap_id, end_ap_id + - For info markers: name, notes, image (optional) """ __tablename__ = "map_items" diff --git a/app/schemas/map_item.py b/app/schemas/map_item.py index 30fb2a5..c9383ea 100644 --- a/app/schemas/map_item.py +++ b/app/schemas/map_item.py @@ -6,7 +6,7 @@ from uuid import UUID class MapItemBase(BaseModel): """Base map item schema with common attributes.""" - type: str = Field(..., description="Item type: cable, wireless_mesh, switch, indoor_ap, outdoor_ap") + type: str = Field(..., description="Item type: cable, wireless_mesh, switch, indoor_ap, outdoor_ap, other_device, info") geometry: Dict[str, Any] = Field(..., description="GeoJSON geometry (Point or LineString)") properties: Dict[str, Any] = Field(default_factory=dict, description="Item-specific properties") diff --git a/public/src/components/map/DrawingHandler.tsx b/public/src/components/map/DrawingHandler.tsx index a1450c0..f440cf3 100644 --- a/public/src/components/map/DrawingHandler.tsx +++ b/public/src/components/map/DrawingHandler.tsx @@ -20,7 +20,7 @@ export function DrawingHandler({ mapId, onItemCreated }: DrawingHandlerProps) { const [endDeviceId, setEndDeviceId] = useState(null); const isCableTool = ['fiber', 'cat6', 'cat6_poe'].includes(activeTool); - const isDeviceTool = ['switch', 'indoor_ap', 'outdoor_ap', 'info'].includes(activeTool); + const isDeviceTool = ['switch', 'indoor_ap', 'outdoor_ap', 'other_device', 'info'].includes(activeTool); const isWirelessTool = activeTool === 'wireless_mesh'; // Load all map items for snapping @@ -39,7 +39,7 @@ export function DrawingHandler({ mapId, onItemCreated }: DrawingHandlerProps) { // Find nearby device for snapping (exclude info markers) const findNearbyDevice = (lat: number, lng: number, radiusMeters = 5): any | null => { const devices = allItems.filter(item => - ['switch', 'indoor_ap', 'outdoor_ap'].includes(item.type) && + ['switch', 'indoor_ap', 'outdoor_ap', 'other_device'].includes(item.type) && item.geometry.type === 'Point' ); @@ -116,7 +116,7 @@ export function DrawingHandler({ mapId, onItemCreated }: DrawingHandlerProps) { // 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.port_count = activeTool === 'switch' ? 5 : (activeTool === 'outdoor_ap' || activeTool === 'other_device') ? 1 : 4; properties.connections = []; } diff --git a/public/src/components/map/ItemContextMenu.tsx b/public/src/components/map/ItemContextMenu.tsx index 23a8934..a7a5704 100644 --- a/public/src/components/map/ItemContextMenu.tsx +++ b/public/src/components/map/ItemContextMenu.tsx @@ -27,7 +27,7 @@ export function ItemContextMenu({ item, position, onClose, onUpdate }: ItemConte const fileInputRef = useRef(null); const isSwitch = item.type === 'switch'; - const isDevice = ['switch', 'indoor_ap', 'outdoor_ap'].includes(item.type); + const isDevice = ['switch', 'indoor_ap', 'outdoor_ap', 'other_device'].includes(item.type); const hasConnections = item.properties.connections && item.properties.connections.length > 0; useEffect(() => { diff --git a/public/src/components/map/MapItemsLayer.tsx b/public/src/components/map/MapItemsLayer.tsx index 7e7f5e2..3398bc7 100644 --- a/public/src/components/map/MapItemsLayer.tsx +++ b/public/src/components/map/MapItemsLayer.tsx @@ -88,6 +88,22 @@ const outdoorApIcon = new L.DivIcon({ iconAnchor: [20, 40], }); +const otherDeviceIcon = new L.DivIcon({ + html: `
+ + + + + + + + +
`, + className: 'custom-device-marker', + iconSize: [40, 40], + iconAnchor: [20, 40], +}); + const infoIcon = new L.DivIcon({ html: `
@@ -127,7 +143,7 @@ export function MapItemsLayer({ mapId, refreshTrigger, readOnly = false }: MapIt console.log('Loaded items:', data); // Log devices with their connections data.forEach(item => { - if (['switch', 'indoor_ap', 'outdoor_ap'].includes(item.type)) { + if (['switch', 'indoor_ap', 'outdoor_ap', 'other_device'].includes(item.type)) { console.log(`Device ${item.type} (${item.id}): ${item.properties.connections?.length || 0} / ${item.properties.port_count} ports`); } }); @@ -153,6 +169,8 @@ export function MapItemsLayer({ mapId, refreshTrigger, readOnly = false }: MapIt return indoorApIcon; case 'outdoor_ap': return outdoorApIcon; + case 'other_device': + return otherDeviceIcon; case 'info': return infoIcon; default: @@ -379,7 +397,7 @@ export function MapItemsLayer({ mapId, refreshTrigger, readOnly = false }: MapIt // Render devices and info markers if ( - ['switch', 'indoor_ap', 'outdoor_ap', 'info'].includes(item.type) && + ['switch', 'indoor_ap', 'outdoor_ap', 'other_device', 'info'].includes(item.type) && item.geometry.type === 'Point' ) { const [lng, lat] = item.geometry.coordinates; diff --git a/public/src/components/map/Toolbar.tsx b/public/src/components/map/Toolbar.tsx index cb521fd..0d9f25f 100644 --- a/public/src/components/map/Toolbar.tsx +++ b/public/src/components/map/Toolbar.tsx @@ -71,6 +71,12 @@ const DEVICE_TOOLS: ToolButton[] = [ icon: 'outdoor_ap', description: 'Outdoor Access Point', }, + { + id: 'other_device', + label: 'Other Device', + icon: 'other_device', + description: 'Other Device', + }, ]; const INFO_TOOL: ToolButton = { @@ -128,6 +134,19 @@ export function Toolbar({ readOnly = false }: ToolbarProps) { ); } + if (tool.icon === 'other_device') { + return ( + + + + + + + + + ); + } + if (tool.icon === 'info') { return ( diff --git a/public/src/index.css b/public/src/index.css index cc0f4c2..a061ee7 100644 --- a/public/src/index.css +++ b/public/src/index.css @@ -138,6 +138,11 @@ color: #F59E0B; } +.other-device-icon { + border-color: #6B7280; + color: #6B7280; +} + /* Info marker icon */ .custom-info-marker { background: transparent !important; diff --git a/public/src/types/mapItem.ts b/public/src/types/mapItem.ts index 2a29918..fa8acd4 100644 --- a/public/src/types/mapItem.ts +++ b/public/src/types/mapItem.ts @@ -1,4 +1,4 @@ -export type MapItemType = 'cable' | 'switch' | 'indoor_ap' | 'outdoor_ap' | 'wireless_mesh' | 'info'; +export type MapItemType = 'cable' | 'switch' | 'indoor_ap' | 'outdoor_ap' | 'other_device' | 'wireless_mesh' | 'info'; export type CableType = 'fiber' | 'cat6' | 'cat6_poe'; @@ -60,6 +60,7 @@ export type DrawingTool = | 'switch' | 'indoor_ap' | 'outdoor_ap' + | 'other_device' | 'wireless_mesh' | 'info';