diff --git a/AGENTS.md b/AGENTS.md index cff6c9d..a9c8fd7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,3 +50,6 @@ Wenn ein Skill genannt wird (z. B. `$skill-creator`) oder die Aufgabe exakt zu ## Einschränkungen - Sandbox ist lesend; bitte selbst `AGENTS.md` anlegen. - Jegliche Ausgaben/Antworten sollten den Developer-Guidelines folgen (kurz, teamorientiert, klare nächste Schritte). + +## Wichtig: +- Nutze UTF-8 wenn nicht anders angegeben \ No newline at end of file diff --git a/TODO.md b/TODO.md index fdbfaa5..e3d6d12 100644 --- a/TODO.md +++ b/TODO.md @@ -162,7 +162,7 @@ Hinweis: Die Eintraege sind direkt aus den Quelldateien aggregiert. ## app\modules\floor_infrastructure\list.php -- [ ] L143:

TODO: SVG-Editor mit Drag & Drop für diese Objekte erweitern (siehe Stockwerke-Modul).

+- [ ] L143:

//TODO: SVG-Editor mit Drag & Drop für diese Objekte erweitern (siehe Stockwerke-Modul).

## app\modules\floors\list.php @@ -176,7 +176,6 @@ Hinweis: Die Eintraege sind direkt aus den Quelldateien aggregiert. - [ ] L134: //TODO design schlecht, mach es hübscher - [ ] L208: //TODO style in css file -- [ ] L406: // TODO: AJAX-Delete implementieren ## app\modules\racks\edit.php @@ -234,4 +233,4 @@ Hinweis: Die Eintraege sind direkt aus den Quelldateien aggregiert. - [ ] L241: ### TODO: Patchpanel-Infrastruktur - [ ] L253: - TODO: SVG-Editor um Drag & Drop für diese Objekte erweitern und Klicks direkt mit dem Modul verbinden. -- [ ] //TODO infrastruktur patchfelder löschen soll implementiert werden. \ No newline at end of file +- [ ] //TODO infrastruktur patchfelder löschen soll implementiert werden. diff --git a/app/index.php b/app/index.php index 9ac3a36..a76aab9 100644 --- a/app/index.php +++ b/app/index.php @@ -34,8 +34,7 @@ $validActions = ['list', 'edit', 'save', 'ports', 'delete']; // Prüfen auf gültige Werte if (!in_array($module, $validModules)) { - // TODO: Fehlerseite anzeigen, nutze renderClientError(...) - die('Ungültiges Modul'); + renderClientError(400, 'Ungültiges Modul'); } if (!in_array($action, $validActions)) { diff --git a/app/modules/connections/list.php b/app/modules/connections/list.php index 2f84393..5c24022 100644 --- a/app/modules/connections/list.php +++ b/app/modules/connections/list.php @@ -68,6 +68,66 @@ $connections = $sql->get( // ========================= $devices = $sql->get("SELECT id, name FROM devices ORDER BY name", "", []); +$selectedDevice = null; +$selectedDevicePorts = []; +$selectedDeviceVlans = []; + +if ($deviceId > 0) { + $selectedDevice = $sql->single( + "SELECT d.id, d.name, dt.name AS type_name + FROM devices d + LEFT JOIN device_types dt ON d.device_type_id = dt.id + WHERE d.id = ?", + "i", + [$deviceId] + ); + + if ($selectedDevice) { + $selectedDevice['port_count'] = (int)($sql->single( + "SELECT COUNT(*) AS cnt FROM device_ports WHERE device_id = ?", + "i", + [$deviceId] + )['cnt'] ?? 0); + + $selectedDevice['connection_count'] = (int)($sql->single( + "SELECT COUNT(DISTINCT c.id) AS cnt + FROM connections c + LEFT JOIN device_ports dpt1 ON c.port_a_type = 'device' AND c.port_a_id = dpt1.id + LEFT JOIN device_ports dpt2 ON c.port_b_type = 'device' AND c.port_b_id = dpt2.id + WHERE dpt1.device_id = ? OR dpt2.device_id = ?", + "ii", + [$deviceId, $deviceId] + )['cnt'] ?? 0); + + $selectedDevicePorts = $sql->get( + "SELECT name, vlan_config + FROM device_ports + WHERE device_id = ? + ORDER BY id + LIMIT 12", + "i", + [$deviceId] + ); + + foreach ($selectedDevicePorts as $port) { + if (empty($port['vlan_config'])) { + continue; + } + + $vlans = json_decode($port['vlan_config'], true); + foreach ((array)$vlans as $vlan) { + $vlan = trim((string)$vlan); + if ($vlan !== '') { + $selectedDeviceVlans[$vlan] = true; + } + } + } + + $selectedDeviceVlans = array_keys($selectedDeviceVlans); + natcasesort($selectedDeviceVlans); + } +} + ?>
@@ -195,14 +255,31 @@ $devices = $sql->get("SELECT id, name FROM devices ORDER BY name", "", []); ========================= --> diff --git a/app/modules/floor_infrastructure/edit.php b/app/modules/floor_infrastructure/edit.php index c45b729..709cbb5 100644 --- a/app/modules/floor_infrastructure/edit.php +++ b/app/modules/floor_infrastructure/edit.php @@ -94,6 +94,7 @@ if ($type === 'patchpanel') { $defaultPanelSize = ['width' => 140, 'height' => 40]; $defaultOutletSize = 32; +$showPanelPlacementFields = $type === 'patchpanel' && $selectedFloorId > 0; if ($type === 'patchpanel') { $panel['width'] = $panel['width'] ?? $defaultPanelSize['width']; @@ -160,26 +161,26 @@ $markerHeight = $type === 'patchpanel' ? $panel['height'] : $defaultOutletSize;
-
+
>
- +
- +
- +
- +
-
+
>
+

> + Position, Groesse und Stockwerkskarte werden erst angezeigt, sobald ein Stockwerk ausgewaehlt ist. +

+
@@ -476,6 +481,9 @@ document.addEventListener('DOMContentLoaded', () => { const panelFloorSelect = document.getElementById('panel-floor-select'); const outletRoomSelect = document.getElementById('outlet-room-select'); const floorPlanSvg = document.getElementById('floor-plan-svg'); + const panelPlacementFields = document.getElementById('panel-placement-fields'); + const panelFloorPlanGroup = document.getElementById('panel-floor-plan-group'); + const panelFloorMissingHint = document.getElementById('panel-floor-missing-hint'); const buildingOptions = panelBuildingSelect ? Array.from(panelBuildingSelect.options).filter((option) => option.value !== '') : []; const floorOptions = panelFloorSelect ? Array.from(panelFloorSelect.options).filter((option) => option.value !== '') : []; @@ -491,8 +499,30 @@ document.addEventListener('DOMContentLoaded', () => { if (svgUrl) { floorPlanSvg.src = svgUrl; + floorPlanSvg.hidden = false; } else { floorPlanSvg.removeAttribute('src'); + floorPlanSvg.hidden = true; + } + }; + + if (floorPlanSvg) { + floorPlanSvg.addEventListener('error', () => { + floorPlanSvg.removeAttribute('src'); + floorPlanSvg.hidden = true; + }); + } + + const updatePanelPlacementVisibility = () => { + if (!panelFloorSelect || !panelPlacementFields || !panelFloorPlanGroup) { + return; + } + + const hasFloor = !!panelFloorSelect.value; + panelPlacementFields.hidden = !hasFloor; + panelFloorPlanGroup.hidden = !hasFloor; + if (panelFloorMissingHint) { + panelFloorMissingHint.hidden = hasFloor; } }; @@ -517,6 +547,7 @@ document.addEventListener('DOMContentLoaded', () => { panelFloorSelect.value = firstMatch; } + updatePanelPlacementVisibility(); updateFloorPlanImage(); }; @@ -557,6 +588,7 @@ document.addEventListener('DOMContentLoaded', () => { if (panelFloorSelect) { panelFloorSelect.addEventListener('change', () => { + updatePanelPlacementVisibility(); updateFloorPlanImage(); }); } @@ -573,5 +605,8 @@ document.addEventListener('DOMContentLoaded', () => { } else { updateFloorPlanImage(); } + + updatePanelPlacementVisibility(); }); + diff --git a/app/modules/floor_infrastructure/list.php b/app/modules/floor_infrastructure/list.php index ad9db53..b4c1117 100644 --- a/app/modules/floor_infrastructure/list.php +++ b/app/modules/floor_infrastructure/list.php @@ -2,12 +2,12 @@ /** * app/modules/floor_infrastructure/list.php * - * Übersicht über Patchpanels und Netzwerkbuchsen auf Stockwerken + * Uebersicht ueber Patchpanels und Netzwerkbuchsen auf Stockwerken */ $floorId = (int)($_GET['floor_id'] ?? 0); -$floors = $sql->get("SELECT id, name FROM floors ORDER BY name", "", []); +$floors = $sql->get("SELECT id, name, svg_path FROM floors ORDER BY name", "", []); $where = ''; $types = ''; @@ -30,7 +30,7 @@ $patchPanels = $sql->get( ); $networkOutlets = $sql->get( - "SELECT o.*, r.name AS room_name, f.name AS floor_name + "SELECT o.*, r.name AS room_name, f.name AS floor_name, f.id AS floor_id FROM network_outlets o LEFT JOIN rooms r ON r.id = o.room_id LEFT JOIN floors f ON f.id = r.floor_id @@ -38,6 +38,62 @@ $networkOutlets = $sql->get( "", [] ); + +$floorMap = []; +foreach ($floors as $floor) { + $id = (int)$floor['id']; + $svgPath = trim((string)($floor['svg_path'] ?? '')); + $floorMap[$id] = [ + 'id' => $id, + 'name' => (string)($floor['name'] ?? ''), + 'svg_url' => $svgPath !== '' ? '/' . ltrim($svgPath, '/\\') : '' + ]; +} + +$editorFloorId = $floorId; +if ($editorFloorId <= 0) { + foreach ($floorMap as $candidate) { + if ($candidate['svg_url'] !== '') { + $editorFloorId = (int)$candidate['id']; + break; + } + } +} + +$editorFloor = $floorMap[$editorFloorId] ?? null; +$editorPatchPanels = []; +$editorOutlets = []; + +if ($editorFloorId > 0) { + foreach ($patchPanels as $panel) { + if ((int)$panel['floor_id'] === $editorFloorId) { + $editorPatchPanels[] = [ + 'id' => (int)$panel['id'], + 'name' => (string)$panel['name'], + 'floor_id' => (int)$panel['floor_id'], + 'x' => (int)$panel['pos_x'], + 'y' => (int)$panel['pos_y'], + 'width' => max(1, (int)$panel['width']), + 'height' => max(1, (int)$panel['height']), + 'port_count' => (int)$panel['port_count'], + 'comment' => (string)($panel['comment'] ?? '') + ]; + } + } + + foreach ($networkOutlets as $outlet) { + if ((int)$outlet['floor_id'] === $editorFloorId) { + $editorOutlets[] = [ + 'id' => (int)$outlet['id'], + 'name' => (string)$outlet['name'], + 'room_id' => (int)$outlet['room_id'], + 'x' => (int)$outlet['x'], + 'y' => (int)$outlet['y'], + 'comment' => (string)($outlet['comment'] ?? '') + ]; + } + } +} ?>
@@ -45,10 +101,10 @@ $networkOutlets = $sql->get( @@ -66,7 +122,7 @@ $networkOutlets = $sql->get( - Zurücksetzen + Zuruecksetzen
@@ -78,7 +134,7 @@ $networkOutlets = $sql->get( Name Stockwerk Position - Größe + Groesse Ports Aktionen @@ -87,9 +143,9 @@ $networkOutlets = $sql->get( - - - + + + Bearbeiten @@ -121,9 +177,9 @@ $networkOutlets = $sql->get( - - - + + + Bearbeiten @@ -139,8 +195,20 @@ $networkOutlets = $sql->get(

Stockwerksplan-Verortung

-

Die eingetragenen Patchpanels und Wandbuchsen erscheinen später als feste Objekte auf dem Stockwerks-SVG. Die Polygon-Positionen werden momentan noch durch numerische X/Y-Werte gesteuert.

-

TODO: SVG-Editor mit Drag & Drop für diese Objekte erweitern (siehe Stockwerke-Modul).

+ +

Kein Stockwerk fuer die Planansicht verfuegbar.

+ +

Das gewaehlte Stockwerk hat noch keinen SVG-Plan hinterlegt.

+ +

Drag & Drop ist aktiv fuer . Aenderungen werden direkt gespeichert.

+
+ Stockwerksplan +
+

Patchpanels: blau, Wandbuchsen: gruen. Ziehen und loslassen zum Speichern.

+
@@ -184,6 +252,54 @@ $networkOutlets = $sql->get( border: 1px dashed #ccc; border-radius: 6px; } +.infra-floor-canvas { + position: relative; + margin-top: 12px; + width: 100%; + min-height: 420px; + border: 1px solid #d4d4d4; + border-radius: 8px; + background: #fff; + overflow: hidden; +} +.infra-floor-svg { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: contain; + pointer-events: none; + opacity: 0.85; +} +.infra-marker { + position: absolute; + top: 0; + left: 0; + user-select: none; + touch-action: none; + cursor: move; + z-index: 2; +} +.infra-marker.patchpanel { + background: rgba(13, 110, 253, 0.25); + border: 2px solid #0d6efd; + border-radius: 6px; +} +.infra-marker.outlet { + width: 32px; + height: 32px; + background: rgba(25, 135, 84, 0.25); + border: 2px solid #198754; + border-radius: 4px; +} +.infra-marker.is-saving { + opacity: 0.65; +} +.floor-plan-hint { + margin: 8px 0 0; + font-size: 0.85em; + color: #444; +} .empty-state { padding: 20px; background: #fafafa; @@ -194,3 +310,125 @@ $networkOutlets = $sql->get( margin-right: 6px; } + + + + diff --git a/app/modules/floor_infrastructure/save.php b/app/modules/floor_infrastructure/save.php index 5a61f27..8d00d5d 100644 --- a/app/modules/floor_infrastructure/save.php +++ b/app/modules/floor_infrastructure/save.php @@ -18,6 +18,13 @@ if ($type === 'patchpanel') { $portCount = (int)($_POST['port_count'] ?? 0); $comment = trim($_POST['comment'] ?? ''); + if ($width <= 0) { + $width = 140; + } + if ($height <= 0) { + $height = 40; + } + if ($id > 0) { $sql->set( "UPDATE floor_patchpanels SET name = ?, floor_id = ?, pos_x = ?, pos_y = ?, width = ?, height = ?, port_count = ?, comment = ? WHERE id = ?", diff --git a/app/modules/locations/delete.php b/app/modules/locations/delete.php new file mode 100644 index 0000000..813b39f --- /dev/null +++ b/app/modules/locations/delete.php @@ -0,0 +1,55 @@ + false, + 'message' => 'Ungueltige Standort-ID' + ]); + exit; +} + +$location = $sql->single( + "SELECT id, name FROM locations WHERE id = ?", + "i", + [$locationId] +); + +if (!$location) { + http_response_code(404); + echo json_encode([ + 'success' => false, + 'message' => 'Standort nicht gefunden' + ]); + exit; +} + +$deleted = $sql->set( + "DELETE FROM locations WHERE id = ?", + "i", + [$locationId] +); + +if ($deleted <= 0) { + http_response_code(500); + echo json_encode([ + 'success' => false, + 'message' => 'Standort konnte nicht geloescht werden' + ]); + exit; +} + +echo json_encode([ + 'success' => true, + 'message' => 'Standort geloescht' +]); +exit; diff --git a/app/modules/locations/list.php b/app/modules/locations/list.php index f0e78fb..3d46586 100644 --- a/app/modules/locations/list.php +++ b/app/modules/locations/list.php @@ -403,8 +403,23 @@ foreach ($floors as $floor) { +