Compare commits
21 Commits
c8fb5b140c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 96f885efde | |||
| dbe977f62c | |||
| b973d2857b | |||
| 0642a3b6ef | |||
| 4214ac45d9 | |||
| 900b110ee0 | |||
| 346cf33eb7 | |||
| 1a51d2507b | |||
| 9ece132df5 | |||
| 9121a2ddfd | |||
| d9be0e1482 | |||
| ebd4740b7e | |||
| 0ac1889946 | |||
| 9b8dc17d20 | |||
| 17a5bc4812 | |||
| 4dc1530402 | |||
| 20638cb3a5 | |||
| 77758f71d3 | |||
| f4ce7f360d | |||
| ec20fa2f96 | |||
| ce4ef5527f |
89
AGENTS.md
89
AGENTS.md
@@ -1,55 +1,58 @@
|
|||||||
# AGENTS.md
|
# AGENTS.md
|
||||||
|
|
||||||
## Ziel der Datei
|
## Ziel
|
||||||
Dieses Dokument beschreibt, welche Informationen ich als Agent für `p:\netwatch` erwarten würde: Projektziel, Setup, Regeln, Skills, bekannte Issues, Kontext & Einschränkungen.
|
Codex arbeitet pragmatisch bei Aufgaben aus `NEXT.md` und User-Requests.
|
||||||
|
Ein Gitea-Issue ist optional.
|
||||||
|
|
||||||
## Projektüberblick
|
## Kernregeln
|
||||||
- Name: **netwatch** – ein Netzwerk-Dokumentations- und Verkabelungsverwaltungs-Tool (Alpha v0.2, Core-Module funktionsfähig, Stand: 13. Februar 2026).
|
|
||||||
- Features: Dashboard, Gerätetypen-/Geräteverwaltung, Racks/Floors mit SVG-Planung, Verbindungen inkl. VLANs, Module, grafische Ansichten (Rack, Netzwerkgraph, Stockwerke/Räume).
|
|
||||||
- Datenmodell: zentrales SQL-Schema (`locations`, `device_types`, `devices`, `connections` etc.) mit JSON-Erweiterungsmöglichkeiten.
|
|
||||||
- Projektphasen (Phase 1–4) sind im README gelistet, siehe letzte Abschnitte.
|
|
||||||
|
|
||||||
## Schneller Projektstart
|
1. Ein Issue ist **nicht erforderlich**, um eine Aufgabe umzusetzen.
|
||||||
```powershell
|
2. Skills duerfen jederzeit verwendet werden (z. B. `gitea-issues`).
|
||||||
docker-compose up -d --build
|
3. Ein `NEXT.md`-Punkt darf erst auf erledigt (`[x]`) gesetzt werden, wenn die Umsetzung im Code erfolgt ist.
|
||||||
# danach: http://localhost
|
4. Nur wenn ein Gitea-Issue konkret referenziert ist **und** durch die Aenderung abgeschlossen wird, muss die Commit-Message `closes #<id>` enthalten.
|
||||||
|
5. Jede `closes`-Referenz steht in einer **eigenen Zeile**.
|
||||||
|
6. Kein `closes #<id>`, wenn das Issue nicht tatsaechlich abgeschlossen ist.
|
||||||
|
7. `git push` nur auf explizite Aufforderung; standardmaessig nur committen.
|
||||||
|
|
||||||
|
## Verbindlicher Ablauf
|
||||||
|
|
||||||
|
1. Aufgabe umsetzen (aus `NEXT.md` oder User-Anfrage).
|
||||||
|
2. Optional Issues laden, wenn Kontext/Zuordnung noetig ist:
|
||||||
|
- `python C:/Users/s.titz/.codex/skills/gitea-issues/scripts/list_issues.py <owner> <repo> --state open --limit 100 --json`
|
||||||
|
3. `NEXT.md` bei Bedarf aktualisieren (mit oder ohne `[#<id>]`).
|
||||||
|
4. Commit erstellen.
|
||||||
|
5. Wenn Issue abgeschlossen wird, Commit-Message mit eigener `closes`-Zeile schreiben.
|
||||||
|
|
||||||
|
## Commit-Format bei Issue-Abschluss
|
||||||
|
|
||||||
|
Beispiel mit einem Issue:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Kurzbeschreibung der Aenderung
|
||||||
|
|
||||||
|
closes #42
|
||||||
```
|
```
|
||||||
Das Docker-Setup (Compose + Portainer) liegt in `docker-compose.yml` und `docker-portainer.yml`, ergänzende Infos in `Dockerfile`.
|
|
||||||
|
|
||||||
## Skills & Nutzungshinweise
|
Beispiel mit mehreren Issues:
|
||||||
- **skill-creator** – Anleitung zum Erstellen bzw. Erweitern eigener Skills. Pfad: `C:/Users/s.titz/.codex/skills/.system/skill-creator/SKILL.md`.
|
|
||||||
- **skill-installer** – Anleitung zum Installieren zusätzlicher Skills aus Kurationslisten oder GitHub. Pfad: `C:/Users/s.titz/.codex/skills/.system/skill-installer/SKILL.md`.
|
|
||||||
|
|
||||||
Wenn ein Skill genannt wird (z. B. `$skill-creator`) oder die Aufgabe exakt zur Beschreibung passt, muss dieser Skill in dem Turn verwendet werden. Skills immer erst öffnen (`SKILL.md`), nur nötige Teile lesen, relative Pfade innerhalb des Skill-Verzeichnisses auflösen. Bei mehreren Skills: minimaler Satz in sinnvoller Reihenfolge, kurz ankündigen, warum welche Skills genutzt wurden.
|
```text
|
||||||
|
Kurzbeschreibung der Aenderung
|
||||||
|
|
||||||
## Lokale Arbeitsregeln
|
closes #12
|
||||||
- Arbeitsumgebung: Windows, Pfad `P:\netwatch`, Shell `powershell`. Schreibzugriff für mich hier ist verboten; Änderungen müssen vom Nutzer übernommen werden.
|
closes #18
|
||||||
- Suche: Nutze `rg`/`rg --files` statt `grep`/`find` für Geschwindigkeit.
|
```
|
||||||
- Codeänderungen: Nur ASCII-Zeichen einführen (außer bestehende Dateien nutzen Unicode); Formate ohne `apply_patch` nur wenn nötig; preferiere `apply_patch`.
|
|
||||||
- Keine destruktiven Git-Befehle ohne ausdrückliche Aufforderung (z. B. keinen `reset --hard`).
|
|
||||||
- Tests/Builds: Wenn nötig, nenne passende Tests / Prüfmethoden als nächsten Schritt.
|
|
||||||
- Kommunikation: Verwende beim Antworten absolute Datumsangaben (z. B. „13. Februar 2026“) wenn sich jemand auf „heute/morgen“ bezieht, um Missverständnisse zu vermeiden.
|
|
||||||
|
|
||||||
## Bekannte Bugs (aus `BUGS.md`)
|
## Format fuer NEXT.md
|
||||||
- Gerät löschen funktioniert nicht (Status unklar).
|
|
||||||
- Gerätetypen SVG-Modul: Malfunktion.
|
|
||||||
- Ports Drag & Drop (Funktion unklar).
|
|
||||||
- Beim Erstellen von Gerätetypen soll ein voreingestelltes Rechteck basierend auf 19-Zoll & HE-Größe erzeugt werden, das als Grundgerüst dient.
|
|
||||||
- Device-Typ-Erstellung: Klick auf Objekt-Typ-Button, dann Drag-Drop für Diagonale und Loslassen fixiert Position.
|
|
||||||
|
|
||||||
## Weitere Ressourcen
|
- Offen ohne Issue:
|
||||||
- `NEXT_STEPS.md` (aktuelles ToDo / Roadmap).
|
- `- [ ] //TODO Backup-Runbook erstellen`
|
||||||
- `IMPLEMENTATION_STATUS.md` (Status-Tracking).
|
- Offen mit Issue:
|
||||||
- `README.md` (Feature- und Architekturübersicht).
|
- `- [ ] [#42] //TODO Backup-Runbook erstellen`
|
||||||
|
- Erledigt mit Issue:
|
||||||
|
- `- [x] [#42] Backup-Runbook erstellen`
|
||||||
|
- Erledigt ohne Issue:
|
||||||
|
- `- [x] Backup-Runbook erstellen`
|
||||||
|
|
||||||
## Besonderheiten / Kommunikation
|
## Annahme
|
||||||
- Aktuelles Datum: Freitag, 13. Februar 2026 (nicht überschreiben).
|
|
||||||
- Keine Netzwerkanfragen möglich; Referenzen nur lokal nutzen.
|
|
||||||
- Wenn ein Agent spezielle Instruktionen benötigt (z. B. Skill-Anwendung), immer darauf hinweisen und ggf. den Nutzer nach Bestätigung fragen.
|
|
||||||
|
|
||||||
## Einschränkungen
|
- Gitea ist so konfiguriert, dass `closes #<id>` in Commit-Messages das Issue schliesst.
|
||||||
- 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
|
|
||||||
|
|||||||
3
BUGS.md
3
BUGS.md
@@ -1,3 +0,0 @@
|
|||||||
# gefundene bugs
|
|
||||||
- [ ] device löschen geht nicht
|
|
||||||
- [ ] TODO Design vereinheitlichen
|
|
||||||
@@ -70,16 +70,16 @@
|
|||||||
2. **Datenbank-Zugriff** - SQL-Klasse lädt und speichert Daten
|
2. **Datenbank-Zugriff** - SQL-Klasse lädt und speichert Daten
|
||||||
3. **Responsive Design** - Alle Formulare und Listen sind formatiert
|
3. **Responsive Design** - Alle Formulare und Listen sind formatiert
|
||||||
4. **Filter & Suche** - Alle Module haben Suchfunktionen
|
4. **Filter & Suche** - Alle Module haben Suchfunktionen
|
||||||
5. **CRUD-Operationen** - Create, Read, Update für alle Hauptmodule
|
5. **CRUD-Operationen** - Create, Read, Update, Delete für alle Hauptmodule
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ⚠️ Noch zu machen (Not-Must-Have)
|
## ⚠️ Noch zu machen (Not-Must-Have)
|
||||||
|
|
||||||
### Höhere Priorität:
|
### Höhere Priorität:
|
||||||
- [ ] **Delete-Funktionen** - Löschen noch als TODO (als AJAX implementieren)
|
- [x] **Delete-Funktionen** - Delete-Endpoints für Kernmodule inkl. `connections` und `floor_infrastructure` umgesetzt
|
||||||
- [ ] **Fehlerbehandlung** - Error Pages, Validierungsmeldungen
|
- [ ] **Fehlerbehandlung** - Error Pages, Validierungsmeldungen
|
||||||
- [ ] **Session/Auth** - Single-User Auth in bootstrap.php
|
- [x] **Session/Auth** - Single-User-Auth mit `requireAuth()` und `app/lib/auth.php` eingebunden
|
||||||
- [ ] **SVG-Editor** - Interaktiver Floorplan-Editor für Räume/Dosen
|
- [ ] **SVG-Editor** - Interaktiver Floorplan-Editor für Räume/Dosen
|
||||||
- [ ] **Port-Management** - Ports zu Geräten zuweisen
|
- [ ] **Port-Management** - Ports zu Geräten zuweisen
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ app/
|
|||||||
├── lib/
|
├── lib/
|
||||||
│ ├── _sql.php ✅ DB-Wrapper
|
│ ├── _sql.php ✅ DB-Wrapper
|
||||||
│ ├── helpers.php ✅ Utility-Funktionen
|
│ ├── helpers.php ✅ Utility-Funktionen
|
||||||
│ └── auth.php 🚧 TODO: Auth
|
│ └── auth.php ✅ Auth-Helper + requireAuth()
|
||||||
├── templates/
|
├── templates/
|
||||||
│ ├── layout.php ✅ HTML-Layout
|
│ ├── layout.php ✅ HTML-Layout
|
||||||
│ ├── header.php ✅ Header/Nav
|
│ ├── header.php ✅ Header/Nav
|
||||||
@@ -132,7 +132,7 @@ app/
|
|||||||
## 💡 Nächste Schritte (empfohlen)
|
## 💡 Nächste Schritte (empfohlen)
|
||||||
|
|
||||||
1. **Testen Sie die Module** - Probieren Sie Anlegen/Bearbeiten aus
|
1. **Testen Sie die Module** - Probieren Sie Anlegen/Bearbeiten aus
|
||||||
2. **Implementieren Sie Delete-Funktionen** - Mit AJAX oder POST
|
2. **Delete-Flows prüfen** - Sonderfälle und Fehlermeldungen bei Abhängigkeiten testen
|
||||||
3. **Bessere Fehlerbehandlung** - Sessions für Error-Messages
|
3. **Bessere Fehlerbehandlung** - Sessions für Error-Messages
|
||||||
4. **Mobile-Optimierung** - Responsive Verbesserungen
|
4. **Mobile-Optimierung** - Responsive Verbesserungen
|
||||||
5. **SVG-Editor für Floorplans** - Visuelles Raumdesign
|
5. **SVG-Editor für Floorplans** - Visuelles Raumdesign
|
||||||
|
|||||||
18
NEXT.md
Normal file
18
NEXT.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# NEXT_STEPS
|
||||||
|
|
||||||
|
## Aktive Aufgaben (priorisiert)
|
||||||
|
- [x] [#25] bei unverbundenen ports direkt eine verbindung zu einem patchfeld anbieten und das formular vorausfuellen
|
||||||
|
- [x] [#26] patchfelder haben natürlich auf den gleichen port eine feste verdrahtung und dann ein patchkabel zum switch, bei wand buchsen muss das auch erlaubt sein
|
||||||
|
- [x] [#24] infrastruktur stockerkkarte zoomen wird die grundrisskarten overlay nicht mitgezoomt
|
||||||
|
- [x] [#23] netzwerkdosen haben nur port 1 und brauche in den auswahlen nicht mit port 1 angezeigt zu werden
|
||||||
|
- [x] [#22] für neue verbindungen nur ports anbieten die noch keine verbingung haben
|
||||||
|
- [x] [#20] Gesamt-Topologie-Wand im dashboard ist schwarze
|
||||||
|
- [x] [#19] gerät nicht löschbar wegen ports, ports sind aber nicht löschbar
|
||||||
|
- [x] [#18] wandbuchsen direkt beim erstellen schon an patchpanel bindfen
|
||||||
|
- [x] [#17] infrastruktur karten zoombar, um objekte besser positionieren zu können, steps soll aber immernoch 1 bleiben
|
||||||
|
|
||||||
|
## Verifikation (Status unklar, nicht als erledigt markieren ohne Reproduktion + Commit)
|
||||||
|
- [x] [#15] Neue Verbindung: Netzwerkdose auswählbar (Regressionstest in UI durchgeführt)
|
||||||
|
|
||||||
|
## gefundene bugs
|
||||||
|
- [x] Design vereinheitlichen
|
||||||
162
NEXT_STEPS.md
162
NEXT_STEPS.md
@@ -1,162 +0,0 @@
|
|||||||
# 📋 NÄCHSTE ARBEITSPAKETE
|
|
||||||
|
|
||||||
## 🎯 Für die nächsten Sessions
|
|
||||||
|
|
||||||
### Package 1: Fehlerbehandlung & Sessions (1-2h)
|
|
||||||
- [ ] Session-Handling in `bootstrap.php` implementieren
|
|
||||||
- [ ] Error-Messages in Session speichern ($SESSION['error'], $SESSION['success'])
|
|
||||||
- [ ] Header mit Fehlermeldungen in Layout
|
|
||||||
- [ ] Validierungsfehler anzeigen
|
|
||||||
|
|
||||||
### Package 2: Delete-Funktionen (1h)
|
|
||||||
- [ ] DELETE-Endpoints für alle Module
|
|
||||||
- [ ] AJAX-Bestätigung vor Löschen
|
|
||||||
- [ ] Kaskadierendes Löschen prüfen (z.B. Floor → Racks)
|
|
||||||
|
|
||||||
### Package 3: Port-Management (2-3h)
|
|
||||||
- [ ] Ports zu Device-Types verwalten
|
|
||||||
- [ ] Ports zu Devices anzeigen
|
|
||||||
- [ ] Port-Status (aktiv/inaktiv)
|
|
||||||
- [ ] VLAN-Zuordnung zu Ports
|
|
||||||
|
|
||||||
### Package 4: SVG-Editor für Floorplans (4-5h)
|
|
||||||
- [ ] Interaktiver SVG-Editor für Rooms
|
|
||||||
- [ ] Netzwerkdosen platzieren
|
|
||||||
- [ ] Dosen nummerieren
|
|
||||||
- [ ] Speicher-Integration
|
|
||||||
|
|
||||||
### Package 5: Navigation & UI (1-2h)
|
|
||||||
- [ ] Breadcrumbs hinzufügen
|
|
||||||
- [ ] Mobile-Menü verbessern
|
|
||||||
- [ ] CSS polieren (Farben, Abstände)
|
|
||||||
- [ ] Dark-Mode (optional)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Code-Referenzen
|
|
||||||
|
|
||||||
### Template für neue CRUD-Module:
|
|
||||||
```php
|
|
||||||
// list.php: Filter + Tabelle
|
|
||||||
// edit.php: Formular
|
|
||||||
// save.php: POST-Handler mit Validierung
|
|
||||||
|
|
||||||
// Immer verwenden:
|
|
||||||
$sql->get() // SELECT mit Bind-Params
|
|
||||||
$sql->single() // SELECT LIMIT 1
|
|
||||||
$sql->set() // INSERT/UPDATE
|
|
||||||
```
|
|
||||||
|
|
||||||
### Filter-Pattern (in allen List-Modules):
|
|
||||||
```php
|
|
||||||
$where = [];
|
|
||||||
$types = '';
|
|
||||||
$params = [];
|
|
||||||
|
|
||||||
if ($search !== '') {
|
|
||||||
$where[] = "name LIKE ?";
|
|
||||||
$types .= "s";
|
|
||||||
$params[] = "%$search%";
|
|
||||||
}
|
|
||||||
|
|
||||||
$whereSql = $where ? "WHERE " . implode(" AND ", $where) : "";
|
|
||||||
// Dann in Query einsetzen
|
|
||||||
```
|
|
||||||
|
|
||||||
### Styling-Pattern:
|
|
||||||
- Buttons: `.button`, `.button-primary`, `.button-danger`, `.button-small`
|
|
||||||
- Tabellen: `.*.list` Klasse mit th/td Styling
|
|
||||||
- Forms: `.edit-form`, `.form-group`, `.form-actions`
|
|
||||||
- States: `.empty-state`, `.filter-form`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 Bekannte TODOs im Code
|
|
||||||
|
|
||||||
Alle noch offenen Punkte sind mit `// TODO:` gekennzeichnet:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Alle TODOs finden:
|
|
||||||
grep -r "TODO:" app/modules/ --include="*.php"
|
|
||||||
```
|
|
||||||
|
|
||||||
Wichtigste TODOs:
|
|
||||||
- `index.php:19` - Session starten
|
|
||||||
- `*/save.php` - Fehlerbehandlung
|
|
||||||
- `connections/` - Port-Verknüpfung
|
|
||||||
- `lib/auth.php` - Auth-Logik
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💾 Datenbank-Setup
|
|
||||||
|
|
||||||
Die Datenbank wird automatisch durch `init.sql` initialisiert.
|
|
||||||
|
|
||||||
Wichtige Tabellen:
|
|
||||||
- `locations` - Standorte
|
|
||||||
- `buildings` - Gebäude
|
|
||||||
- `floors` - Stockwerke
|
|
||||||
- `rooms` - Räume
|
|
||||||
- `network_outlets` - Netzwerkdosen
|
|
||||||
- `device_types` - Gerätetypen
|
|
||||||
- `device_type_ports` - Port-Templates
|
|
||||||
- `devices` - konkrete Geräte
|
|
||||||
- `device_ports` - Gerätports
|
|
||||||
- `racks` - Racks
|
|
||||||
- `connections` - Verbindungen zwischen Ports
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 Testing-Checklist
|
|
||||||
|
|
||||||
Bei jeder Änderung checken:
|
|
||||||
- [ ] Formular sendet Daten korrekt
|
|
||||||
- [ ] Daten werden in DB gespeichert
|
|
||||||
- [ ] Liste zeigt neue Daten
|
|
||||||
- [ ] Edit lädt existierende Daten vor
|
|
||||||
- [ ] Filter funktioniert
|
|
||||||
- [ ] Validierungsfehler werden angezeigt
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎨 Design-Richtlinien
|
|
||||||
|
|
||||||
### Farben:
|
|
||||||
- Primary (Buttons): `#007bff` (Blau)
|
|
||||||
- Success (Speichern): `#28a745` (Grün)
|
|
||||||
- Danger (Löschen): `#dc3545` (Rot)
|
|
||||||
- Background: `#f9f9f9` (Hell)
|
|
||||||
- Border: `#ddd` (Hell-Grau)
|
|
||||||
|
|
||||||
### Spacing:
|
|
||||||
- Padding in Forms: `15px` (fieldset), `8px` (input)
|
|
||||||
- Gap zwischen Buttons: `10px`
|
|
||||||
- Margin: `20px` (oben/unten), `0` (inline)
|
|
||||||
|
|
||||||
### Schriftarten:
|
|
||||||
- Erben von HTML (derzeit: System)
|
|
||||||
- Monospace für Code/IDs: `font-family: monospace`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Happy Coding! 🚀**
|
|
||||||
|
|
||||||
## Aktuell offene TODOs (Stand: 18. Februar 2026)
|
|
||||||
|
|
||||||
- [ ] #15 Neue Verbindung: Es kann keine Netzwerkdose ausgewahlt werden.
|
|
||||||
- [ ] #14 Hilfslinien der Stockwerkskarten nur im Edit-Mode anzeigen; im Anzeige-Mode ausblenden.
|
|
||||||
- [ ] #11 Encoding- und Umlautfehler beheben (inkl. ae/oe/ue-Themen).
|
|
||||||
- [ ] #10 Dashboard-Grafik erzeugen:
|
|
||||||
- Oberste Ebene: Locations, ggf. mit Unterordnung.
|
|
||||||
- Darunter: Gebaudekomplexe mit Rack-Verbindungen.
|
|
||||||
- Darunter: Stockwerke.
|
|
||||||
- Darunter: Etagenweise Verbindungen.
|
|
||||||
- [ ] #8 Gerate löschen fehlt: Es erfolgt Weiterleitung, aber keine echte Fehlermeldung.
|
|
||||||
- [ ] #7 Letzten Punkt im Floor-Editor löschen:
|
|
||||||
- URL: `http://localhost/?module=floors&action=edit&id=1`
|
|
||||||
- [ ] #5 Dashboard als zoombare und verschiebbare SVG-Flache:
|
|
||||||
- Gerate anordnen.
|
|
||||||
- Gerate, Ports und Verbindungen anklickbar.
|
|
||||||
- Sprechblase mit Infos und Buttons zu Bereichen (editieren, entfernen, ...).
|
|
||||||
- [ ] #4 `device_types/edit`: Option "Ports automatisch erstellen" nur beim Erstellen anzeigen, nicht beim Editieren.
|
|
||||||
|
|
||||||
14
README.md
14
README.md
@@ -238,19 +238,19 @@ Verbindungen werden:
|
|||||||
- Die untereinander verbundenen Patchpanels lassen sich direkt auf der SVG-Stockwerkskarte verorten, damit jeder Port physisch nachvollziehbar bleibt.
|
- Die untereinander verbundenen Patchpanels lassen sich direkt auf der SVG-Stockwerkskarte verorten, damit jeder Port physisch nachvollziehbar bleibt.
|
||||||
- Verbindungen zu Racks / Switches darstellbar
|
- Verbindungen zu Racks / Switches darstellbar
|
||||||
|
|
||||||
### TODO: Patchpanel-Infrastruktur
|
### Patchpanel-Infrastruktur (Status: 18. Februar 2026)
|
||||||
- [ ] Floorplans erweitern, damit Patchpanels als feste Infrastrukturobjekte (nicht als rack-basierte `devices`) angelegt, verschoben und mit `x/y`/Größe verankert werden.
|
- [x] Floorplans erweitert: Patchpanels können als feste Infrastrukturobjekte (ohne Rack-Device) inkl. `x/y` und Größe verwaltet werden.
|
||||||
- [ ] Backend und SVG-Editor dahingehend adaptieren, dass Patchpanel-Ports unabhängig von Racks definiert werden können.
|
- [x] Backend + SVG-Editor angepasst: Patchpanel-Ports werden über `floor_patchpanel_ports` gepflegt.
|
||||||
- [ ] Patchpanel ↔ Patchpanel- und Patchpanel ↔ Netzwerkbuchse-Verbindungen als permanente Kabel zwischen Floorplan-Objekten darstellen und über die `connections`-Tabelle verwalten.
|
- [x] Patchpanel ↔ Patchpanel und Patchpanel ↔ Netzwerkbuchse werden über `connections` verwaltet.
|
||||||
- [ ] UI/Schema-Dokumentation aktualisieren (README + Datenbank-Docs) sowie neue SQL-Tabellen (`floor_patchpanels` / `floor_patchpanel_ports`) fertigstellen.
|
- [x] SQL-Tabellen `floor_patchpanels` / `floor_patchpanel_ports` sind im Schema enthalten.
|
||||||
- [ ] Floorplan-Filter/Legend leiten die Nutzung dieser Infrastrukturobjekte, Kampagnen und Search & Filter integrieren.
|
- [ ] Floorplan-Filter/Legend und erweiterte Suche für Infrastrukturobjekte weiter ausbauen.
|
||||||
|
|
||||||
### Stockwerksinfrastruktur-Modul
|
### Stockwerksinfrastruktur-Modul
|
||||||
- Das neue Modul „Stockwerksinfrastruktur“ sammelt Patchpanels und Wandbuchsen an einem Ort.
|
- Das neue Modul „Stockwerksinfrastruktur“ sammelt Patchpanels und Wandbuchsen an einem Ort.
|
||||||
- Patchfelder bekommen feste X/Y-Positionen, Maße, Portanzahl und verknüpfen zu Floorplans.
|
- Patchfelder bekommen feste X/Y-Positionen, Maße, Portanzahl und verknüpfen zu Floorplans.
|
||||||
- Wandbuchsen sind direkt mit Räumen verbunden, können aber auch später im SVG verteilt werden.
|
- Wandbuchsen sind direkt mit Räumen verbunden, können aber auch später im SVG verteilt werden.
|
||||||
- Ziel: Die Floorplan-Grafik zeigt die permanente Infrastruktur samt fest verlegter Kabelverläufe.
|
- Ziel: Die Floorplan-Grafik zeigt die permanente Infrastruktur samt fest verlegter Kabelverläufe.
|
||||||
- TODO: SVG-Editor um Drag & Drop für diese Objekte erweitern und Klicks direkt mit dem Modul verbinden.
|
- [x] SVG-Editor für diese Objekte ist mit Drag & Drop umgesetzt und direkt mit dem Modul verbunden.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
36
TODO.md
36
TODO.md
@@ -1,36 +0,0 @@
|
|||||||
# TODO
|
|
||||||
|
|
||||||
Bereinigte und aktuelle TODO-Liste (Stand: 18. Februar 2026).
|
|
||||||
Quelle: vorhandene `TODO`-Marker im Repository plus offene Architekturpunkte.
|
|
||||||
|
|
||||||
## Erledigt (bereits umgesetzt)
|
|
||||||
|
|
||||||
- [x] API-Basis umgesetzt (`app/api/connections.php`, `app/api/device_type_ports.php`, `app/api/upload.php`).
|
|
||||||
- [x] Bootstrap/Auth/Config/Routing-Grundlagen umgesetzt (`app/config.php`, `app/bootstrap.php`, `app/lib/_sql.php`, `app/lib/auth.php`, `app/index.php`).
|
|
||||||
- [x] Frontend-Grundlagen aktualisiert (`app/assets/js/app.js`, `app/assets/js/dashboard.js`, `app/assets/js/svg-editor.js`, `app/assets/js/network-view.js`).
|
|
||||||
- [x] Delete-Flow fuer zentrale Module umgesetzt (`buildings`, `floors`, `racks`, `device_types`, `floor_infrastructure`).
|
|
||||||
- [x] Legacy-Mock in `app/modules/device_types/ports.php` ersetzt.
|
|
||||||
- [x] Veraltete Sammel-TODO-Liste (nicht mehr im Code vorhanden) entfernt.
|
|
||||||
|
|
||||||
## Offen (direkt im Code markiert)
|
|
||||||
|
|
||||||
- [x] `app/modules/dashboard/list.php`: zoombare/verschiebbare SVG-Wand mit klickbaren Punkten und Overlay-Drilldown umgesetzt.
|
|
||||||
- [x] `app/modules/connections/list.php`: Detailbereich fuer ausgewaehlte Verbindung inkl. Bearbeiten/Loeschen im UI umgesetzt.
|
|
||||||
- [x] `app/lib/helpers.php`: konkrete allgemeine Helper ergaenzt (`formatDateTime`, `formatBytes`, `generateUuidV4`, `normalizeSvgCoordinate`).
|
|
||||||
|
|
||||||
## Offen (Bugs / Doku / Statusdateien)
|
|
||||||
|
|
||||||
- [ ] `BUGS.md:3`: Design vereinheitlichen.
|
|
||||||
- [ ] `IMPLEMENTATION_STATUS.md:80`: Delete-Funktionen-Status aktualisieren.
|
|
||||||
- [ ] `IMPLEMENTATION_STATUS.md:109`: Auth-Status aktualisieren.
|
|
||||||
- [ ] `README.md:241`: Patchpanel-Infrastruktur umsetzen und danach Doku abhaken.
|
|
||||||
- [ ] `README.md:253`: SVG-Editor fuer Floor-Infrastruktur (Drag-and-Drop + direkte Modul-Interaktion).
|
|
||||||
- [ ] `doc/DATABASE.md:127`: TODO-Abschnitt fuer Patchpanel/Floorplan finalisieren.
|
|
||||||
- [ ] `init.sql:379`: Kommentar zur Port-Konfiguration in verbindliche Implementierungsregeln ueberfuehren.
|
|
||||||
|
|
||||||
## Topologie-Backlog (ausstehend)
|
|
||||||
|
|
||||||
- [ ] `connections.port_a_type` / `connections.port_b_type` um `patchpanel` erweitern und auf `floor_patchpanel_ports.id` referenzieren.
|
|
||||||
- [ ] Validierungsregeln fuer Topologie fest verdrahten (Patchpanel-Port nur mit Patchpanel-Port oder Netzwerkbuchsen-Port).
|
|
||||||
- [ ] Port-CRUD fuer Patchpanels: `floor_patchpanel_ports` aus `port_count` erzeugen/synchronisieren.
|
|
||||||
- [ ] Port-CRUD fuer Netzwerkbuchsen: `network_outlet_ports` pflegen (mindestens ein Port je Buchse) und in Verbindungen nutzbar machen.
|
|
||||||
@@ -77,6 +77,100 @@ function endpointExists($sql, string $type, int $id): bool
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isTopologyPairAllowed(string $typeA, string $typeB): bool
|
||||||
|
{
|
||||||
|
$allowed = ['device' => true, 'module' => true, 'outlet' => true, 'patchpanel' => true];
|
||||||
|
if (!isset($allowed[$typeA]) || !isset($allowed[$typeB])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($typeA === 'patchpanel' || $typeB === 'patchpanel') {
|
||||||
|
return ($typeA === 'patchpanel' && in_array($typeB, ['patchpanel', 'outlet'], true))
|
||||||
|
|| ($typeB === 'patchpanel' && in_array($typeA, ['patchpanel', 'outlet'], true));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildEndpointUsageMap($sql, int $excludeConnectionId = 0): array
|
||||||
|
{
|
||||||
|
$usage = [
|
||||||
|
'device' => [],
|
||||||
|
'module' => [],
|
||||||
|
'outlet' => [],
|
||||||
|
'patchpanel' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
$rows = $sql->get(
|
||||||
|
"SELECT id, port_a_type, port_a_id, port_b_type, port_b_id
|
||||||
|
FROM connections
|
||||||
|
WHERE id <> ?",
|
||||||
|
'i',
|
||||||
|
[$excludeConnectionId]
|
||||||
|
);
|
||||||
|
|
||||||
|
$track = static function (string $endpointType, int $endpointId, string $otherType) use (&$usage): void {
|
||||||
|
if ($endpointId <= 0 || !isset($usage[$endpointType])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isset($usage[$endpointType][$endpointId])) {
|
||||||
|
$usage[$endpointType][$endpointId] = [
|
||||||
|
'total' => 0,
|
||||||
|
'fixed' => 0,
|
||||||
|
'patch' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$usage[$endpointType][$endpointId]['total']++;
|
||||||
|
if (in_array($endpointType, ['outlet', 'patchpanel'], true)) {
|
||||||
|
if (in_array($otherType, ['outlet', 'patchpanel'], true)) {
|
||||||
|
$usage[$endpointType][$endpointId]['fixed']++;
|
||||||
|
} elseif (in_array($otherType, ['device', 'module'], true)) {
|
||||||
|
$usage[$endpointType][$endpointId]['patch']++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach ((array)$rows as $row) {
|
||||||
|
$typeA = normalizeEndpointType((string)($row['port_a_type'] ?? ''));
|
||||||
|
$typeB = normalizeEndpointType((string)($row['port_b_type'] ?? ''));
|
||||||
|
$idA = (int)($row['port_a_id'] ?? 0);
|
||||||
|
$idB = (int)($row['port_b_id'] ?? 0);
|
||||||
|
if ($typeA === null || $typeB === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$track($typeA, $idA, $typeB);
|
||||||
|
$track($typeB, $idB, $typeA);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $usage;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateEndpointCapacity(array $usage, string $endpointType, int $endpointId, string $otherType, string $label): ?string
|
||||||
|
{
|
||||||
|
if ($endpointId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stats = $usage[$endpointType][$endpointId] ?? ['total' => 0, 'fixed' => 0, 'patch' => 0];
|
||||||
|
if ((int)$stats['total'] <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($endpointType, ['outlet', 'patchpanel'], true)) {
|
||||||
|
if ((int)$stats['total'] >= 2) {
|
||||||
|
return $label . ' hat bereits die maximale Anzahl von 2 Verbindungen';
|
||||||
|
}
|
||||||
|
if (in_array($otherType, ['outlet', 'patchpanel'], true) && (int)$stats['fixed'] >= 1) {
|
||||||
|
return $label . ' hat bereits eine feste Verdrahtung';
|
||||||
|
}
|
||||||
|
if (in_array($otherType, ['device', 'module'], true) && (int)$stats['patch'] >= 1) {
|
||||||
|
return $label . ' hat bereits ein Patchkabel';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $label . ' ist bereits in Verwendung';
|
||||||
|
}
|
||||||
|
|
||||||
function loadConnections($sql): void
|
function loadConnections($sql): void
|
||||||
{
|
{
|
||||||
$contextType = strtolower(trim((string)($_GET['context_type'] ?? 'all')));
|
$contextType = strtolower(trim((string)($_GET['context_type'] ?? 'all')));
|
||||||
@@ -189,6 +283,9 @@ function saveConnection($sql): void
|
|||||||
if ($portAId <= 0 || $portBId <= 0) {
|
if ($portAId <= 0 || $portBId <= 0) {
|
||||||
jsonError('port_a_id und port_b_id sind erforderlich', 400);
|
jsonError('port_a_id und port_b_id sind erforderlich', 400);
|
||||||
}
|
}
|
||||||
|
if (!isTopologyPairAllowed($portAType, $portBType)) {
|
||||||
|
jsonError('Patchpanel-Ports duerfen nur mit Patchpanel-Ports oder Netzwerkdosen-Ports verbunden werden', 400);
|
||||||
|
}
|
||||||
|
|
||||||
if ($portAType === $portBType && $portAId === $portBId) {
|
if ($portAType === $portBType && $portAId === $portBId) {
|
||||||
jsonError('Port A und Port B duerfen nicht identisch sein', 400);
|
jsonError('Port A und Port B duerfen nicht identisch sein', 400);
|
||||||
@@ -210,9 +307,20 @@ function saveConnection($sql): void
|
|||||||
$mode = isset($data['mode']) ? (string)$data['mode'] : null;
|
$mode = isset($data['mode']) ? (string)$data['mode'] : null;
|
||||||
$comment = isset($data['comment']) ? (string)$data['comment'] : null;
|
$comment = isset($data['comment']) ? (string)$data['comment'] : null;
|
||||||
|
|
||||||
if (!empty($data['id'])) {
|
$connectionId = !empty($data['id']) ? (int)$data['id'] : 0;
|
||||||
$id = (int)$data['id'];
|
$usage = buildEndpointUsageMap($sql, $connectionId);
|
||||||
$existing = $sql->single('SELECT id FROM connections WHERE id = ?', 'i', [$id]);
|
$capacityErrorA = validateEndpointCapacity($usage, $portAType, $portAId, $portBType, 'Port an Endpunkt A');
|
||||||
|
if ($capacityErrorA !== null) {
|
||||||
|
jsonError($capacityErrorA, 409);
|
||||||
|
}
|
||||||
|
$capacityErrorB = validateEndpointCapacity($usage, $portBType, $portBId, $portAType, 'Port an Endpunkt B');
|
||||||
|
if ($capacityErrorB !== null) {
|
||||||
|
jsonError($capacityErrorB, 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($connectionId > 0) {
|
||||||
|
$id = $connectionId;
|
||||||
|
$existing = $sql->single('SELECT id FROM connections WHERE id = ?', 'i', [$connectionId]);
|
||||||
if (!$existing) {
|
if (!$existing) {
|
||||||
jsonError('Verbindung existiert nicht', 404);
|
jsonError('Verbindung existiert nicht', 404);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,42 @@ main {
|
|||||||
min-height: calc(100vh - 200px);
|
min-height: calc(100vh - 200px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flash-stack {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 0 auto 18px;
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-message {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-message--success {
|
||||||
|
border-color: #99dfba;
|
||||||
|
background: #ebf9f1;
|
||||||
|
color: #165938;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-message--error {
|
||||||
|
border-color: #efb4b4;
|
||||||
|
background: #fff1f1;
|
||||||
|
color: #8a1f1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-message__text {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-message__list {
|
||||||
|
margin: 8px 0 0 18px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Shared components -------------------------------------------------- */
|
/* Shared components -------------------------------------------------- */
|
||||||
.filter-form {
|
.filter-form {
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
@@ -172,6 +208,48 @@ main {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.connections-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 320px;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connections-sidebar {
|
||||||
|
position: sticky;
|
||||||
|
top: 92px;
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e0e6ef;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 14px;
|
||||||
|
box-shadow: 0 8px 24px rgba(15, 26, 45, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-card h3,
|
||||||
|
.sidebar-card h4 {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-card p {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-row-selected {
|
||||||
|
background: #edf5ff;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.connections-list th,
|
.connections-list th,
|
||||||
.connections-list td {
|
.connections-list td {
|
||||||
@@ -179,6 +257,17 @@ main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.connections-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connections-sidebar {
|
||||||
|
position: static;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.app-header {
|
.app-header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -44,17 +44,6 @@
|
|||||||
cursor: crosshair;
|
cursor: crosshair;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.floor-plan-svg {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 0;
|
|
||||||
opacity: 0.75;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
.floor-plan-overlay {
|
.floor-plan-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -63,6 +52,10 @@
|
|||||||
z-index: 2;
|
z-index: 2;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
}
|
}
|
||||||
|
.floor-plan-overlay .floor-plan-background {
|
||||||
|
opacity: 0.75;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
.floor-plan-overlay .active-marker {
|
.floor-plan-overlay .active-marker {
|
||||||
cursor: move;
|
cursor: move;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,8 +51,49 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function enforceTopologyTypeRules(typeA, typeB) {
|
||||||
|
const allowWithPatchpanel = { patchpanel: true, outlet: true };
|
||||||
|
const selectedA = typeA.value;
|
||||||
|
const selectedB = typeB.value;
|
||||||
|
|
||||||
|
const applyRules = (sourceType, targetSelect) => {
|
||||||
|
for (const option of targetSelect.options) {
|
||||||
|
const value = option.value;
|
||||||
|
if (!value) {
|
||||||
|
option.disabled = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (sourceType === 'patchpanel') {
|
||||||
|
option.disabled = !allowWithPatchpanel[value];
|
||||||
|
} else {
|
||||||
|
option.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetSelect.selectedOptions.length > 0 && targetSelect.selectedOptions[0].disabled) {
|
||||||
|
targetSelect.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
applyRules(selectedA, typeB);
|
||||||
|
applyRules(selectedB, typeA);
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
bindPair('port_a_type', 'port_a_id');
|
bindPair('port_a_type', 'port_a_id');
|
||||||
bindPair('port_b_type', 'port_b_id');
|
bindPair('port_b_type', 'port_b_id');
|
||||||
|
|
||||||
|
const typeA = document.getElementById('port_a_type');
|
||||||
|
const typeB = document.getElementById('port_b_type');
|
||||||
|
if (!typeA || !typeB) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncRules = () => {
|
||||||
|
enforceTopologyTypeRules(typeA, typeB);
|
||||||
|
};
|
||||||
|
|
||||||
|
syncRules();
|
||||||
|
typeA.addEventListener('change', syncRules);
|
||||||
|
typeB.addEventListener('change', syncRules);
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -29,8 +29,26 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
let markerX = 0;
|
let markerX = 0;
|
||||||
let markerY = 0;
|
let markerY = 0;
|
||||||
let dragging = false;
|
let dragging = false;
|
||||||
|
let panning = false;
|
||||||
|
let panStart = null;
|
||||||
let dragOffsetX = 0;
|
let dragOffsetX = 0;
|
||||||
let dragOffsetY = 0;
|
let dragOffsetY = 0;
|
||||||
|
let viewX = 0;
|
||||||
|
let viewY = 0;
|
||||||
|
let viewWidth = DEFAULT_PLAN_SIZE.width;
|
||||||
|
let viewHeight = DEFAULT_PLAN_SIZE.height;
|
||||||
|
|
||||||
|
overlay.setAttribute('preserveAspectRatio', 'none');
|
||||||
|
|
||||||
|
const backgroundImage = document.createElementNS(SVG_NS, 'image');
|
||||||
|
backgroundImage.classList.add('floor-plan-background');
|
||||||
|
backgroundImage.setAttribute('x', '0');
|
||||||
|
backgroundImage.setAttribute('y', '0');
|
||||||
|
backgroundImage.setAttribute('width', String(DEFAULT_PLAN_SIZE.width));
|
||||||
|
backgroundImage.setAttribute('height', String(DEFAULT_PLAN_SIZE.height));
|
||||||
|
backgroundImage.setAttribute('preserveAspectRatio', 'none');
|
||||||
|
backgroundImage.setAttribute('display', 'none');
|
||||||
|
overlay.appendChild(backgroundImage);
|
||||||
|
|
||||||
const activeMarker = document.createElementNS(SVG_NS, 'rect');
|
const activeMarker = document.createElementNS(SVG_NS, 'rect');
|
||||||
activeMarker.classList.add('active-marker');
|
activeMarker.classList.add('active-marker');
|
||||||
@@ -45,7 +63,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
const planSize = { ...DEFAULT_PLAN_SIZE };
|
const planSize = { ...DEFAULT_PLAN_SIZE };
|
||||||
const updateOverlayViewBox = () => {
|
const updateOverlayViewBox = () => {
|
||||||
overlay.setAttribute('viewBox', `0 0 ${planSize.width} ${planSize.height}`);
|
overlay.setAttribute('viewBox', `${viewX} ${viewY} ${viewWidth} ${viewHeight}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatePositionLabel = (x, y) => {
|
const updatePositionLabel = (x, y) => {
|
||||||
@@ -72,17 +90,57 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const toOverlayPoint = (clientX, clientY) => {
|
const toOverlayPoint = (clientX, clientY) => {
|
||||||
const pt = overlay.createSVGPoint();
|
const rect = overlay.getBoundingClientRect();
|
||||||
pt.x = clientX;
|
if (rect.width <= 0 || rect.height <= 0) {
|
||||||
pt.y = clientY;
|
|
||||||
const ctm = overlay.getScreenCTM();
|
|
||||||
if (!ctm) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const transformed = pt.matrixTransform(ctm.inverse());
|
const ratioX = (clientX - rect.left) / rect.width;
|
||||||
|
const ratioY = (clientY - rect.top) / rect.height;
|
||||||
|
const transformed = {
|
||||||
|
x: viewX + (ratioX * viewWidth),
|
||||||
|
y: viewY + (ratioY * viewHeight)
|
||||||
|
};
|
||||||
return { x: transformed.x, y: transformed.y };
|
return { x: transformed.x, y: transformed.y };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clampView = () => {
|
||||||
|
const minWidth = Math.max(30, planSize.width * 0.1);
|
||||||
|
const minHeight = Math.max(30, planSize.height * 0.1);
|
||||||
|
viewWidth = clamp(viewWidth, minWidth, planSize.width);
|
||||||
|
viewHeight = clamp(viewHeight, minHeight, planSize.height);
|
||||||
|
viewX = clamp(viewX, 0, Math.max(0, planSize.width - viewWidth));
|
||||||
|
viewY = clamp(viewY, 0, Math.max(0, planSize.height - viewHeight));
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyView = () => {
|
||||||
|
clampView();
|
||||||
|
updateOverlayViewBox();
|
||||||
|
};
|
||||||
|
|
||||||
|
const zoomAt = (clientX, clientY, factor) => {
|
||||||
|
const point = toOverlayPoint(clientX, clientY);
|
||||||
|
if (!point) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ratioX = (point.x - viewX) / viewWidth;
|
||||||
|
const ratioY = (point.y - viewY) / viewHeight;
|
||||||
|
const nextWidth = viewWidth * factor;
|
||||||
|
const nextHeight = viewHeight * factor;
|
||||||
|
viewX = point.x - (ratioX * nextWidth);
|
||||||
|
viewY = point.y - (ratioY * nextHeight);
|
||||||
|
viewWidth = nextWidth;
|
||||||
|
viewHeight = nextHeight;
|
||||||
|
applyView();
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetView = () => {
|
||||||
|
viewX = 0;
|
||||||
|
viewY = 0;
|
||||||
|
viewWidth = planSize.width;
|
||||||
|
viewHeight = planSize.height;
|
||||||
|
applyView();
|
||||||
|
};
|
||||||
|
|
||||||
const updateFromInputs = () => {
|
const updateFromInputs = () => {
|
||||||
setMarkerPosition(Number(xField.value) || 0, Number(yField.value) || 0);
|
setMarkerPosition(Number(xField.value) || 0, Number(yField.value) || 0);
|
||||||
};
|
};
|
||||||
@@ -191,10 +249,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const panelBuildingSelect = document.getElementById('panel-building-select');
|
const panelBuildingSelect = document.getElementById('panel-building-select');
|
||||||
const panelFloorSelect = document.getElementById('panel-floor-select');
|
const panelFloorSelect = document.getElementById('panel-floor-select');
|
||||||
const outletRoomSelect = document.getElementById('outlet-room-select');
|
const outletRoomSelect = document.getElementById('outlet-room-select');
|
||||||
const floorPlanSvg = document.getElementById('floor-plan-svg');
|
|
||||||
const panelPlacementFields = document.getElementById('panel-placement-fields');
|
const panelPlacementFields = document.getElementById('panel-placement-fields');
|
||||||
const panelFloorPlanGroup = document.getElementById('panel-floor-plan-group');
|
const panelFloorPlanGroup = document.getElementById('panel-floor-plan-group');
|
||||||
const panelFloorMissingHint = document.getElementById('panel-floor-missing-hint');
|
const panelFloorMissingHint = document.getElementById('panel-floor-missing-hint');
|
||||||
|
const outletBindPatchpanelSelect = document.getElementById('outlet-bind-patchpanel-port-id');
|
||||||
|
|
||||||
const buildingOptions = panelBuildingSelect ? Array.from(panelBuildingSelect.options).filter((option) => option.value !== '') : [];
|
const buildingOptions = panelBuildingSelect ? Array.from(panelBuildingSelect.options).filter((option) => option.value !== '') : [];
|
||||||
const floorOptions = panelFloorSelect ? Array.from(panelFloorSelect.options).filter((option) => option.value !== '') : [];
|
const floorOptions = panelFloorSelect ? Array.from(panelFloorSelect.options).filter((option) => option.value !== '') : [];
|
||||||
@@ -208,6 +266,32 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return Number(roomOption?.dataset?.floorId || 0);
|
return Number(roomOption?.dataset?.floorId || 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const filterPatchpanelBindOptions = () => {
|
||||||
|
if (!outletBindPatchpanelSelect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentFloorId = getCurrentFloorId();
|
||||||
|
const options = Array.from(outletBindPatchpanelSelect.options).filter((option) => option.value !== '');
|
||||||
|
let firstMatch = '';
|
||||||
|
let selectedStillVisible = false;
|
||||||
|
|
||||||
|
options.forEach((option) => {
|
||||||
|
const optionFloorId = Number(option.dataset.floorId || 0);
|
||||||
|
const matchesFloor = !currentFloorId || optionFloorId === currentFloorId;
|
||||||
|
option.hidden = !matchesFloor;
|
||||||
|
if (matchesFloor && !option.disabled && !firstMatch) {
|
||||||
|
firstMatch = option.value;
|
||||||
|
}
|
||||||
|
if (matchesFloor && option.selected) {
|
||||||
|
selectedStillVisible = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!selectedStillVisible && firstMatch && !outletBindPatchpanelSelect.value) {
|
||||||
|
outletBindPatchpanelSelect.value = firstMatch;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const renderReferenceMarkers = () => {
|
const renderReferenceMarkers = () => {
|
||||||
clearRoomHighlight();
|
clearRoomHighlight();
|
||||||
clearReferenceMarkers();
|
clearReferenceMarkers();
|
||||||
@@ -239,35 +323,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateFloorPlanImage = () => {
|
const updateFloorPlanImage = () => {
|
||||||
if (!floorPlanSvg) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const floorOption = panelFloorSelect?.selectedOptions?.[0];
|
const floorOption = panelFloorSelect?.selectedOptions?.[0];
|
||||||
const roomOption = outletRoomSelect?.selectedOptions?.[0];
|
const roomOption = outletRoomSelect?.selectedOptions?.[0];
|
||||||
const svgUrl = floorOption?.dataset?.svgUrl || roomOption?.dataset?.floorSvgUrl || '';
|
const svgUrl = floorOption?.dataset?.svgUrl || roomOption?.dataset?.floorSvgUrl || '';
|
||||||
|
|
||||||
if (svgUrl) {
|
if (svgUrl) {
|
||||||
floorPlanSvg.src = svgUrl;
|
backgroundImage.setAttribute('href', svgUrl);
|
||||||
floorPlanSvg.hidden = false;
|
backgroundImage.setAttribute('width', String(planSize.width));
|
||||||
|
backgroundImage.setAttribute('height', String(planSize.height));
|
||||||
|
backgroundImage.setAttribute('display', 'block');
|
||||||
loadPlanDimensions(svgUrl);
|
loadPlanDimensions(svgUrl);
|
||||||
} else {
|
} else {
|
||||||
floorPlanSvg.removeAttribute('src');
|
backgroundImage.removeAttribute('href');
|
||||||
floorPlanSvg.hidden = true;
|
backgroundImage.setAttribute('display', 'none');
|
||||||
planSize.width = DEFAULT_PLAN_SIZE.width;
|
planSize.width = DEFAULT_PLAN_SIZE.width;
|
||||||
planSize.height = DEFAULT_PLAN_SIZE.height;
|
planSize.height = DEFAULT_PLAN_SIZE.height;
|
||||||
updateOverlayViewBox();
|
resetView();
|
||||||
}
|
}
|
||||||
renderReferenceMarkers();
|
renderReferenceMarkers();
|
||||||
|
filterPatchpanelBindOptions();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (floorPlanSvg) {
|
|
||||||
floorPlanSvg.addEventListener('error', () => {
|
|
||||||
floorPlanSvg.removeAttribute('src');
|
|
||||||
floorPlanSvg.hidden = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadPlanDimensions = async (svgUrl) => {
|
const loadPlanDimensions = async (svgUrl) => {
|
||||||
if (!svgUrl) {
|
if (!svgUrl) {
|
||||||
return;
|
return;
|
||||||
@@ -291,7 +367,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (parts.length === 4 && parts.every((value) => Number.isFinite(value))) {
|
if (parts.length === 4 && parts.every((value) => Number.isFinite(value))) {
|
||||||
planSize.width = Math.max(1, parts[2]);
|
planSize.width = Math.max(1, parts[2]);
|
||||||
planSize.height = Math.max(1, parts[3]);
|
planSize.height = Math.max(1, parts[3]);
|
||||||
updateOverlayViewBox();
|
backgroundImage.setAttribute('width', String(planSize.width));
|
||||||
|
backgroundImage.setAttribute('height', String(planSize.height));
|
||||||
|
resetView();
|
||||||
renderReferenceMarkers();
|
renderReferenceMarkers();
|
||||||
updateFromInputs();
|
updateFromInputs();
|
||||||
return;
|
return;
|
||||||
@@ -307,13 +385,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
planSize.width = DEFAULT_PLAN_SIZE.width;
|
planSize.width = DEFAULT_PLAN_SIZE.width;
|
||||||
planSize.height = DEFAULT_PLAN_SIZE.height;
|
planSize.height = DEFAULT_PLAN_SIZE.height;
|
||||||
}
|
}
|
||||||
updateOverlayViewBox();
|
backgroundImage.setAttribute('width', String(planSize.width));
|
||||||
|
backgroundImage.setAttribute('height', String(planSize.height));
|
||||||
|
resetView();
|
||||||
renderReferenceMarkers();
|
renderReferenceMarkers();
|
||||||
updateFromInputs();
|
updateFromInputs();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
planSize.width = DEFAULT_PLAN_SIZE.width;
|
planSize.width = DEFAULT_PLAN_SIZE.width;
|
||||||
planSize.height = DEFAULT_PLAN_SIZE.height;
|
planSize.height = DEFAULT_PLAN_SIZE.height;
|
||||||
updateOverlayViewBox();
|
backgroundImage.setAttribute('width', String(planSize.width));
|
||||||
|
backgroundImage.setAttribute('height', String(planSize.height));
|
||||||
|
resetView();
|
||||||
renderReferenceMarkers();
|
renderReferenceMarkers();
|
||||||
updateFromInputs();
|
updateFromInputs();
|
||||||
}
|
}
|
||||||
@@ -382,6 +464,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
activeMarker.addEventListener('pointerdown', (event) => {
|
activeMarker.addEventListener('pointerdown', (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
dragging = true;
|
dragging = true;
|
||||||
|
panning = false;
|
||||||
const point = toOverlayPoint(event.clientX, event.clientY);
|
const point = toOverlayPoint(event.clientX, event.clientY);
|
||||||
if (!point) {
|
if (!point) {
|
||||||
return;
|
return;
|
||||||
@@ -403,13 +486,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const stopDrag = (event) => {
|
const stopDrag = (event) => {
|
||||||
if (!dragging) {
|
if (dragging) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
dragging = false;
|
dragging = false;
|
||||||
if (activeMarker.hasPointerCapture(event.pointerId)) {
|
if (activeMarker.hasPointerCapture(event.pointerId)) {
|
||||||
activeMarker.releasePointerCapture(event.pointerId);
|
activeMarker.releasePointerCapture(event.pointerId);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (panning) {
|
||||||
|
panning = false;
|
||||||
|
panStart = null;
|
||||||
|
if (overlay.hasPointerCapture(event.pointerId)) {
|
||||||
|
overlay.releasePointerCapture(event.pointerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
['pointerup', 'pointercancel', 'pointerleave'].forEach((evt) => {
|
['pointerup', 'pointercancel', 'pointerleave'].forEach((evt) => {
|
||||||
@@ -417,6 +506,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
overlay.addEventListener('pointerdown', (event) => {
|
overlay.addEventListener('pointerdown', (event) => {
|
||||||
|
if (event.shiftKey || event.button === 1) {
|
||||||
|
event.preventDefault();
|
||||||
|
panning = true;
|
||||||
|
dragging = false;
|
||||||
|
panStart = {
|
||||||
|
clientX: event.clientX,
|
||||||
|
clientY: event.clientY,
|
||||||
|
viewX,
|
||||||
|
viewY
|
||||||
|
};
|
||||||
|
overlay.setPointerCapture(event.pointerId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (event.target !== overlay) {
|
if (event.target !== overlay) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -427,6 +529,32 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
setMarkerPosition(point.x - markerWidth / 2, point.y - markerHeight / 2);
|
setMarkerPosition(point.x - markerWidth / 2, point.y - markerHeight / 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
overlay.addEventListener('pointermove', (event) => {
|
||||||
|
if (!panning || !panStart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rect = overlay.getBoundingClientRect();
|
||||||
|
if (rect.width <= 0 || rect.height <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const scaleX = viewWidth / rect.width;
|
||||||
|
const scaleY = viewHeight / rect.height;
|
||||||
|
const dx = (event.clientX - panStart.clientX) * scaleX;
|
||||||
|
const dy = (event.clientY - panStart.clientY) * scaleY;
|
||||||
|
viewX = panStart.viewX - dx;
|
||||||
|
viewY = panStart.viewY - dy;
|
||||||
|
applyView();
|
||||||
|
});
|
||||||
|
|
||||||
|
overlay.addEventListener('pointerup', stopDrag);
|
||||||
|
overlay.addEventListener('pointercancel', stopDrag);
|
||||||
|
|
||||||
|
overlay.addEventListener('wheel', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const factor = event.deltaY < 0 ? 0.9 : 1.1;
|
||||||
|
zoomAt(event.clientX, event.clientY, factor);
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
[xField, yField].forEach((input) => {
|
[xField, yField].forEach((input) => {
|
||||||
input.addEventListener('input', () => {
|
input.addEventListener('input', () => {
|
||||||
updateFromInputs();
|
updateFromInputs();
|
||||||
@@ -466,6 +594,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
updateOverlayViewBox();
|
updateOverlayViewBox();
|
||||||
updateFromInputs();
|
updateFromInputs();
|
||||||
|
filterPatchpanelBindOptions();
|
||||||
|
|
||||||
if (panelLocationSelect) {
|
if (panelLocationSelect) {
|
||||||
filterBuildingOptions();
|
filterBuildingOptions();
|
||||||
|
|||||||
@@ -254,6 +254,10 @@
|
|||||||
background.setAttribute('stroke-width', '1');
|
background.setAttribute('stroke-width', '1');
|
||||||
svg.appendChild(background);
|
svg.appendChild(background);
|
||||||
|
|
||||||
|
const style = createSvgElement('style');
|
||||||
|
style.textContent = '.floor-guide{display:none;}';
|
||||||
|
svg.appendChild(style);
|
||||||
|
|
||||||
state.guides.forEach((guide) => {
|
state.guides.forEach((guide) => {
|
||||||
const line = createSvgElement('line');
|
const line = createSvgElement('line');
|
||||||
if (guide.orientation === 'horizontal') {
|
if (guide.orientation === 'horizontal') {
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ if (session_status() !== PHP_SESSION_ACTIVE) {
|
|||||||
session_start();
|
session_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isset($_SESSION['validation_errors']) || !is_array($_SESSION['validation_errors'])) {
|
||||||
|
$_SESSION['validation_errors'] = [];
|
||||||
|
}
|
||||||
|
|
||||||
require_once __DIR__ . '/lib/_sql.php';
|
require_once __DIR__ . '/lib/_sql.php';
|
||||||
$sql = new SQL();
|
$sql = new SQL();
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ if ($locationId <= 0) {
|
|||||||
|
|
||||||
if (!empty($errors)) {
|
if (!empty($errors)) {
|
||||||
$_SESSION['error'] = implode(', ', $errors);
|
$_SESSION['error'] = implode(', ', $errors);
|
||||||
|
$_SESSION['validation_errors'] = $errors;
|
||||||
$redirectUrl = $buildingId ? "?module=buildings&action=edit&id=$buildingId" : "?module=buildings&action=edit";
|
$redirectUrl = $buildingId ? "?module=buildings&action=edit&id=$buildingId" : "?module=buildings&action=edit";
|
||||||
header("Location: $redirectUrl");
|
header("Location: $redirectUrl");
|
||||||
exit;
|
exit;
|
||||||
|
|||||||
@@ -2,12 +2,19 @@
|
|||||||
/**
|
/**
|
||||||
* app/modules/connections/delete.php
|
* app/modules/connections/delete.php
|
||||||
*
|
*
|
||||||
* Loescht eine Verbindung und leitet zur Liste zurueck.
|
* Loescht eine Verbindung (AJAX-POST bevorzugt, GET-Fallback fuer Redirects).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$connectionId = (int)($_GET['id'] ?? $_POST['id'] ?? 0);
|
$isPost = ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST';
|
||||||
|
$connectionId = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
|
||||||
|
|
||||||
if ($connectionId <= 0) {
|
if ($connectionId <= 0) {
|
||||||
|
if ($isPost) {
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Ungueltige Verbindungs-ID']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
$_SESSION['error'] = 'Ungueltige Verbindungs-ID';
|
$_SESSION['error'] = 'Ungueltige Verbindungs-ID';
|
||||||
header('Location: ?module=connections&action=list');
|
header('Location: ?module=connections&action=list');
|
||||||
exit;
|
exit;
|
||||||
@@ -20,6 +27,12 @@ $connection = $sql->single(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!$connection) {
|
if (!$connection) {
|
||||||
|
if ($isPost) {
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Verbindung nicht gefunden']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
$_SESSION['error'] = 'Verbindung nicht gefunden';
|
$_SESSION['error'] = 'Verbindung nicht gefunden';
|
||||||
header('Location: ?module=connections&action=list');
|
header('Location: ?module=connections&action=list');
|
||||||
exit;
|
exit;
|
||||||
@@ -31,6 +44,17 @@ $rows = $sql->set(
|
|||||||
[$connectionId]
|
[$connectionId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if ($isPost) {
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
if ($rows > 0) {
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Verbindung geloescht']);
|
||||||
|
} else {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Verbindung konnte nicht geloescht werden']);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
if ($rows > 0) {
|
if ($rows > 0) {
|
||||||
$_SESSION['success'] = 'Verbindung geloescht';
|
$_SESSION['success'] = 'Verbindung geloescht';
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -39,6 +39,18 @@ $portBType = $normalizePortType((string)($connection['port_b_type'] ?? 'device')
|
|||||||
$portAId = (int)($connection['port_a_id'] ?? 0);
|
$portAId = (int)($connection['port_a_id'] ?? 0);
|
||||||
$portBId = (int)($connection['port_b_id'] ?? 0);
|
$portBId = (int)($connection['port_b_id'] ?? 0);
|
||||||
|
|
||||||
|
if ($connectionId <= 0) {
|
||||||
|
$requestedPortAType = $normalizePortType((string)($_GET['port_a_type'] ?? $portAType));
|
||||||
|
$requestedPortBType = $normalizePortType((string)($_GET['port_b_type'] ?? $portBType));
|
||||||
|
$requestedPortAId = (int)($_GET['port_a_id'] ?? $portAId);
|
||||||
|
$requestedPortBId = (int)($_GET['port_b_id'] ?? $portBId);
|
||||||
|
|
||||||
|
$portAType = $requestedPortAType;
|
||||||
|
$portBType = $requestedPortBType;
|
||||||
|
$portAId = $requestedPortAId > 0 ? $requestedPortAId : 0;
|
||||||
|
$portBId = $requestedPortBId > 0 ? $requestedPortBId : 0;
|
||||||
|
}
|
||||||
|
|
||||||
$endpointOptions = [
|
$endpointOptions = [
|
||||||
'device' => [],
|
'device' => [],
|
||||||
'module' => [],
|
'module' => [],
|
||||||
@@ -46,7 +58,7 @@ $endpointOptions = [
|
|||||||
'patchpanel' => [],
|
'patchpanel' => [],
|
||||||
];
|
];
|
||||||
|
|
||||||
$occupiedByType = [
|
$occupiedStatsByType = [
|
||||||
'device' => [],
|
'device' => [],
|
||||||
'module' => [],
|
'module' => [],
|
||||||
'outlet' => [],
|
'outlet' => [],
|
||||||
@@ -62,18 +74,24 @@ $occupiedRows = $sql->get(
|
|||||||
foreach ((array)$occupiedRows as $row) {
|
foreach ((array)$occupiedRows as $row) {
|
||||||
$typeA = $normalizePortType((string)($row['port_a_type'] ?? ''));
|
$typeA = $normalizePortType((string)($row['port_a_type'] ?? ''));
|
||||||
$idA = (int)($row['port_a_id'] ?? 0);
|
$idA = (int)($row['port_a_id'] ?? 0);
|
||||||
if ($idA > 0 && isset($occupiedByType[$typeA])) {
|
if ($idA > 0 && isset($occupiedStatsByType[$typeA])) {
|
||||||
$occupiedByType[$typeA][$idA] = true;
|
if (!isset($occupiedStatsByType[$typeA][$idA])) {
|
||||||
|
$occupiedStatsByType[$typeA][$idA] = ['total' => 0];
|
||||||
|
}
|
||||||
|
$occupiedStatsByType[$typeA][$idA]['total']++;
|
||||||
}
|
}
|
||||||
|
|
||||||
$typeB = $normalizePortType((string)($row['port_b_type'] ?? ''));
|
$typeB = $normalizePortType((string)($row['port_b_type'] ?? ''));
|
||||||
$idB = (int)($row['port_b_id'] ?? 0);
|
$idB = (int)($row['port_b_id'] ?? 0);
|
||||||
if ($idB > 0 && isset($occupiedByType[$typeB])) {
|
if ($idB > 0 && isset($occupiedStatsByType[$typeB])) {
|
||||||
$occupiedByType[$typeB][$idB] = true;
|
if (!isset($occupiedStatsByType[$typeB][$idB])) {
|
||||||
|
$occupiedStatsByType[$typeB][$idB] = ['total' => 0];
|
||||||
|
}
|
||||||
|
$occupiedStatsByType[$typeB][$idB]['total']++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$isEndpointAllowed = static function (string $type, int $id) use ($occupiedByType, $portAType, $portAId, $portBType, $portBId): bool {
|
$isEndpointAllowed = static function (string $type, int $id) use ($occupiedStatsByType, $portAType, $portAId, $portBType, $portBId): bool {
|
||||||
if ($id <= 0) {
|
if ($id <= 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -83,7 +101,10 @@ $isEndpointAllowed = static function (string $type, int $id) use ($occupiedByTyp
|
|||||||
if ($type === $portBType && $id === $portBId) {
|
if ($type === $portBType && $id === $portBId) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return empty($occupiedByType[$type][$id]);
|
|
||||||
|
$stats = $occupiedStatsByType[$type][$id] ?? ['total' => 0];
|
||||||
|
$maxConnections = in_array($type, ['outlet', 'patchpanel'], true) ? 2 : 1;
|
||||||
|
return (int)($stats['total'] ?? 0) < $maxConnections;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Auto-heal: ensure each outlet has at least one selectable port.
|
// Auto-heal: ensure each outlet has at least one selectable port.
|
||||||
@@ -170,7 +191,14 @@ foreach ($outletPorts as $row) {
|
|||||||
if (!$isEndpointAllowed('outlet', $id)) {
|
if (!$isEndpointAllowed('outlet', $id)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$parts = array_filter([(string)($row['floor_name'] ?? ''), (string)($row['room_name'] ?? ''), (string)$row['outlet_name'], (string)$row['name']]);
|
$portName = trim((string)($row['name'] ?? ''));
|
||||||
|
$includePortName = ($portName !== '' && strcasecmp($portName, 'Port 1') !== 0);
|
||||||
|
$parts = array_filter([
|
||||||
|
(string)($row['floor_name'] ?? ''),
|
||||||
|
(string)($row['room_name'] ?? ''),
|
||||||
|
(string)$row['outlet_name'],
|
||||||
|
$includePortName ? $portName : '',
|
||||||
|
]);
|
||||||
$endpointOptions['outlet'][] = [
|
$endpointOptions['outlet'][] = [
|
||||||
'id' => $id,
|
'id' => $id,
|
||||||
'label' => implode(' / ', $parts),
|
'label' => implode(' / ', $parts),
|
||||||
|
|||||||
@@ -335,11 +335,12 @@ $buildListUrl = static function (array $extra = []) use ($search, $deviceId): st
|
|||||||
<a href="<?php echo htmlspecialchars($buildListUrl(['connection_id' => $connId])); ?>" class="button button-small">Details</a>
|
<a href="<?php echo htmlspecialchars($buildListUrl(['connection_id' => $connId])); ?>" class="button button-small">Details</a>
|
||||||
<a href="?module=connections&action=edit&id=<?php echo $connId; ?>" class="button button-small">Bearbeiten</a>
|
<a href="?module=connections&action=edit&id=<?php echo $connId; ?>" class="button button-small">Bearbeiten</a>
|
||||||
<a href="?module=connections&action=swap&id=<?php echo $connId; ?>" class="button button-small" onclick="return confirm('Von/Nach fuer diese Verbindung vertauschen?');">Von/Nach tauschen</a>
|
<a href="?module=connections&action=swap&id=<?php echo $connId; ?>" class="button button-small" onclick="return confirm('Von/Nach fuer diese Verbindung vertauschen?');">Von/Nach tauschen</a>
|
||||||
<a href="?module=connections&action=delete&id=<?php echo $connId; ?>" class="button button-small button-danger"
|
<button
|
||||||
data-confirm-delete="true"
|
type="button"
|
||||||
data-confirm-message="Diese Verbindung wirklich loeschen?">
|
class="button button-small button-danger js-connection-delete"
|
||||||
|
data-connection-id="<?php echo $connId; ?>">
|
||||||
Loeschen
|
Loeschen
|
||||||
</a>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -382,7 +383,12 @@ $buildListUrl = static function (array $extra = []) use ($search, $deviceId): st
|
|||||||
<div class="sidebar-actions">
|
<div class="sidebar-actions">
|
||||||
<a href="?module=connections&action=edit&id=<?php echo $selectedConnId; ?>" class="button button-small">Bearbeiten</a>
|
<a href="?module=connections&action=edit&id=<?php echo $selectedConnId; ?>" class="button button-small">Bearbeiten</a>
|
||||||
<a href="?module=connections&action=swap&id=<?php echo $selectedConnId; ?>" class="button button-small" onclick="return confirm('Von/Nach fuer diese Verbindung vertauschen?');">Tauschen</a>
|
<a href="?module=connections&action=swap&id=<?php echo $selectedConnId; ?>" class="button button-small" onclick="return confirm('Von/Nach fuer diese Verbindung vertauschen?');">Tauschen</a>
|
||||||
<a href="?module=connections&action=delete&id=<?php echo $selectedConnId; ?>" class="button button-small button-danger" data-confirm-delete="true" data-confirm-message="Diese Verbindung wirklich loeschen?">Loeschen</a>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button button-small button-danger js-connection-delete"
|
||||||
|
data-connection-id="<?php echo $selectedConnId; ?>">
|
||||||
|
Loeschen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<p><em>Keine Verbindung ausgewaehlt.</em></p>
|
<p><em>Keine Verbindung ausgewaehlt.</em></p>
|
||||||
@@ -420,57 +426,34 @@ $buildListUrl = static function (array $extra = []) use ($search, $deviceId): st
|
|||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<script>
|
||||||
.connections-layout {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
display: grid;
|
document.querySelectorAll('.js-connection-delete').forEach((button) => {
|
||||||
grid-template-columns: minmax(0, 1fr) 320px;
|
button.addEventListener('click', () => {
|
||||||
gap: 20px;
|
const id = Number(button.dataset.connectionId || '0');
|
||||||
align-items: start;
|
if (id <= 0) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connections-sidebar {
|
if (!confirm('Diese Verbindung wirklich loeschen?')) {
|
||||||
position: sticky;
|
return;
|
||||||
top: 92px;
|
|
||||||
display: grid;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-card {
|
fetch('?module=connections&action=delete', {
|
||||||
background: #fff;
|
method: 'POST',
|
||||||
border: 1px solid #e0e6ef;
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||||
border-radius: 12px;
|
body: 'id=' + encodeURIComponent(id)
|
||||||
padding: 14px;
|
})
|
||||||
box-shadow: 0 8px 24px rgba(15, 26, 45, 0.08);
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data && data.success) {
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
|
||||||
.sidebar-card h3,
|
})
|
||||||
.sidebar-card h4 {
|
.catch(() => alert('Loeschen fehlgeschlagen'));
|
||||||
margin: 0 0 10px;
|
});
|
||||||
}
|
});
|
||||||
|
});
|
||||||
.sidebar-card p {
|
</script>
|
||||||
margin: 0 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-actions {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection-row-selected {
|
|
||||||
background: #edf5ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
|
||||||
.connections-layout {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connections-sidebar {
|
|
||||||
position: static;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -42,6 +42,18 @@ $normalizePortType = static function (string $value): string {
|
|||||||
$portAType = $normalizePortType((string)$portAType);
|
$portAType = $normalizePortType((string)$portAType);
|
||||||
$portBType = $normalizePortType((string)$portBType);
|
$portBType = $normalizePortType((string)$portBType);
|
||||||
|
|
||||||
|
$isTopologyPairAllowed = static function (string $typeA, string $typeB): bool {
|
||||||
|
$allowed = ['device' => true, 'module' => true, 'outlet' => true, 'patchpanel' => true];
|
||||||
|
if (!isset($allowed[$typeA]) || !isset($allowed[$typeB])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($typeA === 'patchpanel' || $typeB === 'patchpanel') {
|
||||||
|
return ($typeA === 'patchpanel' && in_array($typeB, ['patchpanel', 'outlet'], true))
|
||||||
|
|| ($typeB === 'patchpanel' && in_array($typeA, ['patchpanel', 'outlet'], true));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// Validierung (einfach)
|
// Validierung (einfach)
|
||||||
// =========================
|
// =========================
|
||||||
@@ -50,6 +62,9 @@ $errors = [];
|
|||||||
if ($portAId <= 0 || $portBId <= 0) {
|
if ($portAId <= 0 || $portBId <= 0) {
|
||||||
$errors[] = "Beide Ports sind erforderlich";
|
$errors[] = "Beide Ports sind erforderlich";
|
||||||
}
|
}
|
||||||
|
if (!$isTopologyPairAllowed($portAType, $portBType)) {
|
||||||
|
$errors[] = "Patchpanel-Ports duerfen nur mit Patchpanel-Ports oder Netzwerkdosen-Ports verbunden werden";
|
||||||
|
}
|
||||||
|
|
||||||
$otherConnections = $sql->get(
|
$otherConnections = $sql->get(
|
||||||
"SELECT id, port_a_type, port_a_id, port_b_type, port_b_id
|
"SELECT id, port_a_type, port_a_id, port_b_type, port_b_id
|
||||||
@@ -59,35 +74,78 @@ $otherConnections = $sql->get(
|
|||||||
[$connId]
|
[$connId]
|
||||||
);
|
);
|
||||||
|
|
||||||
$isEndpointUsed = static function (string $endpointType, int $endpointId) use ($otherConnections, $normalizePortType): bool {
|
$endpointUsage = [];
|
||||||
|
$trackUsage = static function (string $endpointType, int $endpointId, string $otherType) use (&$endpointUsage): void {
|
||||||
if ($endpointId <= 0) {
|
if ($endpointId <= 0) {
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
foreach ((array)$otherConnections as $row) {
|
if (!isset($endpointUsage[$endpointType][$endpointId])) {
|
||||||
$typeA = $normalizePortType((string)($row['port_a_type'] ?? ''));
|
$endpointUsage[$endpointType][$endpointId] = [
|
||||||
$idA = (int)($row['port_a_id'] ?? 0);
|
'total' => 0,
|
||||||
if ($typeA === $endpointType && $idA === $endpointId) {
|
'fixed' => 0,
|
||||||
return true;
|
'patch' => 0,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
$endpointUsage[$endpointType][$endpointId]['total']++;
|
||||||
|
|
||||||
$typeB = $normalizePortType((string)($row['port_b_type'] ?? ''));
|
if (in_array($endpointType, ['outlet', 'patchpanel'], true)) {
|
||||||
$idB = (int)($row['port_b_id'] ?? 0);
|
if (in_array($otherType, ['outlet', 'patchpanel'], true)) {
|
||||||
if ($typeB === $endpointType && $idB === $endpointId) {
|
$endpointUsage[$endpointType][$endpointId]['fixed']++;
|
||||||
return true;
|
} elseif (in_array($otherType, ['device', 'module'], true)) {
|
||||||
|
$endpointUsage[$endpointType][$endpointId]['patch']++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if ($isEndpointUsed($portAType, $portAId)) {
|
foreach ((array)$otherConnections as $row) {
|
||||||
$errors[] = "Port an Endpunkt A ist bereits in Verwendung";
|
$typeA = $normalizePortType((string)($row['port_a_type'] ?? ''));
|
||||||
|
$typeB = $normalizePortType((string)($row['port_b_type'] ?? ''));
|
||||||
|
$idA = (int)($row['port_a_id'] ?? 0);
|
||||||
|
$idB = (int)($row['port_b_id'] ?? 0);
|
||||||
|
|
||||||
|
$trackUsage($typeA, $idA, $typeB);
|
||||||
|
$trackUsage($typeB, $idB, $typeA);
|
||||||
}
|
}
|
||||||
if ($isEndpointUsed($portBType, $portBId)) {
|
|
||||||
$errors[] = "Port an Endpunkt B ist bereits in Verwendung";
|
$validateEndpointUsage = static function (string $endpointType, int $endpointId, string $otherType, string $label) use ($endpointUsage): ?string {
|
||||||
|
if ($endpointId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stats = $endpointUsage[$endpointType][$endpointId] ?? ['total' => 0, 'fixed' => 0, 'patch' => 0];
|
||||||
|
if ((int)$stats['total'] <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($endpointType, ['outlet', 'patchpanel'], true)) {
|
||||||
|
if ((int)$stats['total'] >= 2) {
|
||||||
|
return $label . " hat bereits die maximale Anzahl von 2 Verbindungen";
|
||||||
|
}
|
||||||
|
if (in_array($otherType, ['outlet', 'patchpanel'], true) && (int)$stats['fixed'] >= 1) {
|
||||||
|
return $label . " hat bereits eine feste Verdrahtung";
|
||||||
|
}
|
||||||
|
if (in_array($otherType, ['device', 'module'], true) && (int)$stats['patch'] >= 1) {
|
||||||
|
return $label . " hat bereits ein Patchkabel";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $label . " ist bereits in Verwendung";
|
||||||
|
};
|
||||||
|
|
||||||
|
$errorA = $validateEndpointUsage($portAType, $portAId, $portBType, 'Port an Endpunkt A');
|
||||||
|
if ($errorA !== null) {
|
||||||
|
$errors[] = $errorA;
|
||||||
|
}
|
||||||
|
|
||||||
|
$errorB = $validateEndpointUsage($portBType, $portBId, $portAType, 'Port an Endpunkt B');
|
||||||
|
if ($errorB !== null) {
|
||||||
|
$errors[] = $errorB;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($errors)) {
|
if (!empty($errors)) {
|
||||||
$_SESSION['error'] = implode(', ', $errors);
|
$_SESSION['error'] = implode(', ', $errors);
|
||||||
|
$_SESSION['validation_errors'] = $errors;
|
||||||
$redirectUrl = $connId ? "?module=connections&action=edit&id=$connId" : "?module=connections&action=edit";
|
$redirectUrl = $connId ? "?module=connections&action=edit&id=$connId" : "?module=connections&action=edit";
|
||||||
header("Location: $redirectUrl");
|
header("Location: $redirectUrl");
|
||||||
exit;
|
exit;
|
||||||
@@ -123,6 +181,7 @@ if ($connId > 0) {
|
|||||||
|
|
||||||
if ($connectionTypeId <= 0) {
|
if ($connectionTypeId <= 0) {
|
||||||
$_SESSION['error'] = "Kein Verbindungstyp verfuegbar";
|
$_SESSION['error'] = "Kein Verbindungstyp verfuegbar";
|
||||||
|
$_SESSION['validation_errors'] = ["Kein Verbindungstyp verfuegbar"];
|
||||||
header("Location: ?module=connections&action=edit");
|
header("Location: ?module=connections&action=edit");
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,12 +31,18 @@ $topologyDevices = $sql->get(
|
|||||||
r.id AS rack_id,
|
r.id AS rack_id,
|
||||||
r.name AS rack_name,
|
r.name AS rack_name,
|
||||||
f.id AS floor_id,
|
f.id AS floor_id,
|
||||||
f.name AS floor_name
|
f.name AS floor_name,
|
||||||
|
b.id AS building_id,
|
||||||
|
b.name AS building_name,
|
||||||
|
l.id AS location_id,
|
||||||
|
l.name AS location_name
|
||||||
FROM devices d
|
FROM devices d
|
||||||
LEFT JOIN device_types dt ON dt.id = d.device_type_id
|
LEFT JOIN device_types dt ON dt.id = d.device_type_id
|
||||||
LEFT JOIN racks r ON r.id = d.rack_id
|
LEFT JOIN racks r ON r.id = d.rack_id
|
||||||
LEFT JOIN floors f ON f.id = r.floor_id
|
LEFT JOIN floors f ON f.id = r.floor_id
|
||||||
ORDER BY floor_name, rack_name, device_name",
|
LEFT JOIN buildings b ON b.id = f.building_id
|
||||||
|
LEFT JOIN locations l ON l.id = b.location_id
|
||||||
|
ORDER BY location_name, building_name, floor_name, rack_name, device_name",
|
||||||
"",
|
"",
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
@@ -50,8 +56,195 @@ $topologyPayload = array_map(static function (array $row): array {
|
|||||||
'rack_name' => (string)($row['rack_name'] ?? ''),
|
'rack_name' => (string)($row['rack_name'] ?? ''),
|
||||||
'floor_id' => (int)($row['floor_id'] ?? 0),
|
'floor_id' => (int)($row['floor_id'] ?? 0),
|
||||||
'floor_name' => (string)($row['floor_name'] ?? ''),
|
'floor_name' => (string)($row['floor_name'] ?? ''),
|
||||||
|
'building_id' => (int)($row['building_id'] ?? 0),
|
||||||
|
'building_name' => (string)($row['building_name'] ?? ''),
|
||||||
|
'location_id' => (int)($row['location_id'] ?? 0),
|
||||||
|
'location_name' => (string)($row['location_name'] ?? ''),
|
||||||
];
|
];
|
||||||
}, $topologyDevices);
|
}, $topologyDevices);
|
||||||
|
|
||||||
|
$devicePortCountByDevice = [];
|
||||||
|
foreach ($sql->get(
|
||||||
|
"SELECT device_id, COUNT(*) AS cnt
|
||||||
|
FROM device_ports
|
||||||
|
GROUP BY device_id",
|
||||||
|
"",
|
||||||
|
[]
|
||||||
|
) as $row) {
|
||||||
|
$deviceId = (int)($row['device_id'] ?? 0);
|
||||||
|
if ($deviceId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$devicePortCountByDevice[$deviceId] = (int)($row['cnt'] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$devicePortPreviewByDevice = [];
|
||||||
|
$connectedDevicePorts = [];
|
||||||
|
foreach ($sql->get(
|
||||||
|
"SELECT port_a_type, port_a_id, port_b_type, port_b_id
|
||||||
|
FROM connections",
|
||||||
|
"",
|
||||||
|
[]
|
||||||
|
) as $row) {
|
||||||
|
$portAType = strtolower(trim((string)($row['port_a_type'] ?? '')));
|
||||||
|
$portBType = strtolower(trim((string)($row['port_b_type'] ?? '')));
|
||||||
|
$portAId = (int)($row['port_a_id'] ?? 0);
|
||||||
|
$portBId = (int)($row['port_b_id'] ?? 0);
|
||||||
|
|
||||||
|
if (($portAType === 'device' || $portAType === 'device_ports') && $portAId > 0) {
|
||||||
|
$connectedDevicePorts[$portAId] = true;
|
||||||
|
}
|
||||||
|
if (($portBType === 'device' || $portBType === 'device_ports') && $portBId > 0) {
|
||||||
|
$connectedDevicePorts[$portBId] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($sql->get(
|
||||||
|
"SELECT id, device_id, name
|
||||||
|
FROM device_ports
|
||||||
|
ORDER BY device_id, id",
|
||||||
|
"",
|
||||||
|
[]
|
||||||
|
) as $row) {
|
||||||
|
$deviceId = (int)($row['device_id'] ?? 0);
|
||||||
|
if ($deviceId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!isset($devicePortPreviewByDevice[$deviceId])) {
|
||||||
|
$devicePortPreviewByDevice[$deviceId] = [];
|
||||||
|
}
|
||||||
|
if (count($devicePortPreviewByDevice[$deviceId]) >= 4) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$devicePortPreviewByDevice[$deviceId][] = [
|
||||||
|
'id' => (int)($row['id'] ?? 0),
|
||||||
|
'name' => (string)($row['name'] ?? ''),
|
||||||
|
'is_connected' => isset($connectedDevicePorts[(int)($row['id'] ?? 0)]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($topologyPayload as $idx => $entry) {
|
||||||
|
$deviceId = (int)($entry['device_id'] ?? 0);
|
||||||
|
$topologyPayload[$idx]['port_count'] = (int)($devicePortCountByDevice[$deviceId] ?? 0);
|
||||||
|
$topologyPayload[$idx]['port_preview'] = $devicePortPreviewByDevice[$deviceId] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$rackInfoRows = $sql->get(
|
||||||
|
"SELECT
|
||||||
|
r.id AS rack_id,
|
||||||
|
r.name AS rack_name,
|
||||||
|
f.id AS floor_id,
|
||||||
|
f.name AS floor_name,
|
||||||
|
b.id AS building_id,
|
||||||
|
b.name AS building_name,
|
||||||
|
l.id AS location_id,
|
||||||
|
l.name AS location_name
|
||||||
|
FROM racks r
|
||||||
|
LEFT JOIN floors f ON f.id = r.floor_id
|
||||||
|
LEFT JOIN buildings b ON b.id = f.building_id
|
||||||
|
LEFT JOIN locations l ON l.id = b.location_id",
|
||||||
|
"",
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
$rackInfoById = [];
|
||||||
|
foreach ($rackInfoRows as $row) {
|
||||||
|
$rackId = (int)($row['rack_id'] ?? 0);
|
||||||
|
if ($rackId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$rackInfoById[$rackId] = [
|
||||||
|
'rack_id' => $rackId,
|
||||||
|
'rack_name' => (string)($row['rack_name'] ?? ''),
|
||||||
|
'floor_id' => (int)($row['floor_id'] ?? 0),
|
||||||
|
'floor_name' => (string)($row['floor_name'] ?? ''),
|
||||||
|
'building_id' => (int)($row['building_id'] ?? 0),
|
||||||
|
'building_name' => (string)($row['building_name'] ?? ''),
|
||||||
|
'location_id' => (int)($row['location_id'] ?? 0),
|
||||||
|
'location_name' => (string)($row['location_name'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$devicePortRacks = [];
|
||||||
|
foreach ($sql->get(
|
||||||
|
"SELECT dp.id AS port_id, d.rack_id
|
||||||
|
FROM device_ports dp
|
||||||
|
JOIN devices d ON d.id = dp.device_id
|
||||||
|
WHERE d.rack_id IS NOT NULL",
|
||||||
|
"",
|
||||||
|
[]
|
||||||
|
) as $row) {
|
||||||
|
$devicePortRacks[(int)$row['port_id']] = (int)$row['rack_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$modulePortRacks = [];
|
||||||
|
foreach ($sql->get(
|
||||||
|
"SELECT mp.id AS port_id, d.rack_id
|
||||||
|
FROM module_ports mp
|
||||||
|
JOIN modules m ON m.id = mp.module_id
|
||||||
|
JOIN device_port_modules dpm ON dpm.module_id = m.id
|
||||||
|
JOIN device_ports dp ON dp.id = dpm.device_port_id
|
||||||
|
JOIN devices d ON d.id = dp.device_id
|
||||||
|
WHERE d.rack_id IS NOT NULL",
|
||||||
|
"",
|
||||||
|
[]
|
||||||
|
) as $row) {
|
||||||
|
$modulePortRacks[(int)$row['port_id']] = (int)$row['rack_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolveRackId = static function (string $endpointType, int $endpointId) use ($devicePortRacks, $modulePortRacks): int {
|
||||||
|
if ($endpointType === 'device') {
|
||||||
|
return (int)($devicePortRacks[$endpointId] ?? 0);
|
||||||
|
}
|
||||||
|
if ($endpointType === 'module') {
|
||||||
|
return (int)($modulePortRacks[$endpointId] ?? 0);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
$rackLinksByKey = [];
|
||||||
|
foreach ($sql->get(
|
||||||
|
"SELECT id, port_a_type, port_a_id, port_b_type, port_b_id
|
||||||
|
FROM connections",
|
||||||
|
"",
|
||||||
|
[]
|
||||||
|
) as $row) {
|
||||||
|
$rackA = $resolveRackId((string)($row['port_a_type'] ?? ''), (int)($row['port_a_id'] ?? 0));
|
||||||
|
$rackB = $resolveRackId((string)($row['port_b_type'] ?? ''), (int)($row['port_b_id'] ?? 0));
|
||||||
|
if ($rackA <= 0 || $rackB <= 0 || $rackA === $rackB) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$from = min($rackA, $rackB);
|
||||||
|
$to = max($rackA, $rackB);
|
||||||
|
$key = $from . ':' . $to;
|
||||||
|
if (!isset($rackLinksByKey[$key])) {
|
||||||
|
$rackLinksByKey[$key] = [
|
||||||
|
'from_rack_id' => $from,
|
||||||
|
'to_rack_id' => $to,
|
||||||
|
'count' => 0,
|
||||||
|
'sample_connection_id' => (int)($row['id'] ?? 0)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$rackLinksByKey[$key]['count']++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rackLinkPayload = [];
|
||||||
|
foreach ($rackLinksByKey as $entry) {
|
||||||
|
$fromId = (int)$entry['from_rack_id'];
|
||||||
|
$toId = (int)$entry['to_rack_id'];
|
||||||
|
$fromMeta = $rackInfoById[$fromId] ?? ['rack_name' => 'Rack #' . $fromId];
|
||||||
|
$toMeta = $rackInfoById[$toId] ?? ['rack_name' => 'Rack #' . $toId];
|
||||||
|
|
||||||
|
$rackLinkPayload[] = [
|
||||||
|
'from_rack_id' => $fromId,
|
||||||
|
'to_rack_id' => $toId,
|
||||||
|
'count' => (int)$entry['count'],
|
||||||
|
'from_rack_name' => (string)($fromMeta['rack_name'] ?? ('Rack #' . $fromId)),
|
||||||
|
'to_rack_name' => (string)($toMeta['rack_name'] ?? ('Rack #' . $toId)),
|
||||||
|
'sample_connection_id' => (int)($entry['sample_connection_id'] ?? 0),
|
||||||
|
];
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="dashboard">
|
<div class="dashboard">
|
||||||
@@ -70,11 +263,12 @@ $topologyPayload = array_map(static function (array $row): array {
|
|||||||
<button type="button" class="button button-small" data-topology-zoom="reset">Reset</button>
|
<button type="button" class="button button-small" data-topology-zoom="reset">Reset</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="topology-wall__hint">Mausrad zoomt, Ziehen verschiebt. Klick auf einen Punkt zoomt auf den Rack-Kontext und oeffnet die Detailkarte.</p>
|
<p class="topology-wall__hint">Hierarchie: Standort → Gebaeude → Stockwerk → Rack → Geraet. Linien zeigen Rack-Verbindungen (dicker = mehr Links).</p>
|
||||||
|
|
||||||
<svg id="dashboard-topology-svg" viewBox="0 0 2400 1400" role="img" aria-label="Topologie-Wand">
|
<svg id="dashboard-topology-svg" viewBox="0 0 2400 1400" role="img" aria-label="Topologie-Wand">
|
||||||
<rect x="0" y="0" width="2400" height="1400" class="topology-bg"></rect>
|
<rect id="dashboard-topology-bg" x="0" y="0" width="2400" height="1400" class="topology-bg" fill="#f7faff"></rect>
|
||||||
<g id="dashboard-topology-grid"></g>
|
<g id="dashboard-topology-grid"></g>
|
||||||
|
<g id="dashboard-topology-connections"></g>
|
||||||
<g id="dashboard-topology-layer"></g>
|
<g id="dashboard-topology-layer"></g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
@@ -88,6 +282,7 @@ $topologyPayload = array_map(static function (array $row): array {
|
|||||||
<p data-topology-meta></p>
|
<p data-topology-meta></p>
|
||||||
<p data-topology-rack-link></p>
|
<p data-topology-rack-link></p>
|
||||||
<p data-topology-device-link></p>
|
<p data-topology-device-link></p>
|
||||||
|
<div class="topology-overlay__actions" data-topology-actions></div>
|
||||||
</aside>
|
</aside>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -147,6 +342,7 @@ $topologyPayload = array_map(static function (array $row): array {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script id="dashboard-topology-data" type="application/json"><?php echo json_encode($topologyPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?></script>
|
<script id="dashboard-topology-data" type="application/json"><?php echo json_encode($topologyPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?></script>
|
||||||
|
<script id="dashboard-topology-links" type="application/json"><?php echo json_encode($rackLinkPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?></script>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
const root = document.getElementById('dashboard-topology-wall');
|
const root = document.getElementById('dashboard-topology-wall');
|
||||||
@@ -155,7 +351,9 @@ $topologyPayload = array_map(static function (array $row): array {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const svg = document.getElementById('dashboard-topology-svg');
|
const svg = document.getElementById('dashboard-topology-svg');
|
||||||
|
const bgNode = document.getElementById('dashboard-topology-bg');
|
||||||
const gridLayer = document.getElementById('dashboard-topology-grid');
|
const gridLayer = document.getElementById('dashboard-topology-grid');
|
||||||
|
const connectionLayer = document.getElementById('dashboard-topology-connections');
|
||||||
const nodeLayer = document.getElementById('dashboard-topology-layer');
|
const nodeLayer = document.getElementById('dashboard-topology-layer');
|
||||||
const emptyNode = document.getElementById('dashboard-topology-empty');
|
const emptyNode = document.getElementById('dashboard-topology-empty');
|
||||||
const overlay = document.getElementById('dashboard-topology-overlay');
|
const overlay = document.getElementById('dashboard-topology-overlay');
|
||||||
@@ -163,20 +361,37 @@ $topologyPayload = array_map(static function (array $row): array {
|
|||||||
const overlayMeta = overlay ? overlay.querySelector('[data-topology-meta]') : null;
|
const overlayMeta = overlay ? overlay.querySelector('[data-topology-meta]') : null;
|
||||||
const overlayRackLink = overlay ? overlay.querySelector('[data-topology-rack-link]') : null;
|
const overlayRackLink = overlay ? overlay.querySelector('[data-topology-rack-link]') : null;
|
||||||
const overlayDeviceLink = overlay ? overlay.querySelector('[data-topology-device-link]') : null;
|
const overlayDeviceLink = overlay ? overlay.querySelector('[data-topology-device-link]') : null;
|
||||||
|
const overlayActions = overlay ? overlay.querySelector('[data-topology-actions]') : null;
|
||||||
const closeButton = overlay ? overlay.querySelector('[data-topology-close]') : null;
|
const closeButton = overlay ? overlay.querySelector('[data-topology-close]') : null;
|
||||||
|
|
||||||
const dataTag = document.getElementById('dashboard-topology-data');
|
const dataTag = document.getElementById('dashboard-topology-data');
|
||||||
|
const linkTag = document.getElementById('dashboard-topology-links');
|
||||||
let nodes = [];
|
let nodes = [];
|
||||||
|
let rackLinks = [];
|
||||||
try {
|
try {
|
||||||
nodes = JSON.parse((dataTag && dataTag.textContent) || '[]');
|
nodes = JSON.parse((dataTag && dataTag.textContent) || '[]');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
nodes = [];
|
nodes = [];
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
rackLinks = JSON.parse((linkTag && linkTag.textContent) || '[]');
|
||||||
|
} catch (error) {
|
||||||
|
rackLinks = [];
|
||||||
|
}
|
||||||
|
|
||||||
const scene = { width: 2400, height: 1400 };
|
const scene = { width: 2400, height: 1400 };
|
||||||
let view = { x: 0, y: 0, width: scene.width, height: scene.height };
|
let view = { x: 0, y: 0, width: scene.width, height: scene.height };
|
||||||
let drag = null;
|
let drag = null;
|
||||||
|
|
||||||
|
function clearActiveSelections() {
|
||||||
|
nodeLayer.querySelectorAll('.topology-node.active, .topology-port-node.active').forEach((node) => {
|
||||||
|
node.classList.remove('active');
|
||||||
|
});
|
||||||
|
connectionLayer.querySelectorAll('.topology-connection-line.active').forEach((line) => {
|
||||||
|
line.classList.remove('active');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function clampView() {
|
function clampView() {
|
||||||
view.width = Math.max(220, Math.min(scene.width, view.width));
|
view.width = Math.max(220, Math.min(scene.width, view.width));
|
||||||
view.height = Math.max(180, Math.min(scene.height, view.height));
|
view.height = Math.max(180, Math.min(scene.height, view.height));
|
||||||
@@ -261,59 +476,121 @@ $topologyPayload = array_map(static function (array $row): array {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildLayout(data) {
|
function buildLayout(data) {
|
||||||
const floors = new Map();
|
const hierarchy = new Map();
|
||||||
|
|
||||||
data.forEach((entry) => {
|
data.forEach((entry) => {
|
||||||
|
const locationId = Number(entry.location_id || 0);
|
||||||
|
const locationName = (entry.location_name || '').trim() || 'Ohne Standort';
|
||||||
|
const buildingId = Number(entry.building_id || 0);
|
||||||
|
const buildingName = (entry.building_name || '').trim() || 'Ohne Gebaeude';
|
||||||
|
const floorId = Number(entry.floor_id || 0);
|
||||||
const floorName = (entry.floor_name || '').trim() || 'Ohne Stockwerk';
|
const floorName = (entry.floor_name || '').trim() || 'Ohne Stockwerk';
|
||||||
const rackId = Number(entry.rack_id || 0);
|
const rackId = Number(entry.rack_id || 0);
|
||||||
const rackName = (entry.rack_name || '').trim() || 'Ohne Rack';
|
const rackName = (entry.rack_name || '').trim() || 'Ohne Rack';
|
||||||
|
|
||||||
|
const locationKey = `${locationId}:${locationName}`;
|
||||||
|
const buildingKey = `${buildingId}:${buildingName}`;
|
||||||
|
const floorKey = `${floorId}:${floorName}`;
|
||||||
const rackKey = `${rackId}:${rackName}`;
|
const rackKey = `${rackId}:${rackName}`;
|
||||||
|
|
||||||
if (!floors.has(floorName)) {
|
if (!hierarchy.has(locationKey)) {
|
||||||
floors.set(floorName, new Map());
|
hierarchy.set(locationKey, new Map());
|
||||||
}
|
}
|
||||||
const racks = floors.get(floorName);
|
const buildings = hierarchy.get(locationKey);
|
||||||
|
if (!buildings.has(buildingKey)) {
|
||||||
|
buildings.set(buildingKey, new Map());
|
||||||
|
}
|
||||||
|
const floors = buildings.get(buildingKey);
|
||||||
|
if (!floors.has(floorKey)) {
|
||||||
|
floors.set(floorKey, new Map());
|
||||||
|
}
|
||||||
|
const racks = floors.get(floorKey);
|
||||||
if (!racks.has(rackKey)) {
|
if (!racks.has(rackKey)) {
|
||||||
racks.set(rackKey, []);
|
racks.set(rackKey, []);
|
||||||
}
|
}
|
||||||
racks.get(rackKey).push(entry);
|
racks.get(rackKey).push(entry);
|
||||||
});
|
});
|
||||||
|
|
||||||
const floorNames = Array.from(floors.keys()).sort((a, b) => a.localeCompare(b));
|
|
||||||
const positioned = [];
|
const positioned = [];
|
||||||
|
const rackCenters = new Map();
|
||||||
|
let maxY = 1400;
|
||||||
|
let locationIndex = 0;
|
||||||
|
|
||||||
floorNames.forEach((floorName, floorIndex) => {
|
const locationKeys = Array.from(hierarchy.keys()).sort((a, b) => a.localeCompare(b));
|
||||||
const rackMap = floors.get(floorName);
|
locationKeys.forEach((locationKey) => {
|
||||||
const rackKeys = Array.from(rackMap.keys()).sort((a, b) => a.localeCompare(b));
|
const locationName = locationKey.split(':').slice(1).join(':');
|
||||||
const floorX = 140 + floorIndex * 540;
|
const locationX = 120 + locationIndex * 760;
|
||||||
|
const buildings = hierarchy.get(locationKey);
|
||||||
|
const buildingKeys = Array.from(buildings.keys()).sort((a, b) => a.localeCompare(b));
|
||||||
|
let currentY = 110;
|
||||||
|
const locationTop = currentY - 46;
|
||||||
|
|
||||||
positioned.push({ type: 'floor-label', x: floorX, y: 70, label: floorName });
|
positioned.push({
|
||||||
|
type: 'location-label',
|
||||||
|
x: locationX,
|
||||||
|
y: 70,
|
||||||
|
label: locationName
|
||||||
|
});
|
||||||
|
|
||||||
rackKeys.forEach((rackKey, rackIndex) => {
|
buildingKeys.forEach((buildingKey) => {
|
||||||
const devices = rackMap.get(rackKey);
|
const buildingName = buildingKey.split(':').slice(1).join(':');
|
||||||
const [rackIdPart, rackNamePart] = rackKey.split(':');
|
const floors = buildings.get(buildingKey);
|
||||||
|
const floorKeys = Array.from(floors.keys()).sort((a, b) => a.localeCompare(b));
|
||||||
|
const buildingTop = currentY;
|
||||||
|
|
||||||
|
positioned.push({
|
||||||
|
type: 'building-label',
|
||||||
|
x: locationX + 18,
|
||||||
|
y: buildingTop + 24,
|
||||||
|
label: buildingName
|
||||||
|
});
|
||||||
|
|
||||||
|
let floorCursorY = buildingTop + 32;
|
||||||
|
floorKeys.forEach((floorKey) => {
|
||||||
|
const floorName = floorKey.split(':').slice(1).join(':');
|
||||||
|
const racks = floors.get(floorKey);
|
||||||
|
const rackKeys = Array.from(racks.keys()).sort((a, b) => a.localeCompare(b));
|
||||||
|
const floorTop = floorCursorY;
|
||||||
|
let rackCursorY = floorTop + 36;
|
||||||
|
|
||||||
|
positioned.push({
|
||||||
|
type: 'floor-label',
|
||||||
|
x: locationX + 28,
|
||||||
|
y: floorTop + 22,
|
||||||
|
label: floorName
|
||||||
|
});
|
||||||
|
|
||||||
|
rackKeys.forEach((rackKey) => {
|
||||||
|
const devices = racks.get(rackKey);
|
||||||
|
const [rackIdPart, ...rackNameParts] = rackKey.split(':');
|
||||||
const rackId = Number(rackIdPart || 0);
|
const rackId = Number(rackIdPart || 0);
|
||||||
const rackName = rackNamePart || 'Ohne Rack';
|
const rackName = rackNameParts.join(':') || 'Ohne Rack';
|
||||||
|
const rowCount = Math.ceil(Math.max(1, devices.length) / 6);
|
||||||
const rackTop = 120 + rackIndex * 180;
|
const rackHeight = Math.max(78, 28 + rowCount * 30);
|
||||||
const rowCount = Math.ceil(Math.max(1, devices.length) / 4);
|
const rackX = locationX + 40;
|
||||||
const rackHeight = Math.max(90, 40 + rowCount * 45);
|
|
||||||
|
|
||||||
positioned.push({
|
positioned.push({
|
||||||
type: 'rack-box',
|
type: 'rack-box',
|
||||||
x: floorX - 50,
|
x: rackX,
|
||||||
y: rackTop - 30,
|
y: rackCursorY,
|
||||||
width: 380,
|
width: 640,
|
||||||
height: rackHeight,
|
height: rackHeight,
|
||||||
label: rackName,
|
label: rackName,
|
||||||
rack_id: rackId
|
rack_id: rackId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (rackId > 0) {
|
||||||
|
rackCenters.set(rackId, {
|
||||||
|
x: rackX + 320,
|
||||||
|
y: rackCursorY + rackHeight / 2
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
devices.forEach((device, deviceIndex) => {
|
devices.forEach((device, deviceIndex) => {
|
||||||
const col = deviceIndex % 4;
|
const col = deviceIndex % 6;
|
||||||
const row = Math.floor(deviceIndex / 4);
|
const row = Math.floor(deviceIndex / 6);
|
||||||
const x = floorX + col * 85;
|
const x = rackX + 24 + col * 96;
|
||||||
const y = rackTop + row * 40;
|
const y = rackCursorY + 28 + row * 30;
|
||||||
|
|
||||||
positioned.push({
|
positioned.push({
|
||||||
type: 'node',
|
type: 'node',
|
||||||
@@ -322,46 +599,125 @@ $topologyPayload = array_map(static function (array $row): array {
|
|||||||
rack_id: Number(device.rack_id || 0),
|
rack_id: Number(device.rack_id || 0),
|
||||||
rack_name: device.rack_name || 'Ohne Rack',
|
rack_name: device.rack_name || 'Ohne Rack',
|
||||||
floor_name: device.floor_name || 'Ohne Stockwerk',
|
floor_name: device.floor_name || 'Ohne Stockwerk',
|
||||||
|
building_name: device.building_name || 'Ohne Gebaeude',
|
||||||
|
location_name: device.location_name || 'Ohne Standort',
|
||||||
device_id: Number(device.device_id || 0),
|
device_id: Number(device.device_id || 0),
|
||||||
device_name: device.device_name || 'Unbenannt',
|
device_name: device.device_name || 'Unbenannt',
|
||||||
device_type_name: device.device_type_name || ''
|
device_type_name: device.device_type_name || '',
|
||||||
});
|
port_count: Number(device.port_count || 0),
|
||||||
});
|
port_preview: Array.isArray(device.port_preview) ? device.port_preview : []
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return positioned;
|
rackCursorY += rackHeight + 16;
|
||||||
|
});
|
||||||
|
|
||||||
|
const floorHeight = Math.max(84, rackCursorY - floorTop + 8);
|
||||||
|
positioned.push({
|
||||||
|
type: 'floor-box',
|
||||||
|
x: locationX + 20,
|
||||||
|
y: floorTop,
|
||||||
|
width: 680,
|
||||||
|
height: floorHeight
|
||||||
|
});
|
||||||
|
|
||||||
|
floorCursorY = floorTop + floorHeight + 18;
|
||||||
|
});
|
||||||
|
|
||||||
|
const buildingHeight = Math.max(130, floorCursorY - buildingTop + 8);
|
||||||
|
positioned.push({
|
||||||
|
type: 'building-box',
|
||||||
|
x: locationX + 8,
|
||||||
|
y: buildingTop,
|
||||||
|
width: 708,
|
||||||
|
height: buildingHeight
|
||||||
|
});
|
||||||
|
|
||||||
|
currentY = buildingTop + buildingHeight + 26;
|
||||||
|
});
|
||||||
|
|
||||||
|
const locationHeight = Math.max(220, currentY - locationTop + 16);
|
||||||
|
positioned.push({
|
||||||
|
type: 'location-box',
|
||||||
|
x: locationX - 8,
|
||||||
|
y: locationTop,
|
||||||
|
width: 736,
|
||||||
|
height: locationHeight
|
||||||
|
});
|
||||||
|
|
||||||
|
maxY = Math.max(maxY, locationTop + locationHeight + 40);
|
||||||
|
locationIndex++;
|
||||||
|
});
|
||||||
|
|
||||||
|
const width = Math.max(2400, 220 + locationIndex * 760);
|
||||||
|
const height = Math.max(1400, Math.ceil(maxY));
|
||||||
|
return { entries: positioned, rackCenters, width, height };
|
||||||
}
|
}
|
||||||
|
|
||||||
function showOverlay(node) {
|
function showOverlay(item) {
|
||||||
if (!overlay || !overlayTitle || !overlayMeta || !overlayRackLink || !overlayDeviceLink) {
|
if (!overlay || !overlayTitle || !overlayMeta || !overlayRackLink || !overlayDeviceLink || !overlayActions) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
overlayTitle.textContent = node.rack_name || 'Ohne Rack';
|
overlayRackLink.textContent = '';
|
||||||
const typeLabel = node.device_type_name ? ` (${node.device_type_name})` : '';
|
overlayDeviceLink.textContent = '';
|
||||||
overlayMeta.textContent = `Stockwerk: ${node.floor_name} | Geraet: ${node.device_name}${typeLabel}`;
|
overlayActions.innerHTML = '';
|
||||||
|
|
||||||
if (node.rack_id > 0) {
|
if (item.kind === 'connection') {
|
||||||
overlayRackLink.innerHTML = `<a href="?module=racks&action=edit&id=${node.rack_id}">Rack bearbeiten</a>`;
|
overlayTitle.textContent = 'Verbindung';
|
||||||
|
overlayMeta.textContent = `${item.from_name} <-> ${item.to_name} | Anzahl: ${item.count}`;
|
||||||
|
if (item.sample_connection_id > 0) {
|
||||||
|
overlayRackLink.innerHTML = `<a href="?module=connections&action=edit&id=${item.sample_connection_id}">Verbindung bearbeiten</a>`;
|
||||||
|
overlayDeviceLink.innerHTML = `<a href="?module=connections&action=delete&id=${item.sample_connection_id}" onclick="return confirm('Diese Verbindung wirklich loeschen?');">Verbindung entfernen</a>`;
|
||||||
|
}
|
||||||
|
overlay.hidden = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.kind === 'port') {
|
||||||
|
overlayTitle.textContent = `Port: ${item.port_name}`;
|
||||||
|
overlayMeta.textContent = `Geraet: ${item.device_name} | Rack: ${item.rack_name} | Stockwerk: ${item.floor_name}`;
|
||||||
|
if (item.device_id > 0) {
|
||||||
|
overlayRackLink.innerHTML = `<a href="?module=devices&action=edit&id=${item.device_id}">Geraet bearbeiten</a>`;
|
||||||
|
if (item.port_id > 0 && !item.is_connected) {
|
||||||
|
overlayDeviceLink.innerHTML = `<a href="?module=connections&action=edit&port_a_type=device&port_a_id=${item.port_id}&port_b_type=patchpanel">Mit Patchfeld verbinden</a>`;
|
||||||
|
} else {
|
||||||
|
overlayDeviceLink.textContent = 'Port ist bereits verbunden';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (item.port_id > 0) {
|
||||||
|
overlayActions.innerHTML = `<a class="button button-small" href="?module=devices&action=edit&id=${item.device_id}">Port im Geraet aendern</a>`;
|
||||||
|
}
|
||||||
|
overlay.hidden = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
overlayTitle.textContent = item.rack_name || 'Ohne Rack';
|
||||||
|
const typeLabel = item.device_type_name ? ` (${item.device_type_name})` : '';
|
||||||
|
overlayMeta.textContent = `Standort: ${item.location_name} | Gebaeude: ${item.building_name} | Stockwerk: ${item.floor_name} | Geraet: ${item.device_name}${typeLabel}`;
|
||||||
|
|
||||||
|
if (item.rack_id > 0) {
|
||||||
|
overlayRackLink.innerHTML = `<a href="?module=racks&action=edit&id=${item.rack_id}">Rack bearbeiten</a>`;
|
||||||
} else {
|
} else {
|
||||||
overlayRackLink.textContent = 'Kein Rack verknuepft';
|
overlayRackLink.textContent = 'Kein Rack verknuepft';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.device_id > 0) {
|
if (item.device_id > 0) {
|
||||||
overlayDeviceLink.innerHTML = `<a href="?module=devices&action=edit&id=${node.device_id}">Geraet bearbeiten</a>`;
|
overlayDeviceLink.innerHTML = `<a href="?module=devices&action=edit&id=${item.device_id}">Geraet bearbeiten</a>`;
|
||||||
} else {
|
overlayActions.innerHTML = `
|
||||||
overlayDeviceLink.textContent = '';
|
<a class="button button-small" href="?module=devices&action=edit&id=${item.device_id}">Editieren</a>
|
||||||
|
<a class="button button-small button-danger" href="?module=devices&action=delete&id=${item.device_id}&force=1" onclick="return confirm('Dieses Geraet wirklich loeschen?');">Entfernen</a>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
overlay.hidden = false;
|
overlay.hidden = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTopology() {
|
function renderTopology() {
|
||||||
drawGrid();
|
|
||||||
clearLayer(nodeLayer);
|
|
||||||
|
|
||||||
if (!nodes.length) {
|
if (!nodes.length) {
|
||||||
|
drawGrid();
|
||||||
|
clearLayer(connectionLayer);
|
||||||
|
clearLayer(nodeLayer);
|
||||||
if (emptyNode) {
|
if (emptyNode) {
|
||||||
emptyNode.hidden = false;
|
emptyNode.hidden = false;
|
||||||
}
|
}
|
||||||
@@ -372,9 +728,75 @@ $topologyPayload = array_map(static function (array $row): array {
|
|||||||
emptyNode.hidden = true;
|
emptyNode.hidden = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entries = buildLayout(nodes);
|
const layout = buildLayout(nodes);
|
||||||
|
const entries = layout.entries;
|
||||||
|
scene.width = layout.width;
|
||||||
|
scene.height = layout.height;
|
||||||
|
drawGrid();
|
||||||
|
clearLayer(connectionLayer);
|
||||||
|
clearLayer(nodeLayer);
|
||||||
|
if (bgNode) {
|
||||||
|
bgNode.setAttribute('width', String(scene.width));
|
||||||
|
bgNode.setAttribute('height', String(scene.height));
|
||||||
|
}
|
||||||
|
|
||||||
entries.forEach((entry) => {
|
entries.forEach((entry) => {
|
||||||
|
if (entry.type === 'location-box') {
|
||||||
|
const rect = svgElement('rect');
|
||||||
|
rect.setAttribute('x', String(entry.x));
|
||||||
|
rect.setAttribute('y', String(entry.y));
|
||||||
|
rect.setAttribute('width', String(entry.width));
|
||||||
|
rect.setAttribute('height', String(entry.height));
|
||||||
|
rect.setAttribute('rx', '12');
|
||||||
|
rect.setAttribute('class', 'topology-location-box');
|
||||||
|
nodeLayer.appendChild(rect);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.type === 'building-box') {
|
||||||
|
const rect = svgElement('rect');
|
||||||
|
rect.setAttribute('x', String(entry.x));
|
||||||
|
rect.setAttribute('y', String(entry.y));
|
||||||
|
rect.setAttribute('width', String(entry.width));
|
||||||
|
rect.setAttribute('height', String(entry.height));
|
||||||
|
rect.setAttribute('rx', '10');
|
||||||
|
rect.setAttribute('class', 'topology-building-box');
|
||||||
|
nodeLayer.appendChild(rect);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.type === 'floor-box') {
|
||||||
|
const rect = svgElement('rect');
|
||||||
|
rect.setAttribute('x', String(entry.x));
|
||||||
|
rect.setAttribute('y', String(entry.y));
|
||||||
|
rect.setAttribute('width', String(entry.width));
|
||||||
|
rect.setAttribute('height', String(entry.height));
|
||||||
|
rect.setAttribute('rx', '8');
|
||||||
|
rect.setAttribute('class', 'topology-floor-box');
|
||||||
|
nodeLayer.appendChild(rect);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.type === 'location-label') {
|
||||||
|
const text = svgElement('text');
|
||||||
|
text.setAttribute('x', String(entry.x));
|
||||||
|
text.setAttribute('y', String(entry.y));
|
||||||
|
text.setAttribute('class', 'topology-location-label');
|
||||||
|
text.textContent = entry.label;
|
||||||
|
nodeLayer.appendChild(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.type === 'building-label') {
|
||||||
|
const text = svgElement('text');
|
||||||
|
text.setAttribute('x', String(entry.x));
|
||||||
|
text.setAttribute('y', String(entry.y));
|
||||||
|
text.setAttribute('class', 'topology-building-label');
|
||||||
|
text.textContent = entry.label;
|
||||||
|
nodeLayer.appendChild(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (entry.type === 'floor-label') {
|
if (entry.type === 'floor-label') {
|
||||||
const text = svgElement('text');
|
const text = svgElement('text');
|
||||||
text.setAttribute('x', String(entry.x));
|
text.setAttribute('x', String(entry.x));
|
||||||
@@ -418,12 +840,10 @@ $topologyPayload = array_map(static function (array $row): array {
|
|||||||
circle.appendChild(title);
|
circle.appendChild(title);
|
||||||
|
|
||||||
const activate = () => {
|
const activate = () => {
|
||||||
nodeLayer.querySelectorAll('.topology-node.active').forEach((node) => {
|
clearActiveSelections();
|
||||||
node.classList.remove('active');
|
|
||||||
});
|
|
||||||
circle.classList.add('active');
|
circle.classList.add('active');
|
||||||
zoomToNode(entry.x, entry.y);
|
zoomToNode(entry.x, entry.y);
|
||||||
showOverlay(entry);
|
showOverlay({ ...entry, kind: 'device' });
|
||||||
};
|
};
|
||||||
|
|
||||||
circle.addEventListener('click', activate);
|
circle.addEventListener('click', activate);
|
||||||
@@ -435,6 +855,92 @@ $topologyPayload = array_map(static function (array $row): array {
|
|||||||
});
|
});
|
||||||
|
|
||||||
nodeLayer.appendChild(circle);
|
nodeLayer.appendChild(circle);
|
||||||
|
|
||||||
|
const portPreview = Array.isArray(entry.port_preview) ? entry.port_preview : [];
|
||||||
|
portPreview.forEach((port, portIndex) => {
|
||||||
|
const portNode = svgElement('circle');
|
||||||
|
portNode.setAttribute('cx', String(entry.x + 16 + (portIndex * 10)));
|
||||||
|
portNode.setAttribute('cy', String(entry.y - 16));
|
||||||
|
portNode.setAttribute('r', '5');
|
||||||
|
portNode.setAttribute('tabindex', '0');
|
||||||
|
portNode.setAttribute('class', 'topology-port-node');
|
||||||
|
portNode.setAttribute('data-port-id', String(Number(port.id || 0)));
|
||||||
|
portNode.setAttribute('data-device-id', String(entry.device_id));
|
||||||
|
|
||||||
|
const portTitle = svgElement('title');
|
||||||
|
portTitle.textContent = `${port.name || 'Port'} (${entry.device_name})`;
|
||||||
|
portNode.appendChild(portTitle);
|
||||||
|
|
||||||
|
const activatePort = () => {
|
||||||
|
clearActiveSelections();
|
||||||
|
portNode.classList.add('active');
|
||||||
|
showOverlay({
|
||||||
|
kind: 'port',
|
||||||
|
port_id: Number(port.id || 0),
|
||||||
|
port_name: port.name || `Port ${portIndex + 1}`,
|
||||||
|
is_connected: !!port.is_connected,
|
||||||
|
device_id: entry.device_id,
|
||||||
|
device_name: entry.device_name,
|
||||||
|
rack_name: entry.rack_name,
|
||||||
|
floor_name: entry.floor_name
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
portNode.addEventListener('click', activatePort);
|
||||||
|
portNode.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
event.preventDefault();
|
||||||
|
activatePort();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
nodeLayer.appendChild(portNode);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
rackLinks.forEach((link) => {
|
||||||
|
const fromRackId = Number(link.from_rack_id || 0);
|
||||||
|
const toRackId = Number(link.to_rack_id || 0);
|
||||||
|
const count = Math.max(1, Number(link.count || 1));
|
||||||
|
const fromPoint = layout.rackCenters.get(fromRackId);
|
||||||
|
const toPoint = layout.rackCenters.get(toRackId);
|
||||||
|
if (!fromPoint || !toPoint) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const line = svgElement('line');
|
||||||
|
line.setAttribute('x1', String(fromPoint.x));
|
||||||
|
line.setAttribute('y1', String(fromPoint.y));
|
||||||
|
line.setAttribute('x2', String(toPoint.x));
|
||||||
|
line.setAttribute('y2', String(toPoint.y));
|
||||||
|
line.setAttribute('class', 'topology-connection-line');
|
||||||
|
line.setAttribute('stroke-width', String(Math.min(8, 1 + count)));
|
||||||
|
line.setAttribute('tabindex', '0');
|
||||||
|
|
||||||
|
const title = svgElement('title');
|
||||||
|
title.textContent = `${link.from_rack_name} <-> ${link.to_rack_name}: ${count} Verbindungen`;
|
||||||
|
line.appendChild(title);
|
||||||
|
|
||||||
|
const activateConnection = () => {
|
||||||
|
clearActiveSelections();
|
||||||
|
line.classList.add('active');
|
||||||
|
showOverlay({
|
||||||
|
kind: 'connection',
|
||||||
|
from_name: link.from_rack_name || `Rack ${fromRackId}`,
|
||||||
|
to_name: link.to_rack_name || `Rack ${toRackId}`,
|
||||||
|
count,
|
||||||
|
sample_connection_id: Number(link.sample_connection_id || 0)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
line.addEventListener('click', activateConnection);
|
||||||
|
line.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
event.preventDefault();
|
||||||
|
activateConnection();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connectionLayer.appendChild(line);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,6 +952,11 @@ $topologyPayload = array_map(static function (array $row): array {
|
|||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
|
|
||||||
svg.addEventListener('pointerdown', (event) => {
|
svg.addEventListener('pointerdown', (event) => {
|
||||||
|
const target = event.target;
|
||||||
|
if (target instanceof Element && (target.closest('.topology-node') || target.closest('.topology-port-node') || target.closest('.topology-connection-line'))) {
|
||||||
|
drag = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
drag = {
|
drag = {
|
||||||
startX: event.clientX,
|
startX: event.clientX,
|
||||||
startY: event.clientY,
|
startY: event.clientY,
|
||||||
@@ -497,6 +1008,7 @@ $topologyPayload = array_map(static function (array $row): array {
|
|||||||
if (closeButton && overlay) {
|
if (closeButton && overlay) {
|
||||||
closeButton.addEventListener('click', () => {
|
closeButton.addEventListener('click', () => {
|
||||||
overlay.hidden = true;
|
overlay.hidden = true;
|
||||||
|
clearActiveSelections();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -659,12 +1171,42 @@ $topologyPayload = array_map(static function (array $row): array {
|
|||||||
stroke-width: 1;
|
stroke-width: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topology-floor-label {
|
.topology-location-box {
|
||||||
|
fill: none;
|
||||||
|
stroke: #a9c0de;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topology-building-box {
|
||||||
|
fill: none;
|
||||||
|
stroke: #c3d3e8;
|
||||||
|
stroke-width: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topology-floor-box {
|
||||||
|
fill: none;
|
||||||
|
stroke: #d0deef;
|
||||||
|
stroke-width: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topology-location-label {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
fill: #23304a;
|
fill: #23304a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.topology-building-label {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
fill: #2a3d5c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topology-floor-label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
fill: #3f5679;
|
||||||
|
}
|
||||||
|
|
||||||
.topology-rack-box {
|
.topology-rack-box {
|
||||||
fill: #ffffff;
|
fill: #ffffff;
|
||||||
stroke: #bfd0e6;
|
stroke: #bfd0e6;
|
||||||
@@ -672,15 +1214,31 @@ $topologyPayload = array_map(static function (array $row): array {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.topology-rack-label {
|
.topology-rack-label {
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
fill: #2a3d5c;
|
fill: #2a3d5c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.topology-connection-line {
|
||||||
|
stroke: #426da4;
|
||||||
|
stroke-opacity: 0.55;
|
||||||
|
stroke-linecap: round;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topology-connection-line:hover,
|
||||||
|
.topology-connection-line:focus,
|
||||||
|
.topology-connection-line.active {
|
||||||
|
stroke: #d2612a;
|
||||||
|
stroke-opacity: 0.85;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
.topology-node {
|
.topology-node {
|
||||||
fill: #1f73c9;
|
fill: #1f73c9;
|
||||||
stroke: #ffffff;
|
stroke: #ffffff;
|
||||||
stroke-width: 3;
|
stroke-width: 3;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topology-node:hover,
|
.topology-node:hover,
|
||||||
@@ -690,6 +1248,20 @@ $topologyPayload = array_map(static function (array $row): array {
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.topology-port-node {
|
||||||
|
fill: #3d9640;
|
||||||
|
stroke: #ffffff;
|
||||||
|
stroke-width: 2;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topology-port-node:hover,
|
||||||
|
.topology-port-node:focus,
|
||||||
|
.topology-port-node.active {
|
||||||
|
fill: #f18d32;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
.topology-empty {
|
.topology-empty {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
@@ -725,6 +1297,13 @@ $topologyPayload = array_map(static function (array $row): array {
|
|||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.topology-overlay__actions {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
#dashboard-topology-svg {
|
#dashboard-topology-svg {
|
||||||
height: 360px;
|
height: 360px;
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ $pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType[
|
|||||||
<textarea id="comment" name="comment" rows="3"
|
<textarea id="comment" name="comment" rows="3"
|
||||||
placeholder="z.B. Rack-Mount, 48 RJ45 + 4 SFP"><?php echo htmlspecialchars($deviceType['comment'] ?? ''); ?></textarea>
|
placeholder="z.B. Rack-Mount, 48 RJ45 + 4 SFP"><?php echo htmlspecialchars($deviceType['comment'] ?? ''); ?></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
<?php if (!$isEdit): ?>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="seed_ports">Ports automatisch anlegen</label>
|
<label for="seed_ports">Ports automatisch anlegen</label>
|
||||||
<input type="number" id="seed_ports" name="seed_ports" min="0" step="1"
|
<input type="number" id="seed_ports" name="seed_ports" min="0" step="1"
|
||||||
@@ -106,6 +107,7 @@ $pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType[
|
|||||||
</select>
|
</select>
|
||||||
<small>Wird beim automatischen Erstellen neuer Ports als Startwert gesetzt.</small>
|
<small>Wird beim automatischen Erstellen neuer Ports als Startwert gesetzt.</small>
|
||||||
</div>
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<!-- =========================
|
<!-- =========================
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ $deviceTypeId = (int)($_POST['id'] ?? 0);
|
|||||||
$name = trim($_POST['name'] ?? '');
|
$name = trim($_POST['name'] ?? '');
|
||||||
$category = $_POST['category'] ?? 'other';
|
$category = $_POST['category'] ?? 'other';
|
||||||
$comment = trim($_POST['comment'] ?? '');
|
$comment = trim($_POST['comment'] ?? '');
|
||||||
$seedPortCount = max(0, (int)($_POST['seed_ports'] ?? 0));
|
$isCreate = $deviceTypeId <= 0;
|
||||||
|
$seedPortCount = $isCreate ? max(0, (int)($_POST['seed_ports'] ?? 0)) : 0;
|
||||||
$defaultPortTypeId = normalizeNullableInt($_POST['default_port_type_id'] ?? null);
|
$defaultPortTypeId = normalizeNullableInt($_POST['default_port_type_id'] ?? null);
|
||||||
$portRows = is_array($_POST['port_rows'] ?? null) ? $_POST['port_rows'] : [];
|
$portRows = is_array($_POST['port_rows'] ?? null) ? $_POST['port_rows'] : [];
|
||||||
$rawShapes = trim($_POST['shape_definition'] ?? '');
|
$rawShapes = trim($_POST['shape_definition'] ?? '');
|
||||||
@@ -49,6 +50,7 @@ if (!in_array($category, ['switch', 'server', 'patchpanel', 'other'])) {
|
|||||||
// Falls Fehler: zurück zum Edit-Formular
|
// Falls Fehler: zurück zum Edit-Formular
|
||||||
if (!empty($errors)) {
|
if (!empty($errors)) {
|
||||||
$_SESSION['error'] = implode(', ', $errors);
|
$_SESSION['error'] = implode(', ', $errors);
|
||||||
|
$_SESSION['validation_errors'] = $errors;
|
||||||
header('Location: ?module=device_types&action=edit' . ($deviceTypeId ? "&id=$deviceTypeId" : ""));
|
header('Location: ?module=device_types&action=edit' . ($deviceTypeId ? "&id=$deviceTypeId" : ""));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -67,6 +69,7 @@ if (!empty($_FILES['image']['name'])) {
|
|||||||
// Nur SVG, JPG, PNG erlaubt
|
// Nur SVG, JPG, PNG erlaubt
|
||||||
if (!in_array($fileExt, ['svg', 'jpg', 'jpeg', 'png'])) {
|
if (!in_array($fileExt, ['svg', 'jpg', 'jpeg', 'png'])) {
|
||||||
$_SESSION['error'] = "Nur SVG, JPG und PNG sind erlaubt";
|
$_SESSION['error'] = "Nur SVG, JPG und PNG sind erlaubt";
|
||||||
|
$_SESSION['validation_errors'] = ["Nur SVG, JPG und PNG sind erlaubt"];
|
||||||
header('Location: ?module=device_types&action=edit' . ($deviceTypeId ? "&id=$deviceTypeId" : ""));
|
header('Location: ?module=device_types&action=edit' . ($deviceTypeId ? "&id=$deviceTypeId" : ""));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -86,6 +89,7 @@ if (!empty($_FILES['image']['name'])) {
|
|||||||
$imageType = $fileExt === 'svg' ? 'svg' : 'bitmap';
|
$imageType = $fileExt === 'svg' ? 'svg' : 'bitmap';
|
||||||
} else {
|
} else {
|
||||||
$_SESSION['error'] = "Datei-Upload fehlgeschlagen";
|
$_SESSION['error'] = "Datei-Upload fehlgeschlagen";
|
||||||
|
$_SESSION['validation_errors'] = ["Datei-Upload fehlgeschlagen"];
|
||||||
header('Location: ?module=device_types&action=edit' . ($deviceTypeId ? "&id=$deviceTypeId" : ""));
|
header('Location: ?module=device_types&action=edit' . ($deviceTypeId ? "&id=$deviceTypeId" : ""));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -128,7 +132,9 @@ if ($deviceTypeId > 0) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($isCreate) {
|
||||||
seedDeviceTypePorts($sql, $deviceTypeId, $seedPortCount, $defaultPortTypeId);
|
seedDeviceTypePorts($sql, $deviceTypeId, $seedPortCount, $defaultPortTypeId);
|
||||||
|
}
|
||||||
syncDeviceTypePorts($sql, $deviceTypeId, $portRows);
|
syncDeviceTypePorts($sql, $deviceTypeId, $portRows);
|
||||||
|
|
||||||
$_SESSION['success'] = $deviceTypeId ? "Gerätetyp gespeichert" : "Fehler beim Speichern";
|
$_SESSION['success'] = $deviceTypeId ? "Gerätetyp gespeichert" : "Fehler beim Speichern";
|
||||||
|
|||||||
@@ -3,12 +3,20 @@
|
|||||||
* app/modules/devices/delete.php
|
* app/modules/devices/delete.php
|
||||||
*
|
*
|
||||||
* Loescht ein Geraet. Bei Abhaengigkeiten ist force=1 erforderlich.
|
* Loescht ein Geraet. Bei Abhaengigkeiten ist force=1 erforderlich.
|
||||||
|
* Unterstuetzt GET-Redirects und AJAX-POST.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$deviceId = (int)($_GET['id'] ?? 0);
|
$isPost = ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST';
|
||||||
$forceDelete = (int)($_GET['force'] ?? 0) === 1;
|
$deviceId = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
|
||||||
|
$forceDelete = (int)($_POST['force'] ?? $_GET['force'] ?? 0) === 1;
|
||||||
|
|
||||||
if ($deviceId <= 0) {
|
if ($deviceId <= 0) {
|
||||||
|
if ($isPost) {
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Ungueltige Geraete-ID']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
$_SESSION['error'] = "Ungueltige Geraete-ID";
|
$_SESSION['error'] = "Ungueltige Geraete-ID";
|
||||||
header('Location: ?module=devices&action=list');
|
header('Location: ?module=devices&action=list');
|
||||||
exit;
|
exit;
|
||||||
@@ -21,6 +29,12 @@ $device = $sql->single(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!$device) {
|
if (!$device) {
|
||||||
|
if ($isPost) {
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Geraet nicht gefunden']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
$_SESSION['error'] = "Geraet nicht gefunden";
|
$_SESSION['error'] = "Geraet nicht gefunden";
|
||||||
header('Location: ?module=devices&action=list');
|
header('Location: ?module=devices&action=list');
|
||||||
exit;
|
exit;
|
||||||
@@ -42,10 +56,10 @@ $dependencies = $sql->single(
|
|||||||
(
|
(
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM connections c
|
FROM connections c
|
||||||
WHERE (c.port_a_type = 'device' AND c.port_a_id IN (
|
WHERE ((c.port_a_type = 'device' OR c.port_a_type = 'device_ports') AND c.port_a_id IN (
|
||||||
SELECT dp3.id FROM device_ports dp3 WHERE dp3.device_id = ?
|
SELECT dp3.id FROM device_ports dp3 WHERE dp3.device_id = ?
|
||||||
))
|
))
|
||||||
OR (c.port_b_type = 'device' AND c.port_b_id IN (
|
OR ((c.port_b_type = 'device' OR c.port_b_type = 'device_ports') AND c.port_b_id IN (
|
||||||
SELECT dp4.id FROM device_ports dp4 WHERE dp4.device_id = ?
|
SELECT dp4.id FROM device_ports dp4 WHERE dp4.device_id = ?
|
||||||
))
|
))
|
||||||
) AS connection_count",
|
) AS connection_count",
|
||||||
@@ -70,7 +84,23 @@ if ($hasDependencies && !$forceDelete) {
|
|||||||
$parts[] = $moduleCount . ' Port-Module';
|
$parts[] = $moduleCount . ' Port-Module';
|
||||||
}
|
}
|
||||||
|
|
||||||
$_SESSION['error'] = "Geraet hat abhaengige Daten (" . implode(', ', $parts) . "). Loeschen bitte bestaetigen.";
|
$dependencyMessage = "Geraet hat abhaengige Daten (" . implode(', ', $parts) . "). Loeschen bitte bestaetigen.";
|
||||||
|
if ($isPost) {
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
http_response_code(409);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'requires_force' => true,
|
||||||
|
'message' => $dependencyMessage,
|
||||||
|
'dependencies' => [
|
||||||
|
'connections' => $connectionCount,
|
||||||
|
'ports' => $portCount,
|
||||||
|
'modules' => $moduleCount
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$_SESSION['error'] = $dependencyMessage;
|
||||||
header('Location: ?module=devices&action=edit&id=' . urlencode((string)$deviceId));
|
header('Location: ?module=devices&action=edit&id=' . urlencode((string)$deviceId));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -78,8 +108,8 @@ if ($hasDependencies && !$forceDelete) {
|
|||||||
// Connections referenzieren device_ports nur logisch, daher manuell entfernen.
|
// Connections referenzieren device_ports nur logisch, daher manuell entfernen.
|
||||||
$sql->set(
|
$sql->set(
|
||||||
"DELETE FROM connections
|
"DELETE FROM connections
|
||||||
WHERE (port_a_type = 'device' AND port_a_id IN (SELECT id FROM device_ports WHERE device_id = ?))
|
WHERE ((port_a_type = 'device' OR port_a_type = 'device_ports') AND port_a_id IN (SELECT id FROM device_ports WHERE device_id = ?))
|
||||||
OR (port_b_type = 'device' AND port_b_id IN (SELECT id FROM device_ports WHERE device_id = ?))",
|
OR ((port_b_type = 'device' OR port_b_type = 'device_ports') AND port_b_id IN (SELECT id FROM device_ports WHERE device_id = ?))",
|
||||||
"ii",
|
"ii",
|
||||||
[$deviceId, $deviceId]
|
[$deviceId, $deviceId]
|
||||||
);
|
);
|
||||||
@@ -91,8 +121,19 @@ $deleted = $sql->set(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if ($deleted > 0) {
|
if ($deleted > 0) {
|
||||||
|
if ($isPost) {
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
echo json_encode(['success' => true, 'message' => "Geraet geloescht: " . $device['name']]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
$_SESSION['success'] = "Geraet geloescht: " . $device['name'];
|
$_SESSION['success'] = "Geraet geloescht: " . $device['name'];
|
||||||
} else {
|
} else {
|
||||||
|
if ($isPost) {
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Geraet konnte nicht geloescht werden']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
$_SESSION['error'] = "Geraet konnte nicht geloescht werden";
|
$_SESSION['error'] = "Geraet konnte nicht geloescht werden";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,10 +47,10 @@ if ($isEdit) {
|
|||||||
(
|
(
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM connections c
|
FROM connections c
|
||||||
WHERE (c.port_a_type = 'device' AND c.port_a_id IN (
|
WHERE ((c.port_a_type = 'device' OR c.port_a_type = 'device_ports') AND c.port_a_id IN (
|
||||||
SELECT dp3.id FROM device_ports dp3 WHERE dp3.device_id = ?
|
SELECT dp3.id FROM device_ports dp3 WHERE dp3.device_id = ?
|
||||||
))
|
))
|
||||||
OR (c.port_b_type = 'device' AND c.port_b_id IN (
|
OR ((c.port_b_type = 'device' OR c.port_b_type = 'device_ports') AND c.port_b_id IN (
|
||||||
SELECT dp4.id FROM device_ports dp4 WHERE dp4.device_id = ?
|
SELECT dp4.id FROM device_ports dp4 WHERE dp4.device_id = ?
|
||||||
))
|
))
|
||||||
) AS connection_count",
|
) AS connection_count",
|
||||||
@@ -64,6 +64,19 @@ if ($isEdit) {
|
|||||||
// =========================
|
// =========================
|
||||||
$deviceTypes = $sql->get("SELECT id, name, category FROM device_types ORDER BY name", "", []);
|
$deviceTypes = $sql->get("SELECT id, name, category FROM device_types ORDER BY name", "", []);
|
||||||
$racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
|
$racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
|
||||||
|
$devicePorts = [];
|
||||||
|
|
||||||
|
if ($isEdit) {
|
||||||
|
$devicePorts = $sql->get(
|
||||||
|
"SELECT dp.id, dp.name, dp.status, dp.mode, dp.vlan_config, pt.name AS port_type_name
|
||||||
|
FROM device_ports dp
|
||||||
|
LEFT JOIN port_types pt ON pt.id = dp.port_type_id
|
||||||
|
WHERE dp.device_id = ?
|
||||||
|
ORDER BY dp.id",
|
||||||
|
"i",
|
||||||
|
[$deviceId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
@@ -160,6 +173,67 @@ $racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Ports</legend>
|
||||||
|
<?php if ($isEdit): ?>
|
||||||
|
<?php if (!empty($devicePorts)): ?>
|
||||||
|
<p><small>Portstatus und VLAN-Zuordnung koennen hier direkt gepflegt werden (VLANs kommagetrennt, z. B. 10,20,30).</small></p>
|
||||||
|
<table class="device-port-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Port-Typ</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Modus</th>
|
||||||
|
<th>VLANs</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($devicePorts as $port): ?>
|
||||||
|
<?php
|
||||||
|
$vlanValue = '';
|
||||||
|
if (!empty($port['vlan_config'])) {
|
||||||
|
$decodedVlans = json_decode((string)$port['vlan_config'], true);
|
||||||
|
if (is_array($decodedVlans)) {
|
||||||
|
$vlanValue = implode(', ', array_map('strval', $decodedVlans));
|
||||||
|
} else {
|
||||||
|
$vlanValue = (string)$port['vlan_config'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$statusValue = (string)($port['status'] ?? 'active');
|
||||||
|
if (!in_array($statusValue, ['active', 'disabled'], true)) {
|
||||||
|
$statusValue = 'active';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="device_ports[<?php echo (int)$port['id']; ?>][name]" value="<?php echo htmlspecialchars((string)$port['name']); ?>">
|
||||||
|
</td>
|
||||||
|
<td><?php echo htmlspecialchars((string)($port['port_type_name'] ?? '-')); ?></td>
|
||||||
|
<td>
|
||||||
|
<select name="device_ports[<?php echo (int)$port['id']; ?>][status]">
|
||||||
|
<option value="active" <?php echo $statusValue === 'active' ? 'selected' : ''; ?>>aktiv</option>
|
||||||
|
<option value="disabled" <?php echo $statusValue === 'disabled' ? 'selected' : ''; ?>>inaktiv</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="device_ports[<?php echo (int)$port['id']; ?>][mode]" value="<?php echo htmlspecialchars((string)($port['mode'] ?? '')); ?>" placeholder="z. B. access/trunk">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="device_ports[<?php echo (int)$port['id']; ?>][vlan_config]" value="<?php echo htmlspecialchars($vlanValue); ?>" placeholder="10,20,30">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php else: ?>
|
||||||
|
<p><small>Zu diesem Geraet sind aktuell keine Ports vorhanden.</small></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<p><small>Ports werden nach dem ersten Speichern automatisch aus dem Geraetetyp erzeugt und koennen dann hier gepflegt werden.</small></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<!-- =========================
|
<!-- =========================
|
||||||
Aktionen
|
Aktionen
|
||||||
========================= -->
|
========================= -->
|
||||||
@@ -264,34 +338,64 @@ $racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
|
|||||||
.button:hover {
|
.button:hover {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.device-port-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-port-table th,
|
||||||
|
.device-port-table td {
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
padding: 8px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-port-table input,
|
||||||
|
.device-port-table select {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function confirmDelete(link, id, connectionCount, portCount, moduleCount) {
|
function confirmDelete(link, id, connectionCount, portCount, moduleCount) {
|
||||||
if (confirm('Dieses Gerät wirklich löschen?')) {
|
if (!confirm('Dieses Geraet wirklich loeschen?')) {
|
||||||
const hasDependencies = (connectionCount > 0) || (portCount > 0) || (moduleCount > 0);
|
|
||||||
if (hasDependencies) {
|
|
||||||
const details = [];
|
|
||||||
if (connectionCount > 0) {
|
|
||||||
details.push(connectionCount + ' Verbindungen');
|
|
||||||
}
|
|
||||||
if (portCount > 0) {
|
|
||||||
details.push(portCount + ' Ports');
|
|
||||||
}
|
|
||||||
if (moduleCount > 0) {
|
|
||||||
details.push(moduleCount + ' Port-Module');
|
|
||||||
}
|
|
||||||
const dependencyMessage = 'Es gibt abhängige Daten (' + details.join(', ') + '). Diese auch löschen?';
|
|
||||||
if (!confirm(dependencyMessage)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
window.location.href = (link && link.href ? link.href : ('?module=devices&action=delete&id=' + encodeURIComponent(id))) + '&force=1';
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
const requestDelete = (forceDelete) => {
|
||||||
|
const body = ['id=' + encodeURIComponent(id)];
|
||||||
|
if (forceDelete) {
|
||||||
|
body.push('force=1');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fetch('?module=devices&action=delete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||||
|
body: body.join('&')
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data && data.success) {
|
||||||
|
window.location.href = '?module=devices&action=list';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data && data.requires_force) {
|
||||||
|
if (confirm(data.message || 'Es gibt abhaengige Daten. Trotzdem loeschen?')) {
|
||||||
|
requestDelete(true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
|
||||||
|
})
|
||||||
|
.catch(() => alert('Loeschen fehlgeschlagen'));
|
||||||
|
};
|
||||||
|
|
||||||
|
requestDelete(false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* modules/devices/list.php
|
* modules/devices/list.php
|
||||||
* Vollständige Geräteübersicht mit Filter
|
* Vollstaendige Geraeteuebersicht mit Filter
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
@@ -22,34 +22,34 @@ $params = [];
|
|||||||
|
|
||||||
if ($search !== '') {
|
if ($search !== '') {
|
||||||
$where[] = "(d.name LIKE ? OR d.serial_number LIKE ? OR dt.name LIKE ?)";
|
$where[] = "(d.name LIKE ? OR d.serial_number LIKE ? OR dt.name LIKE ?)";
|
||||||
$types .= "sss";
|
$types .= 'sss';
|
||||||
$params[] = "%$search%";
|
$params[] = "%$search%";
|
||||||
$params[] = "%$search%";
|
$params[] = "%$search%";
|
||||||
$params[] = "%$search%";
|
$params[] = "%$search%";
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($typeId > 0) {
|
if ($typeId > 0) {
|
||||||
$where[] = "d.device_type_id = ?";
|
$where[] = 'd.device_type_id = ?';
|
||||||
$types .= "i";
|
$types .= 'i';
|
||||||
$params[] = $typeId;
|
$params[] = $typeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($floorId > 0) {
|
if ($floorId > 0) {
|
||||||
$where[] = "f.id = ?";
|
$where[] = 'f.id = ?';
|
||||||
$types .= "i";
|
$types .= 'i';
|
||||||
$params[] = $floorId;
|
$params[] = $floorId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($rackId > 0) {
|
if ($rackId > 0) {
|
||||||
$where[] = "d.rack_id = ?";
|
$where[] = 'd.rack_id = ?';
|
||||||
$types .= "i";
|
$types .= 'i';
|
||||||
$params[] = $rackId;
|
$params[] = $rackId;
|
||||||
}
|
}
|
||||||
|
|
||||||
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// Geräte laden
|
// Geraete laden
|
||||||
// =========================
|
// =========================
|
||||||
$devices = $sql->get(
|
$devices = $sql->get(
|
||||||
"
|
"
|
||||||
@@ -69,6 +69,25 @@ $whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
|||||||
FROM device_ports dp
|
FROM device_ports dp
|
||||||
WHERE dp.device_id = d.id
|
WHERE dp.device_id = d.id
|
||||||
) AS port_count,
|
) AS port_count,
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM device_ports dp
|
||||||
|
WHERE dp.device_id = d.id
|
||||||
|
AND dp.status = 'active'
|
||||||
|
) AS active_port_count,
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM device_ports dp
|
||||||
|
WHERE dp.device_id = d.id
|
||||||
|
AND dp.status = 'disabled'
|
||||||
|
) AS disabled_port_count,
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM device_ports dp
|
||||||
|
WHERE dp.device_id = d.id
|
||||||
|
AND dp.vlan_config IS NOT NULL
|
||||||
|
AND dp.vlan_config <> '[]'
|
||||||
|
) AS vlan_port_count,
|
||||||
(
|
(
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM device_port_modules dpm
|
FROM device_port_modules dpm
|
||||||
@@ -78,10 +97,10 @@ $whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
|||||||
(
|
(
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM connections c
|
FROM connections c
|
||||||
WHERE (c.port_a_type = 'device' AND c.port_a_id IN (
|
WHERE ((c.port_a_type = 'device' OR c.port_a_type = 'device_ports') AND c.port_a_id IN (
|
||||||
SELECT dp3.id FROM device_ports dp3 WHERE dp3.device_id = d.id
|
SELECT dp3.id FROM device_ports dp3 WHERE dp3.device_id = d.id
|
||||||
))
|
))
|
||||||
OR (c.port_b_type = 'device' AND c.port_b_id IN (
|
OR ((c.port_b_type = 'device' OR c.port_b_type = 'device_ports') AND c.port_b_id IN (
|
||||||
SELECT dp4.id FROM device_ports dp4 WHERE dp4.device_id = d.id
|
SELECT dp4.id FROM device_ports dp4 WHERE dp4.device_id = d.id
|
||||||
))
|
))
|
||||||
) AS connection_count
|
) AS connection_count
|
||||||
@@ -99,22 +118,19 @@ $whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
|||||||
// =========================
|
// =========================
|
||||||
// Filter-Daten laden
|
// Filter-Daten laden
|
||||||
// =========================
|
// =========================
|
||||||
$deviceTypes = $sql->get("SELECT id, name FROM device_types ORDER BY name", "", []);
|
$deviceTypes = $sql->get('SELECT id, name FROM device_types ORDER BY name', '', []);
|
||||||
$floors = $sql->get("SELECT id, name FROM floors ORDER BY name", "", []);
|
$floors = $sql->get('SELECT id, name FROM floors ORDER BY name', '', []);
|
||||||
$racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
|
$racks = $sql->get('SELECT id, name FROM racks ORDER BY name', '', []);
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="devices-container">
|
<div class="devices-container">
|
||||||
<h1>Geräte</h1>
|
<h1>Geraete</h1>
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
Filter-Toolbar
|
|
||||||
========================= -->
|
|
||||||
<form method="get" class="filter-form">
|
<form method="get" class="filter-form">
|
||||||
<input type="hidden" name="module" value="devices">
|
<input type="hidden" name="module" value="devices">
|
||||||
<input type="hidden" name="action" value="list">
|
<input type="hidden" name="action" value="list">
|
||||||
|
|
||||||
<input type="text" name="search" placeholder="Suche nach Name oder Seriennummer…"
|
<input type="text" name="search" placeholder="Suche nach Name oder Seriennummer..."
|
||||||
value="<?php echo htmlspecialchars($search); ?>" class="search-input">
|
value="<?php echo htmlspecialchars($search); ?>" class="search-input">
|
||||||
|
|
||||||
<select name="type_id">
|
<select name="type_id">
|
||||||
@@ -149,16 +165,13 @@ $racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
|
|||||||
<a href="?module=devices&action=list" class="button">Reset</a>
|
<a href="?module=devices&action=list" class="button">Reset</a>
|
||||||
|
|
||||||
<a href="?module=devices&action=edit" class="button button-primary" style="margin-left: auto;">
|
<a href="?module=devices&action=edit" class="button button-primary" style="margin-left: auto;">
|
||||||
+ Neues Gerät
|
+ Neues Geraet
|
||||||
</a>
|
</a>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
Geräte-Liste
|
|
||||||
========================= -->
|
|
||||||
<?php if (!empty($devices)): ?>
|
<?php if (!empty($devices)): ?>
|
||||||
<div class="device-stats">
|
<div class="device-stats">
|
||||||
<p>Gefundene Geräte: <strong><?php echo count($devices); ?></strong></p>
|
<p>Gefundene Geraete: <strong><?php echo count($devices); ?></strong></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="device-list">
|
<table class="device-list">
|
||||||
@@ -169,6 +182,7 @@ $racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
|
|||||||
<th>Stockwerk</th>
|
<th>Stockwerk</th>
|
||||||
<th>Rack</th>
|
<th>Rack</th>
|
||||||
<th>Position (HE)</th>
|
<th>Position (HE)</th>
|
||||||
|
<th>Ports</th>
|
||||||
<th>Seriennummer</th>
|
<th>Seriennummer</th>
|
||||||
<th>Webconfig</th>
|
<th>Webconfig</th>
|
||||||
<th>Aktionen</th>
|
<th>Aktionen</th>
|
||||||
@@ -186,28 +200,37 @@ $racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<?php echo htmlspecialchars($d['floor_name'] ?? '—'); ?>
|
<?php echo htmlspecialchars($d['floor_name'] ?? '-'); ?>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<?php echo htmlspecialchars($d['rack_name'] ?? '—'); ?>
|
<?php echo htmlspecialchars($d['rack_name'] ?? '-'); ?>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<?php
|
<?php
|
||||||
if ($d['rack_position_he']) {
|
if ($d['rack_position_he']) {
|
||||||
echo $d['rack_position_he'];
|
echo (int)$d['rack_position_he'];
|
||||||
if ($d['rack_height_he']) {
|
if ($d['rack_height_he']) {
|
||||||
echo "–" . ($d['rack_position_he'] + $d['rack_height_he'] - 1);
|
echo '-' . ((int)$d['rack_position_he'] + (int)$d['rack_height_he'] - 1);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
echo "—";
|
echo '-';
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<small><?php echo htmlspecialchars($d['serial_number'] ?? '—'); ?></small>
|
<small>
|
||||||
|
<?php echo (int)$d['port_count']; ?> gesamt<br>
|
||||||
|
<?php echo (int)$d['active_port_count']; ?> aktiv /
|
||||||
|
<?php echo (int)$d['disabled_port_count']; ?> inaktiv<br>
|
||||||
|
VLAN gesetzt: <?php echo (int)$d['vlan_port_count']; ?>
|
||||||
|
</small>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<small><?php echo htmlspecialchars($d['serial_number'] ?? '-'); ?></small>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
@@ -216,13 +239,13 @@ $racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
|
|||||||
Webconfig
|
Webconfig
|
||||||
</a>
|
</a>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
—
|
-
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<a href="?module=devices&action=edit&id=<?php echo $d['id']; ?>" class="button button-small">Bearbeiten</a>
|
<a href="?module=devices&action=edit&id=<?php echo $d['id']; ?>" class="button button-small">Bearbeiten</a>
|
||||||
<a href="?module=devices&action=delete&id=<?php echo (int)$d['id']; ?>" class="button button-small button-danger" onclick="return confirmDelete(this, <?php echo (int)$d['id']; ?>, <?php echo (int)$d['connection_count']; ?>, <?php echo (int)$d['port_count']; ?>, <?php echo (int)$d['module_count']; ?>)">Löschen</a>
|
<a href="?module=devices&action=delete&id=<?php echo (int)$d['id']; ?>" class="button button-small button-danger" onclick="return confirmDelete(this, <?php echo (int)$d['id']; ?>, <?php echo (int)$d['connection_count']; ?>, <?php echo (int)$d['port_count']; ?>, <?php echo (int)$d['module_count']; ?>)">Loeschen</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -231,10 +254,10 @@ $racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
|
|||||||
|
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<p>Keine Geräte gefunden.</p>
|
<p>Keine Geraete gefunden.</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="?module=devices&action=edit" class="button button-primary">
|
<a href="?module=devices&action=edit" class="button button-primary">
|
||||||
Erstes Gerät anlegen
|
Erstes Geraet anlegen
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -351,33 +374,41 @@ $racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
function confirmDelete(link, id, connectionCount, portCount, moduleCount) {
|
function confirmDelete(link, id, connectionCount, portCount, moduleCount) {
|
||||||
if (confirm('Dieses Gerät wirklich löschen?')) {
|
if (!confirm('Dieses Geraet wirklich loeschen?')) {
|
||||||
const hasDependencies = (connectionCount > 0) || (portCount > 0) || (moduleCount > 0);
|
|
||||||
if (hasDependencies) {
|
|
||||||
const details = [];
|
|
||||||
if (connectionCount > 0) {
|
|
||||||
details.push(connectionCount + ' Verbindungen');
|
|
||||||
}
|
|
||||||
if (portCount > 0) {
|
|
||||||
details.push(portCount + ' Ports');
|
|
||||||
}
|
|
||||||
if (moduleCount > 0) {
|
|
||||||
details.push(moduleCount + ' Port-Module');
|
|
||||||
}
|
|
||||||
const dependencyMessage = 'Es gibt abhängige Daten (' + details.join(', ') + '). Diese auch löschen?';
|
|
||||||
if (!confirm(dependencyMessage)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
window.location.href = (link && link.href ? link.href : ('?module=devices&action=delete&id=' + encodeURIComponent(id))) + '&force=1';
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
const requestDelete = (forceDelete) => {
|
||||||
|
const body = ['id=' + encodeURIComponent(id)];
|
||||||
|
if (forceDelete) {
|
||||||
|
body.push('force=1');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fetch('?module=devices&action=delete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||||
|
body: body.join('&')
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data && data.success) {
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data && data.requires_force) {
|
||||||
|
if (confirm(data.message || 'Es gibt abhaengige Daten. Trotzdem loeschen?')) {
|
||||||
|
requestDelete(true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
|
||||||
|
})
|
||||||
|
.catch(() => alert('Loeschen fehlgeschlagen'));
|
||||||
|
};
|
||||||
|
|
||||||
|
requestDelete(false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ $rackHeightHe = (int)($_POST['rack_height_he'] ?? 1);
|
|||||||
$serialNumber = trim($_POST['serial_number'] ?? '');
|
$serialNumber = trim($_POST['serial_number'] ?? '');
|
||||||
$comment = trim($_POST['comment'] ?? '');
|
$comment = trim($_POST['comment'] ?? '');
|
||||||
$webConfigUrl = trim($_POST['web_config_url'] ?? '');
|
$webConfigUrl = trim($_POST['web_config_url'] ?? '');
|
||||||
|
$devicePortRows = is_array($_POST['device_ports'] ?? null) ? $_POST['device_ports'] : [];
|
||||||
if ($webConfigUrl === '') {
|
if ($webConfigUrl === '') {
|
||||||
$webConfigUrl = null;
|
$webConfigUrl = null;
|
||||||
}
|
}
|
||||||
@@ -57,6 +58,7 @@ if ($rackHeightHe < 1) {
|
|||||||
// Falls Fehler: zurück zum Edit-Formular
|
// Falls Fehler: zurück zum Edit-Formular
|
||||||
if (!empty($errors)) {
|
if (!empty($errors)) {
|
||||||
$_SESSION['error'] = implode(', ', $errors);
|
$_SESSION['error'] = implode(', ', $errors);
|
||||||
|
$_SESSION['validation_errors'] = $errors;
|
||||||
$redirectUrl = $deviceId ? "?module=devices&action=edit&id=$deviceId" : "?module=devices&action=edit";
|
$redirectUrl = $deviceId ? "?module=devices&action=edit&id=$deviceId" : "?module=devices&action=edit";
|
||||||
header("Location: $redirectUrl");
|
header("Location: $redirectUrl");
|
||||||
exit;
|
exit;
|
||||||
@@ -87,6 +89,9 @@ if ($isNewDevice) {
|
|||||||
if ($isNewDevice && $deviceId > 0) {
|
if ($isNewDevice && $deviceId > 0) {
|
||||||
copyDevicePortsFromType($sql, $deviceId, $deviceTypeId);
|
copyDevicePortsFromType($sql, $deviceId, $deviceTypeId);
|
||||||
}
|
}
|
||||||
|
if ($deviceId > 0 && !$isNewDevice && !empty($devicePortRows)) {
|
||||||
|
syncDevicePorts($sql, $deviceId, $devicePortRows);
|
||||||
|
}
|
||||||
|
|
||||||
$_SESSION['success'] = "Gerät gespeichert";
|
$_SESSION['success'] = "Gerät gespeichert";
|
||||||
|
|
||||||
@@ -138,3 +143,105 @@ function copyDevicePortsFromType($sql, $deviceId, $deviceTypeId)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncDevicePorts($sql, $deviceId, array $rows)
|
||||||
|
{
|
||||||
|
$ports = $sql->get(
|
||||||
|
"SELECT id, name FROM device_ports WHERE device_id = ?",
|
||||||
|
"i",
|
||||||
|
[$deviceId]
|
||||||
|
);
|
||||||
|
if (empty($ports)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowedIds = [];
|
||||||
|
$currentNames = [];
|
||||||
|
foreach ($ports as $port) {
|
||||||
|
$portId = (int)($port['id'] ?? 0);
|
||||||
|
if ($portId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$allowedIds[$portId] = true;
|
||||||
|
$currentNames[$portId] = (string)($port['name'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($rows as $portIdRaw => $row) {
|
||||||
|
$portId = (int)$portIdRaw;
|
||||||
|
if ($portId <= 0 || !isset($allowedIds[$portId]) || !is_array($row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = trim((string)($row['name'] ?? ''));
|
||||||
|
if ($name === '') {
|
||||||
|
$name = $currentNames[$portId] ?? ('Port ' . $portId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = trim((string)($row['status'] ?? 'active'));
|
||||||
|
if (!in_array($status, ['active', 'disabled'], true)) {
|
||||||
|
$status = 'active';
|
||||||
|
}
|
||||||
|
|
||||||
|
$mode = trim((string)($row['mode'] ?? ''));
|
||||||
|
$mode = $mode !== '' ? $mode : null;
|
||||||
|
$vlanJson = normalizeVlanConfig($row['vlan_config'] ?? '');
|
||||||
|
|
||||||
|
if ($mode !== null && $vlanJson !== null) {
|
||||||
|
$sql->set(
|
||||||
|
"UPDATE device_ports
|
||||||
|
SET name = ?, status = ?, mode = ?, vlan_config = ?
|
||||||
|
WHERE id = ? AND device_id = ?",
|
||||||
|
"ssssii",
|
||||||
|
[$name, $status, $mode, $vlanJson, $portId, $deviceId]
|
||||||
|
);
|
||||||
|
} elseif ($mode !== null) {
|
||||||
|
$sql->set(
|
||||||
|
"UPDATE device_ports
|
||||||
|
SET name = ?, status = ?, mode = ?, vlan_config = NULL
|
||||||
|
WHERE id = ? AND device_id = ?",
|
||||||
|
"sssii",
|
||||||
|
[$name, $status, $mode, $portId, $deviceId]
|
||||||
|
);
|
||||||
|
} elseif ($vlanJson !== null) {
|
||||||
|
$sql->set(
|
||||||
|
"UPDATE device_ports
|
||||||
|
SET name = ?, status = ?, mode = NULL, vlan_config = ?
|
||||||
|
WHERE id = ? AND device_id = ?",
|
||||||
|
"sssii",
|
||||||
|
[$name, $status, $vlanJson, $portId, $deviceId]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$sql->set(
|
||||||
|
"UPDATE device_ports
|
||||||
|
SET name = ?, status = ?, mode = NULL, vlan_config = NULL
|
||||||
|
WHERE id = ? AND device_id = ?",
|
||||||
|
"ssii",
|
||||||
|
[$name, $status, $portId, $deviceId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeVlanConfig($raw)
|
||||||
|
{
|
||||||
|
$value = trim((string)$raw);
|
||||||
|
if ($value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = preg_split('/[\s,;]+/', $value);
|
||||||
|
$normalized = [];
|
||||||
|
foreach ((array)$parts as $part) {
|
||||||
|
$entry = trim((string)$part);
|
||||||
|
if ($entry === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$normalized[$entry] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($normalized)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return json_encode(array_keys($normalized), JSON_UNESCAPED_UNICODE);
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,30 +2,48 @@
|
|||||||
/**
|
/**
|
||||||
* app/modules/floor_infrastructure/delete.php
|
* app/modules/floor_infrastructure/delete.php
|
||||||
*
|
*
|
||||||
* Loescht Patchpanels oder Wandbuchsen.
|
* Loescht Patchpanels oder Wandbuchsen (AJAX-POST bevorzugt).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$type = strtolower(trim((string)($_GET['type'] ?? '')));
|
$isPost = ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST';
|
||||||
$id = (int)($_GET['id'] ?? 0);
|
$type = strtolower(trim((string)($_POST['type'] ?? $_GET['type'] ?? '')));
|
||||||
|
$id = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
|
||||||
|
|
||||||
if ($id <= 0 || !in_array($type, ['patchpanel', 'outlet'], true)) {
|
if ($id <= 0 || !in_array($type, ['patchpanel', 'outlet'], true)) {
|
||||||
|
if ($isPost) {
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Ungueltige Anfrage']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
header('Location: ?module=floor_infrastructure&action=list');
|
header('Location: ?module=floor_infrastructure&action=list');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($type === 'patchpanel') {
|
if ($type === 'patchpanel') {
|
||||||
$sql->set(
|
$rows = $sql->set(
|
||||||
"DELETE FROM floor_patchpanels WHERE id = ?",
|
"DELETE FROM floor_patchpanels WHERE id = ?",
|
||||||
"i",
|
"i",
|
||||||
[$id]
|
[$id]
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$sql->set(
|
$rows = $sql->set(
|
||||||
"DELETE FROM network_outlets WHERE id = ?",
|
"DELETE FROM network_outlets WHERE id = ?",
|
||||||
"i",
|
"i",
|
||||||
[$id]
|
[$id]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($isPost) {
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
if ($rows > 0) {
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Infrastrukturobjekt geloescht']);
|
||||||
|
} else {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Infrastrukturobjekt nicht gefunden oder bereits geloescht']);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
header('Location: ?module=floor_infrastructure&action=list');
|
header('Location: ?module=floor_infrastructure&action=list');
|
||||||
exit;
|
exit;
|
||||||
|
|||||||
@@ -125,6 +125,58 @@ $mapOutlets = $sql->get(
|
|||||||
"",
|
"",
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$patchpanelPortOptions = $sql->get(
|
||||||
|
"SELECT
|
||||||
|
fpp.id,
|
||||||
|
fpp.name,
|
||||||
|
fp.name AS patchpanel_name,
|
||||||
|
fp.floor_id,
|
||||||
|
f.name AS floor_name,
|
||||||
|
EXISTS(
|
||||||
|
SELECT 1
|
||||||
|
FROM connections c
|
||||||
|
WHERE
|
||||||
|
((c.port_a_type = 'patchpanel' OR c.port_a_type = 'floor_patchpanel_ports') AND c.port_a_id = fpp.id)
|
||||||
|
OR
|
||||||
|
((c.port_b_type = 'patchpanel' OR c.port_b_type = 'floor_patchpanel_ports') AND c.port_b_id = fpp.id)
|
||||||
|
) AS is_occupied
|
||||||
|
FROM floor_patchpanel_ports fpp
|
||||||
|
JOIN floor_patchpanels fp ON fp.id = fpp.patchpanel_id
|
||||||
|
LEFT JOIN floors f ON f.id = fp.floor_id
|
||||||
|
ORDER BY f.name, fp.name, fpp.name",
|
||||||
|
"",
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
$selectedBindPatchpanelPortId = 0;
|
||||||
|
if ($type === 'outlet' && $id > 0) {
|
||||||
|
$selectedBindPatchpanelPortId = (int)($sql->single(
|
||||||
|
"SELECT
|
||||||
|
CASE
|
||||||
|
WHEN (c.port_a_type = 'patchpanel' OR c.port_a_type = 'floor_patchpanel_ports') THEN c.port_a_id
|
||||||
|
WHEN (c.port_b_type = 'patchpanel' OR c.port_b_type = 'floor_patchpanel_ports') THEN c.port_b_id
|
||||||
|
ELSE 0
|
||||||
|
END AS patchpanel_port_id
|
||||||
|
FROM connections c
|
||||||
|
JOIN network_outlet_ports nop
|
||||||
|
ON (
|
||||||
|
((c.port_a_type = 'outlet' OR c.port_a_type = 'network_outlet_ports') AND c.port_a_id = nop.id)
|
||||||
|
OR
|
||||||
|
((c.port_b_type = 'outlet' OR c.port_b_type = 'network_outlet_ports') AND c.port_b_id = nop.id)
|
||||||
|
)
|
||||||
|
WHERE nop.outlet_id = ?
|
||||||
|
AND (
|
||||||
|
c.port_a_type = 'patchpanel' OR c.port_a_type = 'floor_patchpanel_ports'
|
||||||
|
OR
|
||||||
|
c.port_b_type = 'patchpanel' OR c.port_b_type = 'floor_patchpanel_ports'
|
||||||
|
)
|
||||||
|
ORDER BY c.id
|
||||||
|
LIMIT 1",
|
||||||
|
"i",
|
||||||
|
[$id]
|
||||||
|
)['patchpanel_port_id'] ?? 0);
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="floor-infra-edit">
|
<div class="floor-infra-edit">
|
||||||
@@ -205,10 +257,9 @@ $mapOutlets = $sql->get(
|
|||||||
data-active-id="<?php echo (int)($panel['id'] ?? 0); ?>"
|
data-active-id="<?php echo (int)($panel['id'] ?? 0); ?>"
|
||||||
data-reference-panels="<?php echo htmlspecialchars(json_encode($mapPatchPanels, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ENT_QUOTES, 'UTF-8'); ?>"
|
data-reference-panels="<?php echo htmlspecialchars(json_encode($mapPatchPanels, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ENT_QUOTES, 'UTF-8'); ?>"
|
||||||
data-reference-outlets="<?php echo htmlspecialchars(json_encode($mapOutlets, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ENT_QUOTES, 'UTF-8'); ?>">
|
data-reference-outlets="<?php echo htmlspecialchars(json_encode($mapOutlets, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ENT_QUOTES, 'UTF-8'); ?>">
|
||||||
<img id="floor-plan-svg" class="floor-plan-svg" alt="Stockwerksplan">
|
|
||||||
<svg id="floor-plan-overlay" class="floor-plan-overlay" viewBox="0 0 1 1" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
|
<svg id="floor-plan-overlay" class="floor-plan-overlay" viewBox="0 0 1 1" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
|
||||||
</div>
|
</div>
|
||||||
<p class="floor-plan-hint">Nur das aktuell bearbeitete Patchpanel ist verschiebbar. Andere Objekte werden als Referenz halbtransparent angezeigt. Neue Objekte starten bei Position 30 x 30.</p>
|
<p class="floor-plan-hint">Nur das aktuell bearbeitete Patchpanel ist verschiebbar. Andere Objekte werden als Referenz halbtransparent angezeigt. Neue Objekte starten bei Position 30 x 30. Zoom mit Mausrad, verschieben mit Shift + Drag.</p>
|
||||||
<p class="floor-plan-position">Koordinate: <span id="floor-plan-position"></span></p>
|
<p class="floor-plan-position">Koordinate: <span id="floor-plan-position"></span></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -255,6 +306,38 @@ $mapOutlets = $sql->get(
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="outlet-bind-patchpanel-port-id">Direkt mit Patchpanel-Port verbinden</label>
|
||||||
|
<select name="bind_patchpanel_port_id" id="outlet-bind-patchpanel-port-id">
|
||||||
|
<option value="">- Kein direkter Link -</option>
|
||||||
|
<?php foreach ($patchpanelPortOptions as $portOption): ?>
|
||||||
|
<?php
|
||||||
|
$portId = (int)($portOption['id'] ?? 0);
|
||||||
|
$isSelected = $selectedBindPatchpanelPortId === $portId;
|
||||||
|
$isOccupied = ((int)($portOption['is_occupied'] ?? 0) === 1);
|
||||||
|
$isDisabled = $isOccupied && !$isSelected;
|
||||||
|
$labelParts = array_filter([
|
||||||
|
(string)($portOption['floor_name'] ?? ''),
|
||||||
|
(string)($portOption['patchpanel_name'] ?? ''),
|
||||||
|
(string)($portOption['name'] ?? ''),
|
||||||
|
]);
|
||||||
|
$label = implode(' / ', $labelParts);
|
||||||
|
if ($isOccupied && !$isSelected) {
|
||||||
|
$label .= ' (belegt)';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<option
|
||||||
|
value="<?php echo $portId; ?>"
|
||||||
|
data-floor-id="<?php echo (int)($portOption['floor_id'] ?? 0); ?>"
|
||||||
|
<?php echo $isSelected ? 'selected' : ''; ?>
|
||||||
|
<?php echo $isDisabled ? 'disabled' : ''; ?>>
|
||||||
|
<?php echo htmlspecialchars($label); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<small>Nur Ports vom gewaehlten Stockwerk sind auswaehlbar. Beim Speichern wird die Verbindung automatisch erstellt.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<input type="hidden" name="x" value="<?php echo (int)($outlet['x'] ?? 30); ?>">
|
<input type="hidden" name="x" value="<?php echo (int)($outlet['x'] ?? 30); ?>">
|
||||||
<input type="hidden" name="y" value="<?php echo (int)($outlet['y'] ?? 30); ?>">
|
<input type="hidden" name="y" value="<?php echo (int)($outlet['y'] ?? 30); ?>">
|
||||||
|
|
||||||
@@ -270,10 +353,9 @@ $mapOutlets = $sql->get(
|
|||||||
data-active-id="<?php echo (int)($outlet['id'] ?? 0); ?>"
|
data-active-id="<?php echo (int)($outlet['id'] ?? 0); ?>"
|
||||||
data-reference-panels="<?php echo htmlspecialchars(json_encode($mapPatchPanels, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ENT_QUOTES, 'UTF-8'); ?>"
|
data-reference-panels="<?php echo htmlspecialchars(json_encode($mapPatchPanels, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ENT_QUOTES, 'UTF-8'); ?>"
|
||||||
data-reference-outlets="<?php echo htmlspecialchars(json_encode($mapOutlets, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ENT_QUOTES, 'UTF-8'); ?>">
|
data-reference-outlets="<?php echo htmlspecialchars(json_encode($mapOutlets, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ENT_QUOTES, 'UTF-8'); ?>">
|
||||||
<img id="floor-plan-svg" class="floor-plan-svg" alt="Stockwerksplan">
|
|
||||||
<svg id="floor-plan-overlay" class="floor-plan-overlay" viewBox="0 0 1 1" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
|
<svg id="floor-plan-overlay" class="floor-plan-overlay" viewBox="0 0 1 1" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
|
||||||
</div>
|
</div>
|
||||||
<p class="floor-plan-hint">Nur die aktuell bearbeitete Wandbuchse ist verschiebbar. Blau = Patchpanel, Gruen = Dosen-Referenz, Orange = gewaehlter Raum. Netzwerkdosen sind immer 10 x 10.</p>
|
<p class="floor-plan-hint">Nur die aktuell bearbeitete Wandbuchse ist verschiebbar. Blau = Patchpanel, Gruen = Dosen-Referenz, Orange = gewaehlter Raum. Netzwerkdosen sind immer 10 x 10. Zoom mit Mausrad, verschieben mit Shift + Drag.</p>
|
||||||
<p class="floor-plan-position">Koordinate: <span id="floor-plan-position"></span></p>
|
<p class="floor-plan-position">Koordinate: <span id="floor-plan-position"></span></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -180,7 +180,14 @@ if ($editorFloor) {
|
|||||||
<td><?php echo (int)$panel['port_count']; ?></td>
|
<td><?php echo (int)$panel['port_count']; ?></td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<a href="?module=floor_infrastructure&action=edit&type=patchpanel&id=<?php echo (int)$panel['id']; ?>" class="button button-small">Bearbeiten</a>
|
<a href="?module=floor_infrastructure&action=edit&type=patchpanel&id=<?php echo (int)$panel['id']; ?>" class="button button-small">Bearbeiten</a>
|
||||||
<a href="?module=floor_infrastructure&action=delete&type=patchpanel&id=<?php echo (int)$panel['id']; ?>" class="button button-small button-danger" onclick="return confirm('Patchpanel wirklich loeschen?');">Loeschen</a>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button button-small button-danger js-floor-infra-delete"
|
||||||
|
data-delete-id="<?php echo (int)$panel['id']; ?>"
|
||||||
|
data-delete-type="patchpanel"
|
||||||
|
data-delete-label="<?php echo htmlspecialchars((string)$panel['name'], ENT_QUOTES, 'UTF-8'); ?>">
|
||||||
|
Loeschen
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -226,7 +233,14 @@ if ($editorFloor) {
|
|||||||
<td><?php echo htmlspecialchars((string)($outlet['comment'] ?? '')); ?></td>
|
<td><?php echo htmlspecialchars((string)($outlet['comment'] ?? '')); ?></td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<a href="?module=floor_infrastructure&action=edit&type=outlet&id=<?php echo (int)$outlet['id']; ?>" class="button button-small">Bearbeiten</a>
|
<a href="?module=floor_infrastructure&action=edit&type=outlet&id=<?php echo (int)$outlet['id']; ?>" class="button button-small">Bearbeiten</a>
|
||||||
<a href="?module=floor_infrastructure&action=delete&type=outlet&id=<?php echo (int)$outlet['id']; ?>" class="button button-small button-danger" onclick="return confirm('Wandbuchse wirklich loeschen?');">Loeschen</a>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button button-small button-danger js-floor-infra-delete"
|
||||||
|
data-delete-id="<?php echo (int)$outlet['id']; ?>"
|
||||||
|
data-delete-type="outlet"
|
||||||
|
data-delete-label="<?php echo htmlspecialchars((string)$outlet['name'], ENT_QUOTES, 'UTF-8'); ?>">
|
||||||
|
Loeschen
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -237,3 +251,38 @@ if ($editorFloor) {
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.querySelectorAll('.js-floor-infra-delete').forEach((button) => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
const id = Number(button.dataset.deleteId || '0');
|
||||||
|
const type = (button.dataset.deleteType || '').trim();
|
||||||
|
const label = button.dataset.deleteLabel || 'Objekt';
|
||||||
|
if (id <= 0 || (type !== 'patchpanel' && type !== 'outlet')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entityLabel = type === 'patchpanel' ? 'Patchpanel' : 'Wandbuchse';
|
||||||
|
if (!confirm(entityLabel + ' "' + label + '" wirklich loeschen?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('?module=floor_infrastructure&action=delete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||||
|
body: 'id=' + encodeURIComponent(id) + '&type=' + encodeURIComponent(type)
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data && data.success) {
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
|
||||||
|
})
|
||||||
|
.catch(() => alert('Loeschen fehlgeschlagen'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -20,6 +20,24 @@ if ($type === 'patchpanel') {
|
|||||||
$height = $fixedPanelHeight;
|
$height = $fixedPanelHeight;
|
||||||
$portCount = (int)($_POST['port_count'] ?? 0);
|
$portCount = (int)($_POST['port_count'] ?? 0);
|
||||||
$comment = trim($_POST['comment'] ?? '');
|
$comment = trim($_POST['comment'] ?? '');
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
if ($name === '') {
|
||||||
|
$errors[] = 'Name ist erforderlich';
|
||||||
|
}
|
||||||
|
if ($floorId <= 0) {
|
||||||
|
$errors[] = 'Stockwerk ist erforderlich';
|
||||||
|
}
|
||||||
|
if ($portCount < 0) {
|
||||||
|
$errors[] = 'Port-Anzahl darf nicht negativ sein';
|
||||||
|
}
|
||||||
|
if (!empty($errors)) {
|
||||||
|
$_SESSION['error'] = implode(', ', $errors);
|
||||||
|
$_SESSION['validation_errors'] = $errors;
|
||||||
|
$redirectUrl = $id > 0 ? "?module=floor_infrastructure&action=edit&type=patchpanel&id=$id" : "?module=floor_infrastructure&action=edit&type=patchpanel";
|
||||||
|
header("Location: $redirectUrl");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
$panelId = $id;
|
$panelId = $id;
|
||||||
|
|
||||||
@@ -55,13 +73,30 @@ if ($type === 'patchpanel') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
$_SESSION['success'] = $id > 0 ? 'Patchpanel gespeichert' : 'Patchpanel erstellt';
|
||||||
} elseif ($type === 'outlet') {
|
} elseif ($type === 'outlet') {
|
||||||
$name = trim($_POST['name'] ?? '');
|
$name = trim($_POST['name'] ?? '');
|
||||||
$roomId = (int)($_POST['room_id'] ?? 0);
|
$roomId = (int)($_POST['room_id'] ?? 0);
|
||||||
$x = (int)($_POST['x'] ?? 0);
|
$x = (int)($_POST['x'] ?? 0);
|
||||||
$y = (int)($_POST['y'] ?? 0);
|
$y = (int)($_POST['y'] ?? 0);
|
||||||
$comment = trim($_POST['comment'] ?? '');
|
$comment = trim($_POST['comment'] ?? '');
|
||||||
|
$bindPatchpanelPortId = (int)($_POST['bind_patchpanel_port_id'] ?? 0);
|
||||||
$outletId = $id;
|
$outletId = $id;
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
if ($name === '') {
|
||||||
|
$errors[] = 'Name ist erforderlich';
|
||||||
|
}
|
||||||
|
if ($roomId <= 0) {
|
||||||
|
$errors[] = 'Raum ist erforderlich';
|
||||||
|
}
|
||||||
|
if (!empty($errors)) {
|
||||||
|
$_SESSION['error'] = implode(', ', $errors);
|
||||||
|
$_SESSION['validation_errors'] = $errors;
|
||||||
|
$redirectUrl = $id > 0 ? "?module=floor_infrastructure&action=edit&type=outlet&id=$id" : "?module=floor_infrastructure&action=edit&type=outlet";
|
||||||
|
header("Location: $redirectUrl");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
if ($id > 0) {
|
if ($id > 0) {
|
||||||
$sql->set(
|
$sql->set(
|
||||||
@@ -92,7 +127,137 @@ if ($type === 'patchpanel') {
|
|||||||
[$outletId]
|
[$outletId]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($bindPatchpanelPortId > 0) {
|
||||||
|
$roomFloorId = (int)($sql->single(
|
||||||
|
"SELECT floor_id FROM rooms WHERE id = ?",
|
||||||
|
"i",
|
||||||
|
[$roomId]
|
||||||
|
)['floor_id'] ?? 0);
|
||||||
|
|
||||||
|
$patchpanelPort = $sql->single(
|
||||||
|
"SELECT
|
||||||
|
fpp.id,
|
||||||
|
fp.floor_id
|
||||||
|
FROM floor_patchpanel_ports fpp
|
||||||
|
JOIN floor_patchpanels fp ON fp.id = fpp.patchpanel_id
|
||||||
|
WHERE fpp.id = ?",
|
||||||
|
"i",
|
||||||
|
[$bindPatchpanelPortId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$patchpanelPort) {
|
||||||
|
$_SESSION['error'] = 'Gewaehlter Patchpanel-Port existiert nicht';
|
||||||
|
$_SESSION['validation_errors'] = ['Gewaehlter Patchpanel-Port existiert nicht'];
|
||||||
|
header('Location: ?module=floor_infrastructure&action=edit&type=outlet&id=' . $outletId);
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($roomFloorId <= 0 || (int)$patchpanelPort['floor_id'] !== $roomFloorId) {
|
||||||
|
$_SESSION['error'] = 'Patchpanel-Port und Raum muessen auf demselben Stockwerk liegen';
|
||||||
|
$_SESSION['validation_errors'] = ['Patchpanel-Port und Raum muessen auf demselben Stockwerk liegen'];
|
||||||
|
header('Location: ?module=floor_infrastructure&action=edit&type=outlet&id=' . $outletId);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$outletPortId = (int)($sql->single(
|
||||||
|
"SELECT id
|
||||||
|
FROM network_outlet_ports
|
||||||
|
WHERE outlet_id = ?
|
||||||
|
ORDER BY id
|
||||||
|
LIMIT 1",
|
||||||
|
"i",
|
||||||
|
[$outletId]
|
||||||
|
)['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($outletPortId <= 0) {
|
||||||
|
$_SESSION['error'] = 'Wandbuchsen-Port konnte nicht ermittelt werden';
|
||||||
|
$_SESSION['validation_errors'] = ['Wandbuchsen-Port konnte nicht ermittelt werden'];
|
||||||
|
header('Location: ?module=floor_infrastructure&action=edit&type=outlet&id=' . $outletId);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$existingPatchpanelUsage = $sql->single(
|
||||||
|
"SELECT
|
||||||
|
id,
|
||||||
|
port_a_type,
|
||||||
|
port_a_id,
|
||||||
|
port_b_type,
|
||||||
|
port_b_id
|
||||||
|
FROM connections
|
||||||
|
WHERE
|
||||||
|
((port_a_type = 'patchpanel' OR port_a_type = 'floor_patchpanel_ports') AND port_a_id = ?)
|
||||||
|
OR
|
||||||
|
((port_b_type = 'patchpanel' OR port_b_type = 'floor_patchpanel_ports') AND port_b_id = ?)
|
||||||
|
LIMIT 1",
|
||||||
|
"ii",
|
||||||
|
[$bindPatchpanelPortId, $bindPatchpanelPortId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($existingPatchpanelUsage) {
|
||||||
|
$sameOutletConnection = (
|
||||||
|
(
|
||||||
|
(($existingPatchpanelUsage['port_a_type'] ?? '') === 'outlet' || ($existingPatchpanelUsage['port_a_type'] ?? '') === 'network_outlet_ports')
|
||||||
|
&& (int)($existingPatchpanelUsage['port_a_id'] ?? 0) === $outletPortId
|
||||||
|
)
|
||||||
|
||
|
||||||
|
(
|
||||||
|
(($existingPatchpanelUsage['port_b_type'] ?? '') === 'outlet' || ($existingPatchpanelUsage['port_b_type'] ?? '') === 'network_outlet_ports')
|
||||||
|
&& (int)($existingPatchpanelUsage['port_b_id'] ?? 0) === $outletPortId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$sameOutletConnection) {
|
||||||
|
$_SESSION['error'] = 'Gewaehlter Patchpanel-Port ist bereits verbunden';
|
||||||
|
$_SESSION['validation_errors'] = ['Gewaehlter Patchpanel-Port ist bereits verbunden'];
|
||||||
|
header('Location: ?module=floor_infrastructure&action=edit&type=outlet&id=' . $outletId);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql->set(
|
||||||
|
"DELETE FROM connections
|
||||||
|
WHERE
|
||||||
|
((port_a_type = 'outlet' OR port_a_type = 'network_outlet_ports') AND port_a_id = ? AND (port_b_type = 'patchpanel' OR port_b_type = 'floor_patchpanel_ports'))
|
||||||
|
OR
|
||||||
|
((port_b_type = 'outlet' OR port_b_type = 'network_outlet_ports') AND port_b_id = ? AND (port_a_type = 'patchpanel' OR port_a_type = 'floor_patchpanel_ports'))",
|
||||||
|
"ii",
|
||||||
|
[$outletPortId, $outletPortId]
|
||||||
|
);
|
||||||
|
|
||||||
|
$connectionTypeId = (int)($sql->single(
|
||||||
|
"SELECT id FROM connection_types ORDER BY id LIMIT 1",
|
||||||
|
"",
|
||||||
|
[]
|
||||||
|
)['id'] ?? 0);
|
||||||
|
if ($connectionTypeId <= 0) {
|
||||||
|
$connectionTypeId = (int)$sql->set(
|
||||||
|
"INSERT INTO connection_types (name, medium, duplex, line_style, comment) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
"sssss",
|
||||||
|
['Default', 'copper', 'custom', 'solid', 'Auto-created by floor_infrastructure/save'],
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($connectionTypeId <= 0) {
|
||||||
|
$_SESSION['error'] = 'Kein Verbindungstyp fuer automatische Bindung verfuegbar';
|
||||||
|
$_SESSION['validation_errors'] = ['Kein Verbindungstyp fuer automatische Bindung verfuegbar'];
|
||||||
|
header('Location: ?module=floor_infrastructure&action=edit&type=outlet&id=' . $outletId);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql->set(
|
||||||
|
"INSERT INTO connections (connection_type_id, port_a_type, port_a_id, port_b_type, port_b_id, vlan_config, comment)
|
||||||
|
VALUES (?, 'outlet', ?, 'patchpanel', ?, NULL, ?)",
|
||||||
|
"iiis",
|
||||||
|
[$connectionTypeId, $outletPortId, $bindPatchpanelPortId, 'Auto-Link bei Wandbuchsen-Erstellung']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$_SESSION['success'] = $id > 0 ? 'Wandbuchse gespeichert' : 'Wandbuchse erstellt';
|
||||||
|
} else {
|
||||||
|
$_SESSION['error'] = 'Ungueltiger Infrastrukturobjekt-Typ';
|
||||||
|
$_SESSION['validation_errors'] = ['Ungueltiger Infrastrukturobjekt-Typ'];
|
||||||
}
|
}
|
||||||
|
|
||||||
header('Location: ?module=floor_infrastructure&action=list');
|
header('Location: ?module=floor_infrastructure&action=list');
|
||||||
|
|||||||
@@ -25,6 +25,47 @@ if (!$exists) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$forceDelete = (int)($_POST['force'] ?? $_GET['force'] ?? 0) === 1;
|
||||||
|
$dependencyCounts = $sql->single(
|
||||||
|
"SELECT
|
||||||
|
(SELECT COUNT(*) FROM rooms WHERE floor_id = ?) AS room_count,
|
||||||
|
(SELECT COUNT(*) FROM racks WHERE floor_id = ?) AS rack_count,
|
||||||
|
(SELECT COUNT(*) FROM floor_patchpanels WHERE floor_id = ?) AS patchpanel_count",
|
||||||
|
"iii",
|
||||||
|
[$id, $id, $id]
|
||||||
|
);
|
||||||
|
|
||||||
|
$roomCount = (int)($dependencyCounts['room_count'] ?? 0);
|
||||||
|
$rackCount = (int)($dependencyCounts['rack_count'] ?? 0);
|
||||||
|
$patchpanelCount = (int)($dependencyCounts['patchpanel_count'] ?? 0);
|
||||||
|
$hasDependencies = $roomCount > 0 || $rackCount > 0 || $patchpanelCount > 0;
|
||||||
|
|
||||||
|
if ($hasDependencies && !$forceDelete) {
|
||||||
|
$parts = [];
|
||||||
|
if ($roomCount > 0) {
|
||||||
|
$parts[] = $roomCount . ' Raeume';
|
||||||
|
}
|
||||||
|
if ($rackCount > 0) {
|
||||||
|
$parts[] = $rackCount . ' Racks';
|
||||||
|
}
|
||||||
|
if ($patchpanelCount > 0) {
|
||||||
|
$parts[] = $patchpanelCount . ' Patchpanels';
|
||||||
|
}
|
||||||
|
|
||||||
|
http_response_code(409);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'requires_force' => true,
|
||||||
|
'message' => 'Beim Loeschen werden abhaengige Daten entfernt (' . implode(', ', $parts) . '). Fortfahren?',
|
||||||
|
'dependencies' => [
|
||||||
|
'rooms' => $roomCount,
|
||||||
|
'racks' => $rackCount,
|
||||||
|
'patchpanels' => $patchpanelCount
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
$rows = $sql->set("DELETE FROM floors WHERE id = ?", "i", [$id]);
|
$rows = $sql->set("DELETE FROM floors WHERE id = ?", "i", [$id]);
|
||||||
if ($rows === false) {
|
if ($rows === false) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
@@ -32,5 +73,13 @@ if ($rows === false) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
echo json_encode(['success' => true, 'message' => 'Stockwerk geloescht']);
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Stockwerk geloescht',
|
||||||
|
'dependencies' => [
|
||||||
|
'rooms' => $roomCount,
|
||||||
|
'racks' => $rackCount,
|
||||||
|
'patchpanels' => $patchpanelCount
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -233,11 +233,20 @@ $buildings = $sql->get("SELECT id, name FROM buildings ORDER BY name", "", []);
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
function confirmDelete(id) {
|
function confirmDelete(id) {
|
||||||
if (confirm('Dieses Stockwerk wirklich loeschen? Alle Raeume und Racks werden geloescht.')) {
|
if (!confirm('Dieses Stockwerk wirklich loeschen?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestDelete = (forceDelete) => {
|
||||||
|
const body = ['id=' + encodeURIComponent(id)];
|
||||||
|
if (forceDelete) {
|
||||||
|
body.push('force=1');
|
||||||
|
}
|
||||||
|
|
||||||
fetch('?module=floors&action=delete', {
|
fetch('?module=floors&action=delete', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||||
body: 'id=' + encodeURIComponent(id)
|
body: body.join('&')
|
||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
@@ -245,12 +254,22 @@ function confirmDelete(id) {
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data && data.requires_force) {
|
||||||
|
if (confirm(data.message || 'Abhaengige Daten ebenfalls loeschen?')) {
|
||||||
|
requestDelete(true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
|
alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
alert('Loeschen fehlgeschlagen');
|
alert('Loeschen fehlgeschlagen');
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
|
requestDelete(false);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ if ($buildingId <= 0) {
|
|||||||
// Falls Fehler: zurück zum Edit-Formular
|
// Falls Fehler: zurück zum Edit-Formular
|
||||||
if (!empty($errors)) {
|
if (!empty($errors)) {
|
||||||
$_SESSION['error'] = implode(', ', $errors);
|
$_SESSION['error'] = implode(', ', $errors);
|
||||||
|
$_SESSION['validation_errors'] = $errors;
|
||||||
$redirectUrl = $floorId ? "?module=floors&action=edit&id=$floorId" : "?module=floors&action=edit";
|
$redirectUrl = $floorId ? "?module=floors&action=edit&id=$floorId" : "?module=floors&action=edit";
|
||||||
header("Location: $redirectUrl");
|
header("Location: $redirectUrl");
|
||||||
exit;
|
exit;
|
||||||
@@ -50,6 +51,7 @@ if ($floorSvgContent !== '') {
|
|||||||
$storedSvgPath = storeSvgEditorContent($sql, $floorId, $floorSvgContent);
|
$storedSvgPath = storeSvgEditorContent($sql, $floorId, $floorSvgContent);
|
||||||
if ($storedSvgPath === false) {
|
if ($storedSvgPath === false) {
|
||||||
$_SESSION['error'] = "SVG aus dem Editor konnte nicht gespeichert werden";
|
$_SESSION['error'] = "SVG aus dem Editor konnte nicht gespeichert werden";
|
||||||
|
$_SESSION['validation_errors'] = ["SVG aus dem Editor konnte nicht gespeichert werden"];
|
||||||
$redirectUrl = $floorId ? "?module=floors&action=edit&id=$floorId" : "?module=floors&action=edit";
|
$redirectUrl = $floorId ? "?module=floors&action=edit&id=$floorId" : "?module=floors&action=edit";
|
||||||
header("Location: $redirectUrl");
|
header("Location: $redirectUrl");
|
||||||
exit;
|
exit;
|
||||||
|
|||||||
@@ -7,6 +7,16 @@
|
|||||||
|
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||||
|
if ($method !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Methode nicht erlaubt'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
$locationId = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
|
$locationId = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
|
||||||
|
|
||||||
if ($locationId <= 0) {
|
if ($locationId <= 0) {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ if (empty($name)) {
|
|||||||
|
|
||||||
if (!empty($errors)) {
|
if (!empty($errors)) {
|
||||||
$_SESSION['error'] = implode(', ', $errors);
|
$_SESSION['error'] = implode(', ', $errors);
|
||||||
|
$_SESSION['validation_errors'] = $errors;
|
||||||
$redirectUrl = $locationId ? "?module=locations&action=edit&id=$locationId" : "?module=locations&action=edit";
|
$redirectUrl = $locationId ? "?module=locations&action=edit&id=$locationId" : "?module=locations&action=edit";
|
||||||
header("Location: $redirectUrl");
|
header("Location: $redirectUrl");
|
||||||
exit;
|
exit;
|
||||||
|
|||||||
84
app/modules/port_types/delete.php
Normal file
84
app/modules/port_types/delete.php
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* app/modules/port_types/delete.php
|
||||||
|
*
|
||||||
|
* Loescht einen Porttyp per AJAX (POST).
|
||||||
|
*/
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Methode nicht erlaubt']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$portTypeId = (int)($_POST['id'] ?? 0);
|
||||||
|
if ($portTypeId <= 0) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Ungueltige Porttyp-ID']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$portType = $sql->single(
|
||||||
|
"SELECT id, name FROM port_types WHERE id = ?",
|
||||||
|
"i",
|
||||||
|
[$portTypeId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$portType) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Porttyp nicht gefunden']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$usage = $sql->single(
|
||||||
|
"SELECT
|
||||||
|
(SELECT COUNT(*) FROM device_type_ports WHERE port_type_id = ?) AS device_type_ports_count,
|
||||||
|
(SELECT COUNT(*) FROM device_ports WHERE port_type_id = ?) AS device_ports_count,
|
||||||
|
(SELECT COUNT(*) FROM module_ports WHERE port_type_id = ?) AS module_ports_count,
|
||||||
|
(SELECT COUNT(*) FROM network_outlet_ports WHERE port_type_id = ?) AS outlet_ports_count,
|
||||||
|
(SELECT COUNT(*) FROM floor_patchpanel_ports WHERE port_type_id = ?) AS patchpanel_ports_count",
|
||||||
|
"iiiii",
|
||||||
|
[$portTypeId, $portTypeId, $portTypeId, $portTypeId, $portTypeId]
|
||||||
|
);
|
||||||
|
|
||||||
|
$references = [
|
||||||
|
'Geraetetyp-Ports' => (int)($usage['device_type_ports_count'] ?? 0),
|
||||||
|
'Geraete-Ports' => (int)($usage['device_ports_count'] ?? 0),
|
||||||
|
'Modul-Ports' => (int)($usage['module_ports_count'] ?? 0),
|
||||||
|
'Netzwerkdosen-Ports' => (int)($usage['outlet_ports_count'] ?? 0),
|
||||||
|
'Patchpanel-Ports' => (int)($usage['patchpanel_ports_count'] ?? 0),
|
||||||
|
];
|
||||||
|
|
||||||
|
$inUse = array_filter($references, static fn ($count) => $count > 0);
|
||||||
|
if (!empty($inUse)) {
|
||||||
|
$parts = [];
|
||||||
|
foreach ($inUse as $label => $count) {
|
||||||
|
$parts[] = $label . ': ' . $count;
|
||||||
|
}
|
||||||
|
http_response_code(409);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Porttyp wird noch verwendet (' . implode(', ', $parts) . ')'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$deleted = $sql->set(
|
||||||
|
"DELETE FROM port_types WHERE id = ?",
|
||||||
|
"i",
|
||||||
|
[$portTypeId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($deleted <= 0) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Porttyp konnte nicht geloescht werden']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Porttyp geloescht: ' . (string)$portType['name']
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
@@ -23,20 +23,11 @@ $isEdit = !empty($portType);
|
|||||||
$pageTitle = $isEdit ? "Porttyp bearbeiten: " . htmlspecialchars($portType['name']) : "Neuen Porttyp anlegen";
|
$pageTitle = $isEdit ? "Porttyp bearbeiten: " . htmlspecialchars($portType['name']) : "Neuen Porttyp anlegen";
|
||||||
$mediaOptions = ['copper' => 'Kupfer', 'fiber' => 'Lichtwelle', 'coax' => 'Koax', 'other' => 'Sonstiges'];
|
$mediaOptions = ['copper' => 'Kupfer', 'fiber' => 'Lichtwelle', 'coax' => 'Koax', 'other' => 'Sonstiges'];
|
||||||
|
|
||||||
$error = $_SESSION['error'] ?? '';
|
|
||||||
unset($_SESSION['error']);
|
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="port-type-edit">
|
<div class="port-type-edit">
|
||||||
<h1><?php echo $pageTitle; ?></h1>
|
<h1><?php echo $pageTitle; ?></h1>
|
||||||
|
|
||||||
<?php if ($error): ?>
|
|
||||||
<div class="error-message">
|
|
||||||
<?php echo htmlspecialchars($error); ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<form method="post" action="?module=port_types&action=save" class="edit-form">
|
<form method="post" action="?module=port_types&action=save" class="edit-form">
|
||||||
|
|
||||||
<?php if ($isEdit): ?>
|
<?php if ($isEdit): ?>
|
||||||
@@ -157,12 +148,4 @@ unset($_SESSION['error']);
|
|||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-message {
|
|
||||||
background: #ffe3e3;
|
|
||||||
color: #a73737;
|
|
||||||
border: 1px solid #f5c2c2;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -34,20 +34,11 @@ $portTypes = $sql->get(
|
|||||||
$params
|
$params
|
||||||
);
|
);
|
||||||
|
|
||||||
$success = $_SESSION['success'] ?? '';
|
|
||||||
unset($_SESSION['success']);
|
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="port-types-container">
|
<div class="port-types-container">
|
||||||
<h1>Porttypen</h1>
|
<h1>Porttypen</h1>
|
||||||
|
|
||||||
<?php if ($success): ?>
|
|
||||||
<div class="success-message">
|
|
||||||
<?php echo htmlspecialchars($success); ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<form method="GET" class="filter-form">
|
<form method="GET" class="filter-form">
|
||||||
<input type="hidden" name="module" value="port_types">
|
<input type="hidden" name="module" value="port_types">
|
||||||
@@ -78,6 +69,13 @@ unset($_SESSION['success']);
|
|||||||
<td><small><?php echo htmlspecialchars($pt['comment'] ?? ''); ?></small></td>
|
<td><small><?php echo htmlspecialchars($pt['comment'] ?? ''); ?></small></td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<a href="?module=port_types&action=edit&id=<?php echo (int)$pt['id']; ?>" class="button button-small">Bearbeiten</a>
|
<a href="?module=port_types&action=edit&id=<?php echo (int)$pt['id']; ?>" class="button button-small">Bearbeiten</a>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button button-small button-danger js-port-type-delete"
|
||||||
|
data-port-type-id="<?php echo (int)$pt['id']; ?>"
|
||||||
|
data-port-type-name="<?php echo htmlspecialchars((string)$pt['name'], ENT_QUOTES, 'UTF-8'); ?>">
|
||||||
|
Loeschen
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -164,8 +162,7 @@ unset($_SESSION['success']);
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state,
|
.empty-state {
|
||||||
.success-message {
|
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -177,13 +174,40 @@ unset($_SESSION['success']);
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.success-message {
|
|
||||||
background: #e9f8f1;
|
|
||||||
border: 1px solid #c7eedc;
|
|
||||||
color: #2c7d59;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.querySelectorAll('.js-port-type-delete').forEach((button) => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
const id = Number(button.dataset.portTypeId || '0');
|
||||||
|
const name = button.dataset.portTypeName || 'Porttyp';
|
||||||
|
if (id <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm('Porttyp "' + name + '" wirklich loeschen?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('?module=port_types&action=delete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||||
|
body: 'id=' + encodeURIComponent(id)
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data && data.success) {
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
|
||||||
|
})
|
||||||
|
.catch(() => alert('Loeschen fehlgeschlagen'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ if (!in_array($medium, $allowedMediums, true)) {
|
|||||||
|
|
||||||
if (!empty($errors)) {
|
if (!empty($errors)) {
|
||||||
$_SESSION['error'] = implode(', ', $errors);
|
$_SESSION['error'] = implode(', ', $errors);
|
||||||
|
$_SESSION['validation_errors'] = $errors;
|
||||||
$redirect = $portTypeId ? "?module=port_types&action=edit&id=$portTypeId" : "?module=port_types&action=edit";
|
$redirect = $portTypeId ? "?module=port_types&action=edit&id=$portTypeId" : "?module=port_types&action=edit";
|
||||||
header("Location: $redirect");
|
header("Location: $redirect");
|
||||||
exit;
|
exit;
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ if ($heightHe < 1) {
|
|||||||
// Falls Fehler: zurück zum Edit-Formular
|
// Falls Fehler: zurück zum Edit-Formular
|
||||||
if (!empty($errors)) {
|
if (!empty($errors)) {
|
||||||
$_SESSION['error'] = implode(', ', $errors);
|
$_SESSION['error'] = implode(', ', $errors);
|
||||||
|
$_SESSION['validation_errors'] = $errors;
|
||||||
$redirectUrl = $rackId ? "?module=racks&action=edit&id=$rackId" : "?module=racks&action=edit";
|
$redirectUrl = $rackId ? "?module=racks&action=edit&id=$rackId" : "?module=racks&action=edit";
|
||||||
header("Location: $redirectUrl");
|
header("Location: $redirectUrl");
|
||||||
exit;
|
exit;
|
||||||
|
|||||||
@@ -18,6 +18,15 @@ $comment = trim((string)($_POST['comment'] ?? ''));
|
|||||||
$rawPolygon = trim((string)($_POST['polygon_points'] ?? ''));
|
$rawPolygon = trim((string)($_POST['polygon_points'] ?? ''));
|
||||||
|
|
||||||
if ($name === '' || $floorId <= 0) {
|
if ($name === '' || $floorId <= 0) {
|
||||||
|
$errors = [];
|
||||||
|
if ($name === '') {
|
||||||
|
$errors[] = 'Name ist erforderlich';
|
||||||
|
}
|
||||||
|
if ($floorId <= 0) {
|
||||||
|
$errors[] = 'Stockwerk ist erforderlich';
|
||||||
|
}
|
||||||
|
$_SESSION['error'] = implode(', ', $errors);
|
||||||
|
$_SESSION['validation_errors'] = $errors;
|
||||||
$redirect = $roomId > 0 ? "?module=rooms&action=edit&id=$roomId" : "?module=rooms&action=edit&floor_id=$floorId";
|
$redirect = $roomId > 0 ? "?module=rooms&action=edit&id=$roomId" : "?module=rooms&action=edit&floor_id=$floorId";
|
||||||
header("Location: $redirect");
|
header("Location: $redirect");
|
||||||
exit;
|
exit;
|
||||||
@@ -100,6 +109,7 @@ if (roomsHasPolygonColumn($sql)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$_SESSION['success'] = $roomId > 0 ? 'Raum gespeichert' : 'Raum erstellt';
|
||||||
header('Location: ?module=locations&action=list');
|
header('Location: ?module=locations&action=list');
|
||||||
exit;
|
exit;
|
||||||
|
|
||||||
|
|||||||
@@ -12,21 +12,5 @@
|
|||||||
| Session: <?php echo session_id() !== '' ? 'aktiv' : 'inaktiv'; ?>
|
| Session: <?php echo session_id() !== '' ? 'aktiv' : 'inaktiv'; ?>
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<?php if (!empty($_SESSION['success']) || !empty($_SESSION['error'])): ?>
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
const success = <?php echo json_encode($_SESSION['success'] ?? '', JSON_UNESCAPED_UNICODE); ?>;
|
|
||||||
const error = <?php echo json_encode($_SESSION['error'] ?? '', JSON_UNESCAPED_UNICODE); ?>;
|
|
||||||
if (success) {
|
|
||||||
alert(success);
|
|
||||||
}
|
|
||||||
if (error) {
|
|
||||||
alert(error);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
<?php unset($_SESSION['success'], $_SESSION['error']); ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -64,4 +64,60 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$flashMessages = [];
|
||||||
|
|
||||||
|
$successMessage = trim((string)($_SESSION['success'] ?? ''));
|
||||||
|
if ($successMessage !== '') {
|
||||||
|
$flashMessages[] = [
|
||||||
|
'type' => 'success',
|
||||||
|
'text' => $successMessage,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$errorMessage = trim((string)($_SESSION['error'] ?? ''));
|
||||||
|
if ($errorMessage !== '') {
|
||||||
|
$flashMessages[] = [
|
||||||
|
'type' => 'error',
|
||||||
|
'text' => $errorMessage,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$validationErrors = $_SESSION['validation_errors'] ?? [];
|
||||||
|
if (!is_array($validationErrors)) {
|
||||||
|
$validationErrors = [];
|
||||||
|
}
|
||||||
|
$validationErrors = array_values(array_filter(array_map(static function ($entry) {
|
||||||
|
return trim((string)$entry);
|
||||||
|
}, $validationErrors), static function ($entry) {
|
||||||
|
return $entry !== '';
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!empty($validationErrors)) {
|
||||||
|
$flashMessages[] = [
|
||||||
|
'type' => 'error',
|
||||||
|
'text' => 'Bitte pruefe die Eingaben:',
|
||||||
|
'details' => $validationErrors,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($_SESSION['success'], $_SESSION['error'], $_SESSION['validation_errors']);
|
||||||
|
?>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
|
<?php if (!empty($flashMessages)): ?>
|
||||||
|
<section class="flash-stack" aria-live="polite">
|
||||||
|
<?php foreach ($flashMessages as $message): ?>
|
||||||
|
<article class="flash-message flash-message--<?php echo htmlspecialchars((string)$message['type']); ?>">
|
||||||
|
<p class="flash-message__text"><?php echo htmlspecialchars((string)($message['text'] ?? '')); ?></p>
|
||||||
|
<?php if (!empty($message['details']) && is_array($message['details'])): ?>
|
||||||
|
<ul class="flash-message__list">
|
||||||
|
<?php foreach ($message['details'] as $detail): ?>
|
||||||
|
<li><?php echo htmlspecialchars((string)$detail); ?></li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
<?php endif; ?>
|
||||||
|
</article>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</section>
|
||||||
|
<?php endif; ?>
|
||||||
|
|||||||
@@ -124,10 +124,10 @@ Die Bühne für Patchpanel-Objekte auf dem Stockwerkplan.
|
|||||||
- Attributes: Panel-Referenz, `name`, `port_type_id`, optionale VLAN- bzw. Status-Attribute.
|
- Attributes: Panel-Referenz, `name`, `port_type_id`, optionale VLAN- bzw. Status-Attribute.
|
||||||
- Ports werden über `connections` sowohl mit anderen Patchpanels als auch mit Netzwerkbuchsen (`network_outlet_ports`) oder Gerätports verbunden; dadurch lassen sich Router-Kabel grafisch darstellen.
|
- Ports werden über `connections` sowohl mit anderen Patchpanels als auch mit Netzwerkbuchsen (`network_outlet_ports`) oder Gerätports verbunden; dadurch lassen sich Router-Kabel grafisch darstellen.
|
||||||
|
|
||||||
**TODO**
|
**Status (18. Februar 2026)**
|
||||||
- [ ] Floorplan- und CRUD-Module so erweitern, dass Patchpanels als Floor-Objekte verwaltet und deren Ports gepflegt werden können (`floor_patchpanels`, `floor_patchpanel_ports`).
|
- [x] Floorplan- und CRUD-Module wurden für Patchpanels als Floor-Objekte inkl. Port-Pflege erweitert (`floor_patchpanels`, `floor_patchpanel_ports`).
|
||||||
- [ ] Verbindungen zwischen Patchpanel ↔ Patchpanel und Patchpanel ↔ Netzwerkbuchse standardisiert in der `connections`-Logik abbilden.
|
- [x] Verbindungen zwischen Patchpanel ↔ Patchpanel und Patchpanel ↔ Netzwerkbuchse sind in der `connections`-Logik abbildbar.
|
||||||
- [ ] UI/CSV/Export/Dokumentation nachziehen, damit Planer sofort sehen, wo die permanent installierten Kabel verlaufen.
|
- [ ] UI/CSV/Export-Dokumentation weiter ausbauen, damit Planer Kabelverläufe direkt auswerten können.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
5
init.sql
5
init.sql
@@ -376,7 +376,10 @@ CREATE TABLE `floor_patchpanel_ports` (
|
|||||||
`comment` text DEFAULT NULL
|
`comment` text DEFAULT NULL
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
|
||||||
|
|
||||||
-- TODO: Port-Konfiguration (Patchpanel ↔ Patchpanel, Patchpanel ↔ Netzwerkbuchse) wird über die `connections`-Tabelle geregelt.
|
-- Port-Regeln fuer Patchpanel-Verbindungen:
|
||||||
|
-- 1) `connections.port_a_type` / `connections.port_b_type` nutzen den Wert `patchpanel` fuer IDs aus `floor_patchpanel_ports`.
|
||||||
|
-- 2) Patchpanel-Ports duerfen mit Patchpanel-Ports, Netzwerkbuchsen-Ports (`outlet`) oder Geraete-/Modulports verbunden werden.
|
||||||
|
-- 3) Die fachliche Validierung erfolgt in den Save-Handlern der Verbindungs-Module; das Schema bleibt polymorph.
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user