Compare commits
23 Commits
4a23713d31
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 96f885efde | |||
| dbe977f62c | |||
| b973d2857b | |||
| 0642a3b6ef | |||
| 4214ac45d9 | |||
| 900b110ee0 | |||
| 346cf33eb7 | |||
| 1a51d2507b | |||
| 9ece132df5 | |||
| 9121a2ddfd | |||
| d9be0e1482 | |||
| ebd4740b7e | |||
| 0ac1889946 | |||
| 9b8dc17d20 | |||
| 17a5bc4812 | |||
| 4dc1530402 | |||
| 20638cb3a5 | |||
| 77758f71d3 | |||
| f4ce7f360d | |||
| ec20fa2f96 | |||
| c8fb5b140c | |||
| 463ab97c4b | |||
| 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
|
||||||
142
NEXT_STEPS.md
142
NEXT_STEPS.md
@@ -1,142 +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! 🚀**
|
|
||||||
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
258
TODO.md
258
TODO.md
@@ -1,258 +0,0 @@
|
|||||||
# TODO
|
|
||||||
|
|
||||||
Zentrale Sammlung aller TODO-Markierungen im Repository (Stand: 13. Februar 2026).
|
|
||||||
|
|
||||||
Hinweis: Die Eintraege sind direkt aus den Quelldateien aggregiert.
|
|
||||||
|
|
||||||
## Arbeitsnotizen (16. Februar 2026)
|
|
||||||
|
|
||||||
- [x] API-Basis umgesetzt: `app/api/connections.php`, `app/api/device_type_ports.php`, `app/api/upload.php` auf aktuelles Schema gebracht (Auth, Validierung, Existenzpruefungen, Fehlerantworten).
|
|
||||||
- [x] Bootstrap/Auth/Config/Routing 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 ersetzt: `app/modules/device_types/ports.php` lauffaehig gemacht (anzeigen, hinzufuegen, loeschen).
|
|
||||||
- [x] TODO-Reste in `header.php`, `footer.php`, `layout.php`, `floor_infrastructure/edit.php` entfernt.
|
|
||||||
|
|
||||||
Offene Blocker / naechste Punkte:
|
|
||||||
- [ ] `app/modules/connections/list.php`: Detailbereich fuer ausgewaehlte Verbindung sowie Bearbeiten/Loeschen im UI fehlen noch.
|
|
||||||
- [ ] `app/modules/dashboard/list.php`: grosse zoombare Gesamt-Topologie-Wand (fachlich/grafisch groesseres Feature).
|
|
||||||
- [ ] `app/lib/helpers.php`: generischer Sammel-TODO ohne konkreten Scope.
|
|
||||||
- [ ] Vollstaendiger End-to-End-Test aktuell nicht moeglich, da in dieser Shell kein `php` CLI verfuegbar ist.
|
|
||||||
|
|
||||||
## app\api\connections.php
|
|
||||||
|
|
||||||
- [ ] L15: // TODO: Single-User-Auth prüfen
|
|
||||||
- [ ] L61: // TODO: Kontext definieren (Standort, Rack, Floor, gesamtes Netz)
|
|
||||||
- [ ] L117: // TODO: Validierung
|
|
||||||
- [ ] L182: // TODO: Prüfen, ob Verbindung existiert
|
|
||||||
|
|
||||||
## app\api\device_type_ports.php
|
|
||||||
|
|
||||||
- [ ] L15: // TODO: Single-User-Auth prüfen
|
|
||||||
- [ ] L96: // TODO: Transaktion starten (falls SQL-Klasse das unterstützt)
|
|
||||||
- [ ] L100: // TODO: Validierung:
|
|
||||||
- [ ] L163: // TODO: Prüfen, ob Port existiert und nicht verwendet wird
|
|
||||||
|
|
||||||
## app\api\upload.php
|
|
||||||
|
|
||||||
- [ ] L18: // TODO: Single-User-Auth prüfen
|
|
||||||
- [ ] L25: // TODO: Upload-Basisverzeichnis aus config.php
|
|
||||||
- [ ] L35: // TODO: Max. Dateigröße festlegen (z.B. 5MB)
|
|
||||||
- [ ] L77: // TODO: Kategorie definieren (device_types, floors, racks, etc.)
|
|
||||||
- [ ] L95: // TODO: Eindeutigen Namen besser definieren (UUID?)
|
|
||||||
- [ ] L114: // TODO: Optional in Tabelle `uploads` speichern
|
|
||||||
|
|
||||||
## app\assets\js\app.js
|
|
||||||
|
|
||||||
- [ ] L15: deviceTypes: [], // TODO: alle Gerätetypen laden
|
|
||||||
- [ ] L16: devices: [], // TODO: alle Geräte laden
|
|
||||||
- [ ] L17: racks: [], // TODO: alle Racks laden
|
|
||||||
- [ ] L18: floors: [], // TODO: alle Floors laden
|
|
||||||
- [ ] L19: connections: [], // TODO: alle Verbindungen laden
|
|
||||||
- [ ] L33: // TODO: import / init svg-editor.js
|
|
||||||
- [ ] L39: // TODO: import / init network-view.js
|
|
||||||
- [ ] L45: // TODO: init drag & drop logic
|
|
||||||
- [ ] L59: // TODO: Save-Button Device-Type
|
|
||||||
- [ ] L64: // TODO: Save Device-Type via AJAX
|
|
||||||
- [ ] L68: // TODO: Save-Button Device
|
|
||||||
- [ ] L73: // TODO: Save Device via AJAX
|
|
||||||
- [ ] L77: // TODO: Save-Button Floor
|
|
||||||
- [ ] L82: // TODO: Save Floor via AJAX
|
|
||||||
- [ ] L86: // TODO: Save-Button Rack
|
|
||||||
- [ ] L91: // TODO: Save Rack via AJAX
|
|
||||||
- [ ] L95: // TODO: Weitere Event-Handler (Import, Export, Filter)
|
|
||||||
- [ ] L132: // TODO: weitere Utility-Funktionen (DOM-Helper, SVG-Helper, etc.)
|
|
||||||
|
|
||||||
## app\assets\js\dashboard.js
|
|
||||||
|
|
||||||
- [ ] L75: // TODO: Dashboard-Container ermitteln
|
|
||||||
- [ ] L78: // TODO: Module rendern
|
|
||||||
- [ ] L81: // TODO: Optional: Status-Daten laden (Counts, Warnings)
|
|
||||||
- [ ] L117: // TODO: loadStats() → Anzahl Geräte, offene Ports, unverbundene Dosen
|
|
||||||
- [ ] L118: // TODO: showWarnings() → unverbundene Ports, VLAN-Konflikte
|
|
||||||
- [ ] L119: // TODO: RecentChanges() → letzte Änderungen
|
|
||||||
|
|
||||||
## app\assets\js\network-view.js
|
|
||||||
|
|
||||||
- [ ] L20: // TODO: Standort / Rack / View-Kontext vom Backend setzen
|
|
||||||
- [ ] L23: // TODO: API-Endpunkte definieren
|
|
||||||
- [ ] L79: // TODO: Datenstruktur validieren
|
|
||||||
- [ ] L123: // TODO: Gerätetyp (SVG oder JPG) korrekt laden
|
|
||||||
- [ ] L144: // TODO: Ports als kleine Kreise anlegen (Position aus Portdefinition)
|
|
||||||
- [ ] L145: // TODO: Ports klickbar machen (für Verbindungs-Erstellung)
|
|
||||||
- [ ] L157: // TODO: Quell- & Ziel-Port-Koordinaten berechnen
|
|
||||||
- [ ] L158: // TODO: unterschiedliche Verbindungstypen (Farbe, Strichart, Dicke)
|
|
||||||
- [ ] L217: // TODO: Positionen optional automatisch speichern
|
|
||||||
- [ ] L232: // TODO: Sidebar mit Gerätedetails füllen
|
|
||||||
- [ ] L254: // TODO: Erfolg / Fehler anzeigen
|
|
||||||
- [ ] L289: // TODO: Delete -> Gerät entfernen?
|
|
||||||
|
|
||||||
## app\assets\js\svg-editor.js
|
|
||||||
|
|
||||||
- [ ] L20: // TODO: vom Backend setzen (z. B. via data-Attribut)
|
|
||||||
- [ ] L23: // TODO: API-Endpunkte festlegen
|
|
||||||
- [ ] L74: // TODO: Modifier-Key prüfen (z. B. nur mit SHIFT neuen Port erstellen?)
|
|
||||||
- [ ] L86: port_type_id: null, // TODO: Default-Porttyp?
|
|
||||||
- [ ] L134: // TODO: Sidebar-Felder mit Portdaten füllen
|
|
||||||
- [ ] L178: // TODO: Sicherheitsabfrage (confirm)
|
|
||||||
- [ ] L184: // TODO: Sidebar zurücksetzen
|
|
||||||
- [ ] L200: // TODO: Datenformat validieren
|
|
||||||
- [ ] L222: // TODO: Erfolg / Fehler anzeigen
|
|
||||||
|
|
||||||
## app\bootstrap.php
|
|
||||||
|
|
||||||
- [ ] L16: // TODO: Config-Datei mit DB-Zugang, Pfaden, globalen Settings füllen
|
|
||||||
- [ ] L22: // TODO: Single-User Auth prüfen
|
|
||||||
- [ ] L30: // TODO: Host, User, Passwort, DB aus config.php nutzen
|
|
||||||
|
|
||||||
## app\index.php
|
|
||||||
|
|
||||||
- [ ] L19: // TODO: Session starten / Single-User-Auth prüfen
|
|
||||||
- [ ] L37: // TODO: Fehlerseite anzeigen, nutze renderClientError(...)
|
|
||||||
- [ ] L42: // TODO: Fehlerseite anzeigen, nutze renderClientError(...)
|
|
||||||
- [ ] L61: // TODO: Fehlerseite oder 404, nutze renderClientError(...)
|
|
||||||
|
|
||||||
## app\lib\auth.php
|
|
||||||
|
|
||||||
- [ ] L24: // TODO: Session-Variable definieren, z.B. $_SESSION['auth'] === true
|
|
||||||
- [ ] L40: // TODO: Passwort aus config.php vergleichen
|
|
||||||
- [ ] L41: // TODO: Passwort-Hash verwenden (password_hash / password_verify)
|
|
||||||
- [ ] L62: // TODO: Session-Variablen löschen
|
|
||||||
- [ ] L65: // TODO: Optional komplette Session zerstören
|
|
||||||
- [ ] L79: // TODO: Redirect auf Login-Seite
|
|
||||||
|
|
||||||
## app\lib\helpers.php
|
|
||||||
|
|
||||||
- [ ] L300: // TODO: Weitere Helfer nach Bedarf
|
|
||||||
|
|
||||||
## app\modules\buildings\edit.php
|
|
||||||
|
|
||||||
- [ ] L176: // TODO: AJAX-Delete implementieren
|
|
||||||
|
|
||||||
## app\modules\buildings\list.php
|
|
||||||
|
|
||||||
- [ ] L245: // TODO: AJAX-Delete implementieren
|
|
||||||
|
|
||||||
## app\modules\connections\list.php
|
|
||||||
|
|
||||||
- [ ] L198: <!-- TODO: Details zum ausgewählten Gerät anzeigen -->
|
|
||||||
- [ ] L207: <!-- TODO: Verbindung bearbeiten / löschen -->
|
|
||||||
|
|
||||||
## app\modules\device_types\edit.php
|
|
||||||
|
|
||||||
- [ ] L18: //TODO port hinzufügen geht nicht
|
|
||||||
- [ ] L378: // TODO: AJAX-Delete implementieren
|
|
||||||
|
|
||||||
## app\modules\device_types\list.php
|
|
||||||
|
|
||||||
- [ ] L252: // TODO: AJAX-Delete implementieren
|
|
||||||
|
|
||||||
## app\modules\device_types\ports.php
|
|
||||||
|
|
||||||
- [ ] L12: // TODO: bootstrap laden
|
|
||||||
- [ ] L15: // TODO: Auth erzwingen
|
|
||||||
- [ ] L22: // TODO: device_type_id aus GET lesen
|
|
||||||
- [ ] L25: // TODO: Gerätetyp laden
|
|
||||||
- [ ] L28: // TODO: Ports dieses Gerätetyps laden
|
|
||||||
- [ ] L43: <!-- TODO: Gerätetyp-Name -->
|
|
||||||
- [ ] L59: <!-- TODO: Port-Typen verwalten -->
|
|
||||||
- [ ] L60: <!-- TODO: Import / Export -->
|
|
||||||
- [ ] L109: <!-- TODO: Port-Nummer -->
|
|
||||||
- [ ] L113: <!-- TODO: Port-Name -->
|
|
||||||
- [ ] L117: <!-- TODO: Port-Typ (RJ45, SFP, ...) -->
|
|
||||||
- [ ] L120: <!-- TODO: Medium (Kupfer, LWL, BNC, Custom) -->
|
|
||||||
- [ ] L123: <!-- TODO: Modus (Access, Trunk, Custom) -->
|
|
||||||
- [ ] L126: <!-- TODO: VLANs -->
|
|
||||||
- [ ] L161: <!-- TODO: SVG des Gerätetyps laden -->
|
|
||||||
- [ ] L262: * TODO: Replace this mock logic with real AJAX once ports are
|
|
||||||
|
|
||||||
## app\modules\devices\list.php
|
|
||||||
|
|
||||||
- [ ] L206: //TODO löschen geht nicht
|
|
||||||
|
|
||||||
## app\modules\floor_infrastructure\edit.php
|
|
||||||
|
|
||||||
- [ ] L277: //TODO drag an drop auf der stockwerkskarte für die patchfelder und wandbuchsen. buchsen haben eine einheitliche größe, und sind quadratisch, patchfelder sind auch für sich einheitlich, sind rechteckig und breiter als hoch
|
|
||||||
- [ ] L278: //TODO style in css files einsortieren
|
|
||||||
|
|
||||||
## app\modules\floor_infrastructure\list.php
|
|
||||||
|
|
||||||
- [ ] L143: <p><small>//TODO: SVG-Editor mit Drag & Drop für diese Objekte erweitern (siehe Stockwerke-Modul).</small></p>
|
|
||||||
|
|
||||||
## app\modules\floors\list.php
|
|
||||||
|
|
||||||
- [ ] L237: // TODO: AJAX-Delete implementieren
|
|
||||||
|
|
||||||
## app\modules\locations\edit.php
|
|
||||||
|
|
||||||
- [ ] L157: // TODO: AJAX-Delete implementieren
|
|
||||||
|
|
||||||
## app\modules\locations\list.php
|
|
||||||
|
|
||||||
- [ ] L134: //TODO design schlecht, mach es hübscher
|
|
||||||
- [ ] L208: //TODO style in css file
|
|
||||||
|
|
||||||
## app\modules\racks\edit.php
|
|
||||||
|
|
||||||
- [ ] L201: // TODO: AJAX-Delete implementieren
|
|
||||||
- [ ] L221: <!-- TODO: Rack-SVG laden -->
|
|
||||||
- [ ] L237: <!-- TODO: Löschen, falls edit -->
|
|
||||||
- [ ] L251: // TODO: Rack-ID aus PHP setzen
|
|
||||||
- [ ] L254: // TODO: Gerätepositionen an JS übergeben
|
|
||||||
|
|
||||||
## app\modules\racks\list.php
|
|
||||||
|
|
||||||
- [ ] L255: // TODO: AJAX-Delete implementieren
|
|
||||||
|
|
||||||
## app\templates\footer.php
|
|
||||||
|
|
||||||
- [ ] L14: <!-- TODO: Optional: Statusanzeige, Debug-Info, Session-Hinweis -->
|
|
||||||
- [ ] L17: <!-- TODO: evtl. JS für modale Fenster oder Flash Messages -->
|
|
||||||
|
|
||||||
## app\templates\header.php
|
|
||||||
|
|
||||||
- [ ] L24: <!-- TODO: Meta-Tags, Favicon -->
|
|
||||||
|
|
||||||
## app\templates\layout.php
|
|
||||||
|
|
||||||
- [ ] L11: * TODO: In Zukunft: zentrales Template-System (z.B. mit $content)
|
|
||||||
- [ ] L18: <!-- TODO: Dynamischen Content hier einfügen -->
|
|
||||||
|
|
||||||
## BUGS.md
|
|
||||||
|
|
||||||
- [ ] L3: - [ ] TODO Design vereinheitlichen
|
|
||||||
|
|
||||||
## doc\DATABASE.md
|
|
||||||
|
|
||||||
- [ ] L126: **TODO**
|
|
||||||
|
|
||||||
## IMPLEMENTATION_STATUS.md
|
|
||||||
|
|
||||||
- [ ] L80: - [ ] **Delete-Funktionen** - Löschen noch als TODO (als AJAX implementieren)
|
|
||||||
- [ ] L109: │ └── auth.php 🚧 TODO: Auth
|
|
||||||
|
|
||||||
## init.sql
|
|
||||||
|
|
||||||
- [ ] L372: -- TODO: Port-Konfiguration (Patchpanel ↔ Patchpanel, Patchpanel ↔ Netzwerkbuchse) wird über die `connections`-Tabelle geregelt.
|
|
||||||
|
|
||||||
## NEXT_STEPS.md
|
|
||||||
|
|
||||||
- [ ] L74: ## 🔧 Bekannte TODOs im Code
|
|
||||||
- [ ] L76: Alle noch offenen Punkte sind mit `// TODO:` gekennzeichnet:
|
|
||||||
- [ ] L79: # Alle TODOs finden:
|
|
||||||
- [ ] L80: grep -r "TODO:" app/modules/ --include="*.php"
|
|
||||||
- [ ] L83: Wichtigste TODOs:
|
|
||||||
|
|
||||||
## README.md
|
|
||||||
|
|
||||||
- [ ] L241: ### TODO: Patchpanel-Infrastruktur
|
|
||||||
- [ ] L253: - TODO: SVG-Editor um Drag & Drop für diese Objekte erweitern und Klicks direkt mit dem Modul verbinden.
|
|
||||||
|
|
||||||
- [ ] //TODO infrastruktur patchfelder löschen soll implementiert werden.
|
|
||||||
|
|
||||||
## Topologie-Abgleich (16. Februar 2026)
|
|
||||||
|
|
||||||
- [ ] #TODO: `connections.port_a_type` und `connections.port_b_type` um einen Patchpanel-Port-Typ erweitern (z. B. `patchpanel`) und auf `floor_patchpanel_ports.id` referenzieren.
|
|
||||||
- [ ] #TODO: Business-Regeln fuer Topologie in der Verbindungs-Validierung hinterlegen: Patchpanel-Port nur mit Patchpanel-Port oder Netzwerkbuchsen-Port verbinden.
|
|
||||||
- [ ] #TODO: Port-CRUD fuer Patchpanels ergaenzen: `floor_patchpanel_ports` beim Speichern aus `port_count` erzeugen/synchronisieren.
|
|
||||||
- [ ] #TODO: Port-CRUD fuer Netzwerkbuchsen ergaenzen: `network_outlet_ports` pflegen (mindestens ein Port je Buchse) und fuer 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();
|
||||||
|
|
||||||
|
|||||||
@@ -297,8 +297,83 @@ HTML, [
|
|||||||
* Sonstiges
|
* Sonstiges
|
||||||
* ========================= */
|
* ========================= */
|
||||||
|
|
||||||
// TODO: Weitere Helfer nach Bedarf
|
/**
|
||||||
// - Datum formatieren
|
* Formatiert Datum/Uhrzeit robust oder gibt Fallback zurueck.
|
||||||
// - Bytes → MB
|
*
|
||||||
// - UUID erzeugen
|
* @param string|null $value
|
||||||
// - SVG-Koordinaten normalisieren
|
* @param string $format
|
||||||
|
* @param string $fallback
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function formatDateTime(?string $value, string $format = 'd.m.Y H:i', string $fallback = '-'): string
|
||||||
|
{
|
||||||
|
if ($value === null || trim($value) === '') {
|
||||||
|
return $fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
$timestamp = strtotime($value);
|
||||||
|
if ($timestamp === false) {
|
||||||
|
return $fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return date($format, $timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formatiert Byte-Werte in menschenlesbare Einheit.
|
||||||
|
*
|
||||||
|
* @param int|float $bytes
|
||||||
|
* @param int $precision
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function formatBytes($bytes, int $precision = 2): string
|
||||||
|
{
|
||||||
|
$value = (float)$bytes;
|
||||||
|
if ($value <= 0) {
|
||||||
|
return '0 B';
|
||||||
|
}
|
||||||
|
|
||||||
|
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
$power = min((int)floor(log($value, 1024)), count($units) - 1);
|
||||||
|
$scaled = $value / (1024 ** $power);
|
||||||
|
|
||||||
|
return number_format($scaled, $precision, '.', '') . ' ' . $units[$power];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erzeugt eine UUID v4.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
function generateUuidV4(): string
|
||||||
|
{
|
||||||
|
$bytes = random_bytes(16);
|
||||||
|
$bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40);
|
||||||
|
$bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80);
|
||||||
|
$hex = bin2hex($bytes);
|
||||||
|
|
||||||
|
return sprintf(
|
||||||
|
'%s-%s-%s-%s-%s',
|
||||||
|
substr($hex, 0, 8),
|
||||||
|
substr($hex, 8, 4),
|
||||||
|
substr($hex, 12, 4),
|
||||||
|
substr($hex, 16, 4),
|
||||||
|
substr($hex, 20, 12)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Klemmt eine SVG-Koordinate auf gueltigen Bereich.
|
||||||
|
*
|
||||||
|
* @param float $value
|
||||||
|
* @param float $min
|
||||||
|
* @param float $max
|
||||||
|
* @param int $precision
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
function normalizeSvgCoordinate(float $value, float $min, float $max, int $precision = 2): float
|
||||||
|
{
|
||||||
|
$normalized = max($min, min($max, $value));
|
||||||
|
return round($normalized, $precision);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
65
app/modules/connections/delete.php
Normal file
65
app/modules/connections/delete.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* app/modules/connections/delete.php
|
||||||
|
*
|
||||||
|
* Loescht eine Verbindung (AJAX-POST bevorzugt, GET-Fallback fuer Redirects).
|
||||||
|
*/
|
||||||
|
|
||||||
|
$isPost = ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST';
|
||||||
|
$connectionId = (int)($_POST['id'] ?? $_GET['id'] ?? 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';
|
||||||
|
header('Location: ?module=connections&action=list');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$connection = $sql->single(
|
||||||
|
"SELECT id FROM connections WHERE id = ?",
|
||||||
|
"i",
|
||||||
|
[$connectionId]
|
||||||
|
);
|
||||||
|
|
||||||
|
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';
|
||||||
|
header('Location: ?module=connections&action=list');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = $sql->set(
|
||||||
|
"DELETE FROM connections WHERE id = ?",
|
||||||
|
"i",
|
||||||
|
[$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) {
|
||||||
|
$_SESSION['success'] = 'Verbindung geloescht';
|
||||||
|
} else {
|
||||||
|
$_SESSION['error'] = 'Verbindung konnte nicht geloescht werden';
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Location: ?module=connections&action=list');
|
||||||
|
exit;
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -2,19 +2,13 @@
|
|||||||
/**
|
/**
|
||||||
* app/modules/connections/list.php
|
* app/modules/connections/list.php
|
||||||
*
|
*
|
||||||
* Übersicht der Netzwerkverbindungen
|
* Uebersicht der Netzwerkverbindungen
|
||||||
* - Tabellarische Liste aller Verbindungen
|
|
||||||
* - Filter nach Geräten, VLANs, Status
|
|
||||||
* - Später: Visuelle Netzwerk-Topologie
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// =========================
|
|
||||||
// Filter einlesen
|
|
||||||
// =========================
|
|
||||||
$search = trim($_GET['search'] ?? '');
|
$search = trim($_GET['search'] ?? '');
|
||||||
$deviceId = (int)($_GET['device_id'] ?? 0);
|
$deviceId = (int)($_GET['device_id'] ?? 0);
|
||||||
|
$selectedConnectionId = (int)($_GET['connection_id'] ?? 0);
|
||||||
|
|
||||||
// Einheitliche Endpunkt-Aufloesung fuer polymorphe Port-Typen.
|
|
||||||
$endpointUnionSql = "
|
$endpointUnionSql = "
|
||||||
SELECT
|
SELECT
|
||||||
'device' AS endpoint_type,
|
'device' AS endpoint_type,
|
||||||
@@ -60,16 +54,13 @@ $endpointUnionSql = "
|
|||||||
LEFT JOIN floors f ON f.id = fp.floor_id
|
LEFT JOIN floors f ON f.id = fp.floor_id
|
||||||
";
|
";
|
||||||
|
|
||||||
// =========================
|
|
||||||
// WHERE-Clause bauen
|
|
||||||
// =========================
|
|
||||||
$where = [];
|
$where = [];
|
||||||
$types = '';
|
$types = '';
|
||||||
$params = [];
|
$params = [];
|
||||||
|
|
||||||
if ($search !== '') {
|
if ($search !== '') {
|
||||||
$where[] = "(e1.owner_name LIKE ? OR e2.owner_name LIKE ? OR e1.port_name LIKE ? OR e2.port_name LIKE ?)";
|
$where[] = "(e1.owner_name LIKE ? OR e2.owner_name LIKE ? OR e1.port_name LIKE ? OR e2.port_name LIKE ?)";
|
||||||
$types .= "ssss";
|
$types .= 'ssss';
|
||||||
$params[] = "%$search%";
|
$params[] = "%$search%";
|
||||||
$params[] = "%$search%";
|
$params[] = "%$search%";
|
||||||
$params[] = "%$search%";
|
$params[] = "%$search%";
|
||||||
@@ -78,16 +69,13 @@ if ($search !== '') {
|
|||||||
|
|
||||||
if ($deviceId > 0) {
|
if ($deviceId > 0) {
|
||||||
$where[] = "(e1.owner_device_id = ? OR e2.owner_device_id = ?)";
|
$where[] = "(e1.owner_device_id = ? OR e2.owner_device_id = ?)";
|
||||||
$types .= "ii";
|
$types .= 'ii';
|
||||||
$params[] = $deviceId;
|
$params[] = $deviceId;
|
||||||
$params[] = $deviceId;
|
$params[] = $deviceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
$whereSql = $where ? "WHERE " . implode(" AND ", $where) : "";
|
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||||
|
|
||||||
// =========================
|
|
||||||
// Verbindungen laden
|
|
||||||
// =========================
|
|
||||||
$connections = $sql->get(
|
$connections = $sql->get(
|
||||||
"SELECT
|
"SELECT
|
||||||
c.id,
|
c.id,
|
||||||
@@ -125,10 +113,22 @@ $connections = $sql->get(
|
|||||||
$params
|
$params
|
||||||
);
|
);
|
||||||
|
|
||||||
// =========================
|
$devices = $sql->get('SELECT id, name FROM devices ORDER BY name', '', []);
|
||||||
// Filter-Daten
|
|
||||||
// =========================
|
$selectedConnection = null;
|
||||||
$devices = $sql->get("SELECT id, name FROM devices ORDER BY name", "", []);
|
if ($selectedConnectionId > 0) {
|
||||||
|
foreach ((array)$connections as $entry) {
|
||||||
|
if ((int)($entry['id'] ?? 0) === $selectedConnectionId) {
|
||||||
|
$selectedConnection = $entry;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($selectedConnection === null && !empty($connections)) {
|
||||||
|
$selectedConnection = $connections[0];
|
||||||
|
$selectedConnectionId = (int)($selectedConnection['id'] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
$selectedDevice = null;
|
$selectedDevice = null;
|
||||||
$selectedDevicePorts = [];
|
$selectedDevicePorts = [];
|
||||||
@@ -140,7 +140,7 @@ if ($deviceId > 0) {
|
|||||||
FROM devices d
|
FROM devices d
|
||||||
LEFT JOIN device_types dt ON d.device_type_id = dt.id
|
LEFT JOIN device_types dt ON d.device_type_id = dt.id
|
||||||
WHERE d.id = ?",
|
WHERE d.id = ?",
|
||||||
"i",
|
'i',
|
||||||
[$deviceId]
|
[$deviceId]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ if ($deviceId > 0) {
|
|||||||
JOIN device_ports dp ON dp.id = dpm.device_port_id
|
JOIN device_ports dp ON dp.id = dpm.device_port_id
|
||||||
WHERE dp.device_id = ?
|
WHERE dp.device_id = ?
|
||||||
) p",
|
) p",
|
||||||
"ii",
|
'ii',
|
||||||
[$deviceId, $deviceId]
|
[$deviceId, $deviceId]
|
||||||
)['cnt'] ?? 0);
|
)['cnt'] ?? 0);
|
||||||
|
|
||||||
@@ -187,7 +187,7 @@ if ($deviceId > 0) {
|
|||||||
OR (c.port_b_type = 'patchpanel' AND e2.endpoint_type = 'floor_patchpanel')
|
OR (c.port_b_type = 'patchpanel' AND e2.endpoint_type = 'floor_patchpanel')
|
||||||
)
|
)
|
||||||
WHERE e1.owner_device_id = ? OR e2.owner_device_id = ?",
|
WHERE e1.owner_device_id = ? OR e2.owner_device_id = ?",
|
||||||
"ii",
|
'ii',
|
||||||
[$deviceId, $deviceId]
|
[$deviceId, $deviceId]
|
||||||
)['cnt'] ?? 0);
|
)['cnt'] ?? 0);
|
||||||
|
|
||||||
@@ -207,7 +207,7 @@ if ($deviceId > 0) {
|
|||||||
) p
|
) p
|
||||||
ORDER BY sort_id
|
ORDER BY sort_id
|
||||||
LIMIT 12",
|
LIMIT 12",
|
||||||
"ii",
|
'ii',
|
||||||
[$deviceId, $deviceId]
|
[$deviceId, $deviceId]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -230,28 +230,42 @@ if ($deviceId > 0) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$buildListUrl = static function (array $extra = []) use ($search, $deviceId): string {
|
||||||
|
$query = ['module' => 'connections', 'action' => 'list'];
|
||||||
|
if ($search !== '') {
|
||||||
|
$query['search'] = $search;
|
||||||
|
}
|
||||||
|
if ($deviceId > 0) {
|
||||||
|
$query['device_id'] = $deviceId;
|
||||||
|
}
|
||||||
|
foreach ($extra as $key => $value) {
|
||||||
|
if ($value === null || $value === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$query[$key] = $value;
|
||||||
|
}
|
||||||
|
return '?' . http_build_query($query);
|
||||||
|
};
|
||||||
?>
|
?>
|
||||||
|
|
||||||
|
<div class="connections-layout">
|
||||||
<div class="connections-container">
|
<div class="connections-container">
|
||||||
<h1>Netzwerkverbindungen</h1>
|
<h1>Netzwerkverbindungen</h1>
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
Filter-Toolbar
|
|
||||||
========================= -->
|
|
||||||
<div class="filter-form">
|
<div class="filter-form">
|
||||||
<form method="GET">
|
<form method="GET">
|
||||||
<input type="hidden" name="module" value="connections">
|
<input type="hidden" name="module" value="connections">
|
||||||
<input type="hidden" name="action" value="list">
|
<input type="hidden" name="action" value="list">
|
||||||
|
|
||||||
<input type="text" name="search" placeholder="Suche nach Gerät oder Port…"
|
<input type="text" name="search" placeholder="Suche nach Geraet oder Port..."
|
||||||
value="<?php echo htmlspecialchars($search); ?>" class="search-input">
|
value="<?php echo htmlspecialchars($search); ?>" class="search-input">
|
||||||
|
|
||||||
<select name="device_id">
|
<select name="device_id">
|
||||||
<option value="">- Alle Geräte -</option>
|
<option value="">- Alle Geraete -</option>
|
||||||
<?php foreach ($devices as $device): ?>
|
<?php foreach ($devices as $device): ?>
|
||||||
<option value="<?php echo $device['id']; ?>"
|
<option value="<?php echo (int)$device['id']; ?>"
|
||||||
<?php echo $device['id'] === $deviceId ? 'selected' : ''; ?>>
|
<?php echo ((int)$device['id'] === $deviceId) ? 'selected' : ''; ?>>
|
||||||
<?php echo htmlspecialchars($device['name']); ?>
|
<?php echo htmlspecialchars((string)$device['name']); ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
@@ -262,15 +276,12 @@ if ($deviceId > 0) {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
Verbindungs-Tabelle
|
|
||||||
========================= -->
|
|
||||||
<?php if (!empty($connections)): ?>
|
<?php if (!empty($connections)): ?>
|
||||||
<table class="connections-list">
|
<table class="connections-list">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Von (Gerät → Port)</th>
|
<th>Von (Geraet -> Port)</th>
|
||||||
<th>Nach (Gerät → Port)</th>
|
<th>Nach (Geraet -> Port)</th>
|
||||||
<th>VLANs</th>
|
<th>VLANs</th>
|
||||||
<th>Beschreibung</th>
|
<th>Beschreibung</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
@@ -280,67 +291,61 @@ if ($deviceId > 0) {
|
|||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($connections as $conn): ?>
|
<?php foreach ($connections as $conn): ?>
|
||||||
<?php
|
<?php
|
||||||
$comment = trim($conn['comment'] ?? '');
|
$connId = (int)($conn['id'] ?? 0);
|
||||||
|
$comment = trim((string)($conn['comment'] ?? ''));
|
||||||
$hasMissingInfo = empty($conn['endpoint_a_name']) || empty($conn['endpoint_b_name'])
|
$hasMissingInfo = empty($conn['endpoint_a_name']) || empty($conn['endpoint_b_name'])
|
||||||
|| empty($conn['port_a_name']) || empty($conn['port_b_name']);
|
|| empty($conn['port_a_name']) || empty($conn['port_b_name']);
|
||||||
$commentLower = mb_strtolower($comment, 'UTF-8');
|
$commentLower = mb_strtolower($comment, 'UTF-8');
|
||||||
$warningFromComment = preg_match('/warn|achtung|critical/', $commentLower);
|
$warningFromComment = preg_match('/warn|achtung|critical/', $commentLower) === 1;
|
||||||
$hasWarning = $hasMissingInfo || $warningFromComment;
|
$hasWarning = $hasMissingInfo || $warningFromComment;
|
||||||
?>
|
$rowClass = $connId === $selectedConnectionId ? 'connection-row-selected' : '';
|
||||||
<tr>
|
$vlanList = [];
|
||||||
<td>
|
if (!empty($conn['vlan_config'])) {
|
||||||
<strong><?php echo htmlspecialchars($conn['endpoint_a_name'] ?? 'N/A'); ?></strong><br>
|
$vlanList = (array)json_decode((string)$conn['vlan_config'], true);
|
||||||
<small><?php echo htmlspecialchars($conn['port_a_name'] ?? '—'); ?></small>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<strong><?php echo htmlspecialchars($conn['endpoint_b_name'] ?? 'N/A'); ?></strong><br>
|
|
||||||
<small><?php echo htmlspecialchars($conn['port_b_name'] ?? '—'); ?></small>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<small>
|
|
||||||
<?php
|
|
||||||
if ($conn['vlan_config']) {
|
|
||||||
$vlan = json_decode($conn['vlan_config'], true);
|
|
||||||
echo htmlspecialchars(implode(', ', (array)$vlan));
|
|
||||||
} else {
|
|
||||||
echo '—';
|
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
</small>
|
<tr class="<?php echo $rowClass; ?>">
|
||||||
|
<td>
|
||||||
|
<strong><?php echo htmlspecialchars((string)($conn['endpoint_a_name'] ?? 'N/A')); ?></strong><br>
|
||||||
|
<small><?php echo htmlspecialchars((string)($conn['port_a_name'] ?? '-')); ?></small>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<small><?php echo htmlspecialchars($conn['comment'] ?? ''); ?></small>
|
<strong><?php echo htmlspecialchars((string)($conn['endpoint_b_name'] ?? 'N/A')); ?></strong><br>
|
||||||
|
<small><?php echo htmlspecialchars((string)($conn['port_b_name'] ?? '-')); ?></small>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<small><?php echo !empty($vlanList) ? htmlspecialchars(implode(', ', $vlanList)) : '-'; ?></small>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<small><?php echo htmlspecialchars($comment); ?></small>
|
||||||
|
</td>
|
||||||
|
|
||||||
<td class="status-cell">
|
<td class="status-cell">
|
||||||
<?php if ($hasWarning): ?>
|
<?php if ($hasWarning): ?>
|
||||||
<span class="status-badge status-badge-warning" title="Unvollständige oder kritische Verbindung">
|
<span class="status-badge status-badge-warning" title="Unvollstaendige oder kritische Verbindung">Warnung</span>
|
||||||
⚠️ Warnung
|
|
||||||
</span>
|
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<span class="status-badge status-badge-ok" title="Verbindung vollständig">
|
<span class="status-badge status-badge-ok" title="Verbindung vollstaendig">OK</span>
|
||||||
✔️ OK
|
|
||||||
</span>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<a href="?module=connections&action=edit&id=<?php echo $conn['id']; ?>" class="button button-small">Bearbeiten</a>
|
<a href="<?php echo htmlspecialchars($buildListUrl(['connection_id' => $connId])); ?>" class="button button-small">Details</a>
|
||||||
<a href="?module=connections&action=swap&id=<?php echo $conn['id']; ?>" class="button button-small" onclick="return confirm('Von/Nach fuer diese Verbindung vertauschen?');">Von/Nach tauschen</a>
|
<a href="?module=connections&action=edit&id=<?php echo $connId; ?>" class="button button-small">Bearbeiten</a>
|
||||||
<a href="#" class="button button-small button-danger"
|
<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>
|
||||||
data-confirm-delete="true"
|
<button
|
||||||
data-confirm-message="Diese Verbindung wirklich löschen?"
|
type="button"
|
||||||
data-confirm-feedback="Löschen noch nicht implementiert">
|
class="button button-small button-danger js-connection-delete"
|
||||||
Löschen
|
data-connection-id="<?php echo $connId; ?>">
|
||||||
</a>
|
Loeschen
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<p>Keine Verbindungen gefunden.</p>
|
<p>Keine Verbindungen gefunden.</p>
|
||||||
@@ -353,15 +358,48 @@ if ($deviceId > 0) {
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- =========================
|
<aside class="connections-sidebar">
|
||||||
Sidebar / Details
|
<section class="sidebar-card">
|
||||||
========================= -->
|
<h3>Ausgewaehlte Verbindung</h3>
|
||||||
|
<?php if ($selectedConnection): ?>
|
||||||
|
<?php
|
||||||
|
$selectedConnId = (int)($selectedConnection['id'] ?? 0);
|
||||||
|
$selectedVlans = [];
|
||||||
|
if (!empty($selectedConnection['vlan_config'])) {
|
||||||
|
$selectedVlans = (array)json_decode((string)$selectedConnection['vlan_config'], true);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<p><strong>ID:</strong> #<?php echo $selectedConnId; ?></p>
|
||||||
|
<p><strong>Von:</strong><br>
|
||||||
|
<?php echo htmlspecialchars((string)($selectedConnection['endpoint_a_name'] ?? 'N/A')); ?><br>
|
||||||
|
<small><?php echo htmlspecialchars((string)($selectedConnection['port_a_name'] ?? '-')); ?></small>
|
||||||
|
</p>
|
||||||
|
<p><strong>Nach:</strong><br>
|
||||||
|
<?php echo htmlspecialchars((string)($selectedConnection['endpoint_b_name'] ?? 'N/A')); ?><br>
|
||||||
|
<small><?php echo htmlspecialchars((string)($selectedConnection['port_b_name'] ?? '-')); ?></small>
|
||||||
|
</p>
|
||||||
|
<p><strong>VLANs:</strong> <?php echo !empty($selectedVlans) ? htmlspecialchars(implode(', ', $selectedVlans)) : '-'; ?></p>
|
||||||
|
<p><strong>Kommentar:</strong> <?php echo htmlspecialchars((string)($selectedConnection['comment'] ?? '-')); ?></p>
|
||||||
|
<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=swap&id=<?php echo $selectedConnId; ?>" class="button button-small" onclick="return confirm('Von/Nach fuer diese Verbindung vertauschen?');">Tauschen</a>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button button-small button-danger js-connection-delete"
|
||||||
|
data-connection-id="<?php echo $selectedConnId; ?>">
|
||||||
|
Loeschen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<p><em>Keine Verbindung ausgewaehlt.</em></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
|
|
||||||
<aside class="sidebar">
|
<section class="sidebar-card">
|
||||||
<?php if ($selectedDevice): ?>
|
<?php if ($selectedDevice): ?>
|
||||||
<h3>Ausgewähltes Gerät</h3>
|
<h3>Ausgewaehltes Geraet</h3>
|
||||||
<p><strong><?php echo htmlspecialchars($selectedDevice['name']); ?></strong></p>
|
<p><strong><?php echo htmlspecialchars((string)$selectedDevice['name']); ?></strong></p>
|
||||||
<p>Typ: <?php echo htmlspecialchars($selectedDevice['type_name'] ?? '—'); ?></p>
|
<p>Typ: <?php echo htmlspecialchars((string)($selectedDevice['type_name'] ?? '-')); ?></p>
|
||||||
<p>Ports: <?php echo (int)$selectedDevice['port_count']; ?></p>
|
<p>Ports: <?php echo (int)$selectedDevice['port_count']; ?></p>
|
||||||
<p>Verbindungen: <?php echo (int)$selectedDevice['connection_count']; ?></p>
|
<p>Verbindungen: <?php echo (int)$selectedDevice['connection_count']; ?></p>
|
||||||
<p>
|
<p>
|
||||||
@@ -369,20 +407,53 @@ if ($deviceId > 0) {
|
|||||||
<?php if (!empty($selectedDeviceVlans)): ?>
|
<?php if (!empty($selectedDeviceVlans)): ?>
|
||||||
<?php echo htmlspecialchars(implode(', ', $selectedDeviceVlans)); ?>
|
<?php echo htmlspecialchars(implode(', ', $selectedDeviceVlans)); ?>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
—
|
-
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</p>
|
</p>
|
||||||
<?php if (!empty($selectedDevicePorts)): ?>
|
<?php if (!empty($selectedDevicePorts)): ?>
|
||||||
<h4>Ports (max. 12)</h4>
|
<h4>Ports (max. 12)</h4>
|
||||||
<ul>
|
<ul>
|
||||||
<?php foreach ($selectedDevicePorts as $port): ?>
|
<?php foreach ($selectedDevicePorts as $port): ?>
|
||||||
<li><?php echo htmlspecialchars($port['name']); ?></li>
|
<li><?php echo htmlspecialchars((string)$port['name']); ?></li>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</ul>
|
</ul>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<p><em>Bitte ein Gerät im Filter auswählen.</em></p>
|
<h3>Ausgewaehltes Geraet</h3>
|
||||||
|
<p><em>Bitte ein Geraet im Filter auswaehlen.</em></p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
<!-- TODO: Verbindung bearbeiten / löschen -->
|
|
||||||
</aside>
|
</aside>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.querySelectorAll('.js-connection-delete').forEach((button) => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
const id = Number(button.dataset.connectionId || '0');
|
||||||
|
if (id <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm('Diese Verbindung wirklich loeschen?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('?module=connections&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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
<!-- JS -->
|
<!-- JS -->
|
||||||
<script src="/assets/js/app.js" defer></script>
|
<script src="/assets/js/app.js" defer></script>
|
||||||
|
<script src="/assets/js/dashboard.js" defer></script>
|
||||||
<script src="/assets/js/svg-editor.js" defer></script>
|
<script src="/assets/js/svg-editor.js" defer></script>
|
||||||
<script src="/assets/js/network-view.js" defer></script>
|
<script src="/assets/js/network-view.js" defer></script>
|
||||||
|
|
||||||
@@ -63,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