Compare commits
46 Commits
a3799dd8f5
...
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 | |||
| 4a23713d31 | |||
| 510a248edb | |||
| 12141485ae | |||
| d1d89dd4e3 | |||
| 2a1732323d | |||
| f80ab4aaa9 | |||
| 3bc5a2ca04 | |||
| 4efd54613a | |||
| f14f92fdd8 | |||
| 3f1e5aacc9 | |||
| 531320b408 | |||
| 63e60cbfcf | |||
| 092811fda8 | |||
| 87ba23cf57 | |||
| 2891f52f84 | |||
| 2c90ff8ddf | |||
| 39f7f9b733 | |||
| 0a7505416e | |||
| 2b9726d362 | |||
| 3507024bc3 | |||
| 8ee3252c51 | |||
| 52746508fa | |||
| e58ee6f41b |
86
AGENTS.md
86
AGENTS.md
@@ -1,52 +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).
|
|
||||||
|
|||||||
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! 🚀**
|
|
||||||
4
NOTES.md
4
NOTES.md
@@ -1,4 +0,0 @@
|
|||||||
# Notizen
|
|
||||||
|
|
||||||
https://chatgpt.com/share/698517b9-b1e4-800e-bbd8-d207dfb326f0
|
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,194 +1,381 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* app/api/connections.php
|
* app/api/connections.php
|
||||||
*
|
|
||||||
* API für Netzwerkverbindungen (Port ↔ Port)
|
|
||||||
* - Laden der Topologie
|
|
||||||
* - Anlegen / Bearbeiten / Löschen von Verbindungen
|
|
||||||
* - Unterstützt freie Verbindungstypen (Kupfer, LWL, BNC, Token Ring, etc.)
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
require_once __DIR__ . '/../bootstrap.php';
|
require_once __DIR__ . '/../bootstrap.php';
|
||||||
|
requireAuth();
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// TODO: Single-User-Auth prüfen
|
|
||||||
// if (!$_SESSION['user']) { http_response_code(403); exit; }
|
|
||||||
|
|
||||||
$action = $_GET['action'] ?? 'load';
|
$action = $_GET['action'] ?? 'load';
|
||||||
|
|
||||||
/* =========================
|
|
||||||
* Router
|
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
switch ($action) {
|
switch ($action) {
|
||||||
|
|
||||||
case 'load':
|
case 'load':
|
||||||
loadConnections($sql);
|
loadConnections($sql);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'save':
|
case 'save':
|
||||||
saveConnection($sql);
|
saveConnection($sql);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'delete':
|
case 'delete':
|
||||||
deleteConnection($sql);
|
deleteConnection($sql);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
http_response_code(400);
|
jsonError('Unbekannte Aktion', 400);
|
||||||
echo json_encode(['error' => 'Unbekannte Aktion']);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
function jsonError(string $message, int $status = 400): void
|
||||||
* Aktionen
|
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lädt alle Geräte + Ports + Verbindungen für eine Netzwerkansicht
|
|
||||||
*/
|
|
||||||
function loadConnections($sql)
|
|
||||||
{
|
{
|
||||||
$contextId = $_GET['context_id'] ?? null;
|
http_response_code($status);
|
||||||
|
echo json_encode(['error' => $message]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
if (!$contextId) {
|
function normalizeEndpointType(string $type): ?string
|
||||||
http_response_code(400);
|
{
|
||||||
echo json_encode(['error' => 'context_id fehlt']);
|
$map = [
|
||||||
return;
|
'device' => 'device',
|
||||||
|
'device_ports' => 'device',
|
||||||
|
'module' => 'module',
|
||||||
|
'module_ports' => 'module',
|
||||||
|
'outlet' => 'outlet',
|
||||||
|
'network_outlet_ports' => 'outlet',
|
||||||
|
'patchpanel' => 'patchpanel',
|
||||||
|
'floor_patchpanel_ports' => 'patchpanel',
|
||||||
|
];
|
||||||
|
|
||||||
|
$key = strtolower(trim($type));
|
||||||
|
return $map[$key] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function endpointExists($sql, string $type, int $id): bool
|
||||||
|
{
|
||||||
|
if ($id <= 0) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Kontext definieren (Standort, Rack, Floor, gesamtes Netz)
|
if ($type === 'device') {
|
||||||
|
$row = $sql->single('SELECT id FROM device_ports WHERE id = ?', 'i', [$id]);
|
||||||
|
return !empty($row);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type === 'module') {
|
||||||
|
$row = $sql->single('SELECT id FROM module_ports WHERE id = ?', 'i', [$id]);
|
||||||
|
return !empty($row);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type === 'outlet') {
|
||||||
|
$row = $sql->single('SELECT id FROM network_outlet_ports WHERE id = ?', 'i', [$id]);
|
||||||
|
return !empty($row);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type === 'patchpanel') {
|
||||||
|
$row = $sql->single('SELECT id FROM floor_patchpanel_ports WHERE id = ?', 'i', [$id]);
|
||||||
|
return !empty($row);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
$contextType = strtolower(trim((string)($_GET['context_type'] ?? 'all')));
|
||||||
|
$contextId = isset($_GET['context_id']) ? (int)$_GET['context_id'] : 0;
|
||||||
|
|
||||||
|
$where = '';
|
||||||
|
$bindType = '';
|
||||||
|
$bindValues = [];
|
||||||
|
|
||||||
|
if ($contextType !== 'all') {
|
||||||
|
if ($contextId <= 0) {
|
||||||
|
jsonError('context_id fehlt oder ist ungueltig', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($contextType === 'location') {
|
||||||
|
$where = ' WHERE b.location_id = ?';
|
||||||
|
$bindType = 'i';
|
||||||
|
$bindValues = [$contextId];
|
||||||
|
} elseif ($contextType === 'building') {
|
||||||
|
$where = ' WHERE f.building_id = ?';
|
||||||
|
$bindType = 'i';
|
||||||
|
$bindValues = [$contextId];
|
||||||
|
} elseif ($contextType === 'floor') {
|
||||||
|
$where = ' WHERE r.floor_id = ?';
|
||||||
|
$bindType = 'i';
|
||||||
|
$bindValues = [$contextId];
|
||||||
|
} elseif ($contextType === 'rack') {
|
||||||
|
$where = ' WHERE d.rack_id = ?';
|
||||||
|
$bindType = 'i';
|
||||||
|
$bindValues = [$contextId];
|
||||||
|
} else {
|
||||||
|
jsonError('Ungueltiger Kontext. Erlaubt: all, location, building, floor, rack', 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- Geräte ---------- */
|
|
||||||
$devices = $sql->get(
|
$devices = $sql->get(
|
||||||
"SELECT id, name, device_type_id, pos_x, pos_y
|
"SELECT d.id, d.name, d.device_type_id, d.rack_id, 0 AS pos_x, 0 AS pos_y
|
||||||
FROM devices
|
FROM devices d
|
||||||
WHERE context_id = ?",
|
LEFT JOIN racks r ON r.id = d.rack_id
|
||||||
"i",
|
LEFT JOIN floors f ON f.id = r.floor_id
|
||||||
[$contextId]
|
LEFT JOIN buildings b ON b.id = f.building_id" . $where,
|
||||||
|
$bindType,
|
||||||
|
$bindValues
|
||||||
);
|
);
|
||||||
|
|
||||||
/* ---------- Ports ---------- */
|
|
||||||
$ports = $sql->get(
|
$ports = $sql->get(
|
||||||
"SELECT p.id, p.device_id, p.name, p.port_type_id
|
"SELECT dp.id, dp.device_id, dp.name, dp.port_type_id
|
||||||
FROM ports p
|
FROM device_ports dp
|
||||||
JOIN devices d ON d.id = p.device_id
|
JOIN devices d ON d.id = dp.device_id
|
||||||
WHERE d.context_id = ?",
|
LEFT JOIN racks r ON r.id = d.rack_id
|
||||||
"i",
|
LEFT JOIN floors f ON f.id = r.floor_id
|
||||||
[$contextId]
|
LEFT JOIN buildings b ON b.id = f.building_id" . $where,
|
||||||
|
$bindType,
|
||||||
|
$bindValues
|
||||||
);
|
);
|
||||||
|
|
||||||
/* ---------- Verbindungen ---------- */
|
|
||||||
$connections = $sql->get(
|
$connections = $sql->get(
|
||||||
"SELECT
|
"SELECT id, connection_type_id, port_a_type, port_a_id, port_b_type, port_b_id, vlan_config, mode, comment
|
||||||
c.id,
|
FROM connections",
|
||||||
c.connection_type_id,
|
'',
|
||||||
c.port_a_id,
|
|
||||||
c.port_b_id,
|
|
||||||
c.vlan,
|
|
||||||
c.mode,
|
|
||||||
c.comment
|
|
||||||
FROM connections c",
|
|
||||||
"",
|
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'devices' => $devices,
|
'devices' => $devices ?: [],
|
||||||
'ports' => $ports,
|
'ports' => $ports ?: [],
|
||||||
'connections' => $connections
|
'connections' => $connections ?: []
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function resolveConnectionTypeId($sql, array $data): int
|
||||||
* Speichert eine Verbindung (neu oder Update)
|
|
||||||
*/
|
|
||||||
function saveConnection($sql)
|
|
||||||
{
|
{
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
if (!empty($data['connection_type_id'])) {
|
||||||
|
$requestedId = (int)$data['connection_type_id'];
|
||||||
if (!$data) {
|
$exists = $sql->single('SELECT id FROM connection_types WHERE id = ?', 'i', [$requestedId]);
|
||||||
http_response_code(400);
|
if (!$exists) {
|
||||||
echo json_encode(['error' => 'Ungültige JSON-Daten']);
|
jsonError('connection_type_id existiert nicht', 400);
|
||||||
return;
|
}
|
||||||
|
return $requestedId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Validierung
|
$defaultType = $sql->single('SELECT id FROM connection_types ORDER BY id ASC LIMIT 1');
|
||||||
// - port_a_id vorhanden
|
if (!$defaultType) {
|
||||||
// - port_b_id vorhanden
|
jsonError('Kein Verbindungstyp vorhanden', 400);
|
||||||
// - Verbindungstyp erlaubt
|
|
||||||
|
|
||||||
if (!empty($data['id'])) {
|
|
||||||
// UPDATE
|
|
||||||
$rows = $sql->set(
|
|
||||||
"UPDATE connections
|
|
||||||
SET connection_type_id = ?, port_a_id = ?, port_b_id = ?, vlan = ?, mode = ?, comment = ?
|
|
||||||
WHERE id = ?",
|
|
||||||
"iiiissi",
|
|
||||||
[
|
|
||||||
$data['connection_type_id'],
|
|
||||||
$data['port_a_id'],
|
|
||||||
$data['port_b_id'],
|
|
||||||
$data['vlan'],
|
|
||||||
$data['mode'],
|
|
||||||
$data['comment'],
|
|
||||||
$data['id']
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
echo json_encode([
|
|
||||||
'status' => 'updated',
|
|
||||||
'rows' => $rows
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
// INSERT
|
|
||||||
$id = $sql->set(
|
|
||||||
"INSERT INTO connections
|
|
||||||
(connection_type_id, port_a_id, port_b_id, vlan, mode, comment)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?)",
|
|
||||||
"iiiiss",
|
|
||||||
[
|
|
||||||
$data['connection_type_id'],
|
|
||||||
$data['port_a_id'],
|
|
||||||
$data['port_b_id'],
|
|
||||||
$data['vlan'],
|
|
||||||
$data['mode'],
|
|
||||||
$data['comment']
|
|
||||||
],
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
echo json_encode([
|
|
||||||
'status' => 'created',
|
|
||||||
'id' => $id
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (int)$defaultType['id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function saveConnection($sql): void
|
||||||
* Löscht eine Verbindung
|
|
||||||
*/
|
|
||||||
function deleteConnection($sql)
|
|
||||||
{
|
{
|
||||||
$id = $_GET['id'] ?? null;
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
jsonError('Methode nicht erlaubt', 405);
|
||||||
|
}
|
||||||
|
|
||||||
if (!$id) {
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
http_response_code(400);
|
if (!is_array($data)) {
|
||||||
echo json_encode(['error' => 'ID fehlt']);
|
jsonError('Ungueltige JSON-Daten', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$portAType = normalizeEndpointType((string)($data['port_a_type'] ?? ''));
|
||||||
|
$portBType = normalizeEndpointType((string)($data['port_b_type'] ?? ''));
|
||||||
|
$portAId = (int)($data['port_a_id'] ?? 0);
|
||||||
|
$portBId = (int)($data['port_b_id'] ?? 0);
|
||||||
|
|
||||||
|
if ($portAType === null || $portBType === null) {
|
||||||
|
jsonError('port_a_type/port_b_type ungueltig', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($portAId <= 0 || $portBId <= 0) {
|
||||||
|
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) {
|
||||||
|
jsonError('Port A und Port B duerfen nicht identisch sein', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!endpointExists($sql, $portAType, $portAId) || !endpointExists($sql, $portBType, $portBId)) {
|
||||||
|
jsonError('Mindestens ein Endpunkt existiert nicht', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$connectionTypeId = resolveConnectionTypeId($sql, $data);
|
||||||
|
|
||||||
|
$vlanConfig = $data['vlan_config'] ?? null;
|
||||||
|
if (is_array($vlanConfig)) {
|
||||||
|
$vlanConfig = json_encode($vlanConfig);
|
||||||
|
} elseif (!is_string($vlanConfig) && $vlanConfig !== null) {
|
||||||
|
jsonError('vlan_config muss String, Array oder null sein', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$mode = isset($data['mode']) ? (string)$data['mode'] : null;
|
||||||
|
$comment = isset($data['comment']) ? (string)$data['comment'] : null;
|
||||||
|
|
||||||
|
$connectionId = !empty($data['id']) ? (int)$data['id'] : 0;
|
||||||
|
$usage = buildEndpointUsageMap($sql, $connectionId);
|
||||||
|
$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) {
|
||||||
|
jsonError('Verbindung existiert nicht', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = $sql->set(
|
||||||
|
'UPDATE connections
|
||||||
|
SET connection_type_id = ?, port_a_type = ?, port_a_id = ?, port_b_type = ?, port_b_id = ?, vlan_config = ?, mode = ?, comment = ?
|
||||||
|
WHERE id = ?',
|
||||||
|
'isisisssi',
|
||||||
|
[$connectionTypeId, $portAType, $portAId, $portBType, $portBId, $vlanConfig, $mode, $comment, $id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($rows === false) {
|
||||||
|
jsonError('Update fehlgeschlagen', 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['status' => 'updated', 'rows' => $rows]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Prüfen, ob Verbindung existiert
|
$id = $sql->set(
|
||||||
|
'INSERT INTO connections (connection_type_id, port_a_type, port_a_id, port_b_type, port_b_id, vlan_config, mode, comment)
|
||||||
$rows = $sql->set(
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||||
"DELETE FROM connections WHERE id = ?",
|
'isisisss',
|
||||||
"i",
|
[$connectionTypeId, $portAType, $portAId, $portBType, $portBId, $vlanConfig, $mode, $comment],
|
||||||
[$id]
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
echo json_encode([
|
if ($id === false) {
|
||||||
'status' => 'deleted',
|
jsonError('Insert fehlgeschlagen', 500);
|
||||||
'rows' => $rows
|
}
|
||||||
]);
|
|
||||||
|
echo json_encode(['status' => 'created', 'id' => $id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteConnection($sql): void
|
||||||
|
{
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST' && $_SERVER['REQUEST_METHOD'] !== 'DELETE') {
|
||||||
|
jsonError('Methode nicht erlaubt', 405);
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||||
|
if ($id <= 0) {
|
||||||
|
jsonError('ID fehlt', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$existing = $sql->single('SELECT id FROM connections WHERE id = ?', 'i', [$id]);
|
||||||
|
if (!$existing) {
|
||||||
|
jsonError('Verbindung existiert nicht', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = $sql->set('DELETE FROM connections WHERE id = ?', 'i', [$id]);
|
||||||
|
if ($rows === false) {
|
||||||
|
jsonError('Loeschen fehlgeschlagen', 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['status' => 'deleted', 'rows' => $rows]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,175 +1,199 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* app/api/device_type_ports.php
|
* app/api/device_type_ports.php
|
||||||
*
|
|
||||||
* API für Ports von Gerätetypen
|
|
||||||
* - Laden der Port-Definitionen (SVG-Port-Editor)
|
|
||||||
* - Speichern (Position, Typ, Name)
|
|
||||||
* - Löschen einzelner Ports
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
require_once __DIR__ . '/../bootstrap.php';
|
require_once __DIR__ . '/../bootstrap.php';
|
||||||
|
requireAuth();
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// TODO: Single-User-Auth prüfen
|
|
||||||
// if (!$_SESSION['user']) { http_response_code(403); exit; }
|
|
||||||
|
|
||||||
$action = $_GET['action'] ?? 'load';
|
$action = $_GET['action'] ?? 'load';
|
||||||
|
|
||||||
/* =========================
|
|
||||||
* Router
|
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
switch ($action) {
|
switch ($action) {
|
||||||
|
|
||||||
case 'load':
|
case 'load':
|
||||||
loadPorts($sql);
|
loadPorts($sql);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'save':
|
case 'save':
|
||||||
savePorts($sql);
|
savePorts($sql);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'delete':
|
case 'delete':
|
||||||
deletePort($sql);
|
deletePort($sql);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
http_response_code(400);
|
jsonError('Unbekannte Aktion', 400);
|
||||||
echo json_encode(['error' => 'Unbekannte Aktion']);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
function jsonError(string $message, int $status = 400): void
|
||||||
* Aktionen
|
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lädt alle Ports eines Gerätetyps
|
|
||||||
*/
|
|
||||||
function loadPorts($sql)
|
|
||||||
{
|
{
|
||||||
$deviceTypeId = $_GET['device_type_id'] ?? null;
|
http_response_code($status);
|
||||||
|
echo json_encode(['error' => $message]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
if (!$deviceTypeId) {
|
function loadPorts($sql): void
|
||||||
http_response_code(400);
|
{
|
||||||
echo json_encode(['error' => 'device_type_id fehlt']);
|
$deviceTypeId = isset($_GET['device_type_id']) ? (int)$_GET['device_type_id'] : 0;
|
||||||
return;
|
if ($deviceTypeId <= 0) {
|
||||||
|
jsonError('device_type_id fehlt', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
$ports = $sql->get(
|
$ports = $sql->get(
|
||||||
"SELECT
|
'SELECT id, name, port_type_id, x, y, metadata
|
||||||
id,
|
|
||||||
name,
|
|
||||||
port_type_id,
|
|
||||||
pos_x,
|
|
||||||
pos_y,
|
|
||||||
comment
|
|
||||||
FROM device_type_ports
|
FROM device_type_ports
|
||||||
WHERE device_type_id = ?
|
WHERE device_type_id = ?
|
||||||
ORDER BY id ASC",
|
ORDER BY id ASC',
|
||||||
"i",
|
'i',
|
||||||
[$deviceTypeId]
|
[$deviceTypeId]
|
||||||
);
|
);
|
||||||
|
|
||||||
echo json_encode($ports);
|
echo json_encode($ports ?: []);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function validatePortTypeId($sql, $portTypeId): ?int
|
||||||
* Speichert alle Ports eines Gerätetyps
|
|
||||||
* (Bulk-Save aus dem SVG-Editor)
|
|
||||||
*/
|
|
||||||
function savePorts($sql)
|
|
||||||
{
|
{
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
if ($portTypeId === null || $portTypeId === '' || (int)$portTypeId <= 0) {
|
||||||
|
return null;
|
||||||
if (!$data || empty($data['device_type_id']) || !is_array($data['ports'])) {
|
|
||||||
http_response_code(400);
|
|
||||||
echo json_encode(['error' => 'Ungültige Daten']);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$deviceTypeId = $data['device_type_id'];
|
$value = (int)$portTypeId;
|
||||||
$ports = $data['ports'];
|
$exists = $sql->single('SELECT id FROM port_types WHERE id = ?', 'i', [$value]);
|
||||||
|
if (!$exists) {
|
||||||
|
jsonError('port_type_id ist ungueltig', 400);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Transaktion starten (falls SQL-Klasse das unterstützt)
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($ports as $port) {
|
function savePorts($sql): void
|
||||||
|
{
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
jsonError('Methode nicht erlaubt', 405);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Validierung:
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
// - name nicht leer
|
if (!is_array($data) || empty($data['device_type_id']) || !isset($data['ports']) || !is_array($data['ports'])) {
|
||||||
// - pos_x / pos_y numerisch
|
jsonError('Ungueltige Daten', 400);
|
||||||
// - port_type_id erlaubt
|
}
|
||||||
|
|
||||||
if (!empty($port['id']) && !str_starts_with($port['id'], 'tmp_')) {
|
$deviceTypeId = (int)$data['device_type_id'];
|
||||||
|
if ($deviceTypeId <= 0) {
|
||||||
|
jsonError('device_type_id ist ungueltig', 400);
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- UPDATE ---------- */
|
$deviceType = $sql->single('SELECT id FROM device_types WHERE id = ?', 'i', [$deviceTypeId]);
|
||||||
$sql->set(
|
if (!$deviceType) {
|
||||||
"UPDATE device_type_ports
|
jsonError('Geraetetyp existiert nicht', 404);
|
||||||
SET name = ?, port_type_id = ?, pos_x = ?, pos_y = ?, comment = ?
|
}
|
||||||
WHERE id = ? AND device_type_id = ?",
|
|
||||||
"siddsii",
|
$sql->set('START TRANSACTION');
|
||||||
[
|
|
||||||
$port['name'],
|
foreach ($data['ports'] as $index => $port) {
|
||||||
$port['port_type_id'],
|
if (!is_array($port)) {
|
||||||
$port['x'],
|
$sql->set('ROLLBACK');
|
||||||
$port['y'],
|
jsonError('Port-Eintrag an Position ' . $index . ' ist ungueltig', 400);
|
||||||
$port['comment'],
|
}
|
||||||
$port['id'],
|
|
||||||
$deviceTypeId
|
$name = trim((string)($port['name'] ?? ''));
|
||||||
]
|
if ($name === '') {
|
||||||
|
$sql->set('ROLLBACK');
|
||||||
|
jsonError('Port-Name darf nicht leer sein', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$x = $port['x'] ?? null;
|
||||||
|
$y = $port['y'] ?? null;
|
||||||
|
if (!is_numeric($x) || !is_numeric($y)) {
|
||||||
|
$sql->set('ROLLBACK');
|
||||||
|
jsonError('x und y muessen numerisch sein', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$x = (int)round((float)$x);
|
||||||
|
$y = (int)round((float)$y);
|
||||||
|
$portTypeId = validatePortTypeId($sql, $port['port_type_id'] ?? null);
|
||||||
|
|
||||||
|
$metadataRaw = $port['metadata'] ?? null;
|
||||||
|
$metadata = null;
|
||||||
|
if (is_array($metadataRaw)) {
|
||||||
|
$metadata = json_encode($metadataRaw);
|
||||||
|
} elseif (is_string($metadataRaw) && $metadataRaw !== '') {
|
||||||
|
json_decode($metadataRaw, true);
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
$sql->set('ROLLBACK');
|
||||||
|
jsonError('metadata ist kein gueltiges JSON', 400);
|
||||||
|
}
|
||||||
|
$metadata = $metadataRaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
$isExisting = !empty($port['id']) && !str_starts_with((string)$port['id'], 'tmp_');
|
||||||
|
if ($isExisting) {
|
||||||
|
$portId = (int)$port['id'];
|
||||||
|
$ok = $sql->set(
|
||||||
|
'UPDATE device_type_ports
|
||||||
|
SET name = ?, port_type_id = ?, x = ?, y = ?, metadata = ?
|
||||||
|
WHERE id = ? AND device_type_id = ?',
|
||||||
|
'siiisii',
|
||||||
|
[$name, $portTypeId, $x, $y, $metadata, $portId, $deviceTypeId]
|
||||||
);
|
);
|
||||||
|
|
||||||
} else {
|
if ($ok === false) {
|
||||||
|
$sql->set('ROLLBACK');
|
||||||
|
jsonError('Update fehlgeschlagen', 500);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- INSERT ---------- */
|
$ok = $sql->set(
|
||||||
$sql->set(
|
'INSERT INTO device_type_ports (device_type_id, name, port_type_id, x, y, metadata)
|
||||||
"INSERT INTO device_type_ports
|
VALUES (?, ?, ?, ?, ?, ?)',
|
||||||
(device_type_id, name, port_type_id, pos_x, pos_y, comment)
|
'isiiis',
|
||||||
VALUES (?, ?, ?, ?, ?, ?)",
|
[$deviceTypeId, $name, $portTypeId, $x, $y, $metadata],
|
||||||
"isidds",
|
true
|
||||||
[
|
);
|
||||||
$deviceTypeId,
|
|
||||||
$port['name'],
|
if ($ok === false) {
|
||||||
$port['port_type_id'],
|
$sql->set('ROLLBACK');
|
||||||
$port['x'],
|
jsonError('Insert fehlgeschlagen', 500);
|
||||||
$port['y'],
|
|
||||||
$port['comment']
|
|
||||||
],
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
echo json_encode([
|
$sql->set('COMMIT');
|
||||||
'status' => 'ok'
|
|
||||||
]);
|
echo json_encode(['status' => 'ok']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function deletePort($sql): void
|
||||||
* Löscht einen einzelnen Port
|
|
||||||
*/
|
|
||||||
function deletePort($sql)
|
|
||||||
{
|
{
|
||||||
$id = $_GET['id'] ?? null;
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST' && $_SERVER['REQUEST_METHOD'] !== 'DELETE') {
|
||||||
|
jsonError('Methode nicht erlaubt', 405);
|
||||||
if (!$id) {
|
|
||||||
http_response_code(400);
|
|
||||||
echo json_encode(['error' => 'ID fehlt']);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Prüfen, ob Port existiert und nicht verwendet wird
|
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||||
|
if ($id <= 0) {
|
||||||
|
jsonError('ID fehlt', 400);
|
||||||
|
}
|
||||||
|
|
||||||
$rows = $sql->set(
|
$port = $sql->single('SELECT id, device_type_id, name FROM device_type_ports WHERE id = ?', 'i', [$id]);
|
||||||
"DELETE FROM device_type_ports WHERE id = ?",
|
if (!$port) {
|
||||||
"i",
|
jsonError('Port existiert nicht', 404);
|
||||||
[$id]
|
}
|
||||||
|
|
||||||
|
$usage = $sql->single(
|
||||||
|
'SELECT COUNT(*) AS cnt
|
||||||
|
FROM devices d
|
||||||
|
JOIN device_ports dp ON dp.device_id = d.id
|
||||||
|
WHERE d.device_type_id = ? AND dp.name = ?',
|
||||||
|
'is',
|
||||||
|
[(int)$port['device_type_id'], (string)$port['name']]
|
||||||
);
|
);
|
||||||
|
|
||||||
echo json_encode([
|
if (!empty($usage) && (int)$usage['cnt'] > 0) {
|
||||||
'status' => 'deleted',
|
jsonError('Port wird bereits von realen Geraeten genutzt und kann nicht geloescht werden', 409);
|
||||||
'rows' => $rows
|
}
|
||||||
]);
|
|
||||||
|
$rows = $sql->set('DELETE FROM device_type_ports WHERE id = ?', 'i', [$id]);
|
||||||
|
if ($rows === false) {
|
||||||
|
jsonError('Loeschen fehlgeschlagen', 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['status' => 'deleted', 'rows' => $rows]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,133 +1,89 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* app/api/upload.php
|
* app/api/upload.php
|
||||||
*
|
|
||||||
* Zentrale Upload-API
|
|
||||||
* - Gerätetyp-Bilder (SVG / JPG / PNG)
|
|
||||||
* - Floorpläne (SVG)
|
|
||||||
* - Rack-Ansichten
|
|
||||||
*
|
|
||||||
* KEINE Logik für automatische Zuordnung
|
|
||||||
* -> Upload + Rückgabe von Pfad / Metadaten
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
require_once __DIR__ . '/../bootstrap.php';
|
require_once __DIR__ . '/../bootstrap.php';
|
||||||
|
requireAuth();
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// TODO: Single-User-Auth prüfen
|
$baseUploadDir = defined('UPLOAD_BASE_DIR') ? UPLOAD_BASE_DIR : (__DIR__ . '/../uploads');
|
||||||
// if (!$_SESSION['user']) { http_response_code(403); exit; }
|
$maxFileSize = defined('UPLOAD_MAX_FILE_SIZE') ? (int)UPLOAD_MAX_FILE_SIZE : (5 * 1024 * 1024);
|
||||||
|
$allowedCategories = defined('UPLOAD_ALLOWED_CATEGORIES') ? UPLOAD_ALLOWED_CATEGORIES : ['misc'];
|
||||||
/* =========================
|
|
||||||
* Konfiguration
|
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
// TODO: Upload-Basisverzeichnis aus config.php
|
|
||||||
$baseUploadDir = __DIR__ . '/../uploads';
|
|
||||||
|
|
||||||
// Erlaubte Typen
|
|
||||||
$allowedMimeTypes = [
|
$allowedMimeTypes = [
|
||||||
'image/svg+xml',
|
'image/svg+xml' => 'svg',
|
||||||
'image/png',
|
'image/png' => 'png',
|
||||||
'image/jpeg'
|
'image/jpeg' => 'jpg',
|
||||||
];
|
];
|
||||||
|
|
||||||
// TODO: Max. Dateigröße festlegen (z.B. 5MB)
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
$maxFileSize = 5 * 1024 * 1024;
|
jsonError('Methode nicht erlaubt', 405);
|
||||||
|
}
|
||||||
/* =========================
|
|
||||||
* Validierung
|
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
if (empty($_FILES['file'])) {
|
if (empty($_FILES['file'])) {
|
||||||
http_response_code(400);
|
jsonError('Keine Datei hochgeladen', 400);
|
||||||
echo json_encode(['error' => 'Keine Datei hochgeladen']);
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$file = $_FILES['file'];
|
$file = $_FILES['file'];
|
||||||
|
if (!is_array($file) || $file['error'] !== UPLOAD_ERR_OK) {
|
||||||
if ($file['error'] !== UPLOAD_ERR_OK) {
|
jsonError('Upload-Fehler', 400);
|
||||||
http_response_code(400);
|
|
||||||
echo json_encode(['error' => 'Upload-Fehler']);
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($file['size'] > $maxFileSize) {
|
if ((int)$file['size'] > $maxFileSize) {
|
||||||
http_response_code(400);
|
jsonError('Datei zu gross', 400);
|
||||||
echo json_encode(['error' => 'Datei zu groß']);
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MIME-Type prüfen
|
|
||||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||||
$mimeType = finfo_file($finfo, $file['tmp_name']);
|
$mimeType = finfo_file($finfo, $file['tmp_name']);
|
||||||
finfo_close($finfo);
|
finfo_close($finfo);
|
||||||
|
|
||||||
if (!in_array($mimeType, $allowedMimeTypes)) {
|
if (!isset($allowedMimeTypes[$mimeType])) {
|
||||||
http_response_code(400);
|
jsonError('Dateityp nicht erlaubt', 400);
|
||||||
echo json_encode(['error' => 'Dateityp nicht erlaubt']);
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
$category = strtolower(trim((string)($_POST['category'] ?? 'misc')));
|
||||||
* Zielverzeichnis
|
if ($category === '' || !in_array($category, $allowedCategories, true)) {
|
||||||
* ========================= */
|
jsonError('Ungueltige Kategorie', 400);
|
||||||
|
|
||||||
// TODO: Kategorie definieren (device_types, floors, racks, etc.)
|
|
||||||
$category = $_POST['category'] ?? 'misc';
|
|
||||||
|
|
||||||
// Zielpfad
|
|
||||||
$targetDir = $baseUploadDir . '/' . preg_replace('/[^a-z0-9_-]/i', '', $category);
|
|
||||||
|
|
||||||
// Verzeichnis anlegen falls nötig
|
|
||||||
if (!is_dir($targetDir)) {
|
|
||||||
mkdir($targetDir, 0755, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
$targetDir = rtrim($baseUploadDir, '/\\') . DIRECTORY_SEPARATOR . $category;
|
||||||
* Dateiname
|
if (!is_dir($targetDir) && !mkdir($targetDir, 0755, true) && !is_dir($targetDir)) {
|
||||||
* ========================= */
|
jsonError('Upload-Verzeichnis konnte nicht erstellt werden', 500);
|
||||||
|
}
|
||||||
|
|
||||||
// Originalname bereinigen
|
$extension = $allowedMimeTypes[$mimeType];
|
||||||
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
|
$filename = sprintf('%s_%s.%s', $category, bin2hex(random_bytes(16)), $extension);
|
||||||
|
$targetPath = $targetDir . DIRECTORY_SEPARATOR . $filename;
|
||||||
// TODO: Eindeutigen Namen besser definieren (UUID?)
|
|
||||||
$filename = uniqid('upload_', true) . '.' . strtolower($extension);
|
|
||||||
|
|
||||||
$targetPath = $targetDir . '/' . $filename;
|
|
||||||
|
|
||||||
/* =========================
|
|
||||||
* Datei speichern
|
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
if (!move_uploaded_file($file['tmp_name'], $targetPath)) {
|
if (!move_uploaded_file($file['tmp_name'], $targetPath)) {
|
||||||
http_response_code(500);
|
jsonError('Datei konnte nicht gespeichert werden', 500);
|
||||||
echo json_encode(['error' => 'Datei konnte nicht gespeichert werden']);
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
$publicPath = '/uploads/' . $category . '/' . $filename;
|
||||||
* Optional: DB-Eintrag
|
$uploadId = null;
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
// TODO: Optional in Tabelle `uploads` speichern
|
$uploadTableExists = $sql->single("SHOW TABLES LIKE 'uploads'");
|
||||||
// $uploadId = $sql->set(
|
if (!empty($uploadTableExists)) {
|
||||||
// "INSERT INTO uploads (filename, path, mime_type, category)
|
$uploadId = $sql->set(
|
||||||
// VALUES (?, ?, ?, ?)",
|
'INSERT INTO uploads (filename, path, mime_type, category) VALUES (?, ?, ?, ?)',
|
||||||
// "ssss",
|
'ssss',
|
||||||
// [$filename, $targetPath, $mimeType, $category],
|
[$filename, $publicPath, $mimeType, $category],
|
||||||
// true
|
true
|
||||||
// );
|
);
|
||||||
|
}
|
||||||
/* =========================
|
|
||||||
* Antwort
|
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'status' => 'ok',
|
'status' => 'ok',
|
||||||
'filename' => $filename,
|
'filename' => $filename,
|
||||||
'path' => str_replace(__DIR__ . '/..', '', $targetPath),
|
'path' => $publicPath,
|
||||||
'mime_type' => $mimeType
|
'mime_type' => $mimeType,
|
||||||
// 'id' => $uploadId ?? null
|
'id' => $uploadId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
function jsonError(string $message, int $status = 400): void
|
||||||
|
{
|
||||||
|
http_response_code($status);
|
||||||
|
echo json_encode(['error' => $message]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -192,4 +281,4 @@ main {
|
|||||||
|
|
||||||
footer>p {
|
footer>p {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
102
app/assets/css/device-types-list.css
Normal file
102
app/assets/css/device-types-list.css
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
.device-types-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-form {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 20px 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-form input,
|
||||||
|
.filter-form select {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-form input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-type-list {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-type-list th,
|
||||||
|
.device-type-list td {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-type-list th {
|
||||||
|
background: #f5f5f5;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-switch { background: #0066cc; }
|
||||||
|
.badge-server { background: #cc0000; }
|
||||||
|
.badge-patchpanel { background: #ff9900; }
|
||||||
|
.badge-other { background: #999; }
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-primary {
|
||||||
|
background: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-primary:hover {
|
||||||
|
background: #218838;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-small {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-danger {
|
||||||
|
background: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-danger:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
101
app/assets/css/floor-infrastructure-edit.css
Normal file
101
app/assets/css/floor-infrastructure-edit.css
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
.floor-infra-edit {
|
||||||
|
padding: 25px;
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
|
.infra-edit-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
.floor-plan-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.floor-plan-canvas {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 560px;
|
||||||
|
border: 1px solid #d4d4d4;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #fff;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(90deg, rgba(0, 0, 0, 0.05) 1px, transparent 1px),
|
||||||
|
linear-gradient(rgba(0, 0, 0, 0.05) 1px, transparent 1px);
|
||||||
|
background-size: 40px 40px;
|
||||||
|
cursor: crosshair;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.floor-plan-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 2;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
.floor-plan-overlay .floor-plan-background {
|
||||||
|
opacity: 0.75;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.floor-plan-overlay .active-marker {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
.floor-plan-overlay .panel-marker {
|
||||||
|
fill: rgba(13, 110, 253, 0.25);
|
||||||
|
stroke: #0d6efd;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
.floor-plan-overlay .outlet-marker {
|
||||||
|
fill: rgba(25, 135, 84, 0.25);
|
||||||
|
stroke: #198754;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
.floor-plan-overlay .reference-marker {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.35;
|
||||||
|
}
|
||||||
|
.floor-plan-overlay .room-highlight {
|
||||||
|
pointer-events: none;
|
||||||
|
fill: rgba(255, 193, 7, 0.22);
|
||||||
|
stroke: #ff9800;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
.floor-plan-overlay .reference-marker.panel-marker {
|
||||||
|
fill: rgba(13, 110, 253, 0.22);
|
||||||
|
stroke: rgba(13, 110, 253, 0.7);
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
.floor-plan-overlay .reference-marker.outlet-marker {
|
||||||
|
fill: rgba(25, 135, 84, 0.22);
|
||||||
|
stroke: rgba(25, 135, 84, 0.7);
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
.floor-plan-hint {
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: #444;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.floor-plan-position {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
109
app/assets/css/floor-infrastructure-list.css
Normal file
109
app/assets/css/floor-infrastructure-list.css
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
.floor-infra {
|
||||||
|
padding: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-form {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-form select {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infra-plan {
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
background: #f7f7f7;
|
||||||
|
border: 1px dashed #ccc;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infra-floor-canvas {
|
||||||
|
position: relative;
|
||||||
|
margin-top: 12px;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 560px;
|
||||||
|
border: 1px solid #d4d4d4;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infra-floor-svg {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infra-floor-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infra-overlay-marker {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infra-overlay-marker.patchpanel {
|
||||||
|
fill: rgba(13, 110, 253, 0.25);
|
||||||
|
stroke: #0d6efd;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infra-overlay-marker.outlet {
|
||||||
|
fill: rgba(25, 135, 84, 0.25);
|
||||||
|
stroke: #198754;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infra-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infra-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infra-table th,
|
||||||
|
.infra-table td {
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-plan-hint {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 20px;
|
||||||
|
background: #fafafa;
|
||||||
|
border: 1px dashed #ccc;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions .button-small {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
187
app/assets/css/locations-list.css
Normal file
187
app/assets/css/locations-list.css
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
.locations-container {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locations-filter-form {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locations-filter-add {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locations-list {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locations-list th {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 2px solid #ddd;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locations-list td {
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locations-list tr:hover {
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-primary {
|
||||||
|
background: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-primary:hover {
|
||||||
|
background: #218838;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-small {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-danger {
|
||||||
|
background: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-danger:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-section {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 30px auto;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-section h2 {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
border: 1px solid #d8dee5;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-table th {
|
||||||
|
background: #f3f6fa;
|
||||||
|
padding: 10px 12px;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 1px solid #d8dee5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-table td {
|
||||||
|
padding: 9px 12px;
|
||||||
|
border-bottom: 1px solid #eef2f6;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-row:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-row--empty {
|
||||||
|
background: #fcfcfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-cell {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-cell--location {
|
||||||
|
background: #fbfcfe;
|
||||||
|
font-weight: 600;
|
||||||
|
min-width: 170px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-cell--building {
|
||||||
|
padding-left: 18px;
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-cell--floor {
|
||||||
|
padding-left: 24px;
|
||||||
|
min-width: 190px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-cell--room {
|
||||||
|
padding-left: 56px;
|
||||||
|
min-width: 190px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-cell--empty {
|
||||||
|
background: #fafbfd;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-meta {
|
||||||
|
color: #4e5c6b;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-meta--muted {
|
||||||
|
color: #8a94a3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-actions .button {
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
4
app/assets/icons/favicon.svg
Normal file
4
app/assets/icons/favicon.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||||
|
<rect width="64" height="64" rx="12" fill="#0c4da2"/>
|
||||||
|
<path d="M14 22h36v6H14zm0 14h36v6H14z" fill="#ffffff"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 183 B |
@@ -1,135 +1,110 @@
|
|||||||
/**
|
/**
|
||||||
* app/assets/js/app.js
|
* app/assets/js/app.js
|
||||||
*
|
|
||||||
* Zentrale JS-Datei für die Webanwendung
|
|
||||||
* - Initialisiert alle Module
|
|
||||||
* - SVG-Editor, Netzwerk-Ansicht, Drag & Drop, Floorplan
|
|
||||||
* - Event-Handler, globale Variablen
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// =========================
|
|
||||||
// Global Variables / Config
|
|
||||||
// =========================
|
|
||||||
|
|
||||||
window.APP = {
|
window.APP = {
|
||||||
deviceTypes: [], // TODO: alle Gerätetypen laden
|
state: {
|
||||||
devices: [], // TODO: alle Geräte laden
|
deviceTypes: [],
|
||||||
racks: [], // TODO: alle Racks laden
|
devices: [],
|
||||||
floors: [], // TODO: alle Floors laden
|
racks: [],
|
||||||
connections: [], // TODO: alle Verbindungen laden
|
floors: [],
|
||||||
|
connections: [],
|
||||||
|
},
|
||||||
|
capabilities: {
|
||||||
|
hasGlobalDataApi: false,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// =========================
|
|
||||||
// Init Functions
|
|
||||||
// =========================
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
initViewModules();
|
||||||
console.log('App initialized');
|
|
||||||
|
|
||||||
// =========================
|
|
||||||
// SVG-Port-Editor initialisieren
|
|
||||||
// =========================
|
|
||||||
// TODO: import / init svg-editor.js
|
|
||||||
// if (window.SVGEditor) window.SVGEditor.init();
|
|
||||||
|
|
||||||
// =========================
|
|
||||||
// Netzwerk-Ansicht initialisieren
|
|
||||||
// =========================
|
|
||||||
// TODO: import / init network-view.js
|
|
||||||
// if (window.NetworkView) window.NetworkView.init();
|
|
||||||
|
|
||||||
// =========================
|
|
||||||
// Drag & Drop für Floors / Racks / Devices
|
|
||||||
// =========================
|
|
||||||
// TODO: init drag & drop logic
|
|
||||||
|
|
||||||
// =========================
|
|
||||||
// Event-Handler für Buttons / Forms
|
|
||||||
// =========================
|
|
||||||
initEventHandlers();
|
initEventHandlers();
|
||||||
});
|
});
|
||||||
|
|
||||||
// =========================
|
function initViewModules() {
|
||||||
// Event Handler Setup
|
if (typeof window.Dashboard?.init === 'function') {
|
||||||
// =========================
|
window.Dashboard.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both modules are loaded via script tags in header.php.
|
||||||
|
// They are self-initializing and only run when expected DOM nodes exist.
|
||||||
|
window.dispatchEvent(new CustomEvent('app:modules-initialized'));
|
||||||
|
}
|
||||||
|
|
||||||
function initEventHandlers() {
|
function initEventHandlers() {
|
||||||
|
bindFormSubmitButton('#save-device-type', 'form[action*="module=device_types"][action*="save"]');
|
||||||
// TODO: Save-Button Device-Type
|
bindFormSubmitButton('#save-device', 'form[action*="module=devices"][action*="save"]');
|
||||||
const saveDeviceTypeBtn = document.querySelector('#save-device-type');
|
bindFormSubmitButton('#save-floor', 'form[action*="module=floors"][action*="save"]');
|
||||||
if (saveDeviceTypeBtn) {
|
bindFormSubmitButton('#save-rack', 'form[action*="module=racks"][action*="save"]');
|
||||||
saveDeviceTypeBtn.addEventListener('click', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
// TODO: Save Device-Type via AJAX
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Save-Button Device
|
|
||||||
const saveDeviceBtn = document.querySelector('#save-device');
|
|
||||||
if (saveDeviceBtn) {
|
|
||||||
saveDeviceBtn.addEventListener('click', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
// TODO: Save Device via AJAX
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Save-Button Floor
|
|
||||||
const saveFloorBtn = document.querySelector('#save-floor');
|
|
||||||
if (saveFloorBtn) {
|
|
||||||
saveFloorBtn.addEventListener('click', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
// TODO: Save Floor via AJAX
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Save-Button Rack
|
|
||||||
const saveRackBtn = document.querySelector('#save-rack');
|
|
||||||
if (saveRackBtn) {
|
|
||||||
saveRackBtn.addEventListener('click', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
// TODO: Save Rack via AJAX
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Weitere Event-Handler (Import, Export, Filter)
|
|
||||||
|
|
||||||
document.querySelectorAll('[data-confirm-delete]').forEach((btn) => {
|
document.querySelectorAll('[data-confirm-delete]').forEach((btn) => {
|
||||||
btn.addEventListener('click', (event) => {
|
btn.addEventListener('click', (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const message = btn.getAttribute('data-confirm-message') || 'Aktion ausführen?';
|
const message = btn.getAttribute('data-confirm-message') || 'Aktion ausfuehren?';
|
||||||
if (confirm(message)) {
|
if (confirm(message)) {
|
||||||
alert(btn.getAttribute('data-confirm-feedback') || 'Diese Funktion ist noch nicht verfügbar.');
|
const href = btn.getAttribute('href') || btn.dataset.href;
|
||||||
|
if (href) {
|
||||||
|
window.location.href = href;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-filter-submit]').forEach((el) => {
|
||||||
|
el.addEventListener('change', () => {
|
||||||
|
const form = el.closest('form');
|
||||||
|
if (form) {
|
||||||
|
form.requestSubmit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================
|
function bindFormSubmitButton(buttonSelector, formSelector) {
|
||||||
// Utility Functions
|
const button = document.querySelector(buttonSelector);
|
||||||
// =========================
|
if (!button) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
button.addEventListener('click', (event) => {
|
||||||
* AJAX Request Helper
|
event.preventDefault();
|
||||||
* @param {string} url
|
const form = button.closest('form') || document.querySelector(formSelector);
|
||||||
* @param {object} data
|
if (form) {
|
||||||
* @param {function} callback
|
form.requestSubmit();
|
||||||
*/
|
}
|
||||||
function ajaxPost(url, data, callback) {
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function ajaxPost(url, data, callback, onError) {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.open('POST', url, true);
|
xhr.open('POST', url, true);
|
||||||
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
|
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
|
||||||
xhr.onload = function() {
|
xhr.onload = function onLoad() {
|
||||||
if (xhr.status >= 200 && xhr.status < 300) {
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
callback(JSON.parse(xhr.responseText));
|
let parsed = null;
|
||||||
} else {
|
try {
|
||||||
console.error('AJAX Error:', xhr.statusText);
|
parsed = JSON.parse(xhr.responseText);
|
||||||
|
} catch (error) {
|
||||||
|
if (typeof onError === 'function') {
|
||||||
|
onError(error);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback(parsed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof onError === 'function') {
|
||||||
|
onError(new Error('AJAX error: ' + xhr.status));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
xhr.onerror = function onXhrError() {
|
||||||
|
if (typeof onError === 'function') {
|
||||||
|
onError(new Error('Netzwerkfehler'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
xhr.send(JSON.stringify(data));
|
xhr.send(JSON.stringify(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: weitere Utility-Funktionen (DOM-Helper, SVG-Helper, etc.)
|
window.APP.ajaxPost = ajaxPost;
|
||||||
|
|
||||||
// Dashboard initialisieren
|
|
||||||
if (window.Dashboard) window.Dashboard.init();
|
|
||||||
|
|||||||
99
app/assets/js/connections-edit-form.js
Normal file
99
app/assets/js/connections-edit-form.js
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
(() => {
|
||||||
|
function applyTypeFilter(typeSelect, portSelect, selectedId) {
|
||||||
|
const endpointType = typeSelect.value;
|
||||||
|
let visibleCount = 0;
|
||||||
|
let matchedSelected = false;
|
||||||
|
const currentValue = selectedId || portSelect.value || '';
|
||||||
|
|
||||||
|
for (const option of portSelect.options) {
|
||||||
|
const optionType = option.dataset.endpointType || '';
|
||||||
|
if (!optionType) {
|
||||||
|
option.hidden = false;
|
||||||
|
option.disabled = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visible = optionType === endpointType;
|
||||||
|
option.hidden = !visible;
|
||||||
|
option.disabled = !visible;
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
visibleCount += 1;
|
||||||
|
if (option.value === currentValue) {
|
||||||
|
option.selected = true;
|
||||||
|
matchedSelected = true;
|
||||||
|
}
|
||||||
|
} else if (option.selected) {
|
||||||
|
option.selected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeholder = portSelect.options[0];
|
||||||
|
if (placeholder) {
|
||||||
|
placeholder.textContent = visibleCount > 0 ? '- Port waehlen -' : '- Keine Ports verfuegbar -';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!matchedSelected) {
|
||||||
|
portSelect.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindPair(typeSelectId, portSelectId) {
|
||||||
|
const typeSelect = document.getElementById(typeSelectId);
|
||||||
|
const portSelect = document.getElementById(portSelectId);
|
||||||
|
if (!typeSelect || !portSelect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyTypeFilter(typeSelect, portSelect, portSelect.dataset.selectedId || '');
|
||||||
|
typeSelect.addEventListener('change', () => {
|
||||||
|
applyTypeFilter(typeSelect, portSelect, '');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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', () => {
|
||||||
|
bindPair('port_a_type', 'port_a_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);
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -1,99 +1,35 @@
|
|||||||
/**
|
/**
|
||||||
* app/assets/js/dashboard.js
|
* app/assets/js/dashboard.js
|
||||||
*
|
|
||||||
* Dashboard-Modul
|
|
||||||
* - Zentrale Übersicht aller Grundfunktionen
|
|
||||||
* - Einstiegspunkt für das Tool
|
|
||||||
* - Kann später Status, Warnungen, Statistiken anzeigen
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
window.Dashboard = (function () {
|
window.Dashboard = (function () {
|
||||||
|
|
||||||
// =========================
|
|
||||||
// Interne Daten
|
|
||||||
// =========================
|
|
||||||
|
|
||||||
const modules = [
|
const modules = [
|
||||||
{
|
{ id: 'device_types', label: 'Geraetetypen', description: 'Geraetetypen und Port-Definitionen', url: '?module=device_types&action=list', icon: 'DT' },
|
||||||
id: 'device_types',
|
{ id: 'devices', label: 'Geraete', description: 'Physische Geraete in Racks und Raeumen', url: '?module=devices&action=list', icon: 'DV' },
|
||||||
label: 'Gerätetypen',
|
{ id: 'connections', label: 'Verbindungen', description: 'Kabel, Ports und VLANs', url: '?module=connections&action=list', icon: 'CN' },
|
||||||
description: 'Gerätetypen, Port-Definitionen, Module',
|
{ id: 'floors', label: 'Stockwerke', description: 'Standorte, Gebaeude und Etagen', url: '?module=floors&action=list', icon: 'FL' },
|
||||||
url: '/app/device_types/list.php',
|
{ id: 'racks', label: 'Racks', description: 'Racks und Positionierung', url: '?module=racks&action=list', icon: 'RK' },
|
||||||
icon: '🔌'
|
{ id: 'infra', label: 'Infrastruktur', description: 'Patchpanels und Wandbuchsen', url: '?module=floor_infrastructure&action=list', icon: 'IF' }
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'devices',
|
|
||||||
label: 'Geräte',
|
|
||||||
description: 'Physische Geräte in Racks und Räumen',
|
|
||||||
url: '/app/devices/list.php',
|
|
||||||
icon: '🖥️'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'connections',
|
|
||||||
label: 'Verbindungen',
|
|
||||||
description: 'Kabel, Ports, VLANs, Protokolle',
|
|
||||||
url: '/app/connections/list.php',
|
|
||||||
icon: '🧵'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'floors',
|
|
||||||
label: 'Standorte & Stockwerke',
|
|
||||||
description: 'Gebäude, Etagen, Räume, Dosen',
|
|
||||||
url: '/app/floors/list.php',
|
|
||||||
icon: '🏢'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'racks',
|
|
||||||
label: 'Serverschränke',
|
|
||||||
description: 'Racks, Positionierung, Höheneinheiten',
|
|
||||||
url: '/app/racks/list.php',
|
|
||||||
icon: '🗄️'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'network_view',
|
|
||||||
label: 'Netzwerk-Ansicht',
|
|
||||||
description: 'Grafische Netzwerkdarstellung',
|
|
||||||
url: '/network.php',
|
|
||||||
icon: '🌐'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'svg_editor',
|
|
||||||
label: 'SVG-Port-Editor',
|
|
||||||
description: 'Ports auf Gerätetypen definieren',
|
|
||||||
url: '/svg-editor.php',
|
|
||||||
icon: '✏️'
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// =========================
|
|
||||||
// Public API
|
|
||||||
// =========================
|
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
console.log('Dashboard initialized');
|
const container = document.querySelector('#dashboard-modules');
|
||||||
|
if (container) {
|
||||||
|
renderModules(container);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Dashboard-Container ermitteln
|
loadStats();
|
||||||
// const container = document.querySelector('#dashboard');
|
showWarnings();
|
||||||
|
renderRecentChanges();
|
||||||
// TODO: Module rendern
|
|
||||||
// renderModules(container);
|
|
||||||
|
|
||||||
// TODO: Optional: Status-Daten laden (Counts, Warnings)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================
|
|
||||||
// Render Functions
|
|
||||||
// =========================
|
|
||||||
|
|
||||||
function renderModules(container) {
|
function renderModules(container) {
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
modules.forEach(module => {
|
modules.forEach((module) => {
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('a');
|
||||||
el.className = 'dashboard-tile';
|
el.className = 'dashboard-tile';
|
||||||
|
el.href = module.url;
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<div class="dashboard-icon">${module.icon}</div>
|
<div class="dashboard-icon">${module.icon}</div>
|
||||||
<div class="dashboard-content">
|
<div class="dashboard-content">
|
||||||
@@ -101,30 +37,54 @@ window.Dashboard = (function () {
|
|||||||
<p>${module.description}</p>
|
<p>${module.description}</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
el.addEventListener('click', () => {
|
|
||||||
window.location.href = module.url;
|
|
||||||
});
|
|
||||||
|
|
||||||
container.appendChild(el);
|
container.appendChild(el);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================
|
function loadStats() {
|
||||||
// Optional Erweiterungen
|
const stats = {
|
||||||
// =========================
|
devices: countRows('.device-list tbody tr'),
|
||||||
|
connections: countRows('.connection-list tbody tr'),
|
||||||
|
outlets: countRows('.infra-table tbody tr')
|
||||||
|
};
|
||||||
|
|
||||||
// TODO: loadStats() → Anzahl Geräte, offene Ports, unverbundene Dosen
|
const target = document.querySelector('[data-dashboard-stats]');
|
||||||
// TODO: showWarnings() → unverbundene Ports, VLAN-Konflikte
|
if (!target) {
|
||||||
// TODO: RecentChanges() → letzte Änderungen
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// =========================
|
target.textContent = `Geraete: ${stats.devices} | Verbindungen: ${stats.connections} | Infrastruktur-Eintraege: ${stats.outlets}`;
|
||||||
// Expose Public Methods
|
}
|
||||||
// =========================
|
|
||||||
|
|
||||||
return {
|
function showWarnings() {
|
||||||
init,
|
const target = document.querySelector('[data-dashboard-warnings]');
|
||||||
// renderModules // optional öffentlich machen
|
if (!target) {
|
||||||
};
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const warnings = [];
|
||||||
|
if (countRows('.device-list tbody tr') === 0) {
|
||||||
|
warnings.push('Noch keine Geraete vorhanden');
|
||||||
|
}
|
||||||
|
if (countRows('.connection-list tbody tr') === 0) {
|
||||||
|
warnings.push('Noch keine Verbindungen vorhanden');
|
||||||
|
}
|
||||||
|
|
||||||
|
target.textContent = warnings.length ? warnings.join(' | ') : 'Keine offenen Warnungen erkannt';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRecentChanges() {
|
||||||
|
const target = document.querySelector('[data-dashboard-recent]');
|
||||||
|
if (!target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.textContent = 'Letzte Aenderungen werden serverseitig noch nicht protokolliert.';
|
||||||
|
}
|
||||||
|
|
||||||
|
function countRows(selector) {
|
||||||
|
return document.querySelectorAll(selector).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { init };
|
||||||
})();
|
})();
|
||||||
|
|||||||
102
app/assets/js/device-type-edit-form.js
Normal file
102
app/assets/js/device-type-edit-form.js
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
(() => {
|
||||||
|
function addPortRow() {
|
||||||
|
const body = document.getElementById('port-definition-body');
|
||||||
|
if (!body) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyRow = body.querySelector('tr td em');
|
||||||
|
if (emptyRow) {
|
||||||
|
const emptyTableRow = emptyRow.closest('tr');
|
||||||
|
if (emptyTableRow) {
|
||||||
|
emptyTableRow.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowCount = body.querySelectorAll('tr').length;
|
||||||
|
const index = rowCount;
|
||||||
|
const number = rowCount + 1;
|
||||||
|
|
||||||
|
const optionsTemplate = document.getElementById('port-type-options-template');
|
||||||
|
const portTypeOptions = optionsTemplate
|
||||||
|
? optionsTemplate.innerHTML
|
||||||
|
: '<option value="">- Kein Typ -</option>';
|
||||||
|
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.innerHTML = `
|
||||||
|
<td>${number}</td>
|
||||||
|
<td>
|
||||||
|
<input type="hidden" name="port_rows[${index}][id]" value="">
|
||||||
|
<input type="text" name="port_rows[${index}][name]" value="" placeholder="z.B. Gi1/0/1">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select name="port_rows[${index}][port_type_id]">
|
||||||
|
${portTypeOptions}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<label class="inline-checkbox">
|
||||||
|
<input type="checkbox" name="port_rows[${index}][delete]" value="1">
|
||||||
|
entfernen
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
body.appendChild(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindAddPortRowButton() {
|
||||||
|
const addButton = document.getElementById('add-port-row');
|
||||||
|
if (!addButton || addButton.dataset.portRowBound === '1') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addButton.addEventListener('click', addPortRow);
|
||||||
|
addButton.dataset.portRowBound = '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindDeleteButton() {
|
||||||
|
const deleteButton = document.getElementById('device-type-delete');
|
||||||
|
if (!deleteButton || deleteButton.dataset.deleteBound === '1') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteButton.addEventListener('click', () => {
|
||||||
|
const id = Number(deleteButton.dataset.deviceTypeId || '0');
|
||||||
|
if (id <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!window.confirm('Diesen Geraetetyp wirklich loeschen?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('?module=device_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.href = '?module=device_types&action=list';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
|
||||||
|
})
|
||||||
|
.catch(() => window.alert('Loeschen fehlgeschlagen'));
|
||||||
|
});
|
||||||
|
|
||||||
|
deleteButton.dataset.deleteBound = '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
bindAddPortRowButton();
|
||||||
|
bindDeleteButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
})();
|
||||||
42
app/assets/js/device-types-list.js
Normal file
42
app/assets/js/device-types-list.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
(() => {
|
||||||
|
function bindDeleteButtons() {
|
||||||
|
const buttons = document.querySelectorAll('.js-device-type-delete');
|
||||||
|
buttons.forEach((button) => {
|
||||||
|
if (button.dataset.deleteBound === '1') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
const id = Number(button.dataset.deviceTypeId || '0');
|
||||||
|
if (id <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.confirm('Diesen Geraetetyp wirklich loeschen?')) {
|
||||||
|
fetch('?module=device_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;
|
||||||
|
}
|
||||||
|
window.alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
|
||||||
|
})
|
||||||
|
.catch(() => window.alert('Loeschen fehlgeschlagen'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
button.dataset.deleteBound = '1';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', bindDeleteButtons);
|
||||||
|
} else {
|
||||||
|
bindDeleteButtons();
|
||||||
|
}
|
||||||
|
})();
|
||||||
607
app/assets/js/floor-infrastructure-edit.js
Normal file
607
app/assets/js/floor-infrastructure-edit.js
Normal file
@@ -0,0 +1,607 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||||
|
const DEFAULT_PLAN_SIZE = { width: 2000, height: 1000 };
|
||||||
|
|
||||||
|
const canvas = document.getElementById('floor-plan-canvas');
|
||||||
|
const overlay = document.getElementById('floor-plan-overlay');
|
||||||
|
const positionLabel = document.getElementById('floor-plan-position');
|
||||||
|
if (!canvas || !overlay) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const xFieldName = canvas.dataset.xField;
|
||||||
|
const yFieldName = canvas.dataset.yField;
|
||||||
|
const xField = xFieldName ? document.querySelector(`input[name="${xFieldName}"]`) : null;
|
||||||
|
const yField = yFieldName ? document.querySelector(`input[name="${yFieldName}"]`) : null;
|
||||||
|
if (!xField || !yField) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const markerWidth = Math.max(1, Number(canvas.dataset.markerWidth) || 10);
|
||||||
|
const markerHeight = Math.max(1, Number(canvas.dataset.markerHeight) || 10);
|
||||||
|
const markerType = canvas.dataset.markerType || '';
|
||||||
|
const activeId = Number(canvas.dataset.activeId || 0);
|
||||||
|
const panelReferences = JSON.parse(canvas.dataset.referencePanels || '[]');
|
||||||
|
const outletReferences = JSON.parse(canvas.dataset.referenceOutlets || '[]');
|
||||||
|
|
||||||
|
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
||||||
|
|
||||||
|
let markerX = 0;
|
||||||
|
let markerY = 0;
|
||||||
|
let dragging = false;
|
||||||
|
let panning = false;
|
||||||
|
let panStart = null;
|
||||||
|
let dragOffsetX = 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');
|
||||||
|
activeMarker.classList.add('active-marker');
|
||||||
|
if (markerType === 'patchpanel') {
|
||||||
|
activeMarker.classList.add('panel-marker');
|
||||||
|
} else {
|
||||||
|
activeMarker.classList.add('outlet-marker');
|
||||||
|
}
|
||||||
|
activeMarker.setAttribute('width', String(markerWidth));
|
||||||
|
activeMarker.setAttribute('height', String(markerHeight));
|
||||||
|
overlay.appendChild(activeMarker);
|
||||||
|
|
||||||
|
const planSize = { ...DEFAULT_PLAN_SIZE };
|
||||||
|
const updateOverlayViewBox = () => {
|
||||||
|
overlay.setAttribute('viewBox', `${viewX} ${viewY} ${viewWidth} ${viewHeight}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePositionLabel = (x, y) => {
|
||||||
|
if (positionLabel) {
|
||||||
|
positionLabel.textContent = `${Math.round(x)} x ${Math.round(y)}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const paintActiveMarker = () => {
|
||||||
|
activeMarker.setAttribute('x', String(Math.round(markerX)));
|
||||||
|
activeMarker.setAttribute('y', String(Math.round(markerY)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const setMarkerPosition = (rawX, rawY) => {
|
||||||
|
const maxX = Math.max(0, planSize.width - markerWidth);
|
||||||
|
const maxY = Math.max(0, planSize.height - markerHeight);
|
||||||
|
markerX = clamp(rawX, 0, maxX);
|
||||||
|
markerY = clamp(rawY, 0, maxY);
|
||||||
|
|
||||||
|
paintActiveMarker();
|
||||||
|
xField.value = Math.round(markerX);
|
||||||
|
yField.value = Math.round(markerY);
|
||||||
|
updatePositionLabel(markerX, markerY);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toOverlayPoint = (clientX, clientY) => {
|
||||||
|
const rect = overlay.getBoundingClientRect();
|
||||||
|
if (rect.width <= 0 || rect.height <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
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 };
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = () => {
|
||||||
|
setMarkerPosition(Number(xField.value) || 0, Number(yField.value) || 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearReferenceMarkers = () => {
|
||||||
|
overlay.querySelectorAll('.reference-marker').forEach((node) => node.remove());
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearRoomHighlight = () => {
|
||||||
|
overlay.querySelectorAll('.room-highlight').forEach((node) => node.remove());
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNumericCoord = (value) => {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const num = Number(value);
|
||||||
|
return Number.isFinite(num) ? num : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const appendReference = (entry, cssClass, width, height) => {
|
||||||
|
const rawX = entry.x ?? entry.pos_x;
|
||||||
|
const rawY = entry.y ?? entry.pos_y;
|
||||||
|
const x = getNumericCoord(rawX);
|
||||||
|
const y = getNumericCoord(rawY);
|
||||||
|
if (x === null || y === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ref = document.createElementNS(SVG_NS, 'rect');
|
||||||
|
ref.classList.add('reference-marker', cssClass);
|
||||||
|
ref.setAttribute('x', String(Math.round(x)));
|
||||||
|
ref.setAttribute('y', String(Math.round(y)));
|
||||||
|
ref.setAttribute('width', String(Math.max(1, Math.round(width))));
|
||||||
|
ref.setAttribute('height', String(Math.max(1, Math.round(height))));
|
||||||
|
if (entry.name) {
|
||||||
|
ref.setAttribute('aria-label', String(entry.name));
|
||||||
|
}
|
||||||
|
overlay.insertBefore(ref, activeMarker);
|
||||||
|
};
|
||||||
|
|
||||||
|
const appendRoomHighlight = () => {
|
||||||
|
if (!outletRoomSelect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const selectedRoomOption = outletRoomSelect.selectedOptions?.[0];
|
||||||
|
if (!selectedRoomOption || !selectedRoomOption.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentFloorId = getCurrentFloorId();
|
||||||
|
const roomFloorId = Number(selectedRoomOption.dataset.floorId || 0);
|
||||||
|
if (!currentFloorId || roomFloorId !== currentFloorId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const polygonRaw = String(selectedRoomOption.dataset.roomPolygon || '').trim();
|
||||||
|
if (polygonRaw) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(polygonRaw);
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
const points = parsed
|
||||||
|
.map((point) => ({
|
||||||
|
x: Number(point && point.x),
|
||||||
|
y: Number(point && point.y)
|
||||||
|
}))
|
||||||
|
.filter((point) => Number.isFinite(point.x) && Number.isFinite(point.y));
|
||||||
|
|
||||||
|
if (points.length >= 3) {
|
||||||
|
const polygon = document.createElementNS(SVG_NS, 'polygon');
|
||||||
|
polygon.classList.add('room-highlight');
|
||||||
|
polygon.setAttribute(
|
||||||
|
'points',
|
||||||
|
points.map((point) => `${Math.round(point.x)},${Math.round(point.y)}`).join(' ')
|
||||||
|
);
|
||||||
|
overlay.insertBefore(polygon, activeMarker);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// ignore invalid room polygon json
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = Number(selectedRoomOption.dataset.roomX || 0);
|
||||||
|
const y = Number(selectedRoomOption.dataset.roomY || 0);
|
||||||
|
const width = Number(selectedRoomOption.dataset.roomWidth || 0);
|
||||||
|
const height = Number(selectedRoomOption.dataset.roomHeight || 0);
|
||||||
|
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(width) || !Number.isFinite(height)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = document.createElementNS(SVG_NS, 'rect');
|
||||||
|
rect.classList.add('room-highlight');
|
||||||
|
rect.setAttribute('x', String(Math.round(x)));
|
||||||
|
rect.setAttribute('y', String(Math.round(y)));
|
||||||
|
rect.setAttribute('width', String(Math.round(width)));
|
||||||
|
rect.setAttribute('height', String(Math.round(height)));
|
||||||
|
overlay.insertBefore(rect, activeMarker);
|
||||||
|
};
|
||||||
|
|
||||||
|
const panelLocationSelect = document.getElementById('panel-location-select');
|
||||||
|
const panelBuildingSelect = document.getElementById('panel-building-select');
|
||||||
|
const panelFloorSelect = document.getElementById('panel-floor-select');
|
||||||
|
const outletRoomSelect = document.getElementById('outlet-room-select');
|
||||||
|
const panelPlacementFields = document.getElementById('panel-placement-fields');
|
||||||
|
const panelFloorPlanGroup = document.getElementById('panel-floor-plan-group');
|
||||||
|
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 floorOptions = panelFloorSelect ? Array.from(panelFloorSelect.options).filter((option) => option.value !== '') : [];
|
||||||
|
|
||||||
|
const getCurrentFloorId = () => {
|
||||||
|
const floorOption = panelFloorSelect?.selectedOptions?.[0];
|
||||||
|
if (floorOption?.value) {
|
||||||
|
return Number(floorOption.value);
|
||||||
|
}
|
||||||
|
const roomOption = outletRoomSelect?.selectedOptions?.[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 = () => {
|
||||||
|
clearRoomHighlight();
|
||||||
|
clearReferenceMarkers();
|
||||||
|
const currentFloorId = getCurrentFloorId();
|
||||||
|
if (!currentFloorId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
appendRoomHighlight();
|
||||||
|
|
||||||
|
panelReferences.forEach((entry) => {
|
||||||
|
if (Number(entry.floor_id) !== currentFloorId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (markerType === 'patchpanel' && Number(entry.id) === activeId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
appendReference(entry, 'panel-marker', Math.max(1, Number(entry.width) || 20), Math.max(1, Number(entry.height) || 5));
|
||||||
|
});
|
||||||
|
|
||||||
|
outletReferences.forEach((entry) => {
|
||||||
|
if (Number(entry.floor_id) !== currentFloorId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (markerType === 'outlet' && Number(entry.id) === activeId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
appendReference(entry, 'outlet-marker', 10, 10);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFloorPlanImage = () => {
|
||||||
|
const floorOption = panelFloorSelect?.selectedOptions?.[0];
|
||||||
|
const roomOption = outletRoomSelect?.selectedOptions?.[0];
|
||||||
|
const svgUrl = floorOption?.dataset?.svgUrl || roomOption?.dataset?.floorSvgUrl || '';
|
||||||
|
|
||||||
|
if (svgUrl) {
|
||||||
|
backgroundImage.setAttribute('href', svgUrl);
|
||||||
|
backgroundImage.setAttribute('width', String(planSize.width));
|
||||||
|
backgroundImage.setAttribute('height', String(planSize.height));
|
||||||
|
backgroundImage.setAttribute('display', 'block');
|
||||||
|
loadPlanDimensions(svgUrl);
|
||||||
|
} else {
|
||||||
|
backgroundImage.removeAttribute('href');
|
||||||
|
backgroundImage.setAttribute('display', 'none');
|
||||||
|
planSize.width = DEFAULT_PLAN_SIZE.width;
|
||||||
|
planSize.height = DEFAULT_PLAN_SIZE.height;
|
||||||
|
resetView();
|
||||||
|
}
|
||||||
|
renderReferenceMarkers();
|
||||||
|
filterPatchpanelBindOptions();
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadPlanDimensions = async (svgUrl) => {
|
||||||
|
if (!svgUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await fetch(svgUrl, { credentials: 'same-origin' });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('SVG not available');
|
||||||
|
}
|
||||||
|
const raw = await response.text();
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(raw, 'image/svg+xml');
|
||||||
|
const root = doc.documentElement;
|
||||||
|
if (!root || root.nodeName.toLowerCase() === 'parsererror') {
|
||||||
|
throw new Error('Invalid SVG');
|
||||||
|
}
|
||||||
|
|
||||||
|
const vb = String(root.getAttribute('viewBox') || '').trim();
|
||||||
|
if (vb) {
|
||||||
|
const parts = vb.split(/\s+/).map((value) => Number(value));
|
||||||
|
if (parts.length === 4 && parts.every((value) => Number.isFinite(value))) {
|
||||||
|
planSize.width = Math.max(1, parts[2]);
|
||||||
|
planSize.height = Math.max(1, parts[3]);
|
||||||
|
backgroundImage.setAttribute('width', String(planSize.width));
|
||||||
|
backgroundImage.setAttribute('height', String(planSize.height));
|
||||||
|
resetView();
|
||||||
|
renderReferenceMarkers();
|
||||||
|
updateFromInputs();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = Number(root.getAttribute('width'));
|
||||||
|
const height = Number(root.getAttribute('height'));
|
||||||
|
if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) {
|
||||||
|
planSize.width = width;
|
||||||
|
planSize.height = height;
|
||||||
|
} else {
|
||||||
|
planSize.width = DEFAULT_PLAN_SIZE.width;
|
||||||
|
planSize.height = DEFAULT_PLAN_SIZE.height;
|
||||||
|
}
|
||||||
|
backgroundImage.setAttribute('width', String(planSize.width));
|
||||||
|
backgroundImage.setAttribute('height', String(planSize.height));
|
||||||
|
resetView();
|
||||||
|
renderReferenceMarkers();
|
||||||
|
updateFromInputs();
|
||||||
|
} catch (error) {
|
||||||
|
planSize.width = DEFAULT_PLAN_SIZE.width;
|
||||||
|
planSize.height = DEFAULT_PLAN_SIZE.height;
|
||||||
|
backgroundImage.setAttribute('width', String(planSize.width));
|
||||||
|
backgroundImage.setAttribute('height', String(planSize.height));
|
||||||
|
resetView();
|
||||||
|
renderReferenceMarkers();
|
||||||
|
updateFromInputs();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePanelPlacementVisibility = () => {
|
||||||
|
if (!panelFloorSelect || !panelPlacementFields || !panelFloorPlanGroup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasFloor = !!panelFloorSelect.value;
|
||||||
|
panelPlacementFields.hidden = !hasFloor;
|
||||||
|
panelFloorPlanGroup.hidden = !hasFloor;
|
||||||
|
if (panelFloorMissingHint) {
|
||||||
|
panelFloorMissingHint.hidden = hasFloor;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterFloorOptions = () => {
|
||||||
|
if (!panelFloorSelect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const buildingValue = panelBuildingSelect?.value || '';
|
||||||
|
let firstMatch = '';
|
||||||
|
floorOptions.forEach((option) => {
|
||||||
|
const matches = !buildingValue || option.dataset.buildingId === buildingValue;
|
||||||
|
option.hidden = !matches;
|
||||||
|
option.disabled = !matches;
|
||||||
|
if (matches && !firstMatch) {
|
||||||
|
firstMatch = option.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedOption = panelFloorSelect.querySelector(`option[value="${panelFloorSelect.value}"]`);
|
||||||
|
const isSelectedHidden = selectedOption ? selectedOption.hidden : true;
|
||||||
|
if (buildingValue && (!panelFloorSelect.value || isSelectedHidden) && firstMatch) {
|
||||||
|
panelFloorSelect.value = firstMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePanelPlacementVisibility();
|
||||||
|
updateFloorPlanImage();
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterBuildingOptions = () => {
|
||||||
|
if (!panelBuildingSelect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const locationValue = panelLocationSelect?.value || '';
|
||||||
|
let firstMatch = '';
|
||||||
|
buildingOptions.forEach((option) => {
|
||||||
|
const matches = !locationValue || option.dataset.locationId === locationValue;
|
||||||
|
option.hidden = !matches;
|
||||||
|
option.disabled = !matches;
|
||||||
|
if (matches && !firstMatch) {
|
||||||
|
firstMatch = option.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedOption = panelBuildingSelect.querySelector(`option[value="${panelBuildingSelect.value}"]`);
|
||||||
|
const isSelectedHidden = selectedOption ? selectedOption.hidden : true;
|
||||||
|
if (locationValue && (!panelBuildingSelect.value || isSelectedHidden) && firstMatch) {
|
||||||
|
panelBuildingSelect.value = firstMatch;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
activeMarker.addEventListener('pointerdown', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
dragging = true;
|
||||||
|
panning = false;
|
||||||
|
const point = toOverlayPoint(event.clientX, event.clientY);
|
||||||
|
if (!point) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dragOffsetX = point.x - markerX;
|
||||||
|
dragOffsetY = point.y - markerY;
|
||||||
|
activeMarker.setPointerCapture(event.pointerId);
|
||||||
|
});
|
||||||
|
|
||||||
|
activeMarker.addEventListener('pointermove', (event) => {
|
||||||
|
if (!dragging) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const point = toOverlayPoint(event.clientX, event.clientY);
|
||||||
|
if (!point) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setMarkerPosition(point.x - dragOffsetX, point.y - dragOffsetY);
|
||||||
|
});
|
||||||
|
|
||||||
|
const stopDrag = (event) => {
|
||||||
|
if (dragging) {
|
||||||
|
dragging = false;
|
||||||
|
if (activeMarker.hasPointerCapture(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) => {
|
||||||
|
activeMarker.addEventListener(evt, stopDrag);
|
||||||
|
});
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const point = toOverlayPoint(event.clientX, event.clientY);
|
||||||
|
if (!point) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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) => {
|
||||||
|
input.addEventListener('input', () => {
|
||||||
|
updateFromInputs();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
updateFromInputs();
|
||||||
|
renderReferenceMarkers();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (panelLocationSelect) {
|
||||||
|
panelLocationSelect.addEventListener('change', () => {
|
||||||
|
filterBuildingOptions();
|
||||||
|
filterFloorOptions();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (panelBuildingSelect) {
|
||||||
|
panelBuildingSelect.addEventListener('change', () => {
|
||||||
|
filterFloorOptions();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (panelFloorSelect) {
|
||||||
|
panelFloorSelect.addEventListener('change', () => {
|
||||||
|
updatePanelPlacementVisibility();
|
||||||
|
updateFloorPlanImage();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outletRoomSelect) {
|
||||||
|
outletRoomSelect.addEventListener('change', () => {
|
||||||
|
updateFloorPlanImage();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOverlayViewBox();
|
||||||
|
updateFromInputs();
|
||||||
|
filterPatchpanelBindOptions();
|
||||||
|
|
||||||
|
if (panelLocationSelect) {
|
||||||
|
filterBuildingOptions();
|
||||||
|
filterFloorOptions();
|
||||||
|
} else {
|
||||||
|
updateFloorPlanImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePanelPlacementVisibility();
|
||||||
|
});
|
||||||
148
app/assets/js/floor-infrastructure-list.js
Normal file
148
app/assets/js/floor-infrastructure-list.js
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||||
|
const DEFAULT_PLAN_SIZE = { width: 2000, height: 1000 };
|
||||||
|
|
||||||
|
const filterForm = document.getElementById('infra-filter-form');
|
||||||
|
const floorSelect = document.getElementById('infra-floor-select');
|
||||||
|
if (filterForm && floorSelect) {
|
||||||
|
floorSelect.addEventListener('change', () => {
|
||||||
|
filterForm.submit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = document.getElementById('infra-floor-canvas');
|
||||||
|
const overlay = document.getElementById('infra-floor-overlay');
|
||||||
|
const floorSvg = canvas ? canvas.querySelector('.infra-floor-svg') : null;
|
||||||
|
if (!canvas || !overlay || !floorSvg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const patchPanels = safeJsonParse(canvas.dataset.patchpanels);
|
||||||
|
const outlets = safeJsonParse(canvas.dataset.outlets);
|
||||||
|
const planSize = { ...DEFAULT_PLAN_SIZE };
|
||||||
|
|
||||||
|
const updateOverlayViewBox = () => {
|
||||||
|
overlay.setAttribute('viewBox', `0 0 ${planSize.width} ${planSize.height}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createMarker = (entry, type) => {
|
||||||
|
const x = Number(entry.x);
|
||||||
|
const y = Number(entry.y);
|
||||||
|
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const marker = document.createElementNS(SVG_NS, 'rect');
|
||||||
|
marker.classList.add('infra-overlay-marker', type);
|
||||||
|
marker.setAttribute('x', String(Math.round(x)));
|
||||||
|
marker.setAttribute('y', String(Math.round(y)));
|
||||||
|
|
||||||
|
if (type === 'patchpanel') {
|
||||||
|
marker.setAttribute('width', String(Math.max(1, Number(entry.width) || 20)));
|
||||||
|
marker.setAttribute('height', String(Math.max(1, Number(entry.height) || 5)));
|
||||||
|
} else {
|
||||||
|
marker.setAttribute('width', '10');
|
||||||
|
marker.setAttribute('height', '10');
|
||||||
|
}
|
||||||
|
|
||||||
|
const tooltipLines = buildTooltipLines(entry, type);
|
||||||
|
if (tooltipLines.length > 0) {
|
||||||
|
const titleNode = document.createElementNS(SVG_NS, 'title');
|
||||||
|
titleNode.textContent = tooltipLines.join('\n');
|
||||||
|
marker.appendChild(titleNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay.appendChild(marker);
|
||||||
|
};
|
||||||
|
|
||||||
|
patchPanels.forEach((entry) => createMarker(entry, 'patchpanel'));
|
||||||
|
outlets.forEach((entry) => createMarker(entry, 'outlet'));
|
||||||
|
|
||||||
|
const loadPlanDimensions = async (svgUrl) => {
|
||||||
|
if (!svgUrl) {
|
||||||
|
updateOverlayViewBox();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await fetch(svgUrl, { credentials: 'same-origin' });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('SVG not available');
|
||||||
|
}
|
||||||
|
const raw = await response.text();
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(raw, 'image/svg+xml');
|
||||||
|
const root = doc.documentElement;
|
||||||
|
if (!root || root.nodeName.toLowerCase() === 'parsererror') {
|
||||||
|
throw new Error('Invalid SVG');
|
||||||
|
}
|
||||||
|
|
||||||
|
const vb = String(root.getAttribute('viewBox') || '').trim();
|
||||||
|
if (vb) {
|
||||||
|
const parts = vb.split(/\s+/).map((value) => Number(value));
|
||||||
|
if (parts.length === 4 && parts.every((value) => Number.isFinite(value))) {
|
||||||
|
planSize.width = Math.max(1, parts[2]);
|
||||||
|
planSize.height = Math.max(1, parts[3]);
|
||||||
|
updateOverlayViewBox();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = Number(root.getAttribute('width'));
|
||||||
|
const height = Number(root.getAttribute('height'));
|
||||||
|
if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) {
|
||||||
|
planSize.width = width;
|
||||||
|
planSize.height = height;
|
||||||
|
} else {
|
||||||
|
planSize.width = DEFAULT_PLAN_SIZE.width;
|
||||||
|
planSize.height = DEFAULT_PLAN_SIZE.height;
|
||||||
|
}
|
||||||
|
updateOverlayViewBox();
|
||||||
|
} catch (error) {
|
||||||
|
planSize.width = DEFAULT_PLAN_SIZE.width;
|
||||||
|
planSize.height = DEFAULT_PLAN_SIZE.height;
|
||||||
|
updateOverlayViewBox();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadPlanDimensions(floorSvg.getAttribute('src') || '');
|
||||||
|
});
|
||||||
|
|
||||||
|
function safeJsonParse(value) {
|
||||||
|
if (!value) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
return Array.isArray(parsed) ? parsed : [];
|
||||||
|
} catch (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTooltipLines(entry, type) {
|
||||||
|
if (type === 'patchpanel') {
|
||||||
|
const lines = [
|
||||||
|
`Patchpanel: ${entry.name || '-'}`,
|
||||||
|
`Ports: ${Number(entry.port_count) || 0}`,
|
||||||
|
`Position: ${Number(entry.x) || 0} x ${Number(entry.y) || 0}`
|
||||||
|
];
|
||||||
|
if (entry.comment) {
|
||||||
|
lines.push(`Kommentar: ${entry.comment}`);
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomLabel = `${entry.room_name || '-'}${entry.room_number ? ` (${entry.room_number})` : ''}`;
|
||||||
|
const lines = [
|
||||||
|
`Wandbuchse: ${entry.name || '-'}`,
|
||||||
|
`Raum: ${roomLabel}`,
|
||||||
|
`Position: ${Number(entry.x) || 0} x ${Number(entry.y) || 0}`
|
||||||
|
];
|
||||||
|
if (entry.port_names) {
|
||||||
|
lines.push(`Ports: ${entry.port_names}`);
|
||||||
|
}
|
||||||
|
if (entry.comment) {
|
||||||
|
lines.push(`Kommentar: ${entry.comment}`);
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
@@ -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') {
|
||||||
|
|||||||
96
app/assets/js/locations-list.js
Normal file
96
app/assets/js/locations-list.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
(() => {
|
||||||
|
function postDelete(url) {
|
||||||
|
return fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
|
}).then((response) => response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLocationDelete(id) {
|
||||||
|
if (!confirm('Diesen Standort wirklich loeschen?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
postDelete('?module=locations&action=delete&id=' + encodeURIComponent(id))
|
||||||
|
.then((data) => {
|
||||||
|
if (data && data.success) {
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
|
||||||
|
})
|
||||||
|
.catch(() => alert('Loeschen fehlgeschlagen'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBuildingDelete(id) {
|
||||||
|
if (!confirm('Dieses Gebaeude wirklich loeschen? Alle Stockwerke werden geloescht.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
postDelete('?module=buildings&action=delete&id=' + encodeURIComponent(id))
|
||||||
|
.then((data) => {
|
||||||
|
if (data && (data.success || data.status === 'ok')) {
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert((data && (data.message || data.error)) ? (data.message || data.error) : 'Loeschen fehlgeschlagen');
|
||||||
|
})
|
||||||
|
.catch(() => alert('Loeschen fehlgeschlagen'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFloorDelete(id) {
|
||||||
|
if (!confirm('Dieses Stockwerk wirklich loeschen? Alle Raeume und Racks werden geloescht.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
postDelete('?module=floors&action=delete&id=' + encodeURIComponent(id))
|
||||||
|
.then((data) => {
|
||||||
|
if (data && data.success) {
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
|
||||||
|
})
|
||||||
|
.catch(() => alert('Loeschen fehlgeschlagen'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRoomDelete(id) {
|
||||||
|
if (!confirm('Diesen Raum wirklich loeschen? Zugeordnete Dosen werden mitgeloescht.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
postDelete('?module=rooms&action=delete&id=' + encodeURIComponent(id))
|
||||||
|
.then((data) => {
|
||||||
|
if (data && data.success) {
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
|
||||||
|
})
|
||||||
|
.catch(() => alert('Loeschen fehlgeschlagen'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindDeleteButtons() {
|
||||||
|
document.querySelectorAll('.js-delete-location').forEach((button) => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
handleLocationDelete(Number(button.dataset.locationId || 0));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.js-delete-building').forEach((button) => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
handleBuildingDelete(Number(button.dataset.buildingId || 0));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.js-delete-floor').forEach((button) => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
handleFloorDelete(Number(button.dataset.floorId || 0));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.js-delete-room').forEach((button) => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
handleRoomDelete(Number(button.dataset.roomId || 0));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', bindDeleteButtons);
|
||||||
|
})();
|
||||||
@@ -1,61 +1,23 @@
|
|||||||
// Netzwerk-Graph-Ansicht (Nodes, Kanten, Filter)
|
|
||||||
/**
|
|
||||||
* network-view.js
|
|
||||||
*
|
|
||||||
* Darstellung der Netzwerk-Topologie:
|
|
||||||
* - Geräte als Nodes
|
|
||||||
* - Ports als Ankerpunkte
|
|
||||||
* - Verbindungen als Linien
|
|
||||||
* - Freie / selbstdefinierte Verbindungstypen
|
|
||||||
*
|
|
||||||
* Kein Layout-Framework (kein D3, kein Cytoscape)
|
|
||||||
* -> bewusst simpel & erweiterbar
|
|
||||||
*/
|
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
/* =========================
|
const svgElement = document.querySelector('#network-svg');
|
||||||
* Konfiguration
|
if (!svgElement) {
|
||||||
* ========================= */
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Standort / Rack / View-Kontext vom Backend setzen
|
const CONTEXT_TYPE = svgElement.dataset.contextType || 'all';
|
||||||
const CONTEXT_ID = null;
|
const CONTEXT_ID = Number(svgElement.dataset.contextId || 0);
|
||||||
|
const API_LOAD_NETWORK = '/api/connections.php?action=load';
|
||||||
// TODO: API-Endpunkte definieren
|
|
||||||
const API_LOAD_NETWORK = '/api/network_view.php?action=load';
|
|
||||||
const API_SAVE_POSITIONS = '/api/network_view.php?action=save_positions';
|
const API_SAVE_POSITIONS = '/api/network_view.php?action=save_positions';
|
||||||
|
|
||||||
/* =========================
|
let devices = [];
|
||||||
* State
|
let ports = [];
|
||||||
* ========================= */
|
let connections = [];
|
||||||
|
|
||||||
let svgElement = null;
|
|
||||||
|
|
||||||
let devices = []; // Geräte inkl. Position
|
|
||||||
let connections = []; // Verbindungen zwischen Ports
|
|
||||||
|
|
||||||
let selectedDeviceId = null;
|
let selectedDeviceId = null;
|
||||||
let isDragging = false;
|
let isDragging = false;
|
||||||
let dragOffset = { x: 0, y: 0 };
|
let dragOffset = { x: 0, y: 0 };
|
||||||
|
|
||||||
/* =========================
|
bindSvgEvents();
|
||||||
* Initialisierung
|
loadNetwork();
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
svgElement = document.querySelector('#network-svg');
|
|
||||||
|
|
||||||
if (!svgElement) {
|
|
||||||
console.warn('Network View: #network-svg nicht gefunden');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bindSvgEvents();
|
|
||||||
loadNetwork();
|
|
||||||
});
|
|
||||||
|
|
||||||
/* =========================
|
|
||||||
* Events
|
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
function bindSvgEvents() {
|
function bindSvgEvents() {
|
||||||
svgElement.addEventListener('mousemove', onMouseMove);
|
svgElement.addEventListener('mousemove', onMouseMove);
|
||||||
@@ -63,37 +25,40 @@ function bindSvgEvents() {
|
|||||||
svgElement.addEventListener('click', onSvgClick);
|
svgElement.addEventListener('click', onSvgClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
function buildLoadUrl() {
|
||||||
* Laden
|
const params = new URLSearchParams();
|
||||||
* ========================= */
|
params.set('action', 'load');
|
||||||
|
params.set('context_type', CONTEXT_TYPE);
|
||||||
|
if (CONTEXT_TYPE !== 'all') {
|
||||||
|
params.set('context_id', String(CONTEXT_ID));
|
||||||
|
}
|
||||||
|
return '/api/connections.php?' + params.toString();
|
||||||
|
}
|
||||||
|
|
||||||
function loadNetwork() {
|
function loadNetwork() {
|
||||||
if (!CONTEXT_ID) {
|
fetch(buildLoadUrl())
|
||||||
console.warn('CONTEXT_ID nicht gesetzt');
|
.then((res) => res.json())
|
||||||
return;
|
.then((data) => {
|
||||||
}
|
if (!data || !Array.isArray(data.devices) || !Array.isArray(data.connections)) {
|
||||||
|
throw new Error('Antwortformat ungueltig');
|
||||||
fetch(`${API_LOAD_NETWORK}&context_id=${CONTEXT_ID}`)
|
}
|
||||||
.then(res => res.json())
|
|
||||||
.then(data => {
|
|
||||||
// TODO: Datenstruktur validieren
|
|
||||||
devices = data.devices || [];
|
|
||||||
connections = data.connections || [];
|
|
||||||
|
|
||||||
|
devices = data.devices.map((device, index) => ({
|
||||||
|
...device,
|
||||||
|
x: Number(device.pos_x ?? device.x ?? 50 + (index % 6) * 150),
|
||||||
|
y: Number(device.pos_y ?? device.y ?? 60 + Math.floor(index / 6) * 120)
|
||||||
|
}));
|
||||||
|
ports = Array.isArray(data.ports) ? data.ports : [];
|
||||||
|
connections = data.connections;
|
||||||
renderAll();
|
renderAll();
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
console.error('Fehler beim Laden der Netzwerkansicht', err);
|
console.error('Fehler beim Laden der Netzwerkansicht', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
|
||||||
* Rendering
|
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
function renderAll() {
|
function renderAll() {
|
||||||
clearSvg();
|
clearSvg();
|
||||||
|
|
||||||
renderConnections();
|
renderConnections();
|
||||||
renderDevices();
|
renderDevices();
|
||||||
}
|
}
|
||||||
@@ -104,34 +69,27 @@ function clearSvg() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Geräte ---------- */
|
|
||||||
|
|
||||||
function renderDevices() {
|
function renderDevices() {
|
||||||
devices.forEach(device => renderDevice(device));
|
devices.forEach((device) => renderDevice(device));
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDevice(device) {
|
function renderDevice(device) {
|
||||||
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||||
group.classList.add('device-node');
|
group.classList.add('device-node');
|
||||||
group.dataset.id = device.id;
|
group.dataset.id = device.id;
|
||||||
|
group.setAttribute('transform', `translate(${device.x || 0}, ${device.y || 0})`);
|
||||||
|
|
||||||
group.setAttribute(
|
|
||||||
'transform',
|
|
||||||
`translate(${device.x || 0}, ${device.y || 0})`
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: Gerätetyp (SVG oder JPG) korrekt laden
|
|
||||||
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
||||||
rect.setAttribute('width', 120);
|
rect.setAttribute('width', 120);
|
||||||
rect.setAttribute('height', 60);
|
rect.setAttribute('height', 60);
|
||||||
rect.setAttribute('rx', 6);
|
rect.setAttribute('rx', 6);
|
||||||
|
rect.classList.add('device-node-rect');
|
||||||
|
|
||||||
rect.addEventListener('mousedown', (e) => {
|
rect.addEventListener('mousedown', (e) => {
|
||||||
startDrag(e, device.id);
|
startDrag(e, device.id);
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Label
|
|
||||||
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||||
text.setAttribute('x', 60);
|
text.setAttribute('x', 60);
|
||||||
text.setAttribute('y', 35);
|
text.setAttribute('y', 35);
|
||||||
@@ -141,40 +99,59 @@ function renderDevice(device) {
|
|||||||
group.appendChild(rect);
|
group.appendChild(rect);
|
||||||
group.appendChild(text);
|
group.appendChild(text);
|
||||||
|
|
||||||
// TODO: Ports als kleine Kreise anlegen (Position aus Portdefinition)
|
const devicePorts = ports.filter((port) => Number(port.device_id) === Number(device.id));
|
||||||
// TODO: Ports klickbar machen (für Verbindungs-Erstellung)
|
const spacing = 120 / (Math.max(1, devicePorts.length) + 1);
|
||||||
|
devicePorts.forEach((port, index) => {
|
||||||
|
const dot = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||||
|
dot.setAttribute('cx', String(Math.round((index + 1) * spacing)));
|
||||||
|
dot.setAttribute('cy', '62');
|
||||||
|
dot.setAttribute('r', '3');
|
||||||
|
dot.classList.add('device-port-dot');
|
||||||
|
dot.dataset.portId = String(port.id);
|
||||||
|
dot.dataset.deviceId = String(device.id);
|
||||||
|
dot.addEventListener('click', (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
console.info('Port ausgewaehlt', port.id);
|
||||||
|
});
|
||||||
|
group.appendChild(dot);
|
||||||
|
});
|
||||||
|
|
||||||
svgElement.appendChild(group);
|
svgElement.appendChild(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Verbindungen ---------- */
|
|
||||||
|
|
||||||
function renderConnections() {
|
function renderConnections() {
|
||||||
connections.forEach(conn => renderConnection(conn));
|
connections.forEach((connection) => renderConnection(connection));
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderConnection(connection) {
|
function renderConnection(connection) {
|
||||||
// TODO: Quell- & Ziel-Port-Koordinaten berechnen
|
const sourcePort = ports.find((port) => Number(port.id) === Number(connection.port_a_id));
|
||||||
// TODO: unterschiedliche Verbindungstypen (Farbe, Strichart, Dicke)
|
const targetPort = ports.find((port) => Number(port.id) === Number(connection.port_b_id));
|
||||||
|
if (!sourcePort || !targetPort) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceDevice = devices.find((device) => Number(device.id) === Number(sourcePort.device_id));
|
||||||
|
const targetDevice = devices.find((device) => Number(device.id) === Number(targetPort.device_id));
|
||||||
|
if (!sourceDevice || !targetDevice) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||||
|
line.setAttribute('x1', String(sourceDevice.x + 60));
|
||||||
|
line.setAttribute('y1', String(sourceDevice.y + 60));
|
||||||
|
line.setAttribute('x2', String(targetDevice.x + 60));
|
||||||
|
line.setAttribute('y2', String(targetDevice.y + 60));
|
||||||
|
|
||||||
line.setAttribute('x1', 0);
|
const isFiber = String(connection.mode || '').toLowerCase().includes('fiber');
|
||||||
line.setAttribute('y1', 0);
|
|
||||||
line.setAttribute('x2', 100);
|
|
||||||
line.setAttribute('y2', 100);
|
|
||||||
|
|
||||||
line.classList.add('connection-line');
|
line.classList.add('connection-line');
|
||||||
|
line.setAttribute('stroke', isFiber ? '#2f6fef' : '#1f8b4c');
|
||||||
|
line.setAttribute('stroke-width', isFiber ? '2.5' : '2');
|
||||||
|
line.setAttribute('stroke-dasharray', isFiber ? '6 4' : '');
|
||||||
|
|
||||||
svgElement.appendChild(line);
|
svgElement.appendChild(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
|
||||||
* Interaktion
|
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
function onSvgClick(event) {
|
function onSvgClick(event) {
|
||||||
// Klick ins Leere -> Auswahl aufheben
|
|
||||||
if (event.target === svgElement) {
|
if (event.target === svgElement) {
|
||||||
selectedDeviceId = null;
|
selectedDeviceId = null;
|
||||||
updateSelection();
|
updateSelection();
|
||||||
@@ -202,7 +179,6 @@ function onMouseMove(event) {
|
|||||||
if (!device) return;
|
if (!device) return;
|
||||||
|
|
||||||
const point = getSvgCoordinates(event);
|
const point = getSvgCoordinates(event);
|
||||||
|
|
||||||
device.x = point.x + dragOffset.x;
|
device.x = point.x + dragOffset.x;
|
||||||
device.y = point.y + dragOffset.y;
|
device.y = point.y + dragOffset.y;
|
||||||
|
|
||||||
@@ -210,31 +186,28 @@ function onMouseMove(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onMouseUp() {
|
function onMouseUp() {
|
||||||
if (!isDragging) return;
|
if (!isDragging) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
isDragging = false;
|
isDragging = false;
|
||||||
|
|
||||||
// TODO: Positionen optional automatisch speichern
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
|
||||||
* Auswahl
|
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
function updateSelection() {
|
function updateSelection() {
|
||||||
svgElement.querySelectorAll('.device-node').forEach(el => {
|
svgElement.querySelectorAll('.device-node').forEach((el) => {
|
||||||
el.classList.toggle(
|
el.classList.toggle('selected', el.dataset.id === String(selectedDeviceId));
|
||||||
'selected',
|
|
||||||
el.dataset.id === String(selectedDeviceId)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Sidebar mit Gerätedetails füllen
|
const sidebar = document.querySelector('[data-network-selected-device]');
|
||||||
}
|
if (!sidebar) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/* =========================
|
const device = getDeviceById(selectedDeviceId);
|
||||||
* Speichern
|
sidebar.textContent = device
|
||||||
* ========================= */
|
? `${device.name} (ID ${device.id})`
|
||||||
|
: 'Kein Geraet ausgewaehlt';
|
||||||
|
}
|
||||||
|
|
||||||
function savePositions() {
|
function savePositions() {
|
||||||
fetch(API_SAVE_POSITIONS, {
|
fetch(API_SAVE_POSITIONS, {
|
||||||
@@ -242,27 +215,21 @@ function savePositions() {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
context_id: CONTEXT_ID,
|
context_id: CONTEXT_ID,
|
||||||
devices: devices.map(d => ({
|
devices: devices.map((device) => ({ id: device.id, x: device.x, y: device.y }))
|
||||||
id: d.id,
|
|
||||||
x: d.x,
|
|
||||||
y: d.y
|
|
||||||
}))
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(res => res.json())
|
.then((res) => res.json())
|
||||||
.then(data => {
|
.then((data) => {
|
||||||
// TODO: Erfolg / Fehler anzeigen
|
if (data?.error) {
|
||||||
console.log('Positionen gespeichert', data);
|
throw new Error(data.error);
|
||||||
})
|
}
|
||||||
.catch(err => {
|
alert('Positionen gespeichert');
|
||||||
console.error('Fehler beim Speichern', err);
|
})
|
||||||
});
|
.catch((err) => {
|
||||||
|
alert('Positionen konnten nicht gespeichert werden: ' + err.message);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
|
||||||
* Hilfsfunktionen
|
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
function getSvgCoordinates(event) {
|
function getSvgCoordinates(event) {
|
||||||
const pt = svgElement.createSVGPoint();
|
const pt = svgElement.createSVGPoint();
|
||||||
pt.x = event.clientX;
|
pt.x = event.clientX;
|
||||||
@@ -273,19 +240,23 @@ function getSvgCoordinates(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getDeviceById(id) {
|
function getDeviceById(id) {
|
||||||
return devices.find(d => d.id === id);
|
return devices.find((device) => Number(device.id) === Number(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
document.addEventListener('keydown', (event) => {
|
||||||
* Keyboard Shortcuts
|
if (event.key === 'Escape') {
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
selectedDeviceId = null;
|
selectedDeviceId = null;
|
||||||
updateSelection();
|
updateSelection();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Delete -> Gerät entfernen?
|
if (event.key === 'Delete' && selectedDeviceId) {
|
||||||
|
console.warn('Delete von Geraeten ist in der Netzwerkansicht noch nicht implementiert.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key.toLowerCase() === 's' && (event.ctrlKey || event.metaKey)) {
|
||||||
|
event.preventDefault();
|
||||||
|
savePositions();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
377
app/assets/js/room-polygon-editor.js
Normal file
377
app/assets/js/room-polygon-editor.js
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
(() => {
|
||||||
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||||
|
const DEFAULT_VIEWBOX = { x: 0, y: 0, width: 2000, height: 1000 };
|
||||||
|
const SNAP_TOLERANCE = 16;
|
||||||
|
|
||||||
|
function initRoomPolygonEditor() {
|
||||||
|
const floorSelect = document.getElementById('room-floor-id');
|
||||||
|
const canvas = document.getElementById('room-polygon-canvas');
|
||||||
|
const polygonInput = document.getElementById('room-polygon-points');
|
||||||
|
const snapWalls = document.getElementById('room-snap-walls');
|
||||||
|
const undoButton = document.getElementById('room-undo-point');
|
||||||
|
const clearButton = document.getElementById('room-clear-polygon');
|
||||||
|
const mapHint = document.getElementById('room-map-hint');
|
||||||
|
|
||||||
|
if (!floorSelect || !canvas || !polygonInput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
viewBox: { ...DEFAULT_VIEWBOX },
|
||||||
|
wallPoints: [],
|
||||||
|
floorLayerNodes: [],
|
||||||
|
points: parsePoints(polygonInput.value),
|
||||||
|
draggingIndex: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateInput = () => {
|
||||||
|
if (state.points.length >= 3) {
|
||||||
|
polygonInput.value = JSON.stringify(state.points.map((point) => ({
|
||||||
|
x: Math.round(point.x),
|
||||||
|
y: Math.round(point.y)
|
||||||
|
})));
|
||||||
|
} else {
|
||||||
|
polygonInput.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const render = () => {
|
||||||
|
canvas.innerHTML = '';
|
||||||
|
canvas.setAttribute(
|
||||||
|
'viewBox',
|
||||||
|
`${state.viewBox.x} ${state.viewBox.y} ${state.viewBox.width} ${state.viewBox.height}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const bg = createSvgElement('rect');
|
||||||
|
bg.setAttribute('x', String(state.viewBox.x));
|
||||||
|
bg.setAttribute('y', String(state.viewBox.y));
|
||||||
|
bg.setAttribute('width', String(state.viewBox.width));
|
||||||
|
bg.setAttribute('height', String(state.viewBox.height));
|
||||||
|
bg.setAttribute('fill', '#f7f7f7');
|
||||||
|
bg.setAttribute('stroke', '#e1e1e1');
|
||||||
|
canvas.appendChild(bg);
|
||||||
|
|
||||||
|
if (state.floorLayerNodes.length > 0) {
|
||||||
|
const floorLayer = createSvgElement('g');
|
||||||
|
floorLayer.setAttribute('opacity', '0.55');
|
||||||
|
floorLayer.setAttribute('pointer-events', 'none');
|
||||||
|
state.floorLayerNodes.forEach((node) => {
|
||||||
|
floorLayer.appendChild(node.cloneNode(true));
|
||||||
|
});
|
||||||
|
canvas.appendChild(floorLayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.points.length >= 2) {
|
||||||
|
const polyline = createSvgElement('polyline');
|
||||||
|
polyline.setAttribute('fill', state.points.length >= 3 ? 'rgba(13, 110, 253, 0.16)' : 'none');
|
||||||
|
polyline.setAttribute('stroke', '#0d6efd');
|
||||||
|
polyline.setAttribute('stroke-width', '3');
|
||||||
|
polyline.setAttribute('points', buildPointString(state.points));
|
||||||
|
if (state.points.length >= 3) {
|
||||||
|
polyline.setAttribute('stroke-linejoin', 'round');
|
||||||
|
polyline.setAttribute('stroke-linecap', 'round');
|
||||||
|
polyline.setAttribute('points', `${buildPointString(state.points)} ${state.points[0].x},${state.points[0].y}`);
|
||||||
|
}
|
||||||
|
canvas.appendChild(polyline);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.points.forEach((point, index) => {
|
||||||
|
const vertex = createSvgElement('circle');
|
||||||
|
vertex.setAttribute('cx', String(point.x));
|
||||||
|
vertex.setAttribute('cy', String(point.y));
|
||||||
|
vertex.setAttribute('r', '9');
|
||||||
|
vertex.setAttribute('fill', '#ffffff');
|
||||||
|
vertex.setAttribute('stroke', '#dc3545');
|
||||||
|
vertex.setAttribute('stroke-width', '3');
|
||||||
|
vertex.setAttribute('data-vertex-index', String(index));
|
||||||
|
canvas.appendChild(vertex);
|
||||||
|
});
|
||||||
|
|
||||||
|
updateInput();
|
||||||
|
};
|
||||||
|
|
||||||
|
const snapPoint = (point) => {
|
||||||
|
if (!snapWalls || !snapWalls.checked || state.wallPoints.length === 0) {
|
||||||
|
return point;
|
||||||
|
}
|
||||||
|
let nearest = null;
|
||||||
|
let nearestDist = Number.POSITIVE_INFINITY;
|
||||||
|
state.wallPoints.forEach((wallPoint) => {
|
||||||
|
const dx = wallPoint.x - point.x;
|
||||||
|
const dy = wallPoint.y - point.y;
|
||||||
|
const dist = Math.sqrt((dx * dx) + (dy * dy));
|
||||||
|
if (dist < nearestDist) {
|
||||||
|
nearestDist = dist;
|
||||||
|
nearest = wallPoint;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (nearest && nearestDist <= SNAP_TOLERANCE) {
|
||||||
|
return { x: nearest.x, y: nearest.y };
|
||||||
|
}
|
||||||
|
return point;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addPoint = (event) => {
|
||||||
|
const point = toSvgPoint(canvas, event);
|
||||||
|
if (!point) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.points.push(snapPoint(point));
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerDown = (event) => {
|
||||||
|
const target = event.target;
|
||||||
|
if (!(target instanceof SVGElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const vertex = target.closest('[data-vertex-index]');
|
||||||
|
if (vertex) {
|
||||||
|
const vertexIndex = Number(vertex.getAttribute('data-vertex-index'));
|
||||||
|
if (Number.isInteger(vertexIndex)) {
|
||||||
|
state.draggingIndex = vertexIndex;
|
||||||
|
vertex.setPointerCapture(event.pointerId);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addPoint(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerMove = (event) => {
|
||||||
|
if (state.draggingIndex === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const point = toSvgPoint(canvas, event);
|
||||||
|
if (!point) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.points[state.draggingIndex] = snapPoint(point);
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopDragging = () => {
|
||||||
|
state.draggingIndex = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseFloorSvg = (rawSvg) => {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(rawSvg, 'image/svg+xml');
|
||||||
|
const root = doc.documentElement;
|
||||||
|
if (!root || root.nodeName.toLowerCase() === 'parsererror') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
};
|
||||||
|
|
||||||
|
const readViewBox = (svgRoot) => {
|
||||||
|
const vb = (svgRoot.getAttribute('viewBox') || '').trim();
|
||||||
|
if (vb) {
|
||||||
|
const parts = vb.split(/\s+/).map((value) => Number(value));
|
||||||
|
if (parts.length === 4 && parts.every((value) => Number.isFinite(value))) {
|
||||||
|
return {
|
||||||
|
x: parts[0],
|
||||||
|
y: parts[1],
|
||||||
|
width: parts[2],
|
||||||
|
height: parts[3]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const width = Number(svgRoot.getAttribute('width'));
|
||||||
|
const height = Number(svgRoot.getAttribute('height'));
|
||||||
|
if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) {
|
||||||
|
return { x: 0, y: 0, width, height };
|
||||||
|
}
|
||||||
|
return { ...DEFAULT_VIEWBOX };
|
||||||
|
};
|
||||||
|
|
||||||
|
const collectSnapPoints = (svgRoot) => {
|
||||||
|
const points = [];
|
||||||
|
|
||||||
|
const addPointValue = (x, y) => {
|
||||||
|
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
points.push({ x: Math.round(x), y: Math.round(y) });
|
||||||
|
};
|
||||||
|
|
||||||
|
svgRoot.querySelectorAll('line').forEach((line) => {
|
||||||
|
addPointValue(Number(line.getAttribute('x1')), Number(line.getAttribute('y1')));
|
||||||
|
addPointValue(Number(line.getAttribute('x2')), Number(line.getAttribute('y2')));
|
||||||
|
});
|
||||||
|
|
||||||
|
svgRoot.querySelectorAll('polyline, polygon').forEach((shape) => {
|
||||||
|
parsePointString(shape.getAttribute('points') || '').forEach((point) => addPointValue(point.x, point.y));
|
||||||
|
});
|
||||||
|
|
||||||
|
svgRoot.querySelectorAll('rect').forEach((rect) => {
|
||||||
|
const x = Number(rect.getAttribute('x'));
|
||||||
|
const y = Number(rect.getAttribute('y'));
|
||||||
|
const width = Number(rect.getAttribute('width'));
|
||||||
|
const height = Number(rect.getAttribute('height'));
|
||||||
|
addPointValue(x, y);
|
||||||
|
addPointValue(x + width, y);
|
||||||
|
addPointValue(x + width, y + height);
|
||||||
|
addPointValue(x, y + height);
|
||||||
|
});
|
||||||
|
|
||||||
|
svgRoot.querySelectorAll('path').forEach((path) => {
|
||||||
|
const d = path.getAttribute('d') || '';
|
||||||
|
const numbers = (d.match(/-?\d+(\.\d+)?/g) || []).map((value) => Number(value));
|
||||||
|
for (let i = 0; i < numbers.length - 1; i += 2) {
|
||||||
|
addPointValue(numbers[i], numbers[i + 1]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return dedupePoints(points);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadFloor = async () => {
|
||||||
|
const selected = floorSelect.selectedOptions[0];
|
||||||
|
const svgUrl = selected ? (selected.dataset.svgUrl || '') : '';
|
||||||
|
|
||||||
|
state.viewBox = { ...DEFAULT_VIEWBOX };
|
||||||
|
state.wallPoints = [];
|
||||||
|
state.floorLayerNodes = [];
|
||||||
|
|
||||||
|
if (!svgUrl) {
|
||||||
|
if (mapHint) {
|
||||||
|
mapHint.textContent = 'Fuer dieses Stockwerk ist keine Karte hinterlegt. Polygon kann trotzdem frei gezeichnet werden.';
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(svgUrl, { credentials: 'same-origin' });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('SVG not available');
|
||||||
|
}
|
||||||
|
const raw = await response.text();
|
||||||
|
const root = parseFloorSvg(raw);
|
||||||
|
if (!root) {
|
||||||
|
throw new Error('Invalid SVG');
|
||||||
|
}
|
||||||
|
|
||||||
|
state.viewBox = readViewBox(root);
|
||||||
|
state.wallPoints = collectSnapPoints(root);
|
||||||
|
state.floorLayerNodes = Array.from(root.childNodes)
|
||||||
|
.filter((node) => node.nodeType === Node.ELEMENT_NODE)
|
||||||
|
.map((node) => node.cloneNode(true));
|
||||||
|
|
||||||
|
if (mapHint) {
|
||||||
|
mapHint.textContent = state.wallPoints.length > 0
|
||||||
|
? 'Klick setzt Punkte. Punkte sind per Drag verschiebbar. Snap nutzt Wandpunkte aus der Stockwerkskarte.'
|
||||||
|
: 'Klick setzt Punkte. Punkte sind per Drag verschiebbar.';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (mapHint) {
|
||||||
|
mapHint.textContent = 'Stockwerkskarte konnte nicht geladen werden. Polygon kann frei gezeichnet werden.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
|
||||||
|
canvas.addEventListener('pointerdown', onPointerDown);
|
||||||
|
canvas.addEventListener('pointermove', onPointerMove);
|
||||||
|
canvas.addEventListener('pointerup', stopDragging);
|
||||||
|
canvas.addEventListener('pointercancel', stopDragging);
|
||||||
|
canvas.addEventListener('pointerleave', stopDragging);
|
||||||
|
|
||||||
|
floorSelect.addEventListener('change', () => {
|
||||||
|
loadFloor();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (undoButton) {
|
||||||
|
undoButton.addEventListener('click', () => {
|
||||||
|
if (state.points.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.points.pop();
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clearButton) {
|
||||||
|
clearButton.addEventListener('click', () => {
|
||||||
|
state.points = [];
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render();
|
||||||
|
loadFloor();
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePoints(raw) {
|
||||||
|
if (!raw) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (!Array.isArray(parsed)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
.map((point) => ({
|
||||||
|
x: Number(point && point.x),
|
||||||
|
y: Number(point && point.y)
|
||||||
|
}))
|
||||||
|
.filter((point) => Number.isFinite(point.x) && Number.isFinite(point.y));
|
||||||
|
} catch (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePointString(pointsAttr) {
|
||||||
|
return pointsAttr
|
||||||
|
.trim()
|
||||||
|
.split(/\s+/)
|
||||||
|
.map((pair) => {
|
||||||
|
const values = pair.split(',');
|
||||||
|
if (values.length !== 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const x = Number(values[0]);
|
||||||
|
const y = Number(values[1]);
|
||||||
|
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { x, y };
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dedupePoints(points) {
|
||||||
|
const map = new Map();
|
||||||
|
points.forEach((point) => {
|
||||||
|
const key = `${Math.round(point.x)}:${Math.round(point.y)}`;
|
||||||
|
if (!map.has(key)) {
|
||||||
|
map.set(key, { x: Math.round(point.x), y: Math.round(point.y) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Array.from(map.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPointString(points) {
|
||||||
|
return points.map((point) => `${Math.round(point.x)},${Math.round(point.y)}`).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSvgElement(name) {
|
||||||
|
return document.createElementNS(SVG_NS, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toSvgPoint(svg, event) {
|
||||||
|
const pt = svg.createSVGPoint();
|
||||||
|
pt.x = event.clientX;
|
||||||
|
pt.y = event.clientY;
|
||||||
|
const ctm = svg.getScreenCTM();
|
||||||
|
if (!ctm) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const transformed = pt.matrixTransform(ctm.inverse());
|
||||||
|
return { x: transformed.x, y: transformed.y };
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', initRoomPolygonEditor);
|
||||||
|
})();
|
||||||
@@ -1,92 +1,60 @@
|
|||||||
// Logik für den SVG-Port-Editor (Klicks, Drag & Drop, Speichern)
|
|
||||||
/**
|
|
||||||
* svg-editor.js
|
|
||||||
*
|
|
||||||
* Logik für den SVG-Port-Editor:
|
|
||||||
* - Ports per Klick anlegen
|
|
||||||
* - Ports auswählen
|
|
||||||
* - Ports verschieben (Drag & Drop)
|
|
||||||
* - Ports löschen
|
|
||||||
* - Ports laden / speichern
|
|
||||||
*
|
|
||||||
* Abhängigkeiten: keine (Vanilla JS)
|
|
||||||
*/
|
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
/* =========================
|
const svgElement = document.querySelector('#device-svg');
|
||||||
* Konfiguration
|
if (!svgElement) {
|
||||||
* ========================= */
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: vom Backend setzen (z. B. via data-Attribut)
|
const DEVICE_TYPE_ID = Number(svgElement.dataset.deviceTypeId || 0);
|
||||||
const DEVICE_TYPE_ID = null;
|
|
||||||
|
|
||||||
// TODO: API-Endpunkte festlegen
|
|
||||||
const API_LOAD_PORTS = '/api/device_type_ports.php?action=load';
|
const API_LOAD_PORTS = '/api/device_type_ports.php?action=load';
|
||||||
const API_SAVE_PORTS = '/api/device_type_ports.php?action=save';
|
const API_SAVE_PORTS = '/api/device_type_ports.php?action=save';
|
||||||
|
const DEFAULT_PORT_TYPE_ID = null;
|
||||||
|
|
||||||
/* =========================
|
|
||||||
* State
|
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
let svgElement = null;
|
|
||||||
let ports = [];
|
let ports = [];
|
||||||
let selectedPortId = null;
|
let selectedPortId = null;
|
||||||
let isDragging = false;
|
let isDragging = false;
|
||||||
let dragOffset = { x: 0, y: 0 };
|
let dragOffset = { x: 0, y: 0 };
|
||||||
|
|
||||||
/* =========================
|
bindSvgEvents();
|
||||||
* Initialisierung
|
loadPorts();
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
svgElement = document.querySelector('#device-svg');
|
|
||||||
|
|
||||||
if (!svgElement) {
|
|
||||||
console.warn('SVG Editor: #device-svg nicht gefunden');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bindSvgEvents();
|
|
||||||
loadPorts();
|
|
||||||
});
|
|
||||||
|
|
||||||
/* =========================
|
|
||||||
* SVG Events
|
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
function bindSvgEvents() {
|
function bindSvgEvents() {
|
||||||
svgElement.addEventListener('click', onSvgClick);
|
svgElement.addEventListener('click', onSvgClick);
|
||||||
svgElement.addEventListener('mousemove', onSvgMouseMove);
|
svgElement.addEventListener('mousemove', onSvgMouseMove);
|
||||||
svgElement.addEventListener('mouseup', onSvgMouseUp);
|
svgElement.addEventListener('mouseup', onSvgMouseUp);
|
||||||
|
|
||||||
|
const saveButton = document.querySelector('[data-save-svg-ports]');
|
||||||
|
if (saveButton) {
|
||||||
|
saveButton.addEventListener('click', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
savePorts();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
|
||||||
* Port-Erstellung
|
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
function onSvgClick(event) {
|
function onSvgClick(event) {
|
||||||
// Klick auf bestehenden Port?
|
|
||||||
if (event.target.classList.contains('port-point')) {
|
if (event.target.classList.contains('port-point')) {
|
||||||
selectPort(event.target.dataset.id);
|
selectPort(event.target.dataset.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Modifier-Key prüfen (z. B. nur mit SHIFT neuen Port erstellen?)
|
// New ports are only created while SHIFT is held.
|
||||||
const point = getSvgCoordinates(event);
|
if (!event.shiftKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const point = getSvgCoordinates(event);
|
||||||
createPort(point.x, point.y);
|
createPort(point.x, point.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPort(x, y) {
|
function createPort(x, y) {
|
||||||
const id = generateTempId();
|
const id = generateTempId();
|
||||||
|
|
||||||
const port = {
|
const port = {
|
||||||
id: id,
|
id,
|
||||||
name: `Port ${ports.length + 1}`,
|
name: `Port ${ports.length + 1}`,
|
||||||
port_type_id: null, // TODO: Default-Porttyp?
|
port_type_id: DEFAULT_PORT_TYPE_ID,
|
||||||
x: x,
|
x,
|
||||||
y: y,
|
y,
|
||||||
comment: ''
|
metadata: null
|
||||||
};
|
};
|
||||||
|
|
||||||
ports.push(port);
|
ports.push(port);
|
||||||
@@ -94,13 +62,8 @@ function createPort(x, y) {
|
|||||||
selectPort(id);
|
selectPort(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
|
||||||
* Rendering
|
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
function renderPort(port) {
|
function renderPort(port) {
|
||||||
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||||
|
|
||||||
circle.setAttribute('cx', port.x);
|
circle.setAttribute('cx', port.x);
|
||||||
circle.setAttribute('cy', port.y);
|
circle.setAttribute('cy', port.y);
|
||||||
circle.setAttribute('r', 6);
|
circle.setAttribute('r', 6);
|
||||||
@@ -116,27 +79,39 @@ function renderPort(port) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function rerenderPorts() {
|
function rerenderPorts() {
|
||||||
svgElement.querySelectorAll('.port-point').forEach(p => p.remove());
|
svgElement.querySelectorAll('.port-point').forEach((node) => node.remove());
|
||||||
ports.forEach(renderPort);
|
ports.forEach(renderPort);
|
||||||
|
if (selectedPortId !== null) {
|
||||||
|
selectPort(selectedPortId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
|
||||||
* Auswahl
|
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
function selectPort(id) {
|
function selectPort(id) {
|
||||||
selectedPortId = id;
|
selectedPortId = id;
|
||||||
|
|
||||||
document.querySelectorAll('.port-point').forEach(el => {
|
document.querySelectorAll('.port-point').forEach((el) => {
|
||||||
el.classList.toggle('selected', el.dataset.id === id);
|
el.classList.toggle('selected', el.dataset.id === String(id));
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Sidebar-Felder mit Portdaten füllen
|
const selected = getPortById(id);
|
||||||
|
fillSidebar(selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
function fillSidebar(port) {
|
||||||
* Drag & Drop
|
const nameField = document.querySelector('[data-port-name]');
|
||||||
* ========================= */
|
const typeField = document.querySelector('[data-port-type-id]');
|
||||||
|
const xField = document.querySelector('[data-port-x]');
|
||||||
|
const yField = document.querySelector('[data-port-y]');
|
||||||
|
|
||||||
|
if (nameField) nameField.value = port?.name || '';
|
||||||
|
if (typeField) typeField.value = port?.port_type_id || '';
|
||||||
|
if (xField) xField.value = port ? Math.round(port.x) : '';
|
||||||
|
if (yField) yField.value = port ? Math.round(port.y) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSidebar() {
|
||||||
|
fillSidebar(null);
|
||||||
|
}
|
||||||
|
|
||||||
function startDrag(event, portId) {
|
function startDrag(event, portId) {
|
||||||
const port = getPortById(portId);
|
const port = getPortById(portId);
|
||||||
@@ -157,7 +132,6 @@ function onSvgMouseMove(event) {
|
|||||||
if (!port) return;
|
if (!port) return;
|
||||||
|
|
||||||
const point = getSvgCoordinates(event);
|
const point = getSvgCoordinates(event);
|
||||||
|
|
||||||
port.x = point.x + dragOffset.x;
|
port.x = point.x + dragOffset.x;
|
||||||
port.y = point.y + dragOffset.y;
|
port.y = point.y + dragOffset.y;
|
||||||
|
|
||||||
@@ -168,92 +142,95 @@ function onSvgMouseUp() {
|
|||||||
isDragging = false;
|
isDragging = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
|
||||||
* Löschen
|
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
function deleteSelectedPort() {
|
function deleteSelectedPort() {
|
||||||
if (!selectedPortId) return;
|
if (!selectedPortId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Sicherheitsabfrage (confirm)
|
if (!confirm('Ausgewaehlten Port loeschen?')) {
|
||||||
ports = ports.filter(p => p.id !== selectedPortId);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ports = ports.filter((port) => String(port.id) !== String(selectedPortId));
|
||||||
selectedPortId = null;
|
selectedPortId = null;
|
||||||
|
|
||||||
rerenderPorts();
|
rerenderPorts();
|
||||||
|
resetSidebar();
|
||||||
// TODO: Sidebar zurücksetzen
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
|
||||||
* Laden / Speichern
|
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
function loadPorts() {
|
function loadPorts() {
|
||||||
if (!DEVICE_TYPE_ID) {
|
if (!DEVICE_TYPE_ID) {
|
||||||
console.warn('DEVICE_TYPE_ID nicht gesetzt');
|
console.warn('SVG Editor: DEVICE_TYPE_ID fehlt auf #device-svg');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(`${API_LOAD_PORTS}&device_type_id=${DEVICE_TYPE_ID}`)
|
fetch(`${API_LOAD_PORTS}&device_type_id=${DEVICE_TYPE_ID}`)
|
||||||
.then(res => res.json())
|
.then((res) => res.json())
|
||||||
.then(data => {
|
.then((data) => {
|
||||||
// TODO: Datenformat validieren
|
if (!Array.isArray(data)) {
|
||||||
ports = data;
|
throw new Error('Antwortformat ungueltig');
|
||||||
|
}
|
||||||
|
|
||||||
|
ports = data
|
||||||
|
.filter((entry) => entry && typeof entry === 'object')
|
||||||
|
.map((entry) => ({
|
||||||
|
id: entry.id,
|
||||||
|
name: String(entry.name || ''),
|
||||||
|
port_type_id: entry.port_type_id ? Number(entry.port_type_id) : null,
|
||||||
|
x: Number(entry.x || 0),
|
||||||
|
y: Number(entry.y || 0),
|
||||||
|
metadata: entry.metadata || null
|
||||||
|
}));
|
||||||
|
|
||||||
rerenderPorts();
|
rerenderPorts();
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
console.error('Fehler beim Laden der Ports', err);
|
console.error('Fehler beim Laden der Ports', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function savePorts() {
|
function savePorts() {
|
||||||
if (!DEVICE_TYPE_ID) return;
|
if (!DEVICE_TYPE_ID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
fetch(API_SAVE_PORTS, {
|
fetch(API_SAVE_PORTS, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
device_type_id: DEVICE_TYPE_ID,
|
device_type_id: DEVICE_TYPE_ID,
|
||||||
ports: ports
|
ports
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(res => res.json())
|
.then((res) => res.json())
|
||||||
.then(data => {
|
.then((data) => {
|
||||||
// TODO: Erfolg / Fehler anzeigen
|
if (data?.error) {
|
||||||
console.log('Ports gespeichert', data);
|
throw new Error(data.error);
|
||||||
})
|
}
|
||||||
.catch(err => {
|
alert('Ports gespeichert');
|
||||||
console.error('Fehler beim Speichern', err);
|
})
|
||||||
});
|
.catch((err) => {
|
||||||
|
alert('Speichern fehlgeschlagen: ' + err.message);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
|
||||||
* Hilfsfunktionen
|
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
function getSvgCoordinates(event) {
|
function getSvgCoordinates(event) {
|
||||||
const pt = svgElement.createSVGPoint();
|
const pt = svgElement.createSVGPoint();
|
||||||
pt.x = event.clientX;
|
pt.x = event.clientX;
|
||||||
pt.y = event.clientY;
|
pt.y = event.clientY;
|
||||||
|
|
||||||
const transformed = pt.matrixTransform(svgElement.getScreenCTM().inverse());
|
const transformed = pt.matrixTransform(svgElement.getScreenCTM().inverse());
|
||||||
return { x: transformed.x, y: transformed.y };
|
return { x: transformed.x, y: transformed.y };
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPortById(id) {
|
function getPortById(id) {
|
||||||
return ports.find(p => p.id === id);
|
return ports.find((port) => String(port.id) === String(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateTempId() {
|
function generateTempId() {
|
||||||
return 'tmp_' + Math.random().toString(36).substr(2, 9);
|
return 'tmp_' + Math.random().toString(36).slice(2, 11);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
document.addEventListener('keydown', (event) => {
|
||||||
* Keyboard Shortcuts
|
if (event.key === 'Delete') {
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'Delete') {
|
|
||||||
deleteSelectedPort();
|
deleteSelectedPort();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,41 +2,22 @@
|
|||||||
/**
|
/**
|
||||||
* bootstrap.php
|
* bootstrap.php
|
||||||
*
|
*
|
||||||
* Initialisierung der Anwendung
|
* Application initialization.
|
||||||
* - Config laden
|
|
||||||
* - Session starten
|
|
||||||
* - DB-Verbindung über _sql.php
|
|
||||||
* - Helper einbinden
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* =========================
|
|
||||||
* Config laden
|
|
||||||
* ========================= */
|
|
||||||
require_once __DIR__ . '/config.php';
|
require_once __DIR__ . '/config.php';
|
||||||
// TODO: Config-Datei mit DB-Zugang, Pfaden, globalen Settings füllen
|
date_default_timezone_set(defined('APP_TIMEZONE') ? APP_TIMEZONE : 'UTC');
|
||||||
|
|
||||||
/* =========================
|
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||||
* Session starten
|
session_start();
|
||||||
* ========================= */
|
}
|
||||||
session_start();
|
|
||||||
// TODO: Single-User Auth prüfen
|
if (!isset($_SESSION['validation_errors']) || !is_array($_SESSION['validation_errors'])) {
|
||||||
// z.B. $_SESSION['user'] setzen oder Login erzwingen
|
$_SESSION['validation_errors'] = [];
|
||||||
|
}
|
||||||
|
|
||||||
/* =========================
|
|
||||||
* DB-Verbindung initialisieren
|
|
||||||
* ========================= */
|
|
||||||
require_once __DIR__ . '/lib/_sql.php';
|
require_once __DIR__ . '/lib/_sql.php';
|
||||||
|
|
||||||
// TODO: Host, User, Passwort, DB aus config.php nutzen
|
|
||||||
$sql = new SQL();
|
$sql = new SQL();
|
||||||
|
|
||||||
/* =========================
|
|
||||||
* Helper laden
|
|
||||||
* ========================= */
|
|
||||||
require_once __DIR__ . '/lib/helpers.php';
|
require_once __DIR__ . '/lib/helpers.php';
|
||||||
|
require_once __DIR__ . '/lib/auth.php';
|
||||||
/* =========================
|
|
||||||
* Optional: Fehlerbehandlung
|
|
||||||
* ========================= */
|
|
||||||
// error_reporting(E_ALL);
|
|
||||||
// ini_set('display_errors', 1);
|
|
||||||
|
|||||||
@@ -1,2 +1,19 @@
|
|||||||
<?php
|
<?php
|
||||||
// Zentrale Konfiguration (DB-Zugangsdaten, Pfade, globale Settings)
|
// Zentrale Konfiguration (DB-Zugangsdaten, Pfade, globale Settings)
|
||||||
|
|
||||||
|
define('APP_NAME', 'netwatch');
|
||||||
|
define('APP_ENV', getenv('APP_ENV') ?: 'development');
|
||||||
|
define('APP_TIMEZONE', getenv('APP_TIMEZONE') ?: 'Europe/Berlin');
|
||||||
|
|
||||||
|
define('DB_HOST', getenv('DB_HOST') ?: 'netdoc_db');
|
||||||
|
define('DB_USER', getenv('DB_USER') ?: 'netdoc');
|
||||||
|
define('DB_PASS', getenv('DB_PASS') ?: 'netdoc');
|
||||||
|
define('DB_NAME', getenv('DB_NAME') ?: 'netdoc');
|
||||||
|
|
||||||
|
define('AUTH_REQUIRED', (getenv('AUTH_REQUIRED') ?: '0') === '1');
|
||||||
|
define('ADMIN_PASSWORD_HASH', getenv('ADMIN_PASSWORD_HASH') ?: '');
|
||||||
|
define('LOGIN_PATH', getenv('LOGIN_PATH') ?: '/login.php');
|
||||||
|
|
||||||
|
define('UPLOAD_BASE_DIR', __DIR__ . '/uploads');
|
||||||
|
define('UPLOAD_MAX_FILE_SIZE', (int)(getenv('UPLOAD_MAX_FILE_SIZE') ?: 5 * 1024 * 1024));
|
||||||
|
define('UPLOAD_ALLOWED_CATEGORIES', ['device_types', 'floors', 'racks', 'rooms', 'misc']);
|
||||||
|
|||||||
@@ -1,72 +1,37 @@
|
|||||||
<?php
|
<?php
|
||||||
// Einstiegspunkt der Anwendung, Routing zur jeweiligen Modul-Seite
|
// Einstiegspunkt der Anwendung, Routing zur jeweiligen Modul-Seite
|
||||||
|
|
||||||
|
require_once __DIR__ . '/bootstrap.php';
|
||||||
|
requireAuth();
|
||||||
|
|
||||||
/**
|
|
||||||
* index.php
|
|
||||||
*
|
|
||||||
* Einstiegspunkt der Anwendung
|
|
||||||
* - Single-User
|
|
||||||
* - Modulbasiertes Routing
|
|
||||||
* - Basierend auf _sql.php
|
|
||||||
* - HTML-Layout via templates/layout.php
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* =========================
|
|
||||||
* Bootstrap
|
|
||||||
* ========================= */
|
|
||||||
require_once __DIR__ . '/bootstrap.php'; // lädt config, DB, helper
|
|
||||||
// TODO: Session starten / Single-User-Auth prüfen
|
|
||||||
|
|
||||||
/* =========================
|
|
||||||
* Routing
|
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
// Standard-Modul / Aktion
|
|
||||||
$module = $_GET['module'] ?? 'dashboard';
|
$module = $_GET['module'] ?? 'dashboard';
|
||||||
$action = $_GET['action'] ?? 'list';
|
$action = $_GET['action'] ?? 'list';
|
||||||
|
|
||||||
// Whitelist der Module
|
$validModules = ['dashboard', 'locations', 'buildings', 'rooms', 'device_types', 'devices', 'racks', 'floors', 'floor_infrastructure', 'connections', 'port_types'];
|
||||||
$validModules = ['dashboard', 'locations', 'buildings', 'device_types', 'devices', 'racks', 'floors', 'floor_infrastructure', 'connections', 'port_types'];
|
$validActions = ['list', 'edit', 'save', 'ports', 'delete', 'swap'];
|
||||||
|
|
||||||
// Whitelist der Aktionen
|
if (!in_array($module, $validModules, true)) {
|
||||||
$validActions = ['list', 'edit', 'save', 'ports', 'delete'];
|
renderClientError(400, 'Ungueltiges Modul');
|
||||||
|
exit;
|
||||||
// Prüfen auf gültige Werte
|
|
||||||
if (!in_array($module, $validModules)) {
|
|
||||||
// TODO: Fehlerseite anzeigen
|
|
||||||
die('Ungültiges Modul');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!in_array($action, $validActions)) {
|
if (!in_array($action, $validActions, true)) {
|
||||||
// TODO: Fehlerseite anzeigen
|
renderClientError(400, 'Ungueltige Aktion');
|
||||||
die('Ungültige Aktion');
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
if (!in_array($action, ['save', 'delete', 'swap'], true)) {
|
||||||
* Template-Header laden (nur für View-Aktionen)
|
|
||||||
* ========================= */
|
|
||||||
if (!in_array($action, ['save', 'delete'], true)) {
|
|
||||||
require_once __DIR__ . '/templates/header.php';
|
require_once __DIR__ . '/templates/header.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
|
||||||
* Modul laden
|
|
||||||
* ========================= */
|
|
||||||
$modulePath = __DIR__ . "/modules/$module/$action.php";
|
$modulePath = __DIR__ . "/modules/$module/$action.php";
|
||||||
|
|
||||||
if (file_exists($modulePath)) {
|
if (file_exists($modulePath)) {
|
||||||
require_once $modulePath;
|
require_once $modulePath;
|
||||||
} else {
|
} else {
|
||||||
// TODO: Fehlerseite oder 404
|
renderClientError(404, 'Die angeforderte Seite existiert nicht.');
|
||||||
if ($action !== 'save') {
|
|
||||||
echo "<p>Die Seite existiert noch nicht.</p>".$modulePath;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
if (!in_array($action, ['save', 'delete', 'swap'], true)) {
|
||||||
* Template-Footer laden (nur für View-Aktionen)
|
|
||||||
* ========================= */
|
|
||||||
if (!in_array($action, ['save', 'delete'], true)) {
|
|
||||||
require_once __DIR__ . '/templates/footer.php';
|
require_once __DIR__ . '/templates/footer.php';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,19 @@ class SQL {
|
|||||||
public $cnt_get = 0;
|
public $cnt_get = 0;
|
||||||
public $cnt_set = 0;
|
public $cnt_set = 0;
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
require_once ('secret.php');
|
if (defined('DB_HOST') && defined('DB_USER') && defined('DB_PASS') && defined('DB_NAME')) {
|
||||||
|
$this->m = [
|
||||||
|
'host' => DB_HOST,
|
||||||
|
'user' => DB_USER,
|
||||||
|
'pass' => DB_PASS,
|
||||||
|
'data' => DB_NAME
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
require_once ('secret.php');
|
||||||
|
$this->m = $_m;
|
||||||
|
}
|
||||||
|
|
||||||
$this->m = $_m;
|
$this->h = new mysqli ( $this->m ['host'], $this->m ['user'], $this->m ['pass'], $this->m ['data'] );
|
||||||
|
|
||||||
$this->h = new mysqli ( $_m ['host'], $_m ['user'], $_m ['pass'], $_m ['data'] );
|
|
||||||
if ($this->h->connect_errno) {
|
if ($this->h->connect_errno) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -171,4 +179,4 @@ class SQL {
|
|||||||
// echo 'DESTROY';
|
// echo 'DESTROY';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|||||||
@@ -2,82 +2,60 @@
|
|||||||
/**
|
/**
|
||||||
* app/lib/auth.php
|
* app/lib/auth.php
|
||||||
*
|
*
|
||||||
* Single-User-Authentifizierung
|
* Single-user authentication helpers.
|
||||||
* - Login / Logout
|
|
||||||
* - Session-Check
|
|
||||||
* - Optional: Passwortschutz für Admin-Tool
|
|
||||||
*
|
|
||||||
* KEIN Mehrbenutzer-System
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* =========================
|
|
||||||
* Login prüfen
|
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prüft, ob der Benutzer eingeloggt ist
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
function isAuthenticated(): bool
|
function isAuthenticated(): bool
|
||||||
{
|
{
|
||||||
// TODO: Session-Variable definieren, z.B. $_SESSION['auth'] === true
|
if (!defined('AUTH_REQUIRED') || AUTH_REQUIRED === false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return isset($_SESSION['auth']) && $_SESSION['auth'] === true;
|
return isset($_SESSION['auth']) && $_SESSION['auth'] === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
|
||||||
* Login durchführen
|
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Führt einen Login durch
|
|
||||||
*
|
|
||||||
* @param string $password
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
function login(string $password): bool
|
function login(string $password): bool
|
||||||
{
|
{
|
||||||
// TODO: Passwort aus config.php vergleichen
|
$hash = defined('ADMIN_PASSWORD_HASH') ? trim((string)ADMIN_PASSWORD_HASH) : '';
|
||||||
// TODO: Passwort-Hash verwenden (password_hash / password_verify)
|
if ($hash === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
if (password_verify($password, $hash)) {
|
||||||
if (password_verify($password, ADMIN_PASSWORD_HASH)) {
|
|
||||||
$_SESSION['auth'] = true;
|
$_SESSION['auth'] = true;
|
||||||
|
$_SESSION['auth_at'] = time();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
|
||||||
* Logout
|
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loggt den Benutzer aus
|
|
||||||
*/
|
|
||||||
function logout(): void
|
function logout(): void
|
||||||
{
|
{
|
||||||
// TODO: Session-Variablen löschen
|
unset($_SESSION['auth'], $_SESSION['auth_at']);
|
||||||
// unset($_SESSION['auth']);
|
|
||||||
|
|
||||||
// TODO: Optional komplette Session zerstören
|
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||||
// session_destroy();
|
session_regenerate_id(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
|
||||||
* Zugriff erzwingen
|
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Erzwingt Login, sonst Redirect
|
|
||||||
*/
|
|
||||||
function requireAuth(): void
|
function requireAuth(): void
|
||||||
{
|
{
|
||||||
|
if (!defined('AUTH_REQUIRED') || AUTH_REQUIRED === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isAuthenticated()) {
|
if (!isAuthenticated()) {
|
||||||
// TODO: Redirect auf Login-Seite
|
$isApiRequest = str_starts_with($_SERVER['REQUEST_URI'] ?? '', '/api/');
|
||||||
// header('Location: /login.php');
|
if ($isApiRequest) {
|
||||||
|
http_response_code(401);
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['error' => 'Nicht authentifiziert']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$target = defined('LOGIN_PATH') ? LOGIN_PATH : '/login.php';
|
||||||
|
header('Location: ' . $target);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,12 +206,174 @@ function dd($value): void
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zeigt eine sauber gestaltete Fehlerseite für 40x-Status-Codes mit Erklärung.
|
||||||
|
*
|
||||||
|
* @param int $statusCode Client-Error-Status (400–499)
|
||||||
|
* @param string $explanation Optionale Zusatzinfo zur Problemursache
|
||||||
|
* @param string[] $tips Handlungsvorschläge für Benutzer (optional)
|
||||||
|
*/
|
||||||
|
function renderClientError(int $statusCode, string $explanation = '', array $tips = []): void
|
||||||
|
{
|
||||||
|
if ($statusCode < 400 || $statusCode >= 500) {
|
||||||
|
$statusCode = 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
$reasons = [
|
||||||
|
400 => 'Ungültige Anfrage',
|
||||||
|
401 => 'Nicht authentifiziert',
|
||||||
|
403 => 'Zugriff verweigert',
|
||||||
|
404 => 'Seite nicht gefunden',
|
||||||
|
405 => 'Methode nicht erlaubt',
|
||||||
|
408 => 'Anfrage abgelaufen',
|
||||||
|
429 => 'Zu viele Anfragen',
|
||||||
|
];
|
||||||
|
|
||||||
|
$reason = $reasons[$statusCode] ?? 'Clientseitiger Fehler';
|
||||||
|
$defaultExplanation = match ($statusCode) {
|
||||||
|
400 => 'Die Anfrage konnte aufgrund fehlender oder falscher Daten nicht verstanden werden.',
|
||||||
|
401 => 'Bitte melden Sie sich an oder verwenden gültige Zugangsdaten.',
|
||||||
|
403 => 'Sie besitzen keinen Zugriff auf diesen Bereich.',
|
||||||
|
404 => 'Die angeforderte Ressource existiert nicht oder wurde verschoben.',
|
||||||
|
405 => 'Diese Aktion ist auf dem Server nicht erlaubt.',
|
||||||
|
408 => 'Die Anfrage hat zu lange gedauert; bitte erneut versuchen.',
|
||||||
|
429 => 'Sie senden zu viele Anfragen in kurzer Zeit.',
|
||||||
|
default => 'Die Anfrage kann nicht verarbeitet werden; überprüfen Sie die Eingaben.',
|
||||||
|
};
|
||||||
|
|
||||||
|
$message = $explanation !== '' ? $explanation : $defaultExplanation;
|
||||||
|
|
||||||
|
http_response_code($statusCode);
|
||||||
|
|
||||||
|
$css = <<<CSS
|
||||||
|
body { font-family: 'Segoe UI', system-ui, sans-serif; margin: 0; background: #0b1220; color: #e0e7ff; }
|
||||||
|
.page { min-height: 100vh; display: flex; justify-content: center; align-items: center; padding: 2rem; }
|
||||||
|
.card { max-width: 640px; background: linear-gradient(145deg, #16213d, #0d0f1f); border: 1px solid rgba(224,231,255,0.2); border-radius: 1rem; box-shadow: 0 20px 45px rgba(0,0,0,0.45); padding: 2rem; }
|
||||||
|
h1 { margin: 0 0 0.5rem; font-size: clamp(2.5rem, 3vw, 3.5rem); }
|
||||||
|
p { margin: 0 0 1rem; line-height: 1.6; color: rgba(224,231,255,0.9); }
|
||||||
|
.badge { display: inline-flex; align-items: center; padding: 0.25rem 0.75rem; border-radius: 999px; font-size: 0.85rem; background: rgba(255,255,255,0.08); color: #a5b4fc; margin-bottom: 1rem; }
|
||||||
|
ul { margin: 1rem 0 0; padding-left: 1.25rem; color: rgba(224,231,255,0.85); }
|
||||||
|
a { color: #7dd3fc; text-decoration: none; }
|
||||||
|
a:hover { text-decoration: underline; }
|
||||||
|
CSS;
|
||||||
|
|
||||||
|
$rendered = strtr(<<<'HTML'
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>Fehler {{code}}</title>
|
||||||
|
<style>{{css}}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
<section class="card">
|
||||||
|
<div class="badge">{{status}}</div>
|
||||||
|
<h1>{{code}} · {{reason}}</h1>
|
||||||
|
<p>{{message}}</p>
|
||||||
|
{{tips}}
|
||||||
|
<p>Zurück zur Startseite: <a href="/">Dashboard</a></p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML, [
|
||||||
|
'{{code}}' => e((string)$statusCode),
|
||||||
|
'{{reason}}' => e($reason),
|
||||||
|
'{{status}}' => e(sprintf('%d Fehler', $statusCode)),
|
||||||
|
'{{message}}' => e($message),
|
||||||
|
'{{css}}' => $css,
|
||||||
|
'{{tips}}' => count($tips) === 0
|
||||||
|
? ''
|
||||||
|
: '<ul>' . implode('', array_map(fn($tip) => '<li>' . e($tip) . '</li>', $tips)) . '</ul>',
|
||||||
|
]);
|
||||||
|
|
||||||
|
echo $rendered;
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
* 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);
|
||||||
|
}
|
||||||
|
|||||||
35
app/modules/buildings/delete.php
Normal file
35
app/modules/buildings/delete.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* app/modules/buildings/delete.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['error' => 'Methode nicht erlaubt']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'ID fehlt']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$exists = $sql->single("SELECT id FROM buildings WHERE id = ?", "i", [$id]);
|
||||||
|
if (!$exists) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['error' => 'Gebaeude nicht gefunden']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = $sql->set("DELETE FROM buildings WHERE id = ?", "i", [$id]);
|
||||||
|
if ($rows === false) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Loeschen fehlgeschlagen']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['status' => 'ok', 'success' => true, 'rows' => $rows]);
|
||||||
@@ -47,7 +47,7 @@ $selectedLocationId = $building['location_id'] ?? $prefillLocationId;
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="name">Name <span class="required">*</span></label>
|
<label for="name">Name <span class="required">*</span></label>
|
||||||
<input type="text" id="name" name="name" required
|
<input type="text" id="name" name="name" required
|
||||||
value="<?php echo htmlspecialchars($building['name'] ?? ''); ?>"
|
value="<?php echo htmlspecialchars($building['name'] ?? '); ?>"
|
||||||
placeholder="z.B. Gebäude A, Verwaltungsgebäude">
|
placeholder="z.B. Gebäude A, Verwaltungsgebäude">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ $selectedLocationId = $building['location_id'] ?? $prefillLocationId;
|
|||||||
<option value="">- Wählen -</option>
|
<option value="">- Wählen -</option>
|
||||||
<?php foreach ($locations as $location): ?>
|
<?php foreach ($locations as $location): ?>
|
||||||
<option value="<?php echo $location['id']; ?>"
|
<option value="<?php echo $location['id']; ?>"
|
||||||
<?php echo ((int)$selectedLocationId === (int)$location['id']) ? 'selected' : ''; ?>>
|
<?php echo ((int)$selectedLocationId === (int)$location['id']) ? 'selected' : '; ?>>
|
||||||
<?php echo htmlspecialchars($location['name']); ?>
|
<?php echo htmlspecialchars($location['name']); ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -67,7 +67,7 @@ $selectedLocationId = $building['location_id'] ?? $prefillLocationId;
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="comment">Beschreibung</label>
|
<label for="comment">Beschreibung</label>
|
||||||
<textarea id="comment" name="comment" rows="3"
|
<textarea id="comment" name="comment" rows="3"
|
||||||
placeholder="Adresse, Besonderheiten, etc."><?php echo htmlspecialchars($building['comment'] ?? ''); ?></textarea>
|
placeholder="Adresse, Besonderheiten, etc."><?php echo htmlspecialchars($building['comment'] ?? '); ?></textarea>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
@@ -172,9 +172,25 @@ $selectedLocationId = $building['location_id'] ?? $prefillLocationId;
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
function confirmDelete(id) {
|
function confirmDelete(id) {
|
||||||
if (confirm('Dieses Gebäude wirklich löschen? Alle Stockwerke werden gelöscht.')) {
|
if (confirm('Dieses Gebaeude wirklich loeschen? Alle Stockwerke werden geloescht.')) {
|
||||||
// TODO: AJAX-Delete implementieren
|
fetch('?module=buildings&action=delete', {
|
||||||
alert('Löschen noch nicht implementiert');
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||||
|
body: 'id=' + encodeURIComponent(id)
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data && data.status === 'ok') {
|
||||||
|
window.location.href = '?module=buildings&action=list';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert((data && data.error) ? data.error : 'Loeschen fehlgeschlagen');
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
alert('Loeschen fehlgeschlagen');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,17 +8,17 @@
|
|||||||
// =========================
|
// =========================
|
||||||
// Filter einlesen
|
// Filter einlesen
|
||||||
// =========================
|
// =========================
|
||||||
$search = trim($_GET['search'] ?? '');
|
$search = trim($_GET['search'] ?? ');
|
||||||
$locationId = (int)($_GET['location_id'] ?? 0);
|
$locationId = (int)($_GET['location_id'] ?? 0);
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// WHERE-Clause bauen
|
// WHERE-Clause bauen
|
||||||
// =========================
|
// =========================
|
||||||
$where = [];
|
$where = [];
|
||||||
$types = '';
|
$types = ';
|
||||||
$params = [];
|
$params = [];
|
||||||
|
|
||||||
if ($search !== '') {
|
if ($search !== ') {
|
||||||
$where[] = "b.name LIKE ? OR b.comment LIKE ?";
|
$where[] = "b.name LIKE ? OR b.comment LIKE ?";
|
||||||
$types .= "ss";
|
$types .= "ss";
|
||||||
$params[] = "%$search%";
|
$params[] = "%$search%";
|
||||||
@@ -70,7 +70,7 @@ $locations = $sql->get("SELECT id, name FROM locations ORDER BY name", "", []);
|
|||||||
<option value="">- Alle Standorte -</option>
|
<option value="">- Alle Standorte -</option>
|
||||||
<?php foreach ($locations as $loc): ?>
|
<?php foreach ($locations as $loc): ?>
|
||||||
<option value="<?php echo $loc['id']; ?>"
|
<option value="<?php echo $loc['id']; ?>"
|
||||||
<?php echo $loc['id'] === $locationId ? 'selected' : ''; ?>>
|
<?php echo $loc['id'] === $locationId ? 'selected' : '; ?>>
|
||||||
<?php echo htmlspecialchars($loc['name']); ?>
|
<?php echo htmlspecialchars($loc['name']); ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -112,7 +112,7 @@ $locations = $sql->get("SELECT id, name FROM locations ORDER BY name", "", []);
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<small><?php echo htmlspecialchars($building['comment'] ?? ''); ?></small>
|
<small><?php echo htmlspecialchars($building['comment'] ?? '); ?></small>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
@@ -241,9 +241,24 @@ $locations = $sql->get("SELECT id, name FROM locations ORDER BY name", "", []);
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
function confirmDelete(id) {
|
function confirmDelete(id) {
|
||||||
if (confirm('Dieses Gebäude wirklich löschen?')) {
|
if (confirm('Dieses Gebaeude wirklich loeschen?')) {
|
||||||
// TODO: AJAX-Delete implementieren
|
fetch('?module=buildings&action=delete', {
|
||||||
alert('Löschen noch nicht implementiert');
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||||
|
body: 'id=' + encodeURIComponent(id)
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data && data.status === 'ok') {
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert((data && data.error) ? data.error : 'Loeschen fehlgeschlagen');
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
alert('Loeschen fehlgeschlagen');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
317
app/modules/connections/edit.php
Normal file
317
app/modules/connections/edit.php
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* app/modules/connections/edit.php
|
||||||
|
*
|
||||||
|
* Verbindung anlegen / bearbeiten
|
||||||
|
*/
|
||||||
|
|
||||||
|
$connectionId = (int)($_GET['id'] ?? 0);
|
||||||
|
$connection = null;
|
||||||
|
|
||||||
|
if ($connectionId > 0) {
|
||||||
|
$connection = $sql->single(
|
||||||
|
"SELECT id, port_a_type, port_a_id, port_b_type, port_b_id, vlan_config, comment
|
||||||
|
FROM connections
|
||||||
|
WHERE id = ?",
|
||||||
|
"i",
|
||||||
|
[$connectionId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizePortType = static function (string $value): string {
|
||||||
|
$map = [
|
||||||
|
'device' => 'device',
|
||||||
|
'device_ports' => 'device',
|
||||||
|
'module' => 'module',
|
||||||
|
'module_ports' => 'module',
|
||||||
|
'outlet' => 'outlet',
|
||||||
|
'network_outlet_ports' => 'outlet',
|
||||||
|
'patchpanel' => 'patchpanel',
|
||||||
|
'floor_patchpanel' => 'patchpanel',
|
||||||
|
'floor_patchpanel_ports' => 'patchpanel',
|
||||||
|
];
|
||||||
|
$key = strtolower(trim($value));
|
||||||
|
return $map[$key] ?? 'device';
|
||||||
|
};
|
||||||
|
|
||||||
|
$portAType = $normalizePortType((string)($connection['port_a_type'] ?? 'device'));
|
||||||
|
$portBType = $normalizePortType((string)($connection['port_b_type'] ?? 'device'));
|
||||||
|
$portAId = (int)($connection['port_a_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 = [
|
||||||
|
'device' => [],
|
||||||
|
'module' => [],
|
||||||
|
'outlet' => [],
|
||||||
|
'patchpanel' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
$occupiedStatsByType = [
|
||||||
|
'device' => [],
|
||||||
|
'module' => [],
|
||||||
|
'outlet' => [],
|
||||||
|
'patchpanel' => [],
|
||||||
|
];
|
||||||
|
$occupiedRows = $sql->get(
|
||||||
|
"SELECT id, port_a_type, port_a_id, port_b_type, port_b_id
|
||||||
|
FROM connections
|
||||||
|
WHERE id <> ?",
|
||||||
|
"i",
|
||||||
|
[$connectionId]
|
||||||
|
);
|
||||||
|
foreach ((array)$occupiedRows as $row) {
|
||||||
|
$typeA = $normalizePortType((string)($row['port_a_type'] ?? ''));
|
||||||
|
$idA = (int)($row['port_a_id'] ?? 0);
|
||||||
|
if ($idA > 0 && isset($occupiedStatsByType[$typeA])) {
|
||||||
|
if (!isset($occupiedStatsByType[$typeA][$idA])) {
|
||||||
|
$occupiedStatsByType[$typeA][$idA] = ['total' => 0];
|
||||||
|
}
|
||||||
|
$occupiedStatsByType[$typeA][$idA]['total']++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$typeB = $normalizePortType((string)($row['port_b_type'] ?? ''));
|
||||||
|
$idB = (int)($row['port_b_id'] ?? 0);
|
||||||
|
if ($idB > 0 && isset($occupiedStatsByType[$typeB])) {
|
||||||
|
if (!isset($occupiedStatsByType[$typeB][$idB])) {
|
||||||
|
$occupiedStatsByType[$typeB][$idB] = ['total' => 0];
|
||||||
|
}
|
||||||
|
$occupiedStatsByType[$typeB][$idB]['total']++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$isEndpointAllowed = static function (string $type, int $id) use ($occupiedStatsByType, $portAType, $portAId, $portBType, $portBId): bool {
|
||||||
|
if ($id <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($type === $portAType && $id === $portAId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ($type === $portBType && $id === $portBId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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.
|
||||||
|
$outletsWithoutPorts = $sql->get(
|
||||||
|
"SELECT o.id
|
||||||
|
FROM network_outlets o
|
||||||
|
LEFT JOIN network_outlet_ports nop ON nop.outlet_id = o.id
|
||||||
|
GROUP BY o.id
|
||||||
|
HAVING COUNT(nop.id) = 0",
|
||||||
|
"",
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
foreach ((array)$outletsWithoutPorts as $outletRow) {
|
||||||
|
$outletId = (int)($outletRow['id'] ?? 0);
|
||||||
|
if ($outletId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$sql->set(
|
||||||
|
"INSERT INTO network_outlet_ports (outlet_id, name) VALUES (?, 'Port 1')",
|
||||||
|
"i",
|
||||||
|
[$outletId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$devicePorts = $sql->get(
|
||||||
|
"SELECT dp.id, dp.name, d.name AS owner_name
|
||||||
|
FROM device_ports dp
|
||||||
|
JOIN devices d ON d.id = dp.device_id
|
||||||
|
ORDER BY d.name, dp.name",
|
||||||
|
"",
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
foreach ($devicePorts as $row) {
|
||||||
|
$id = (int)$row['id'];
|
||||||
|
if (!$isEndpointAllowed('device', $id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$endpointOptions['device'][] = [
|
||||||
|
'id' => $id,
|
||||||
|
'label' => $row['owner_name'] . ' / ' . $row['name'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$modulePorts = $sql->get(
|
||||||
|
"SELECT
|
||||||
|
mp.id,
|
||||||
|
mp.name,
|
||||||
|
m.name AS module_name,
|
||||||
|
MIN(d.name) AS device_name
|
||||||
|
FROM module_ports mp
|
||||||
|
JOIN modules m ON m.id = mp.module_id
|
||||||
|
LEFT JOIN device_port_modules dpm ON dpm.module_id = m.id
|
||||||
|
LEFT JOIN device_ports dp ON dp.id = dpm.device_port_id
|
||||||
|
LEFT JOIN devices d ON d.id = dp.device_id
|
||||||
|
GROUP BY mp.id, mp.name, m.name
|
||||||
|
ORDER BY device_name, module_name, mp.name",
|
||||||
|
"",
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
foreach ($modulePorts as $row) {
|
||||||
|
$id = (int)$row['id'];
|
||||||
|
if (!$isEndpointAllowed('module', $id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$deviceName = trim((string)($row['device_name'] ?? '')) ?: 'Unzugeordnet';
|
||||||
|
$endpointOptions['module'][] = [
|
||||||
|
'id' => $id,
|
||||||
|
'label' => $deviceName . ' / ' . $row['module_name'] . ' / ' . $row['name'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$outletPorts = $sql->get(
|
||||||
|
"SELECT nop.id, nop.name, o.name AS outlet_name, r.name AS room_name, f.name AS floor_name
|
||||||
|
FROM network_outlet_ports nop
|
||||||
|
JOIN network_outlets o ON o.id = nop.outlet_id
|
||||||
|
LEFT JOIN rooms r ON r.id = o.room_id
|
||||||
|
LEFT JOIN floors f ON f.id = r.floor_id
|
||||||
|
ORDER BY floor_name, room_name, outlet_name, nop.name",
|
||||||
|
"",
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
foreach ($outletPorts as $row) {
|
||||||
|
$id = (int)$row['id'];
|
||||||
|
if (!$isEndpointAllowed('outlet', $id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$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'][] = [
|
||||||
|
'id' => $id,
|
||||||
|
'label' => implode(' / ', $parts),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$patchpanelPorts = $sql->get(
|
||||||
|
"SELECT fpp.id, fpp.name, fp.name AS patchpanel_name, f.name AS floor_name
|
||||||
|
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 floor_name, patchpanel_name, fpp.name",
|
||||||
|
"",
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
foreach ($patchpanelPorts as $row) {
|
||||||
|
$id = (int)$row['id'];
|
||||||
|
if (!$isEndpointAllowed('patchpanel', $id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$parts = array_filter([(string)($row['floor_name'] ?? ''), (string)$row['patchpanel_name'], (string)$row['name']]);
|
||||||
|
$endpointOptions['patchpanel'][] = [
|
||||||
|
'id' => $id,
|
||||||
|
'label' => implode(' / ', $parts),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$vlanValue = '';
|
||||||
|
if (!empty($connection['vlan_config'])) {
|
||||||
|
$decoded = json_decode((string)$connection['vlan_config'], true);
|
||||||
|
if (is_array($decoded)) {
|
||||||
|
$vlanValue = implode(',', array_map('trim', $decoded));
|
||||||
|
} else {
|
||||||
|
$vlanValue = (string)$connection['vlan_config'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$renderEndpointOptions = static function (array $optionsByType, int $selectedId): void {
|
||||||
|
foreach ($optionsByType as $type => $options) {
|
||||||
|
foreach ($options as $entry) {
|
||||||
|
$isSelected = ((int)$entry['id'] === $selectedId) ? ' selected' : '';
|
||||||
|
echo '<option value="' . (int)$entry['id'] . '" data-endpoint-type="' . htmlspecialchars((string)$type) . '"' . $isSelected . '>';
|
||||||
|
echo htmlspecialchars((string)$entry['label']);
|
||||||
|
echo '</option>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="device-edit">
|
||||||
|
<h1><?php echo $connection ? 'Verbindung bearbeiten' : 'Neue Verbindung'; ?></h1>
|
||||||
|
|
||||||
|
<form method="post" action="?module=connections&action=save" class="edit-form">
|
||||||
|
<?php if ($connection): ?>
|
||||||
|
<input type="hidden" name="id" value="<?php echo (int)$connection['id']; ?>">
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Endpunkt A</legend>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="port_a_type">Typ</label>
|
||||||
|
<select id="port_a_type" name="port_a_type" required>
|
||||||
|
<option value="device" <?php echo $portAType === 'device' ? 'selected' : ''; ?>>Geraeteport</option>
|
||||||
|
<option value="module" <?php echo $portAType === 'module' ? 'selected' : ''; ?>>Modulport</option>
|
||||||
|
<option value="outlet" <?php echo $portAType === 'outlet' ? 'selected' : ''; ?>>Netzwerkdose</option>
|
||||||
|
<option value="patchpanel" <?php echo $portAType === 'patchpanel' ? 'selected' : ''; ?>>Patchpanel-Port</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="port_a_id">Port</label>
|
||||||
|
<select id="port_a_id" name="port_a_id" required data-selected-id="<?php echo (int)$portAId; ?>">
|
||||||
|
<option value="">- Port waehlen -</option>
|
||||||
|
<?php $renderEndpointOptions($endpointOptions, $portAId); ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Endpunkt B</legend>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="port_b_type">Typ</label>
|
||||||
|
<select id="port_b_type" name="port_b_type" required>
|
||||||
|
<option value="device" <?php echo $portBType === 'device' ? 'selected' : ''; ?>>Geraeteport</option>
|
||||||
|
<option value="module" <?php echo $portBType === 'module' ? 'selected' : ''; ?>>Modulport</option>
|
||||||
|
<option value="outlet" <?php echo $portBType === 'outlet' ? 'selected' : ''; ?>>Netzwerkdose</option>
|
||||||
|
<option value="patchpanel" <?php echo $portBType === 'patchpanel' ? 'selected' : ''; ?>>Patchpanel-Port</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="port_b_id">Port</label>
|
||||||
|
<select id="port_b_id" name="port_b_id" required data-selected-id="<?php echo (int)$portBId; ?>">
|
||||||
|
<option value="">- Port waehlen -</option>
|
||||||
|
<?php $renderEndpointOptions($endpointOptions, $portBId); ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Details</legend>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="vlan_config">VLANs (kommagetrennt)</label>
|
||||||
|
<input type="text" id="vlan_config" name="vlan_config" value="<?php echo htmlspecialchars($vlanValue); ?>" placeholder="z.B. 10,20,30">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="comment">Kommentar</label>
|
||||||
|
<textarea id="comment" name="comment" rows="3"><?php echo htmlspecialchars($connection['comment'] ?? ''); ?></textarea>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="form-actions">
|
||||||
|
<button type="submit" class="button button-primary">Speichern</button>
|
||||||
|
<a href="?module=connections&action=list" class="button">Abbrechen</a>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<script src="/assets/js/connections-edit-form.js" defer></script>
|
||||||
@@ -2,28 +2,65 @@
|
|||||||
/**
|
/**
|
||||||
* 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);
|
||||||
|
|
||||||
|
$endpointUnionSql = "
|
||||||
|
SELECT
|
||||||
|
'device' AS endpoint_type,
|
||||||
|
dp.id AS endpoint_id,
|
||||||
|
dp.name AS port_name,
|
||||||
|
d.name AS owner_name,
|
||||||
|
d.id AS owner_device_id
|
||||||
|
FROM device_ports dp
|
||||||
|
JOIN devices d ON d.id = dp.device_id
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
'module' AS endpoint_type,
|
||||||
|
mp.id AS endpoint_id,
|
||||||
|
mp.name AS port_name,
|
||||||
|
CONCAT(IFNULL(MIN(d.name), 'Unzugeordnet'), ' / ', m.name) AS owner_name,
|
||||||
|
MIN(d.id) AS owner_device_id
|
||||||
|
FROM module_ports mp
|
||||||
|
JOIN modules m ON m.id = mp.module_id
|
||||||
|
LEFT JOIN device_port_modules dpm ON dpm.module_id = m.id
|
||||||
|
LEFT JOIN device_ports dp ON dp.id = dpm.device_port_id
|
||||||
|
LEFT JOIN devices d ON d.id = dp.device_id
|
||||||
|
GROUP BY mp.id, mp.name, m.name
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
'outlet' AS endpoint_type,
|
||||||
|
nop.id AS endpoint_id,
|
||||||
|
nop.name AS port_name,
|
||||||
|
CONCAT(o.name, ' / ', IFNULL(r.name, ''), ' / ', IFNULL(f.name, '')) AS owner_name,
|
||||||
|
NULL AS owner_device_id
|
||||||
|
FROM network_outlet_ports nop
|
||||||
|
JOIN network_outlets o ON o.id = nop.outlet_id
|
||||||
|
LEFT JOIN rooms r ON r.id = o.room_id
|
||||||
|
LEFT JOIN floors f ON f.id = r.floor_id
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
'floor_patchpanel' AS endpoint_type,
|
||||||
|
fpp.id AS endpoint_id,
|
||||||
|
fpp.name AS port_name,
|
||||||
|
CONCAT(fp.name, ' / ', IFNULL(f.name, '')) AS owner_name,
|
||||||
|
NULL AS owner_device_id
|
||||||
|
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
|
||||||
|
";
|
||||||
|
|
||||||
// =========================
|
|
||||||
// WHERE-Clause bauen
|
|
||||||
// =========================
|
|
||||||
$where = [];
|
$where = [];
|
||||||
$types = '';
|
$types = '';
|
||||||
$params = [];
|
$params = [];
|
||||||
|
|
||||||
if ($search !== '') {
|
if ($search !== '') {
|
||||||
$where[] = "(d1.name LIKE ? OR d2.name LIKE ? OR dpt1.name LIKE ? OR dpt2.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%";
|
||||||
@@ -31,178 +68,392 @@ if ($search !== '') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($deviceId > 0) {
|
if ($deviceId > 0) {
|
||||||
$where[] = "(d1.id = ? OR d2.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,
|
||||||
c.port_a_type, c.port_a_id, c.port_b_type, c.port_b_id,
|
c.port_a_type, c.port_a_id, c.port_b_type, c.port_b_id,
|
||||||
d1.name AS device_a_name,
|
e1.owner_name AS endpoint_a_name,
|
||||||
d2.name AS device_b_name,
|
e2.owner_name AS endpoint_b_name,
|
||||||
dpt1.name AS port_a_name,
|
e1.port_name AS port_a_name,
|
||||||
dpt2.name AS port_b_name,
|
e2.port_name AS port_b_name,
|
||||||
c.vlan_config,
|
c.vlan_config,
|
||||||
c.comment
|
c.comment
|
||||||
FROM connections c
|
FROM connections c
|
||||||
LEFT JOIN device_ports dpt1 ON c.port_a_type = 'device' AND c.port_a_id = dpt1.id
|
LEFT JOIN ($endpointUnionSql) e1
|
||||||
LEFT JOIN devices d1 ON dpt1.device_id = d1.id
|
ON c.port_a_id = e1.endpoint_id
|
||||||
LEFT JOIN device_ports dpt2 ON c.port_b_type = 'device' AND c.port_b_id = dpt2.id
|
AND (
|
||||||
LEFT JOIN devices d2 ON dpt2.device_id = d2.id
|
c.port_a_type = e1.endpoint_type
|
||||||
|
OR (c.port_a_type = 'device_ports' AND e1.endpoint_type = 'device')
|
||||||
|
OR (c.port_a_type = 'module_ports' AND e1.endpoint_type = 'module')
|
||||||
|
OR (c.port_a_type = 'network_outlet_ports' AND e1.endpoint_type = 'outlet')
|
||||||
|
OR (c.port_a_type = 'floor_patchpanel_ports' AND e1.endpoint_type = 'floor_patchpanel')
|
||||||
|
OR (c.port_a_type = 'patchpanel' AND e1.endpoint_type = 'floor_patchpanel')
|
||||||
|
)
|
||||||
|
LEFT JOIN ($endpointUnionSql) e2
|
||||||
|
ON c.port_b_id = e2.endpoint_id
|
||||||
|
AND (
|
||||||
|
c.port_b_type = e2.endpoint_type
|
||||||
|
OR (c.port_b_type = 'device_ports' AND e2.endpoint_type = 'device')
|
||||||
|
OR (c.port_b_type = 'module_ports' AND e2.endpoint_type = 'module')
|
||||||
|
OR (c.port_b_type = 'network_outlet_ports' AND e2.endpoint_type = 'outlet')
|
||||||
|
OR (c.port_b_type = 'floor_patchpanel_ports' AND e2.endpoint_type = 'floor_patchpanel')
|
||||||
|
OR (c.port_b_type = 'patchpanel' AND e2.endpoint_type = 'floor_patchpanel')
|
||||||
|
)
|
||||||
$whereSql
|
$whereSql
|
||||||
ORDER BY d1.name, d2.name",
|
ORDER BY e1.owner_name, e2.owner_name",
|
||||||
$types,
|
$types,
|
||||||
$params
|
$params
|
||||||
);
|
);
|
||||||
|
|
||||||
// =========================
|
$devices = $sql->get('SELECT id, name FROM devices ORDER BY name', '', []);
|
||||||
// Filter-Daten
|
|
||||||
// =========================
|
|
||||||
$devices = $sql->get("SELECT id, name FROM devices ORDER BY name", "", []);
|
|
||||||
|
|
||||||
|
$selectedConnection = null;
|
||||||
|
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;
|
||||||
|
$selectedDevicePorts = [];
|
||||||
|
$selectedDeviceVlans = [];
|
||||||
|
|
||||||
|
if ($deviceId > 0) {
|
||||||
|
$selectedDevice = $sql->single(
|
||||||
|
"SELECT d.id, d.name, dt.name AS type_name
|
||||||
|
FROM devices d
|
||||||
|
LEFT JOIN device_types dt ON d.device_type_id = dt.id
|
||||||
|
WHERE d.id = ?",
|
||||||
|
'i',
|
||||||
|
[$deviceId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($selectedDevice) {
|
||||||
|
$selectedDevice['port_count'] = (int)($sql->single(
|
||||||
|
"SELECT COUNT(*) AS cnt
|
||||||
|
FROM (
|
||||||
|
SELECT dp.id
|
||||||
|
FROM device_ports dp
|
||||||
|
WHERE dp.device_id = ?
|
||||||
|
UNION ALL
|
||||||
|
SELECT DISTINCT mp.id
|
||||||
|
FROM module_ports mp
|
||||||
|
JOIN modules m ON m.id = mp.module_id
|
||||||
|
JOIN device_port_modules dpm ON dpm.module_id = m.id
|
||||||
|
JOIN device_ports dp ON dp.id = dpm.device_port_id
|
||||||
|
WHERE dp.device_id = ?
|
||||||
|
) p",
|
||||||
|
'ii',
|
||||||
|
[$deviceId, $deviceId]
|
||||||
|
)['cnt'] ?? 0);
|
||||||
|
|
||||||
|
$selectedDevice['connection_count'] = (int)($sql->single(
|
||||||
|
"SELECT COUNT(DISTINCT c.id) AS cnt
|
||||||
|
FROM connections c
|
||||||
|
LEFT JOIN ($endpointUnionSql) e1
|
||||||
|
ON c.port_a_id = e1.endpoint_id
|
||||||
|
AND (
|
||||||
|
c.port_a_type = e1.endpoint_type
|
||||||
|
OR (c.port_a_type = 'device_ports' AND e1.endpoint_type = 'device')
|
||||||
|
OR (c.port_a_type = 'module_ports' AND e1.endpoint_type = 'module')
|
||||||
|
OR (c.port_a_type = 'network_outlet_ports' AND e1.endpoint_type = 'outlet')
|
||||||
|
OR (c.port_a_type = 'floor_patchpanel_ports' AND e1.endpoint_type = 'floor_patchpanel')
|
||||||
|
OR (c.port_a_type = 'patchpanel' AND e1.endpoint_type = 'floor_patchpanel')
|
||||||
|
)
|
||||||
|
LEFT JOIN ($endpointUnionSql) e2
|
||||||
|
ON c.port_b_id = e2.endpoint_id
|
||||||
|
AND (
|
||||||
|
c.port_b_type = e2.endpoint_type
|
||||||
|
OR (c.port_b_type = 'device_ports' AND e2.endpoint_type = 'device')
|
||||||
|
OR (c.port_b_type = 'module_ports' AND e2.endpoint_type = 'module')
|
||||||
|
OR (c.port_b_type = 'network_outlet_ports' AND e2.endpoint_type = 'outlet')
|
||||||
|
OR (c.port_b_type = 'floor_patchpanel_ports' 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 = ?",
|
||||||
|
'ii',
|
||||||
|
[$deviceId, $deviceId]
|
||||||
|
)['cnt'] ?? 0);
|
||||||
|
|
||||||
|
$selectedDevicePorts = $sql->get(
|
||||||
|
"SELECT name, vlan_config
|
||||||
|
FROM (
|
||||||
|
SELECT dp.name, dp.vlan_config, dp.id AS sort_id
|
||||||
|
FROM device_ports dp
|
||||||
|
WHERE dp.device_id = ?
|
||||||
|
UNION ALL
|
||||||
|
SELECT DISTINCT CONCAT(m.name, ' / ', mp.name) AS name, NULL AS vlan_config, (1000000 + mp.id) AS sort_id
|
||||||
|
FROM module_ports mp
|
||||||
|
JOIN modules m ON m.id = mp.module_id
|
||||||
|
JOIN device_port_modules dpm ON dpm.module_id = m.id
|
||||||
|
JOIN device_ports dp ON dp.id = dpm.device_port_id
|
||||||
|
WHERE dp.device_id = ?
|
||||||
|
) p
|
||||||
|
ORDER BY sort_id
|
||||||
|
LIMIT 12",
|
||||||
|
'ii',
|
||||||
|
[$deviceId, $deviceId]
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($selectedDevicePorts as $port) {
|
||||||
|
if (empty($port['vlan_config'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$vlans = json_decode($port['vlan_config'], true);
|
||||||
|
foreach ((array)$vlans as $vlan) {
|
||||||
|
$vlan = trim((string)$vlan);
|
||||||
|
if ($vlan !== '') {
|
||||||
|
$selectedDeviceVlans[$vlan] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$selectedDeviceVlans = array_keys($selectedDeviceVlans);
|
||||||
|
natcasesort($selectedDeviceVlans);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$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-container">
|
<div class="connections-layout">
|
||||||
<h1>Netzwerkverbindungen</h1>
|
<div class="connections-container">
|
||||||
|
<h1>Netzwerkverbindungen</h1>
|
||||||
|
|
||||||
<!-- =========================
|
<div class="filter-form">
|
||||||
Filter-Toolbar
|
<form method="GET">
|
||||||
========================= -->
|
<input type="hidden" name="module" value="connections">
|
||||||
<div class="filter-form">
|
<input type="hidden" name="action" value="list">
|
||||||
<form method="GET">
|
|
||||||
<input type="hidden" name="module" value="connections">
|
|
||||||
<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; ?>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button type="submit" class="button">Filter</button>
|
||||||
|
<a href="?module=connections&action=list" class="button">Reset</a>
|
||||||
|
<a href="?module=connections&action=edit" class="button button-primary">+ Neue Verbindung</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (!empty($connections)): ?>
|
||||||
|
<table class="connections-list">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Von (Geraet -> Port)</th>
|
||||||
|
<th>Nach (Geraet -> Port)</th>
|
||||||
|
<th>VLANs</th>
|
||||||
|
<th>Beschreibung</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($connections as $conn): ?>
|
||||||
|
<?php
|
||||||
|
$connId = (int)($conn['id'] ?? 0);
|
||||||
|
$comment = trim((string)($conn['comment'] ?? ''));
|
||||||
|
$hasMissingInfo = empty($conn['endpoint_a_name']) || empty($conn['endpoint_b_name'])
|
||||||
|
|| empty($conn['port_a_name']) || empty($conn['port_b_name']);
|
||||||
|
$commentLower = mb_strtolower($comment, 'UTF-8');
|
||||||
|
$warningFromComment = preg_match('/warn|achtung|critical/', $commentLower) === 1;
|
||||||
|
$hasWarning = $hasMissingInfo || $warningFromComment;
|
||||||
|
$rowClass = $connId === $selectedConnectionId ? 'connection-row-selected' : '';
|
||||||
|
$vlanList = [];
|
||||||
|
if (!empty($conn['vlan_config'])) {
|
||||||
|
$vlanList = (array)json_decode((string)$conn['vlan_config'], true);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<small><?php echo !empty($vlanList) ? htmlspecialchars(implode(', ', $vlanList)) : '-'; ?></small>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<small><?php echo htmlspecialchars($comment); ?></small>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="status-cell">
|
||||||
|
<?php if ($hasWarning): ?>
|
||||||
|
<span class="status-badge status-badge-warning" title="Unvollstaendige oder kritische Verbindung">Warnung</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="status-badge status-badge-ok" title="Verbindung vollstaendig">OK</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="actions">
|
||||||
|
<a href="<?php echo htmlspecialchars($buildListUrl(['connection_id' => $connId])); ?>" class="button button-small">Details</a>
|
||||||
|
<a href="?module=connections&action=edit&id=<?php echo $connId; ?>" class="button button-small">Bearbeiten</a>
|
||||||
|
<a href="?module=connections&action=swap&id=<?php echo $connId; ?>" class="button button-small" onclick="return confirm('Von/Nach fuer diese Verbindung vertauschen?');">Von/Nach tauschen</a>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button button-small button-danger js-connection-delete"
|
||||||
|
data-connection-id="<?php echo $connId; ?>">
|
||||||
|
Loeschen
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</tbody>
|
||||||
|
</table>
|
||||||
<button type="submit" class="button">Filter</button>
|
<?php else: ?>
|
||||||
<a href="?module=connections&action=list" class="button">Reset</a>
|
<div class="empty-state">
|
||||||
<a href="?module=connections&action=save" class="button button-primary">+ Neue Verbindung</a>
|
<p>Keine Verbindungen gefunden.</p>
|
||||||
</form>
|
<p>
|
||||||
|
<a href="?module=connections&action=edit" class="button button-primary">
|
||||||
|
Erste Verbindung anlegen
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- =========================
|
<aside class="connections-sidebar">
|
||||||
Verbindungs-Tabelle
|
<section class="sidebar-card">
|
||||||
========================= -->
|
<h3>Ausgewaehlte Verbindung</h3>
|
||||||
<?php if (!empty($connections)): ?>
|
<?php if ($selectedConnection): ?>
|
||||||
<table class="connections-list">
|
<?php
|
||||||
<thead>
|
$selectedConnId = (int)($selectedConnection['id'] ?? 0);
|
||||||
<tr>
|
$selectedVlans = [];
|
||||||
<th>Von (Gerät → Port)</th>
|
if (!empty($selectedConnection['vlan_config'])) {
|
||||||
<th>Nach (Gerät → Port)</th>
|
$selectedVlans = (array)json_decode((string)$selectedConnection['vlan_config'], true);
|
||||||
<th>VLANs</th>
|
}
|
||||||
<th>Beschreibung</th>
|
?>
|
||||||
<th>Status</th>
|
<p><strong>ID:</strong> #<?php echo $selectedConnId; ?></p>
|
||||||
<th>Aktionen</th>
|
<p><strong>Von:</strong><br>
|
||||||
</tr>
|
<?php echo htmlspecialchars((string)($selectedConnection['endpoint_a_name'] ?? 'N/A')); ?><br>
|
||||||
</thead>
|
<small><?php echo htmlspecialchars((string)($selectedConnection['port_a_name'] ?? '-')); ?></small>
|
||||||
<tbody>
|
</p>
|
||||||
<?php foreach ($connections as $conn): ?>
|
<p><strong>Nach:</strong><br>
|
||||||
<?php
|
<?php echo htmlspecialchars((string)($selectedConnection['endpoint_b_name'] ?? 'N/A')); ?><br>
|
||||||
$comment = trim($conn['comment'] ?? '');
|
<small><?php echo htmlspecialchars((string)($selectedConnection['port_b_name'] ?? '-')); ?></small>
|
||||||
$hasMissingInfo = empty($conn['device_a_name']) || empty($conn['device_b_name'])
|
</p>
|
||||||
|| empty($conn['port_a_name']) || empty($conn['port_b_name']);
|
<p><strong>VLANs:</strong> <?php echo !empty($selectedVlans) ? htmlspecialchars(implode(', ', $selectedVlans)) : '-'; ?></p>
|
||||||
$commentLower = mb_strtolower($comment, 'UTF-8');
|
<p><strong>Kommentar:</strong> <?php echo htmlspecialchars((string)($selectedConnection['comment'] ?? '-')); ?></p>
|
||||||
$warningFromComment = preg_match('/warn|achtung|critical/', $commentLower);
|
<div class="sidebar-actions">
|
||||||
$hasWarning = $hasMissingInfo || $warningFromComment;
|
<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>
|
||||||
<tr>
|
<button
|
||||||
<td>
|
type="button"
|
||||||
<strong><?php echo htmlspecialchars($conn['device_a_name'] ?? 'N/A'); ?></strong><br>
|
class="button button-small button-danger js-connection-delete"
|
||||||
<small><?php echo htmlspecialchars($conn['port_a_name'] ?? '—'); ?></small>
|
data-connection-id="<?php echo $selectedConnId; ?>">
|
||||||
</td>
|
Loeschen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<p><em>Keine Verbindung ausgewaehlt.</em></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
|
|
||||||
<td>
|
<section class="sidebar-card">
|
||||||
<strong><?php echo htmlspecialchars($conn['device_b_name'] ?? 'N/A'); ?></strong><br>
|
<?php if ($selectedDevice): ?>
|
||||||
<small><?php echo htmlspecialchars($conn['port_b_name'] ?? '—'); ?></small>
|
<h3>Ausgewaehltes Geraet</h3>
|
||||||
</td>
|
<p><strong><?php echo htmlspecialchars((string)$selectedDevice['name']); ?></strong></p>
|
||||||
|
<p>Typ: <?php echo htmlspecialchars((string)($selectedDevice['type_name'] ?? '-')); ?></p>
|
||||||
<td>
|
<p>Ports: <?php echo (int)$selectedDevice['port_count']; ?></p>
|
||||||
<small>
|
<p>Verbindungen: <?php echo (int)$selectedDevice['connection_count']; ?></p>
|
||||||
<?php
|
<p>
|
||||||
if ($conn['vlan_config']) {
|
VLANs:
|
||||||
$vlan = json_decode($conn['vlan_config'], true);
|
<?php if (!empty($selectedDeviceVlans)): ?>
|
||||||
echo htmlspecialchars(implode(', ', (array)$vlan));
|
<?php echo htmlspecialchars(implode(', ', $selectedDeviceVlans)); ?>
|
||||||
} else {
|
|
||||||
echo '—';
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</small>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<small><?php echo htmlspecialchars($conn['comment'] ?? ''); ?></small>
|
|
||||||
</td>
|
|
||||||
<td class="status-cell">
|
|
||||||
<?php if ($hasWarning): ?>
|
|
||||||
<span class="status-badge status-badge-warning" title="Unvollständige oder kritische Verbindung">
|
|
||||||
⚠️ Warnung
|
|
||||||
</span>
|
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<span class="status-badge status-badge-ok" title="Verbindung vollständig">
|
-
|
||||||
✔️ OK
|
|
||||||
</span>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</p>
|
||||||
|
<?php if (!empty($selectedDevicePorts)): ?>
|
||||||
<td class="actions">
|
<h4>Ports (max. 12)</h4>
|
||||||
<a href="?module=connections&action=edit&id=<?php echo $conn['id']; ?>" class="button button-small">Bearbeiten</a>
|
<ul>
|
||||||
<a href="#" class="button button-small button-danger"
|
<?php foreach ($selectedDevicePorts as $port): ?>
|
||||||
data-confirm-delete="true"
|
<li><?php echo htmlspecialchars((string)$port['name']); ?></li>
|
||||||
data-confirm-message="Diese Verbindung wirklich löschen?"
|
<?php endforeach; ?>
|
||||||
data-confirm-feedback="Löschen noch nicht implementiert">
|
</ul>
|
||||||
Löschen
|
<?php endif; ?>
|
||||||
</a>
|
<?php else: ?>
|
||||||
</td>
|
<h3>Ausgewaehltes Geraet</h3>
|
||||||
</tr>
|
<p><em>Bitte ein Geraet im Filter auswaehlen.</em></p>
|
||||||
<?php endforeach; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</section>
|
||||||
</table>
|
</aside>
|
||||||
|
|
||||||
<?php else: ?>
|
|
||||||
<div class="empty-state">
|
|
||||||
<p>Keine Verbindungen gefunden.</p>
|
|
||||||
<p>
|
|
||||||
<a href="?module=connections&action=save" class="button button-primary">
|
|
||||||
Erste Verbindung anlegen
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- =========================
|
<script>
|
||||||
Sidebar / Details
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
========================= -->
|
document.querySelectorAll('.js-connection-delete').forEach((button) => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
const id = Number(button.dataset.connectionId || '0');
|
||||||
|
if (id <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
<aside class="sidebar">
|
if (!confirm('Diese Verbindung wirklich loeschen?')) {
|
||||||
<!-- TODO: Details zum ausgewählten Gerät anzeigen -->
|
return;
|
||||||
<!--
|
}
|
||||||
- Gerätename
|
|
||||||
- Gerätetyp
|
|
||||||
- Ports
|
|
||||||
- VLANs
|
|
||||||
- Verbindungen
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- TODO: Verbindung bearbeiten / löschen -->
|
fetch('?module=connections&action=delete', {
|
||||||
</aside>
|
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>
|
||||||
|
|||||||
@@ -23,6 +23,37 @@ $portBId = (int)($_POST['port_b_id'] ?? 0);
|
|||||||
$vlanConfig = $_POST['vlan_config'] ?? '';
|
$vlanConfig = $_POST['vlan_config'] ?? '';
|
||||||
$comment = trim($_POST['comment'] ?? '');
|
$comment = trim($_POST['comment'] ?? '');
|
||||||
|
|
||||||
|
$normalizePortType = static function (string $value): string {
|
||||||
|
$map = [
|
||||||
|
'device' => 'device',
|
||||||
|
'device_ports' => 'device',
|
||||||
|
'module' => 'module',
|
||||||
|
'module_ports' => 'module',
|
||||||
|
'outlet' => 'outlet',
|
||||||
|
'network_outlet_ports' => 'outlet',
|
||||||
|
'patchpanel' => 'patchpanel',
|
||||||
|
'floor_patchpanel' => 'patchpanel',
|
||||||
|
'floor_patchpanel_ports' => 'patchpanel',
|
||||||
|
];
|
||||||
|
$key = strtolower(trim($value));
|
||||||
|
return $map[$key] ?? $key;
|
||||||
|
};
|
||||||
|
|
||||||
|
$portAType = $normalizePortType((string)$portAType);
|
||||||
|
$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)
|
||||||
// =========================
|
// =========================
|
||||||
@@ -31,10 +62,91 @@ $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(
|
||||||
|
"SELECT id, port_a_type, port_a_id, port_b_type, port_b_id
|
||||||
|
FROM connections
|
||||||
|
WHERE id <> ?",
|
||||||
|
"i",
|
||||||
|
[$connId]
|
||||||
|
);
|
||||||
|
|
||||||
|
$endpointUsage = [];
|
||||||
|
$trackUsage = static function (string $endpointType, int $endpointId, string $otherType) use (&$endpointUsage): void {
|
||||||
|
if ($endpointId <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isset($endpointUsage[$endpointType][$endpointId])) {
|
||||||
|
$endpointUsage[$endpointType][$endpointId] = [
|
||||||
|
'total' => 0,
|
||||||
|
'fixed' => 0,
|
||||||
|
'patch' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$endpointUsage[$endpointType][$endpointId]['total']++;
|
||||||
|
|
||||||
|
if (in_array($endpointType, ['outlet', 'patchpanel'], true)) {
|
||||||
|
if (in_array($otherType, ['outlet', 'patchpanel'], true)) {
|
||||||
|
$endpointUsage[$endpointType][$endpointId]['fixed']++;
|
||||||
|
} elseif (in_array($otherType, ['device', 'module'], true)) {
|
||||||
|
$endpointUsage[$endpointType][$endpointId]['patch']++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach ((array)$otherConnections as $row) {
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
|
||||||
|
$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);
|
||||||
$redirectUrl = $connId ? "?module=connections&action=edit&id=$connId" : "?module=connections&action=list";
|
$_SESSION['validation_errors'] = $errors;
|
||||||
|
$redirectUrl = $connId ? "?module=connections&action=edit&id=$connId" : "?module=connections&action=edit";
|
||||||
header("Location: $redirectUrl");
|
header("Location: $redirectUrl");
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -48,15 +160,37 @@ if ($connId > 0) {
|
|||||||
// UPDATE
|
// UPDATE
|
||||||
$sql->set(
|
$sql->set(
|
||||||
"UPDATE connections SET port_a_type = ?, port_a_id = ?, port_b_type = ?, port_b_id = ?, vlan_config = ?, comment = ? WHERE id = ?",
|
"UPDATE connections SET port_a_type = ?, port_a_id = ?, port_b_type = ?, port_b_id = ?, vlan_config = ?, comment = ? WHERE id = ?",
|
||||||
"siisisi",
|
"sisissi",
|
||||||
[$portAType, $portAId, $portBType, $portBId, $vlanJson, $comment, $connId]
|
[$portAType, $portAId, $portBType, $portBId, $vlanJson, $comment, $connId]
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
$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 connections/save'],
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($connectionTypeId <= 0) {
|
||||||
|
$_SESSION['error'] = "Kein Verbindungstyp verfuegbar";
|
||||||
|
$_SESSION['validation_errors'] = ["Kein Verbindungstyp verfuegbar"];
|
||||||
|
header("Location: ?module=connections&action=edit");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
// INSERT
|
// INSERT
|
||||||
$sql->set(
|
$sql->set(
|
||||||
"INSERT INTO connections (port_a_type, port_a_id, port_b_type, port_b_id, vlan_config, comment) VALUES (?, ?, ?, ?, ?, ?)",
|
"INSERT INTO connections (connection_type_id, port_a_type, port_a_id, port_b_type, port_b_id, vlan_config, comment) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
"siisis",
|
"isisiss",
|
||||||
[$portAType, $portAId, $portBType, $portBId, $vlanJson, $comment]
|
[$connectionTypeId, $portAType, $portAId, $portBType, $portBId, $vlanJson, $comment]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
46
app/modules/connections/swap.php
Normal file
46
app/modules/connections/swap.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* app/modules/connections/swap.php
|
||||||
|
*
|
||||||
|
* Vertauscht Endpunkt A und Endpunkt B einer Verbindung.
|
||||||
|
*/
|
||||||
|
|
||||||
|
$connectionId = (int)($_GET['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($connectionId <= 0) {
|
||||||
|
$_SESSION['error'] = 'Ungueltige Verbindungs-ID';
|
||||||
|
header('Location: ?module=connections&action=list');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$connection = $sql->single(
|
||||||
|
"SELECT id, port_a_type, port_a_id, port_b_type, port_b_id
|
||||||
|
FROM connections
|
||||||
|
WHERE id = ?",
|
||||||
|
"i",
|
||||||
|
[$connectionId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$connection) {
|
||||||
|
$_SESSION['error'] = 'Verbindung nicht gefunden';
|
||||||
|
header('Location: ?module=connections&action=list');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql->set(
|
||||||
|
"UPDATE connections
|
||||||
|
SET port_a_type = ?, port_a_id = ?, port_b_type = ?, port_b_id = ?
|
||||||
|
WHERE id = ?",
|
||||||
|
"sisii",
|
||||||
|
[
|
||||||
|
(string)$connection['port_b_type'],
|
||||||
|
(int)$connection['port_b_id'],
|
||||||
|
(string)$connection['port_a_type'],
|
||||||
|
(int)$connection['port_a_id'],
|
||||||
|
$connectionId
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$_SESSION['success'] = 'Endpunkte wurden vertauscht';
|
||||||
|
header('Location: ?module=connections&action=list');
|
||||||
|
exit;
|
||||||
File diff suppressed because it is too large
Load Diff
39
app/modules/device_types/delete.php
Normal file
39
app/modules/device_types/delete.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* app/modules/device_types/delete.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID fehlt']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$exists = $sql->single("SELECT id FROM device_types WHERE id = ?", "i", [$id]);
|
||||||
|
if (!$exists) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Geraetetyp nicht gefunden']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = $sql->set("DELETE FROM device_types WHERE id = ?", "i", [$id]);
|
||||||
|
if ($rows === false) {
|
||||||
|
http_response_code(409);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Geraetetyp konnte nicht geloescht werden (wird ggf. noch von Geraeten verwendet)'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Geraetetyp geloescht']);
|
||||||
|
|
||||||
@@ -15,7 +15,6 @@ $deviceTypeId = (int)($_GET['id'] ?? 0);
|
|||||||
$deviceType = null;
|
$deviceType = null;
|
||||||
$ports = [];
|
$ports = [];
|
||||||
|
|
||||||
//TODO port hinzufügen geht nicht
|
|
||||||
|
|
||||||
if ($deviceTypeId > 0) {
|
if ($deviceTypeId > 0) {
|
||||||
$deviceType = $sql->single(
|
$deviceType = $sql->single(
|
||||||
@@ -87,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"
|
||||||
@@ -107,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>
|
||||||
|
|
||||||
<!-- =========================
|
<!-- =========================
|
||||||
@@ -304,6 +305,12 @@ $pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType[
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<input type="hidden" id="shape-port-options" value="<?php echo htmlspecialchars(json_encode(array_values(array_column($ports, 'name'))), ENT_QUOTES); ?>">
|
<input type="hidden" id="shape-port-options" value="<?php echo htmlspecialchars(json_encode(array_values(array_column($ports, 'name'))), ENT_QUOTES); ?>">
|
||||||
|
<template id="port-type-options-template">
|
||||||
|
<option value="">- Kein Typ -</option>
|
||||||
|
<?php foreach ($portTypes as $pt): ?>
|
||||||
|
<option value="<?php echo (int)$pt['id']; ?>"><?php echo htmlspecialchars($pt['name']); ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div class="port-actions">
|
<div class="port-actions">
|
||||||
<button type="button" class="button" id="add-port-row">+ Port hinzufügen</button>
|
<button type="button" class="button" id="add-port-row">+ Port hinzufügen</button>
|
||||||
@@ -319,73 +326,18 @@ $pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType[
|
|||||||
<button type="submit" class="button button-primary">Speichern</button>
|
<button type="submit" class="button button-primary">Speichern</button>
|
||||||
<a href="?module=device_types&action=list" class="button">Abbrechen</a>
|
<a href="?module=device_types&action=list" class="button">Abbrechen</a>
|
||||||
<?php if ($isEdit): ?>
|
<?php if ($isEdit): ?>
|
||||||
<a href="#" class="button button-danger" onclick="confirmDelete(<?php echo $deviceTypeId; ?>)">Löschen</a>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button button-danger"
|
||||||
|
id="device-type-delete"
|
||||||
|
data-device-type-id="<?php echo (int)$deviceTypeId; ?>">
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function addPortRow() {
|
|
||||||
const body = document.getElementById('port-definition-body');
|
|
||||||
if (!body) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emptyRow = body.querySelector('tr td em');
|
|
||||||
if (emptyRow) {
|
|
||||||
emptyRow.closest('tr').remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
const rowCount = body.querySelectorAll('tr').length;
|
|
||||||
const index = rowCount;
|
|
||||||
const number = rowCount + 1;
|
|
||||||
|
|
||||||
const portTypeOptions = `<?php
|
|
||||||
$optionHtml = '<option value="">- Kein Typ -</option>';
|
|
||||||
foreach ($portTypes as $pt) {
|
|
||||||
$optionHtml .= '<option value="' . (int)$pt['id'] . '">' . htmlspecialchars($pt['name'], ENT_QUOTES) . '</option>';
|
|
||||||
}
|
|
||||||
echo str_replace('`', '\`', $optionHtml);
|
|
||||||
?>`;
|
|
||||||
|
|
||||||
const row = document.createElement('tr');
|
|
||||||
row.innerHTML = `
|
|
||||||
<td>${number}</td>
|
|
||||||
<td>
|
|
||||||
<input type="hidden" name="port_rows[${index}][id]" value="">
|
|
||||||
<input type="text" name="port_rows[${index}][name]" value="" placeholder="z.B. Gi1/0/1">
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<select name="port_rows[${index}][port_type_id]">
|
|
||||||
${portTypeOptions}
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<label class="inline-checkbox">
|
|
||||||
<input type="checkbox" name="port_rows[${index}][delete]" value="1">
|
|
||||||
entfernen
|
|
||||||
</label>
|
|
||||||
</td>
|
|
||||||
`;
|
|
||||||
body.appendChild(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmDelete(id) {
|
|
||||||
if (confirm('Diesen Gerätetyp wirklich löschen? Alle zugeordneten Geräte werden angepasst.')) {
|
|
||||||
// TODO: AJAX-Delete implementieren
|
|
||||||
alert('Löschen noch nicht implementiert');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const addButton = document.getElementById('add-port-row');
|
|
||||||
if (addButton) {
|
|
||||||
addButton.addEventListener('click', addPortRow);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script src="/assets/js/device-type-shape-editor.js" defer></script>
|
<script src="/assets/js/device-type-shape-editor.js" defer></script>
|
||||||
|
<script src="/assets/js/device-type-edit-form.js" defer></script>
|
||||||
|
|
||||||
|
|||||||
@@ -123,7 +123,12 @@ $deviceTypes = $sql->get(
|
|||||||
<td>
|
<td>
|
||||||
<a href="?module=device_types&action=edit&id=<?php echo $type['id']; ?>" class="button button-small">Bearbeiten</a>
|
<a href="?module=device_types&action=edit&id=<?php echo $type['id']; ?>" class="button button-small">Bearbeiten</a>
|
||||||
<a href="?module=device_types&action=ports&id=<?php echo $type['id']; ?>" class="button button-small">Ports</a>
|
<a href="?module=device_types&action=ports&id=<?php echo $type['id']; ?>" class="button button-small">Ports</a>
|
||||||
<a href="#" class="button button-small button-danger" onclick="confirmDelete(<?php echo $type['id']; ?>)">Löschen</a>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button button-small button-danger js-device-type-delete"
|
||||||
|
data-device-type-id="<?php echo (int)$type['id']; ?>">
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -141,116 +146,6 @@ $deviceTypes = $sql->get(
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<link rel="stylesheet" href="/assets/css/device-types-list.css">
|
||||||
.device-types-container {
|
<script src="/assets/js/device-types-list.js" defer></script>
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-form {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
margin: 20px 0;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-form input,
|
|
||||||
.filter-form select {
|
|
||||||
padding: 8px 12px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-form input {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-type-list {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-type-list th,
|
|
||||||
.device-type-list td {
|
|
||||||
padding: 12px;
|
|
||||||
text-align: left;
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-type-list th {
|
|
||||||
background: #f5f5f5;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.9em;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-switch { background: #0066cc; }
|
|
||||||
.badge-server { background: #cc0000; }
|
|
||||||
.badge-patchpanel { background: #ff9900; }
|
|
||||||
.badge-other { background: #999; }
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
text-align: center;
|
|
||||||
padding: 40px 20px;
|
|
||||||
background: #f9f9f9;
|
|
||||||
border: 1px solid #eee;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 8px 12px;
|
|
||||||
background: #007bff;
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:hover {
|
|
||||||
background: #0056b3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-primary {
|
|
||||||
background: #28a745;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-primary:hover {
|
|
||||||
background: #218838;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-small {
|
|
||||||
padding: 4px 8px;
|
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-danger {
|
|
||||||
background: #dc3545;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-danger:hover {
|
|
||||||
background: #c82333;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function confirmDelete(id) {
|
|
||||||
if (confirm('Diesen Gerätetyp wirklich löschen?')) {
|
|
||||||
// TODO: AJAX-Delete implementieren
|
|
||||||
alert('Löschen noch nicht implementiert');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,265 +1,165 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* app/device_types/ports.php
|
* app/modules/device_types/ports.php
|
||||||
*
|
*
|
||||||
* Verwaltung der Ports eines Gerätetyps
|
* Verwaltung der Ports eines Geraetetyps.
|
||||||
* - Port anlegen / bearbeiten / löschen
|
|
||||||
* - Port-Typ (RJ45, SFP, BNC, Custom)
|
|
||||||
* - VLAN / Modus / Medienart
|
|
||||||
* - Übergabe an SVG-Port-Editor
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// TODO: bootstrap laden
|
$deviceTypeId = (int)($_GET['id'] ?? ($_GET['device_type_id'] ?? 0));
|
||||||
// require_once __DIR__ . '/../../bootstrap.php';
|
if ($deviceTypeId <= 0) {
|
||||||
|
renderClientError(400, 'device_type_id fehlt');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Auth erzwingen
|
$deviceType = $sql->single(
|
||||||
// requireAuth();
|
"SELECT id, name, image_path, image_type FROM device_types WHERE id = ?",
|
||||||
|
'i',
|
||||||
|
[$deviceTypeId]
|
||||||
|
);
|
||||||
|
|
||||||
// =========================
|
if (!$deviceType) {
|
||||||
// Kontext bestimmen
|
renderClientError(404, 'Geraetetyp nicht gefunden');
|
||||||
// =========================
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: device_type_id aus GET lesen
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
// $deviceTypeId = (int)($_GET['id'] ?? 0);
|
$formAction = $_POST['form_action'] ?? '';
|
||||||
|
|
||||||
// TODO: Gerätetyp laden
|
if ($formAction === 'add_port') {
|
||||||
// $deviceType = null;
|
$name = trim((string)($_POST['name'] ?? ''));
|
||||||
|
$portTypeId = (int)($_POST['port_type_id'] ?? 0);
|
||||||
|
$x = (int)($_POST['x'] ?? 0);
|
||||||
|
$y = (int)($_POST['y'] ?? 0);
|
||||||
|
|
||||||
// TODO: Ports dieses Gerätetyps laden
|
if ($name !== '') {
|
||||||
// $ports = [];
|
if ($portTypeId > 0) {
|
||||||
|
$sql->set(
|
||||||
|
"INSERT INTO device_type_ports (device_type_id, name, port_type_id, x, y) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
"isiii",
|
||||||
|
[$deviceTypeId, $name, $portTypeId, $x, $y]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$sql->set(
|
||||||
|
"INSERT INTO device_type_ports (device_type_id, name, port_type_id, x, y) VALUES (?, ?, NULL, ?, ?)",
|
||||||
|
"isii",
|
||||||
|
[$deviceTypeId, $name, $x, $y]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$_SESSION['success'] = 'Port hinzugefuegt';
|
||||||
|
} else {
|
||||||
|
$_SESSION['error'] = 'Portname darf nicht leer sein';
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Location: ?module=device_types&action=ports&id=' . $deviceTypeId);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($formAction === 'delete_port') {
|
||||||
|
$portId = (int)($_POST['port_id'] ?? 0);
|
||||||
|
if ($portId > 0) {
|
||||||
|
$sql->set(
|
||||||
|
"DELETE FROM device_type_ports WHERE id = ? AND device_type_id = ?",
|
||||||
|
"ii",
|
||||||
|
[$portId, $deviceTypeId]
|
||||||
|
);
|
||||||
|
$_SESSION['success'] = 'Port geloescht';
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Location: ?module=device_types&action=ports&id=' . $deviceTypeId);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$ports = $sql->get(
|
||||||
|
"SELECT dtp.id, dtp.name, dtp.port_type_id, dtp.x, dtp.y, pt.name AS port_type_name
|
||||||
|
FROM device_type_ports dtp
|
||||||
|
LEFT JOIN port_types pt ON pt.id = dtp.port_type_id
|
||||||
|
WHERE dtp.device_type_id = ?
|
||||||
|
ORDER BY dtp.id ASC",
|
||||||
|
'i',
|
||||||
|
[$deviceTypeId]
|
||||||
|
);
|
||||||
|
|
||||||
|
$portTypes = $sql->get("SELECT id, name FROM port_types ORDER BY name", '', []);
|
||||||
|
$svgPath = trim((string)($deviceType['image_path'] ?? ''));
|
||||||
|
$svgUrl = $svgPath !== '' ? '/' . ltrim($svgPath, '/\\') : '';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<h2>Ports – Gerätetyp</h2>
|
<div class="device-type-ports">
|
||||||
|
<h2>Ports: <?php echo htmlspecialchars((string)$deviceType['name']); ?></h2>
|
||||||
|
|
||||||
<!-- =========================
|
<div class="breadcrumb">
|
||||||
Zurück / Kontext
|
<a href="?module=device_types&action=list">Geraetetypen</a>
|
||||||
========================= -->
|
->
|
||||||
|
<a href="?module=device_types&action=edit&id=<?php echo (int)$deviceType['id']; ?>"><?php echo htmlspecialchars((string)$deviceType['name']); ?></a>
|
||||||
|
-> Ports
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="breadcrumb">
|
<div class="toolbar">
|
||||||
<a href="/?page=device_types/list">Gerätetypen</a>
|
<a class="button" href="?module=device_types&action=edit&id=<?php echo (int)$deviceType['id']; ?>">Zurueck zum Geraetetyp</a>
|
||||||
→
|
</div>
|
||||||
<a href="/?page=device_types/edit&id=<?= $deviceTypeId ?>">
|
|
||||||
<!-- TODO: Gerätetyp-Name -->
|
<form method="post" class="port-form">
|
||||||
Gerätetyp
|
<input type="hidden" name="form_action" value="add_port">
|
||||||
</a>
|
<div>
|
||||||
→
|
<label for="name">Portname</label>
|
||||||
Ports
|
<input id="name" name="name" required placeholder="z. B. Gi1/0/1">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="port_type_id">Port-Typ</label>
|
||||||
|
<select id="port_type_id" name="port_type_id">
|
||||||
|
<option value="0">- keiner -</option>
|
||||||
|
<?php foreach ($portTypes as $portType): ?>
|
||||||
|
<option value="<?php echo (int)$portType['id']; ?>"><?php echo htmlspecialchars((string)$portType['name']); ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="x">X</label>
|
||||||
|
<input id="x" name="x" type="number" value="0">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="y">Y</label>
|
||||||
|
<input id="y" name="y" type="number" value="0">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="button button-primary">Port hinzufuegen</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<table class="port-list">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Typ</th>
|
||||||
|
<th>X</th>
|
||||||
|
<th>Y</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($ports as $port): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?php echo (int)$port['id']; ?></td>
|
||||||
|
<td><?php echo htmlspecialchars((string)$port['name']); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars((string)($port['port_type_name'] ?? '-')); ?></td>
|
||||||
|
<td><?php echo (int)$port['x']; ?></td>
|
||||||
|
<td><?php echo (int)$port['y']; ?></td>
|
||||||
|
<td>
|
||||||
|
<form method="post" onsubmit="return confirm('Port wirklich loeschen?');" style="display:inline;">
|
||||||
|
<input type="hidden" name="form_action" value="delete_port">
|
||||||
|
<input type="hidden" name="port_id" value="<?php echo (int)$port['id']; ?>">
|
||||||
|
<button type="submit" class="button button-small button-danger">Loeschen</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<?php if ($svgUrl !== '' && ($deviceType['image_type'] ?? '') === 'svg'): ?>
|
||||||
|
<section class="svg-port-editor-section">
|
||||||
|
<h3>SVG Vorschau</h3>
|
||||||
|
<img src="<?php echo htmlspecialchars($svgUrl); ?>" alt="Geraetetyp SVG" style="max-width:100%; height:auto; border:1px solid #ddd;">
|
||||||
|
</section>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
Toolbar
|
|
||||||
========================= -->
|
|
||||||
|
|
||||||
<div class="toolbar">
|
|
||||||
<button id="add-port">
|
|
||||||
+ Port hinzufügen
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- TODO: Port-Typen verwalten -->
|
|
||||||
<!-- TODO: Import / Export -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form id="port-form" class="port-form" aria-hidden="true">
|
|
||||||
<div>
|
|
||||||
<label for="port-name">Portname</label>
|
|
||||||
<input id="port-name" name="name" required placeholder="z. B. Gi1/0/1">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="port-type">Port-Typ</label>
|
|
||||||
<input id="port-type" name="type" required placeholder="RJ45, SFP …">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="port-medium">Medium</label>
|
|
||||||
<input id="port-medium" name="medium" placeholder="Kupfer, LWL …">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="port-mode">Modus</label>
|
|
||||||
<input id="port-mode" name="mode" placeholder="Access, Trunk …">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="port-vlan">VLAN</label>
|
|
||||||
<input id="port-vlan" name="vlan" placeholder="10, 20-30 …">
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="button button-primary">Port hinzufügen</button>
|
|
||||||
<button type="button" class="button" id="cancel-port">Abbrechen</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
Port-Liste
|
|
||||||
========================= -->
|
|
||||||
|
|
||||||
<table class="port-list">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>#</th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Typ</th>
|
|
||||||
<th>Medium</th>
|
|
||||||
<th>Modus</th>
|
|
||||||
<th>VLAN</th>
|
|
||||||
<th>Aktionen</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="port-list-body">
|
|
||||||
|
|
||||||
<?php /* foreach ($ports as $port): */ ?>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<!-- TODO: Port-Nummer -->
|
|
||||||
1
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<!-- TODO: Port-Name -->
|
|
||||||
Port 1
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<!-- TODO: Port-Typ (RJ45, SFP, ...) -->
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<!-- TODO: Medium (Kupfer, LWL, BNC, Custom) -->
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<!-- TODO: Modus (Access, Trunk, Custom) -->
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<!-- TODO: VLANs -->
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button>
|
|
||||||
Bearbeiten
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button>
|
|
||||||
Löschen
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php /* endforeach; */ ?>
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
SVG-Port-Positionierung
|
|
||||||
========================= -->
|
|
||||||
|
|
||||||
<section class="svg-port-editor-section">
|
|
||||||
<h3>Port-Positionen</h3>
|
|
||||||
|
|
||||||
<p class="hint">
|
|
||||||
Ports per Drag & Drop auf dem Gerät platzieren.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="svg-editor-container">
|
|
||||||
<svg
|
|
||||||
id="device-type-svg"
|
|
||||||
viewBox="0 0 800 400"
|
|
||||||
width="100%"
|
|
||||||
height="400"
|
|
||||||
>
|
|
||||||
<!-- TODO: SVG des Gerätetyps laden -->
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
JS-Konfiguration
|
|
||||||
========================= -->
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.port-form {
|
|
||||||
display: none;
|
|
||||||
margin: 20px 0;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 15px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: #f9f9f9;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
||||||
grid-auto-rows: minmax(50px, auto);
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.port-form.visible {
|
|
||||||
display: grid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.port-form div {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.port-form label {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.port-form input {
|
|
||||||
padding: 6px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid #bbb;
|
|
||||||
font-family: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.port-form button {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.port-form .button-primary {
|
|
||||||
justify-self: flex-start;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const addPortButton = document.getElementById('add-port');
|
|
||||||
const portForm = document.getElementById('port-form');
|
|
||||||
const portListBody = document.getElementById('port-list-body');
|
|
||||||
const cancelPortButton = document.getElementById('cancel-port');
|
|
||||||
let portCounter = portListBody.querySelectorAll('tr').length + 1;
|
|
||||||
|
|
||||||
function showPortForm(show = true) {
|
|
||||||
portForm.classList.toggle('visible', show);
|
|
||||||
portForm.setAttribute('aria-hidden', show ? 'false' : 'true');
|
|
||||||
if (show) {
|
|
||||||
portForm.querySelector('input').focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addPortButton.addEventListener('click', (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
showPortForm(portForm.getAttribute('aria-hidden') === 'true');
|
|
||||||
});
|
|
||||||
|
|
||||||
cancelPortButton.addEventListener('click', () => {
|
|
||||||
showPortForm(false);
|
|
||||||
portForm.reset();
|
|
||||||
});
|
|
||||||
|
|
||||||
portForm.addEventListener('submit', (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
const data = new FormData(portForm);
|
|
||||||
const row = document.createElement('tr');
|
|
||||||
row.innerHTML = `
|
|
||||||
<td>${portCounter++}</td>
|
|
||||||
<td>${data.get('name') || '-'}</td>
|
|
||||||
<td>${data.get('type') || '-'}</td>
|
|
||||||
<td>${data.get('medium') || '-'}</td>
|
|
||||||
<td>${data.get('mode') || '-'}</td>
|
|
||||||
<td>${data.get('vlan') || '-'}</td>
|
|
||||||
<td>
|
|
||||||
<button type="button">Bearbeiten</button>
|
|
||||||
<button type="button">Löschen</button>
|
|
||||||
</td>
|
|
||||||
`;
|
|
||||||
portListBody.appendChild(row);
|
|
||||||
showPortForm(false);
|
|
||||||
portForm.reset();
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: Replace this mock logic with real AJAX once ports are
|
|
||||||
* persisted on the backend.
|
|
||||||
*/
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -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) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
seedDeviceTypePorts($sql, $deviceTypeId, $seedPortCount, $defaultPortTypeId);
|
if ($isCreate) {
|
||||||
|
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";
|
||||||
|
|||||||
@@ -2,13 +2,22 @@
|
|||||||
/**
|
/**
|
||||||
* app/modules/devices/delete.php
|
* app/modules/devices/delete.php
|
||||||
*
|
*
|
||||||
* Löscht ein Gerät inkl. abhängiger Verbindungen.
|
* 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';
|
||||||
|
$deviceId = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
|
||||||
|
$forceDelete = (int)($_POST['force'] ?? $_GET['force'] ?? 0) === 1;
|
||||||
|
|
||||||
if ($deviceId <= 0) {
|
if ($deviceId <= 0) {
|
||||||
$_SESSION['error'] = "Ungültige Geräte-ID";
|
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";
|
||||||
header('Location: ?module=devices&action=list');
|
header('Location: ?module=devices&action=list');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -20,16 +29,87 @@ $device = $sql->single(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!$device) {
|
if (!$device) {
|
||||||
$_SESSION['error'] = "Gerät nicht gefunden";
|
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";
|
||||||
header('Location: ?module=devices&action=list');
|
header('Location: ?module=devices&action=list');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verbindungen auf Ports dieses Geräts entfernen (keine FK auf device_ports in connections).
|
$dependencies = $sql->single(
|
||||||
|
"SELECT
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM device_ports dp
|
||||||
|
WHERE dp.device_id = ?
|
||||||
|
) AS port_count,
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM device_port_modules dpm
|
||||||
|
JOIN device_ports dp2 ON dp2.id = dpm.device_port_id
|
||||||
|
WHERE dp2.device_id = ?
|
||||||
|
) AS module_count,
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM connections c
|
||||||
|
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 = ?
|
||||||
|
))
|
||||||
|
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 = ?
|
||||||
|
))
|
||||||
|
) AS connection_count",
|
||||||
|
"iiii",
|
||||||
|
[$deviceId, $deviceId, $deviceId, $deviceId]
|
||||||
|
);
|
||||||
|
|
||||||
|
$portCount = (int)($dependencies['port_count'] ?? 0);
|
||||||
|
$moduleCount = (int)($dependencies['module_count'] ?? 0);
|
||||||
|
$connectionCount = (int)($dependencies['connection_count'] ?? 0);
|
||||||
|
$hasDependencies = $portCount > 0 || $moduleCount > 0 || $connectionCount > 0;
|
||||||
|
|
||||||
|
if ($hasDependencies && !$forceDelete) {
|
||||||
|
$parts = [];
|
||||||
|
if ($connectionCount > 0) {
|
||||||
|
$parts[] = $connectionCount . ' Verbindungen';
|
||||||
|
}
|
||||||
|
if ($portCount > 0) {
|
||||||
|
$parts[] = $portCount . ' Ports';
|
||||||
|
}
|
||||||
|
if ($moduleCount > 0) {
|
||||||
|
$parts[] = $moduleCount . ' Port-Module';
|
||||||
|
}
|
||||||
|
|
||||||
|
$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));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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]
|
||||||
);
|
);
|
||||||
@@ -41,9 +121,20 @@ $deleted = $sql->set(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if ($deleted > 0) {
|
if ($deleted > 0) {
|
||||||
$_SESSION['success'] = "Gerät gelöscht: " . $device['name'];
|
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'];
|
||||||
} else {
|
} else {
|
||||||
$_SESSION['error'] = "Gerät konnte nicht gelöscht werden";
|
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";
|
||||||
}
|
}
|
||||||
|
|
||||||
header('Location: ?module=devices&action=list');
|
header('Location: ?module=devices&action=list');
|
||||||
|
|||||||
@@ -24,12 +24,59 @@ if ($deviceId > 0) {
|
|||||||
|
|
||||||
$isEdit = !empty($device);
|
$isEdit = !empty($device);
|
||||||
$pageTitle = $isEdit ? "Gerät bearbeiten: " . htmlspecialchars($device['name']) : "Neues Gerät";
|
$pageTitle = $isEdit ? "Gerät bearbeiten: " . htmlspecialchars($device['name']) : "Neues Gerät";
|
||||||
|
$dependencyCounts = [
|
||||||
|
'port_count' => 0,
|
||||||
|
'module_count' => 0,
|
||||||
|
'connection_count' => 0
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($isEdit) {
|
||||||
|
$dependencyCounts = $sql->single(
|
||||||
|
"SELECT
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM device_ports dp
|
||||||
|
WHERE dp.device_id = ?
|
||||||
|
) AS port_count,
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM device_port_modules dpm
|
||||||
|
JOIN device_ports dp2 ON dp2.id = dpm.device_port_id
|
||||||
|
WHERE dp2.device_id = ?
|
||||||
|
) AS module_count,
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM connections c
|
||||||
|
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 = ?
|
||||||
|
))
|
||||||
|
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 = ?
|
||||||
|
))
|
||||||
|
) AS connection_count",
|
||||||
|
"iiii",
|
||||||
|
[$deviceId, $deviceId, $deviceId, $deviceId]
|
||||||
|
) ?: $dependencyCounts;
|
||||||
|
}
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// Optionen laden
|
// Optionen laden
|
||||||
// =========================
|
// =========================
|
||||||
$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]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
@@ -126,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
|
||||||
========================= -->
|
========================= -->
|
||||||
@@ -133,7 +241,7 @@ $racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
|
|||||||
<button type="submit" class="button button-primary">Speichern</button>
|
<button type="submit" class="button button-primary">Speichern</button>
|
||||||
<a href="?module=devices&action=list" class="button">Abbrechen</a>
|
<a href="?module=devices&action=list" class="button">Abbrechen</a>
|
||||||
<?php if ($isEdit): ?>
|
<?php if ($isEdit): ?>
|
||||||
<a href="#" class="button button-danger" onclick="confirmDelete(<?php echo $deviceId; ?>)">Löschen</a>
|
<a href="?module=devices&action=delete&id=<?php echo (int)$deviceId; ?>" class="button button-danger" onclick="return confirmDelete(this, <?php echo (int)$deviceId; ?>, <?php echo (int)$dependencyCounts['connection_count']; ?>, <?php echo (int)$dependencyCounts['port_count']; ?>, <?php echo (int)$dependencyCounts['module_count']; ?>)">Löschen</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
@@ -230,12 +338,67 @@ $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(id) {
|
function confirmDelete(link, id, connectionCount, portCount, moduleCount) {
|
||||||
if (confirm('Dieses Gerät wirklich löschen?')) {
|
if (!confirm('Dieses Geraet wirklich loeschen?')) {
|
||||||
window.location.href = '?module=devices&action=delete&id=' + encodeURIComponent(id);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
</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,48 +22,88 @@ $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(
|
||||||
"
|
"
|
||||||
SELECT
|
SELECT
|
||||||
d.id,
|
d.id,
|
||||||
d.name,
|
d.name,
|
||||||
d.serial_number,
|
d.serial_number,
|
||||||
d.rack_position_he,
|
d.rack_position_he,
|
||||||
d.rack_height_he,
|
d.rack_height_he,
|
||||||
d.web_config_url,
|
d.web_config_url,
|
||||||
dt.name AS device_type,
|
dt.name AS device_type,
|
||||||
dt.image_path,
|
dt.image_path,
|
||||||
f.name AS floor_name,
|
f.name AS floor_name,
|
||||||
r.name AS rack_name
|
r.name AS rack_name,
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM device_ports dp
|
||||||
|
WHERE dp.device_id = d.id
|
||||||
|
) 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(*)
|
||||||
|
FROM device_port_modules dpm
|
||||||
|
JOIN device_ports dp2 ON dp2.id = dpm.device_port_id
|
||||||
|
WHERE dp2.device_id = d.id
|
||||||
|
) AS module_count,
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM connections c
|
||||||
|
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
|
||||||
|
))
|
||||||
|
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
|
||||||
|
))
|
||||||
|
) AS connection_count
|
||||||
FROM devices d
|
FROM devices d
|
||||||
JOIN device_types dt ON dt.id = d.device_type_id
|
JOIN device_types dt ON dt.id = d.device_type_id
|
||||||
LEFT JOIN racks r ON r.id = d.rack_id
|
LEFT JOIN racks r ON r.id = d.rack_id
|
||||||
@@ -78,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">
|
||||||
@@ -128,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">
|
||||||
@@ -148,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>
|
||||||
@@ -165,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>
|
||||||
@@ -195,16 +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="#" class="button button-small button-danger" onclick="confirmDelete(<?php echo $d['id']; ?>)">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>
|
||||||
<?php
|
|
||||||
//TODO löschen geht nicht
|
|
||||||
?>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -213,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>
|
||||||
@@ -332,9 +373,42 @@ $racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function confirmDelete(id) {
|
function confirmDelete(link, id, connectionCount, portCount, moduleCount) {
|
||||||
if (confirm('Dieses Gerät wirklich löschen?')) {
|
if (!confirm('Dieses Geraet wirklich loeschen?')) {
|
||||||
window.location.href = '?module=devices&action=delete&id=' + encodeURIComponent(id);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
</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);
|
||||||
|
}
|
||||||
|
|||||||
49
app/modules/floor_infrastructure/delete.php
Normal file
49
app/modules/floor_infrastructure/delete.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* app/modules/floor_infrastructure/delete.php
|
||||||
|
*
|
||||||
|
* Loescht Patchpanels oder Wandbuchsen (AJAX-POST bevorzugt).
|
||||||
|
*/
|
||||||
|
|
||||||
|
$isPost = ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST';
|
||||||
|
$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 ($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');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type === 'patchpanel') {
|
||||||
|
$rows = $sql->set(
|
||||||
|
"DELETE FROM floor_patchpanels WHERE id = ?",
|
||||||
|
"i",
|
||||||
|
[$id]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$rows = $sql->set(
|
||||||
|
"DELETE FROM network_outlets WHERE id = ?",
|
||||||
|
"i",
|
||||||
|
[$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');
|
||||||
|
exit;
|
||||||
@@ -5,20 +5,53 @@
|
|||||||
* Formular zum Anlegen/Bearbeiten von Patchpanels und Wandbuchsen
|
* Formular zum Anlegen/Bearbeiten von Patchpanels und Wandbuchsen
|
||||||
*/
|
*/
|
||||||
|
|
||||||
//TODO die auswahl der stockwerke in gebäude gruppieren und gebäude in locations gruppieren
|
|
||||||
|
|
||||||
$type = $_GET['type'] ?? 'patchpanel';
|
$type = $_GET['type'] ?? 'patchpanel';
|
||||||
$id = (int)($_GET['id'] ?? 0);
|
$id = (int)($_GET['id'] ?? 0);
|
||||||
|
|
||||||
$floors = $sql->get("SELECT id, name FROM floors ORDER BY name", "", []);
|
$locations = $sql->get("SELECT id, name FROM locations ORDER BY name", "", []);
|
||||||
$rooms = $sql->get(
|
$buildings = $sql->get("SELECT id, name, location_id FROM buildings ORDER BY name", "", []);
|
||||||
"SELECT r.id, r.name, f.name AS floor_name
|
$floors = $sql->get(
|
||||||
FROM rooms r
|
"SELECT f.*, b.name AS building_name, b.location_id, l.name AS location_name
|
||||||
LEFT JOIN floors f ON f.id = r.floor_id
|
FROM floors f
|
||||||
ORDER BY f.name, r.name",
|
LEFT JOIN buildings b ON b.id = f.building_id
|
||||||
|
LEFT JOIN locations l ON l.id = b.location_id
|
||||||
|
ORDER BY l.name, b.name, f.level, f.name",
|
||||||
"",
|
"",
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
$rooms = $sql->get(
|
||||||
|
"SELECT r.id, r.name, r.floor_id, r.x, r.y, r.width, r.height, r.polygon_points,
|
||||||
|
f.name AS floor_name, f.svg_path, b.id AS building_id, l.id AS location_id
|
||||||
|
FROM rooms r
|
||||||
|
LEFT JOIN floors f ON f.id = r.floor_id
|
||||||
|
LEFT JOIN buildings b ON b.id = f.building_id
|
||||||
|
LEFT JOIN locations l ON l.id = b.location_id
|
||||||
|
ORDER BY l.name, b.name, f.level, f.name, r.name",
|
||||||
|
"",
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
$locationMap = [];
|
||||||
|
foreach ($locations as $location) {
|
||||||
|
$locationMap[$location['id']] = $location['name'];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($floors as &$floor) {
|
||||||
|
$paths = trim((string)($floor['svg_path'] ?? ''));
|
||||||
|
$floor['svg_url'] = $paths !== '' ? '/' . ltrim($paths, '/\\') : '';
|
||||||
|
}
|
||||||
|
unset($floor);
|
||||||
|
|
||||||
|
foreach ($rooms as &$room) {
|
||||||
|
$roomPath = trim((string)($room['svg_path'] ?? ''));
|
||||||
|
$room['floor_svg_url'] = $roomPath !== '' ? '/' . ltrim($roomPath, '/\\') : '';
|
||||||
|
}
|
||||||
|
unset($room);
|
||||||
|
|
||||||
|
$floorIndex = [];
|
||||||
|
foreach ($floors as $floor) {
|
||||||
|
$floorIndex[$floor['id']] = $floor;
|
||||||
|
}
|
||||||
|
|
||||||
$panel = null;
|
$panel = null;
|
||||||
$outlet = null;
|
$outlet = null;
|
||||||
@@ -49,19 +82,105 @@ if ($type === 'outlet' && $id > 0) {
|
|||||||
$panel = $panel ?? [];
|
$panel = $panel ?? [];
|
||||||
$outlet = $outlet ?? [];
|
$outlet = $outlet ?? [];
|
||||||
|
|
||||||
$defaultPanelSize = ['width' => 140, 'height' => 40];
|
$selectedLocationId = 0;
|
||||||
$defaultOutletSize = 32;
|
$selectedBuildingId = 0;
|
||||||
|
$selectedFloorId = 0;
|
||||||
|
if ($type === 'patchpanel') {
|
||||||
|
$selectedFloorId = (int)($panel['floor_id'] ?? 0);
|
||||||
|
if ($selectedFloorId && isset($floorIndex[$selectedFloorId])) {
|
||||||
|
$selectedBuildingId = (int)($floorIndex[$selectedFloorId]['building_id'] ?? 0);
|
||||||
|
$selectedLocationId = (int)($floorIndex[$selectedFloorId]['location_id'] ?? 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$defaultPanelSize = ['width' => 20, 'height' => 5];
|
||||||
|
$defaultOutletSize = 10;
|
||||||
|
$showPanelPlacementFields = $type === 'patchpanel' && $selectedFloorId > 0;
|
||||||
|
|
||||||
if ($type === 'patchpanel') {
|
if ($type === 'patchpanel') {
|
||||||
$panel['width'] = $panel['width'] ?? $defaultPanelSize['width'];
|
$panel['width'] = $defaultPanelSize['width'];
|
||||||
$panel['height'] = $panel['height'] ?? $defaultPanelSize['height'];
|
$panel['height'] = $defaultPanelSize['height'];
|
||||||
|
$panel['pos_x'] = $panel['pos_x'] ?? 30;
|
||||||
|
$panel['pos_y'] = $panel['pos_y'] ?? 30;
|
||||||
|
} else {
|
||||||
|
$outlet['x'] = $outlet['x'] ?? 30;
|
||||||
|
$outlet['y'] = $outlet['y'] ?? 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
$markerWidth = $type === 'patchpanel' ? $panel['width'] : $defaultOutletSize;
|
$markerWidth = $type === 'patchpanel' ? $panel['width'] : $defaultOutletSize;
|
||||||
$markerHeight = $type === 'patchpanel' ? $panel['height'] : $defaultOutletSize;
|
$markerHeight = $type === 'patchpanel' ? $panel['height'] : $defaultOutletSize;
|
||||||
|
|
||||||
|
$mapPatchPanels = $sql->get(
|
||||||
|
"SELECT id, floor_id, name, pos_x, pos_y, width, height
|
||||||
|
FROM floor_patchpanels
|
||||||
|
ORDER BY floor_id, name",
|
||||||
|
"",
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
$mapOutlets = $sql->get(
|
||||||
|
"SELECT o.id, r.floor_id, o.name, o.x, o.y
|
||||||
|
FROM network_outlets o
|
||||||
|
JOIN rooms r ON r.id = o.room_id
|
||||||
|
ORDER BY r.floor_id, o.name",
|
||||||
|
"",
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
$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">
|
||||||
|
<link rel="stylesheet" href="/assets/css/floor-infrastructure-edit.css">
|
||||||
<h1><?php echo $pageTitle; ?></h1>
|
<h1><?php echo $pageTitle; ?></h1>
|
||||||
|
|
||||||
<form method="post" action="?module=floor_infrastructure&action=save" class="infra-edit-form">
|
<form method="post" action="?module=floor_infrastructure&action=save" class="infra-edit-form">
|
||||||
@@ -74,38 +193,59 @@ $markerHeight = $type === 'patchpanel' ? $panel['height'] : $defaultOutletSize;
|
|||||||
<input type="text" name="name" value="<?php echo htmlspecialchars($panel['name'] ?? ''); ?>" required>
|
<input type="text" name="name" value="<?php echo htmlspecialchars($panel['name'] ?? ''); ?>" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Stockwerk</label>
|
|
||||||
<select name="floor_id" required>
|
|
||||||
<option value="">- wählen -</option>
|
|
||||||
<?php foreach ($floors as $floor): ?>
|
|
||||||
<option value="<?php echo $floor['id']; ?>" <?php echo ($panel['floor_id'] ?? 0) === $floor['id'] ? 'selected' : ''; ?>>
|
|
||||||
<?php echo htmlspecialchars($floor['name']); ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>X</label>
|
<label>Location</label>
|
||||||
<input type="number" name="pos_x" value="<?php echo $panel['pos_x'] ?? 0; ?>" required>
|
<select id="panel-location-select">
|
||||||
|
<option value="">- Location wählen -</option>
|
||||||
|
<?php foreach ($locations as $location): ?>
|
||||||
|
<option value="<?php echo $location['id']; ?>" <?php echo $selectedLocationId === $location['id'] ? 'selected' : ''; ?>>
|
||||||
|
<?php echo htmlspecialchars($location['name']); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Y</label>
|
<label>Gebäude</label>
|
||||||
<input type="number" name="pos_y" value="<?php echo $panel['pos_y'] ?? 0; ?>" required>
|
<select id="panel-building-select">
|
||||||
|
<option value="">- Gebäude wählen -</option>
|
||||||
|
<?php foreach ($buildings as $building): ?>
|
||||||
|
<?php $buildingLocation = $locationMap[$building['location_id']] ?? ''; ?>
|
||||||
|
<option value="<?php echo $building['id']; ?>"
|
||||||
|
data-location-id="<?php echo $building['location_id'] ?? 0; ?>"
|
||||||
|
<?php echo $selectedBuildingId === $building['id'] ? 'selected' : ''; ?>>
|
||||||
|
<?php echo htmlspecialchars($building['name'] . ($buildingLocation ? ' · ' . $buildingLocation : '')); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Breite</label>
|
<label>Stockwerk</label>
|
||||||
<input type="number" name="width" value="<?php echo $panel['width']; ?>" required readonly title="Breite wird automatisch nach Standardwerten vorgegeben.">
|
<select id="panel-floor-select" name="floor_id" required>
|
||||||
</div>
|
<option value="">- Stockwerk wählen -</option>
|
||||||
<div class="form-group">
|
<?php foreach ($floors as $floor): ?>
|
||||||
<label>Höhe</label>
|
<option value="<?php echo $floor['id']; ?>"
|
||||||
<input type="number" name="height" value="<?php echo $panel['height']; ?>" required readonly title="Höhe wird automatisch nach Standardwerten vorgegeben.">
|
data-building-id="<?php echo $floor['building_id'] ?? 0; ?>"
|
||||||
|
data-location-id="<?php echo $floor['location_id'] ?? 0; ?>"
|
||||||
|
data-svg-url="<?php echo htmlspecialchars($floor['svg_url']); ?>"
|
||||||
|
<?php echo $selectedFloorId === $floor['id'] ? 'selected' : ''; ?>>
|
||||||
|
<?php echo htmlspecialchars($floor['name']); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<input type="hidden" name="pos_x" value="<?php echo (int)($panel['pos_x'] ?? 30); ?>">
|
||||||
|
<input type="hidden" name="pos_y" value="<?php echo (int)($panel['pos_y'] ?? 30); ?>">
|
||||||
|
<input type="hidden" name="width" value="<?php echo (int)$panel['width']; ?>">
|
||||||
|
<input type="hidden" name="height" value="<?php echo (int)$panel['height']; ?>">
|
||||||
|
|
||||||
|
<div id="panel-placement-fields" class="form-grid" <?php echo $showPanelPlacementFields ? '' : 'hidden'; ?>>
|
||||||
|
<p class="info">Position, Breite und Höhe werden über die Karte gesetzt.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="panel-floor-plan-group" class="form-group" <?php echo $showPanelPlacementFields ? '' : 'hidden'; ?>>
|
||||||
<label>Stockwerkskarte</label>
|
<label>Stockwerkskarte</label>
|
||||||
<div class="floor-plan-block">
|
<div class="floor-plan-block">
|
||||||
<div id="floor-plan-canvas" class="floor-plan-canvas"
|
<div id="floor-plan-canvas" class="floor-plan-canvas"
|
||||||
@@ -113,11 +253,13 @@ $markerHeight = $type === 'patchpanel' ? $panel['height'] : $defaultOutletSize;
|
|||||||
data-marker-height="<?php echo $markerHeight; ?>"
|
data-marker-height="<?php echo $markerHeight; ?>"
|
||||||
data-marker-type="patchpanel"
|
data-marker-type="patchpanel"
|
||||||
data-x-field="pos_x"
|
data-x-field="pos_x"
|
||||||
data-y-field="pos_y">
|
data-y-field="pos_y"
|
||||||
<div id="floor-plan-marker" class="floor-plan-marker panel-marker"
|
data-active-id="<?php echo (int)($panel['id'] ?? 0); ?>"
|
||||||
style="--marker-width: <?php echo $markerWidth; ?>px; --marker-height: <?php echo $markerHeight; ?>px;"></div>
|
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'); ?>">
|
||||||
|
<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">Ziehe das Patchpanel oder klicke auf den Plan, um die Position zu setzen.</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>
|
||||||
@@ -127,6 +269,10 @@ $markerHeight = $type === 'patchpanel' ? $panel['height'] : $defaultOutletSize;
|
|||||||
<input type="number" name="port_count" value="<?php echo $panel['port_count'] ?? 0; ?>" min="0">
|
<input type="number" name="port_count" value="<?php echo $panel['port_count'] ?? 0; ?>" min="0">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p id="panel-floor-missing-hint" class="info" <?php echo $showPanelPlacementFields ? 'hidden' : ''; ?>>
|
||||||
|
Position, Größe und Stockwerkskarte werden erst angezeigt, sobald ein Stockwerk ausgewählt ist.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Kommentar</label>
|
<label>Kommentar</label>
|
||||||
<textarea name="comment"><?php echo htmlspecialchars($panel['comment'] ?? ''); ?></textarea>
|
<textarea name="comment"><?php echo htmlspecialchars($panel['comment'] ?? ''); ?></textarea>
|
||||||
@@ -142,10 +288,18 @@ $markerHeight = $type === 'patchpanel' ? $panel['height'] : $defaultOutletSize;
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Raum</label>
|
<label>Raum</label>
|
||||||
<select name="room_id" required>
|
<select name="room_id" id="outlet-room-select" required>
|
||||||
<option value="">- Raum wählen -</option>
|
<option value="">- Raum wählen -</option>
|
||||||
<?php foreach ($rooms as $room): ?>
|
<?php foreach ($rooms as $room): ?>
|
||||||
<option value="<?php echo $room['id']; ?>" <?php echo ($outlet['room_id'] ?? 0) === $room['id'] ? 'selected' : ''; ?>>
|
<option value="<?php echo $room['id']; ?>"
|
||||||
|
data-floor-id="<?php echo $room['floor_id'] ?? 0; ?>"
|
||||||
|
data-floor-svg-url="<?php echo htmlspecialchars($room['floor_svg_url']); ?>"
|
||||||
|
data-room-x="<?php echo (int)($room['x'] ?? 0); ?>"
|
||||||
|
data-room-y="<?php echo (int)($room['y'] ?? 0); ?>"
|
||||||
|
data-room-width="<?php echo (int)($room['width'] ?? 0); ?>"
|
||||||
|
data-room-height="<?php echo (int)($room['height'] ?? 0); ?>"
|
||||||
|
data-room-polygon="<?php echo htmlspecialchars((string)($room['polygon_points'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>"
|
||||||
|
<?php echo ($outlet['room_id'] ?? 0) === $room['id'] ? 'selected' : ''; ?>>
|
||||||
<?php echo htmlspecialchars($room['floor_name'] . ' / ' . $room['name']); ?>
|
<?php echo htmlspecialchars($room['floor_name'] . ' / ' . $room['name']); ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -153,14 +307,39 @@ $markerHeight = $type === 'patchpanel' ? $panel['height'] : $defaultOutletSize;
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>X</label>
|
<label for="outlet-bind-patchpanel-port-id">Direkt mit Patchpanel-Port verbinden</label>
|
||||||
<input type="number" name="x" value="<?php echo $outlet['x'] ?? 0; ?>">
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<input type="hidden" name="x" value="<?php echo (int)($outlet['x'] ?? 30); ?>">
|
||||||
<label>Y</label>
|
<input type="hidden" name="y" value="<?php echo (int)($outlet['y'] ?? 30); ?>">
|
||||||
<input type="number" name="y" value="<?php echo $outlet['y'] ?? 0; ?>">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Stockwerkskarte</label>
|
<label>Stockwerkskarte</label>
|
||||||
@@ -170,11 +349,13 @@ $markerHeight = $type === 'patchpanel' ? $panel['height'] : $defaultOutletSize;
|
|||||||
data-marker-height="<?php echo $markerHeight; ?>"
|
data-marker-height="<?php echo $markerHeight; ?>"
|
||||||
data-marker-type="outlet"
|
data-marker-type="outlet"
|
||||||
data-x-field="x"
|
data-x-field="x"
|
||||||
data-y-field="y">
|
data-y-field="y"
|
||||||
<div id="floor-plan-marker" class="floor-plan-marker outlet-marker"
|
data-active-id="<?php echo (int)($outlet['id'] ?? 0); ?>"
|
||||||
style="--marker-width: <?php echo $markerWidth; ?>px; --marker-height: <?php echo $markerHeight; ?>px;"></div>
|
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'); ?>">
|
||||||
|
<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">Klicke oder ziehe die Wandbuchse auf dem Plan. Die Größe bleibt quadratisch.</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>
|
||||||
@@ -194,190 +375,8 @@ $markerHeight = $type === 'patchpanel' ? $panel['height'] : $defaultOutletSize;
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php
|
<script src="/assets/js/floor-infrastructure-edit.js" defer></script>
|
||||||
//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
|
|
||||||
//TODO style in css files einsortieren
|
|
||||||
?>
|
|
||||||
<style>
|
|
||||||
.floor-infra-edit {
|
|
||||||
padding: 25px;
|
|
||||||
max-width: 700px;
|
|
||||||
}
|
|
||||||
.infra-edit-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
.form-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
.form-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
.form-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
.info {
|
|
||||||
font-size: 0.9em;
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
.floor-plan-block {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
.floor-plan-canvas {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 260px;
|
|
||||||
border: 1px solid #d4d4d4;
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: #fff;
|
|
||||||
background-image:
|
|
||||||
linear-gradient(90deg, rgba(0, 0, 0, 0.05) 1px, transparent 1px),
|
|
||||||
linear-gradient(rgba(0, 0, 0, 0.05) 1px, transparent 1px);
|
|
||||||
background-size: 40px 40px;
|
|
||||||
cursor: crosshair;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.floor-plan-marker {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: var(--marker-width, 32px);
|
|
||||||
height: var(--marker-height, 32px);
|
|
||||||
transition: left 0.1s ease, top 0.1s ease;
|
|
||||||
touch-action: none;
|
|
||||||
}
|
|
||||||
.floor-plan-marker.panel-marker {
|
|
||||||
background: rgba(13, 110, 253, 0.25);
|
|
||||||
border: 2px solid #0d6efd;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
.floor-plan-marker.outlet-marker {
|
|
||||||
background: rgba(25, 135, 84, 0.25);
|
|
||||||
border: 2px solid #198754;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.floor-plan-hint {
|
|
||||||
font-size: 0.85em;
|
|
||||||
color: #444;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.floor-plan-position {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.85em;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const canvas = document.getElementById('floor-plan-canvas');
|
|
||||||
const marker = document.getElementById('floor-plan-marker');
|
|
||||||
const positionLabel = document.getElementById('floor-plan-position');
|
|
||||||
if (!canvas || !marker) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const xFieldName = canvas.dataset.xField;
|
|
||||||
const yFieldName = canvas.dataset.yField;
|
|
||||||
const xField = xFieldName ? document.querySelector(`input[name="${xFieldName}"]`) : null;
|
|
||||||
const yField = yFieldName ? document.querySelector(`input[name="${yFieldName}"]`) : null;
|
|
||||||
if (!xField || !yField) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const markerWidth = Math.max(1, Number(canvas.dataset.markerWidth) || marker.offsetWidth);
|
|
||||||
const markerHeight = Math.max(1, Number(canvas.dataset.markerHeight) || marker.offsetHeight);
|
|
||||||
|
|
||||||
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
||||||
|
|
||||||
const updatePositionLabel = (x, y) => {
|
|
||||||
if (positionLabel) {
|
|
||||||
positionLabel.textContent = `${Math.round(x)} x ${Math.round(y)}`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setMarkerPosition = (rawX, rawY) => {
|
|
||||||
const rect = canvas.getBoundingClientRect();
|
|
||||||
const maxX = Math.max(0, rect.width - markerWidth);
|
|
||||||
const maxY = Math.max(0, rect.height - markerHeight);
|
|
||||||
const left = clamp(rawX, 0, maxX);
|
|
||||||
const top = clamp(rawY, 0, maxY);
|
|
||||||
marker.style.left = `${left}px`;
|
|
||||||
marker.style.top = `${top}px`;
|
|
||||||
xField.value = Math.round(left);
|
|
||||||
yField.value = Math.round(top);
|
|
||||||
updatePositionLabel(left, top);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateFromInputs = () => {
|
|
||||||
setMarkerPosition(Number(xField.value) || 0, Number(yField.value) || 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
updateFromInputs();
|
|
||||||
|
|
||||||
let dragging = false;
|
|
||||||
let offsetX = 0;
|
|
||||||
let offsetY = 0;
|
|
||||||
|
|
||||||
const startDrag = (clientX, clientY) => {
|
|
||||||
const markerRect = marker.getBoundingClientRect();
|
|
||||||
offsetX = clientX - markerRect.left;
|
|
||||||
offsetY = clientY - markerRect.top;
|
|
||||||
};
|
|
||||||
|
|
||||||
marker.addEventListener('pointerdown', (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
dragging = true;
|
|
||||||
startDrag(event.clientX, event.clientY);
|
|
||||||
marker.setPointerCapture(event.pointerId);
|
|
||||||
});
|
|
||||||
|
|
||||||
marker.addEventListener('pointermove', (event) => {
|
|
||||||
if (!dragging) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const rect = canvas.getBoundingClientRect();
|
|
||||||
setMarkerPosition(event.clientX - rect.left - offsetX, event.clientY - rect.top - offsetY);
|
|
||||||
});
|
|
||||||
|
|
||||||
const stopDrag = (event) => {
|
|
||||||
if (!dragging) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dragging = false;
|
|
||||||
if (marker.hasPointerCapture(event.pointerId)) {
|
|
||||||
marker.releasePointerCapture(event.pointerId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
['pointerup', 'pointercancel', 'pointerleave'].forEach((evt) => {
|
|
||||||
marker.addEventListener(evt, stopDrag);
|
|
||||||
});
|
|
||||||
|
|
||||||
canvas.addEventListener('pointerdown', (event) => {
|
|
||||||
if (event.target !== canvas) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const rect = canvas.getBoundingClientRect();
|
|
||||||
setMarkerPosition(event.clientX - rect.left - markerWidth / 2, event.clientY - rect.top - markerHeight / 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
[xField, yField].forEach((input) => {
|
|
||||||
input.addEventListener('input', () => {
|
|
||||||
updateFromInputs();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
updateFromInputs();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -2,12 +2,18 @@
|
|||||||
/**
|
/**
|
||||||
* app/modules/floor_infrastructure/list.php
|
* app/modules/floor_infrastructure/list.php
|
||||||
*
|
*
|
||||||
* Übersicht über Patchpanels und Netzwerkbuchsen auf Stockwerken
|
* Uebersicht ueber Patchpanels und Netzwerkbuchsen auf Stockwerken.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$floorId = (int)($_GET['floor_id'] ?? 0);
|
$floorId = (int)($_GET['floor_id'] ?? 0);
|
||||||
|
|
||||||
$floors = $sql->get("SELECT id, name FROM floors ORDER BY name", "", []);
|
$floors = $sql->get(
|
||||||
|
"SELECT id, name, svg_path
|
||||||
|
FROM floors
|
||||||
|
ORDER BY name",
|
||||||
|
"",
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
$where = '';
|
$where = '';
|
||||||
$types = '';
|
$types = '';
|
||||||
@@ -15,7 +21,7 @@ $params = [];
|
|||||||
|
|
||||||
if ($floorId > 0) {
|
if ($floorId > 0) {
|
||||||
$where = "WHERE p.floor_id = ?";
|
$where = "WHERE p.floor_id = ?";
|
||||||
$types = "i";
|
$types = 'i';
|
||||||
$params[] = $floorId;
|
$params[] = $floorId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,45 +36,126 @@ $patchPanels = $sql->get(
|
|||||||
);
|
);
|
||||||
|
|
||||||
$networkOutlets = $sql->get(
|
$networkOutlets = $sql->get(
|
||||||
"SELECT o.*, r.name AS room_name, f.name AS floor_name
|
"SELECT o.id, o.room_id, o.name, o.x, o.y, o.comment,
|
||||||
|
r.name AS room_name, r.number AS room_number,
|
||||||
|
f.name AS floor_name, f.id AS floor_id,
|
||||||
|
GROUP_CONCAT(nop.name ORDER BY nop.name SEPARATOR ', ') AS port_names
|
||||||
FROM network_outlets o
|
FROM network_outlets o
|
||||||
LEFT JOIN rooms r ON r.id = o.room_id
|
LEFT JOIN rooms r ON r.id = o.room_id
|
||||||
LEFT JOIN floors f ON f.id = r.floor_id
|
LEFT JOIN floors f ON f.id = r.floor_id
|
||||||
|
LEFT JOIN network_outlet_ports nop ON nop.outlet_id = o.id
|
||||||
|
GROUP BY o.id
|
||||||
ORDER BY f.name, r.name, o.name",
|
ORDER BY f.name, r.name, o.name",
|
||||||
"",
|
"",
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$floorMap = [];
|
||||||
|
foreach ($floors as $floor) {
|
||||||
|
$id = (int)$floor['id'];
|
||||||
|
$svgPath = trim((string)($floor['svg_path'] ?? ''));
|
||||||
|
$floorMap[$id] = [
|
||||||
|
'id' => $id,
|
||||||
|
'name' => (string)($floor['name'] ?? ''),
|
||||||
|
'svg_url' => $svgPath !== '' ? '/' . ltrim($svgPath, '/\\') : ''
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$editorFloor = ($floorId > 0 && isset($floorMap[$floorId])) ? $floorMap[$floorId] : null;
|
||||||
|
$editorPatchPanels = [];
|
||||||
|
$editorOutlets = [];
|
||||||
|
|
||||||
|
if ($editorFloor) {
|
||||||
|
foreach ($patchPanels as $panel) {
|
||||||
|
if ((int)$panel['floor_id'] !== $floorId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$editorPatchPanels[] = [
|
||||||
|
'id' => (int)$panel['id'],
|
||||||
|
'name' => (string)$panel['name'],
|
||||||
|
'x' => (int)$panel['pos_x'],
|
||||||
|
'y' => (int)$panel['pos_y'],
|
||||||
|
'width' => max(1, (int)$panel['width']),
|
||||||
|
'height' => max(1, (int)$panel['height']),
|
||||||
|
'port_count' => (int)$panel['port_count'],
|
||||||
|
'comment' => (string)($panel['comment'] ?? '')
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($networkOutlets as $outlet) {
|
||||||
|
if ((int)$outlet['floor_id'] !== $floorId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$editorOutlets[] = [
|
||||||
|
'id' => (int)$outlet['id'],
|
||||||
|
'name' => (string)$outlet['name'],
|
||||||
|
'x' => (int)$outlet['x'],
|
||||||
|
'y' => (int)$outlet['y'],
|
||||||
|
'room_name' => (string)($outlet['room_name'] ?? ''),
|
||||||
|
'room_number' => (string)($outlet['room_number'] ?? ''),
|
||||||
|
'port_names' => (string)($outlet['port_names'] ?? ''),
|
||||||
|
'comment' => (string)($outlet['comment'] ?? '')
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="floor-infra">
|
<div class="floor-infra">
|
||||||
|
<link rel="stylesheet" href="/assets/css/floor-infrastructure-list.css">
|
||||||
|
<script src="/assets/js/floor-infrastructure-list.js" defer></script>
|
||||||
|
|
||||||
<h1>Stockwerksinfrastruktur</h1>
|
<h1>Stockwerksinfrastruktur</h1>
|
||||||
|
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<a href="?module=floor_infrastructure&action=edit&type=patchpanel" class="button button-primary">
|
<a href="?module=floor_infrastructure&action=edit&type=patchpanel" class="button button-primary">
|
||||||
+ Patchpanel hinzufügen
|
+ Patchpanel hinzufuegen
|
||||||
</a>
|
</a>
|
||||||
<a href="?module=floor_infrastructure&action=edit&type=outlet" class="button">
|
<a href="?module=floor_infrastructure&action=edit&type=outlet" class="button">
|
||||||
+ Wandbuchse hinzufügen
|
+ Wandbuchse hinzufuegen
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="get" class="filter-form">
|
<form method="get" class="filter-form" id="infra-filter-form">
|
||||||
<input type="hidden" name="module" value="floor_infrastructure">
|
<input type="hidden" name="module" value="floor_infrastructure">
|
||||||
<input type="hidden" name="action" value="list">
|
<input type="hidden" name="action" value="list">
|
||||||
|
|
||||||
<select name="floor_id">
|
<select name="floor_id" id="infra-floor-select">
|
||||||
<option value="">- Alle Stockwerke -</option>
|
<option value="">- Stockwerk waehlen -</option>
|
||||||
<?php foreach ($floors as $floor): ?>
|
<?php foreach ($floors as $floor): ?>
|
||||||
<option value="<?php echo $floor['id']; ?>" <?php echo $floor['id'] === $floorId ? 'selected' : ''; ?>>
|
<option value="<?php echo (int)$floor['id']; ?>" <?php echo ((int)$floor['id'] === $floorId) ? 'selected' : ''; ?>>
|
||||||
<?php echo htmlspecialchars($floor['name']); ?>
|
<?php echo htmlspecialchars((string)$floor['name']); ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<button class="button">Filter</button>
|
<button class="button" type="submit">Filter</button>
|
||||||
<a href="?module=floor_infrastructure&action=list" class="button">Zurücksetzen</a>
|
<a href="?module=floor_infrastructure&action=list" class="button">Zuruecksetzen</a>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<section class="infra-plan">
|
||||||
|
<h2>Stockwerkskarte</h2>
|
||||||
|
<?php if ($floorId <= 0): ?>
|
||||||
|
<p class="empty-state">Bitte ein Stockwerk auswaehlen, um die Karte anzuzeigen.</p>
|
||||||
|
<?php elseif (!$editorFloor): ?>
|
||||||
|
<p class="empty-state">Gewaehltes Stockwerk wurde nicht gefunden.</p>
|
||||||
|
<?php elseif (($editorFloor['svg_url'] ?? '') === ''): ?>
|
||||||
|
<p class="empty-state">Das gewaehlte Stockwerk hat keinen SVG-Plan hinterlegt.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<p>
|
||||||
|
Read-only Vorschau fuer <strong><?php echo htmlspecialchars((string)$editorFloor['name']); ?></strong>.
|
||||||
|
Alle Objekte werden angezeigt; Hover zeigt Details.
|
||||||
|
</p>
|
||||||
|
<div id="infra-floor-canvas"
|
||||||
|
class="infra-floor-canvas"
|
||||||
|
data-patchpanels="<?php echo htmlspecialchars(json_encode($editorPatchPanels, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ENT_QUOTES, 'UTF-8'); ?>"
|
||||||
|
data-outlets="<?php echo htmlspecialchars(json_encode($editorOutlets, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ENT_QUOTES, 'UTF-8'); ?>">
|
||||||
|
<img src="<?php echo htmlspecialchars((string)$editorFloor['svg_url']); ?>" class="infra-floor-svg" alt="Stockwerksplan">
|
||||||
|
<svg id="infra-floor-overlay" class="infra-floor-overlay" viewBox="0 0 1 1" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
|
||||||
|
</div>
|
||||||
|
<p class="floor-plan-hint">Blau: Patchpanel | Gruen: Wandbuchse. Hover zeigt Name, Raum und Ports (Browser-Tooltip).</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="infra-section">
|
<section class="infra-section">
|
||||||
<h2>Patchpanels</h2>
|
<h2>Patchpanels</h2>
|
||||||
<?php if (!empty($patchPanels)): ?>
|
<?php if (!empty($patchPanels)): ?>
|
||||||
@@ -78,7 +165,7 @@ $networkOutlets = $sql->get(
|
|||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Stockwerk</th>
|
<th>Stockwerk</th>
|
||||||
<th>Position</th>
|
<th>Position</th>
|
||||||
<th>Größe</th>
|
<th>Groesse</th>
|
||||||
<th>Ports</th>
|
<th>Ports</th>
|
||||||
<th>Aktionen</th>
|
<th>Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -86,13 +173,21 @@ $networkOutlets = $sql->get(
|
|||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($patchPanels as $panel): ?>
|
<?php foreach ($patchPanels as $panel): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?php echo htmlspecialchars($panel['name']); ?></td>
|
<td><?php echo htmlspecialchars((string)$panel['name']); ?></td>
|
||||||
<td><?php echo htmlspecialchars($panel['floor_name'] ?? '—'); ?></td>
|
<td><?php echo htmlspecialchars((string)($panel['floor_name'] ?? '-')); ?></td>
|
||||||
<td><?php echo $panel['pos_x'] . ' × ' . $panel['pos_y']; ?></td>
|
<td><?php echo (int)$panel['pos_x'] . ' x ' . (int)$panel['pos_y']; ?></td>
|
||||||
<td><?php echo $panel['width'] . ' × ' . $panel['height']; ?></td>
|
<td><?php echo (int)$panel['width'] . ' x ' . (int)$panel['height']; ?></td>
|
||||||
<td><?php echo $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 $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>
|
||||||
|
<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; ?>
|
||||||
@@ -113,6 +208,7 @@ $networkOutlets = $sql->get(
|
|||||||
<th>Stockwerk</th>
|
<th>Stockwerk</th>
|
||||||
<th>Raum</th>
|
<th>Raum</th>
|
||||||
<th>Koordinaten</th>
|
<th>Koordinaten</th>
|
||||||
|
<th>Ports</th>
|
||||||
<th>Kommentar</th>
|
<th>Kommentar</th>
|
||||||
<th>Aktionen</th>
|
<th>Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -120,13 +216,31 @@ $networkOutlets = $sql->get(
|
|||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($networkOutlets as $outlet): ?>
|
<?php foreach ($networkOutlets as $outlet): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?php echo htmlspecialchars($outlet['name']); ?></td>
|
<td><?php echo htmlspecialchars((string)$outlet['name']); ?></td>
|
||||||
<td><?php echo htmlspecialchars($outlet['floor_name'] ?? '—'); ?></td>
|
<td><?php echo htmlspecialchars((string)($outlet['floor_name'] ?? '-')); ?></td>
|
||||||
<td><?php echo htmlspecialchars($outlet['room_name'] ?? '—'); ?></td>
|
<td>
|
||||||
<td><?php echo $outlet['x'] . ' × ' . $outlet['y']; ?></td>
|
<?php
|
||||||
<td><?php echo htmlspecialchars($outlet['comment']); ?></td>
|
$roomLabel = (string)($outlet['room_name'] ?? '-');
|
||||||
|
$roomNumber = trim((string)($outlet['room_number'] ?? ''));
|
||||||
|
if ($roomNumber !== '') {
|
||||||
|
$roomLabel .= ' (' . $roomNumber . ')';
|
||||||
|
}
|
||||||
|
echo htmlspecialchars($roomLabel);
|
||||||
|
?>
|
||||||
|
</td>
|
||||||
|
<td><?php echo (int)$outlet['x'] . ' x ' . (int)$outlet['y']; ?></td>
|
||||||
|
<td><?php echo htmlspecialchars((string)($outlet['port_names'] ?? '-')); ?></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 $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>
|
||||||
|
<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; ?>
|
||||||
@@ -136,61 +250,39 @@ $networkOutlets = $sql->get(
|
|||||||
<p class="empty-state">Noch keine Wandbuchsen angelegt.</p>
|
<p class="empty-state">Noch keine Wandbuchsen angelegt.</p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="infra-plan">
|
|
||||||
<h2>Stockwerksplan-Verortung</h2>
|
|
||||||
<p>Die eingetragenen Patchpanels und Wandbuchsen erscheinen später als feste Objekte auf dem Stockwerks-SVG. Die Polygon-Positionen werden momentan noch durch numerische X/Y-Werte gesteuert.</p>
|
|
||||||
<p><small>TODO: SVG-Editor mit Drag & Drop für diese Objekte erweitern (siehe Stockwerke-Modul).</small></p>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<script>
|
||||||
.floor-infra {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
padding: 25px;
|
document.querySelectorAll('.js-floor-infra-delete').forEach((button) => {
|
||||||
}
|
button.addEventListener('click', () => {
|
||||||
.toolbar {
|
const id = Number(button.dataset.deleteId || '0');
|
||||||
display: flex;
|
const type = (button.dataset.deleteType || '').trim();
|
||||||
gap: 10px;
|
const label = button.dataset.deleteLabel || 'Objekt';
|
||||||
margin-bottom: 15px;
|
if (id <= 0 || (type !== 'patchpanel' && type !== 'outlet')) {
|
||||||
}
|
return;
|
||||||
.filter-form {
|
}
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
const entityLabel = type === 'patchpanel' ? 'Patchpanel' : 'Wandbuchse';
|
||||||
gap: 10px;
|
if (!confirm(entityLabel + ' "' + label + '" wirklich loeschen?')) {
|
||||||
flex-wrap: wrap;
|
return;
|
||||||
margin-bottom: 25px;
|
}
|
||||||
}
|
|
||||||
.filter-form select {
|
fetch('?module=floor_infrastructure&action=delete', {
|
||||||
padding: 8px 10px;
|
method: 'POST',
|
||||||
border-radius: 4px;
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||||
border: 1px solid #ccc;
|
body: 'id=' + encodeURIComponent(id) + '&type=' + encodeURIComponent(type)
|
||||||
}
|
})
|
||||||
.infra-section {
|
.then((response) => response.json())
|
||||||
margin-bottom: 30px;
|
.then((data) => {
|
||||||
}
|
if (data && data.success) {
|
||||||
.infra-table {
|
window.location.reload();
|
||||||
width: 100%;
|
return;
|
||||||
border-collapse: collapse;
|
}
|
||||||
margin-top: 10px;
|
alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
|
||||||
}
|
})
|
||||||
.infra-table th,
|
.catch(() => alert('Loeschen fehlgeschlagen'));
|
||||||
.infra-table td {
|
});
|
||||||
padding: 10px;
|
});
|
||||||
border-bottom: 1px solid #ddd;
|
});
|
||||||
}
|
</script>
|
||||||
.infra-plan {
|
|
||||||
padding: 15px;
|
|
||||||
background: #f7f7f7;
|
|
||||||
border: 1px dashed #ccc;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
.empty-state {
|
|
||||||
padding: 20px;
|
|
||||||
background: #fafafa;
|
|
||||||
border: 1px dashed #ccc;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
.actions .button-small {
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -9,14 +9,37 @@ $type = $_POST['type'] ?? '';
|
|||||||
$id = (int)($_POST['id'] ?? 0);
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
|
||||||
if ($type === 'patchpanel') {
|
if ($type === 'patchpanel') {
|
||||||
|
$fixedPanelWidth = 20;
|
||||||
|
$fixedPanelHeight = 5;
|
||||||
|
|
||||||
$name = trim($_POST['name'] ?? '');
|
$name = trim($_POST['name'] ?? '');
|
||||||
$floorId = (int)($_POST['floor_id'] ?? 0);
|
$floorId = (int)($_POST['floor_id'] ?? 0);
|
||||||
$posX = (int)($_POST['pos_x'] ?? 0);
|
$posX = (int)($_POST['pos_x'] ?? 0);
|
||||||
$posY = (int)($_POST['pos_y'] ?? 0);
|
$posY = (int)($_POST['pos_y'] ?? 0);
|
||||||
$width = (int)($_POST['width'] ?? 0);
|
$width = $fixedPanelWidth;
|
||||||
$height = (int)($_POST['height'] ?? 0);
|
$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;
|
||||||
|
|
||||||
if ($id > 0) {
|
if ($id > 0) {
|
||||||
$sql->set(
|
$sql->set(
|
||||||
@@ -25,18 +48,55 @@ if ($type === 'patchpanel') {
|
|||||||
[$name, $floorId, $posX, $posY, $width, $height, $portCount, $comment, $id]
|
[$name, $floorId, $posX, $posY, $width, $height, $portCount, $comment, $id]
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$sql->set(
|
$panelId = (int)$sql->set(
|
||||||
"INSERT INTO floor_patchpanels (name, floor_id, pos_x, pos_y, width, height, port_count, comment) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
"INSERT INTO floor_patchpanels (name, floor_id, pos_x, pos_y, width, height, port_count, comment) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
"siiiiiss",
|
"siiiiiss",
|
||||||
[$name, $floorId, $posX, $posY, $width, $height, $portCount, $comment]
|
[$name, $floorId, $posX, $posY, $width, $height, $portCount, $comment],
|
||||||
|
true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($panelId > 0 && $portCount > 0) {
|
||||||
|
$existingCount = (int)($sql->single(
|
||||||
|
"SELECT COUNT(*) AS cnt FROM floor_patchpanel_ports WHERE patchpanel_id = ?",
|
||||||
|
"i",
|
||||||
|
[$panelId]
|
||||||
|
)['cnt'] ?? 0);
|
||||||
|
|
||||||
|
if ($existingCount < $portCount) {
|
||||||
|
for ($i = $existingCount + 1; $i <= $portCount; $i++) {
|
||||||
|
$sql->set(
|
||||||
|
"INSERT INTO floor_patchpanel_ports (patchpanel_id, name) VALUES (?, ?)",
|
||||||
|
"is",
|
||||||
|
[$panelId, 'Port ' . $i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$_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;
|
||||||
|
$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(
|
||||||
@@ -45,12 +105,159 @@ if ($type === 'patchpanel') {
|
|||||||
[$name, $roomId, $x, $y, $comment, $id]
|
[$name, $roomId, $x, $y, $comment, $id]
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$sql->set(
|
$outletId = (int)$sql->set(
|
||||||
"INSERT INTO network_outlets (name, room_id, x, y, comment) VALUES (?, ?, ?, ?, ?)",
|
"INSERT INTO network_outlets (name, room_id, x, y, comment) VALUES (?, ?, ?, ?, ?)",
|
||||||
"siiis",
|
"siiis",
|
||||||
[$name, $roomId, $x, $y, $comment]
|
[$name, $roomId, $x, $y, $comment],
|
||||||
|
true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($outletId > 0) {
|
||||||
|
$existingPortCount = (int)($sql->single(
|
||||||
|
"SELECT COUNT(*) AS cnt FROM network_outlet_ports WHERE outlet_id = ?",
|
||||||
|
"i",
|
||||||
|
[$outletId]
|
||||||
|
)['cnt'] ?? 0);
|
||||||
|
|
||||||
|
if ($existingPortCount === 0) {
|
||||||
|
$sql->set(
|
||||||
|
"INSERT INTO network_outlet_ports (outlet_id, name) VALUES (?, 'Port 1')",
|
||||||
|
"i",
|
||||||
|
[$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');
|
||||||
|
|||||||
85
app/modules/floors/delete.php
Normal file
85
app/modules/floors/delete.php
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* app/modules/floors/delete.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID fehlt']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$exists = $sql->single("SELECT id FROM floors WHERE id = ?", "i", [$id]);
|
||||||
|
if (!$exists) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Stockwerk nicht gefunden']);
|
||||||
|
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]);
|
||||||
|
if ($rows === false) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Loeschen fehlgeschlagen']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Stockwerk geloescht',
|
||||||
|
'dependencies' => [
|
||||||
|
'rooms' => $roomCount,
|
||||||
|
'racks' => $rackCount,
|
||||||
|
'patchpanels' => $patchpanelCount
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ if (!empty($floor['svg_path'])) {
|
|||||||
<div class="floor-edit">
|
<div class="floor-edit">
|
||||||
<h1><?php echo $pageTitle; ?></h1>
|
<h1><?php echo $pageTitle; ?></h1>
|
||||||
|
|
||||||
<form method="post" action="?module=floors&action=save" enctype="multipart/form-data" class="edit-form">
|
<form method="post" action="?module=floors&action=save" class="edit-form">
|
||||||
<?php if ($isEdit): ?>
|
<?php if ($isEdit): ?>
|
||||||
<input type="hidden" name="id" value="<?php echo $floorId; ?>">
|
<input type="hidden" name="id" value="<?php echo $floorId; ?>">
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -86,12 +86,6 @@ if (!empty($floor['svg_path'])) {
|
|||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Grundriss (SVG)</legend>
|
<legend>Grundriss (SVG)</legend>
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="svg_file">SVG-Datei hochladen</label>
|
|
||||||
<input type="file" id="svg_file" name="svg_file" accept=".svg">
|
|
||||||
<small>Optional. Wenn ein Zeichnungsinhalt im Editor erstellt wird, wird dieser beim Speichern bevorzugt.</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<textarea id="floor-svg-content" name="floor_svg_content" hidden><?php echo htmlspecialchars($existingSvgContent); ?></textarea>
|
<textarea id="floor-svg-content" name="floor_svg_content" hidden><?php echo htmlspecialchars($existingSvgContent); ?></textarea>
|
||||||
|
|
||||||
<div id="floor-svg-editor" class="floor-svg-editor">
|
<div id="floor-svg-editor" class="floor-svg-editor">
|
||||||
@@ -178,7 +172,6 @@ if (!empty($floor['svg_path'])) {
|
|||||||
|
|
||||||
.form-group input[type="text"],
|
.form-group input[type="text"],
|
||||||
.form-group input[type="number"],
|
.form-group input[type="number"],
|
||||||
.form-group input[type="file"],
|
|
||||||
.form-group select,
|
.form-group select,
|
||||||
.form-group textarea {
|
.form-group textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
// =========================
|
// =========================
|
||||||
// Filter einlesen
|
// Filter einlesen
|
||||||
// =========================
|
// =========================
|
||||||
$search = trim($_GET['search'] ?? '');
|
$search = trim($_GET['search'] ?? ');
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// Floors laden
|
// Floors laden
|
||||||
@@ -19,7 +19,7 @@ $whereClause = "";
|
|||||||
$types = "";
|
$types = "";
|
||||||
$params = [];
|
$params = [];
|
||||||
|
|
||||||
if ($search !== '') {
|
if ($search !== ') {
|
||||||
$whereClause = "WHERE f.name LIKE ? OR f.comment LIKE ?";
|
$whereClause = "WHERE f.name LIKE ? OR f.comment LIKE ?";
|
||||||
$types = "ss";
|
$types = "ss";
|
||||||
$params = ["%$search%", "%$search%"];
|
$params = ["%$search%", "%$search%"];
|
||||||
@@ -105,7 +105,7 @@ $buildings = $sql->get("SELECT id, name FROM buildings ORDER BY name", "", []);
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<small><?php echo htmlspecialchars($floor['comment'] ?? ''); ?></small>
|
<small><?php echo htmlspecialchars($floor['comment'] ?? '); ?></small>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
@@ -233,9 +233,43 @@ $buildings = $sql->get("SELECT id, name FROM buildings ORDER BY name", "", []);
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
function confirmDelete(id) {
|
function confirmDelete(id) {
|
||||||
if (confirm('Dieses Stockwerk wirklich löschen? Alle Räume und Racks werden gelöscht.')) {
|
if (!confirm('Dieses Stockwerk wirklich loeschen?')) {
|
||||||
// TODO: AJAX-Delete implementieren
|
return;
|
||||||
alert('Löschen noch nicht implementiert');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const requestDelete = (forceDelete) => {
|
||||||
|
const body = ['id=' + encodeURIComponent(id)];
|
||||||
|
if (forceDelete) {
|
||||||
|
body.push('force=1');
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('?module=floors&action=delete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||||
|
body: body.join('&')
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data && data.success) {
|
||||||
|
window.location.reload();
|
||||||
|
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');
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
alert('Loeschen fehlgeschlagen');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
requestDelete(false);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -37,53 +37,21 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// SVG-Upload verarbeiten (optional)
|
// SVG aus Editor verarbeiten (optional)
|
||||||
// =========================
|
// =========================
|
||||||
$svgPath = null;
|
$svgPath = null;
|
||||||
if (!empty($_FILES['svg_file']['name'])) {
|
|
||||||
$file = $_FILES['svg_file'];
|
|
||||||
$tmpName = $file['tmp_name'];
|
|
||||||
$originalName = basename($file['name']);
|
|
||||||
$fileExt = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
|
|
||||||
|
|
||||||
// Nur SVG erlaubt
|
|
||||||
if ($fileExt !== 'svg') {
|
|
||||||
$_SESSION['error'] = "Nur SVG-Dateien sind erlaubt";
|
|
||||||
$redirectUrl = $floorId ? "?module=floors&action=edit&id=$floorId" : "?module=floors&action=edit";
|
|
||||||
header("Location: $redirectUrl");
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Zielverzeichnis
|
|
||||||
$uploadDir = __DIR__ . '/../../uploads/floorplans/';
|
|
||||||
if (!is_dir($uploadDir)) {
|
|
||||||
mkdir($uploadDir, 0755, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Eindeutiger Dateiname
|
|
||||||
$newFileName = uniqid('floor_') . '.svg';
|
|
||||||
$destPath = $uploadDir . $newFileName;
|
|
||||||
|
|
||||||
if (move_uploaded_file($tmpName, $destPath)) {
|
|
||||||
$svgPath = 'uploads/floorplans/' . $newFileName;
|
|
||||||
} else {
|
|
||||||
$_SESSION['error'] = "Datei-Upload fehlgeschlagen";
|
|
||||||
$redirectUrl = $floorId ? "?module=floors&action=edit&id=$floorId" : "?module=floors&action=edit";
|
|
||||||
header("Location: $redirectUrl");
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($floorSvgContent !== '') {
|
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;
|
||||||
|
|||||||
65
app/modules/locations/delete.php
Normal file
65
app/modules/locations/delete.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* app/modules/locations/delete.php
|
||||||
|
*
|
||||||
|
* Loescht einen Standort.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
if ($locationId <= 0) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Ungueltige Standort-ID'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$location = $sql->single(
|
||||||
|
"SELECT id, name FROM locations WHERE id = ?",
|
||||||
|
"i",
|
||||||
|
[$locationId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$location) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Standort nicht gefunden'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$deleted = $sql->set(
|
||||||
|
"DELETE FROM locations WHERE id = ?",
|
||||||
|
"i",
|
||||||
|
[$locationId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($deleted <= 0) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Standort konnte nicht geloescht werden'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Standort geloescht'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
@@ -42,14 +42,14 @@ $pageTitle = $isEdit ? "Standort bearbeiten: " . htmlspecialchars($location['nam
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="name">Name <span class="required">*</span></label>
|
<label for="name">Name <span class="required">*</span></label>
|
||||||
<input type="text" id="name" name="name" required
|
<input type="text" id="name" name="name" required
|
||||||
value="<?php echo htmlspecialchars($location['name'] ?? ''); ?>"
|
value="<?php echo htmlspecialchars($location['name'] ?? '); ?>"
|
||||||
placeholder="z.B. Hauptgebäude, Campus A, Außenstelle Berlin">
|
placeholder="z.B. Hauptgebäude, Campus A, Außenstelle Berlin">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="comment">Beschreibung</label>
|
<label for="comment">Beschreibung</label>
|
||||||
<textarea id="comment" name="comment" rows="3"
|
<textarea id="comment" name="comment" rows="3"
|
||||||
placeholder="Adresse, Kontaktinformationen, Besonderheiten"><?php echo htmlspecialchars($location['comment'] ?? ''); ?></textarea>
|
placeholder="Adresse, Kontaktinformationen, Besonderheiten"><?php echo htmlspecialchars($location['comment'] ?? '); ?></textarea>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
@@ -153,9 +153,24 @@ $pageTitle = $isEdit ? "Standort bearbeiten: " . htmlspecialchars($location['nam
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
function confirmDelete(id) {
|
function confirmDelete(id) {
|
||||||
if (confirm('Diesen Standort wirklich löschen? Alle Gebäude werden gelöscht.')) {
|
if (confirm('Diesen Standort wirklich loeschen? Alle Gebaeude werden geloescht.')) {
|
||||||
// TODO: AJAX-Delete implementieren
|
fetch('?module=locations&action=delete', {
|
||||||
alert('Löschen noch nicht implementiert');
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||||
|
body: 'id=' + encodeURIComponent(id)
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data && data.success) {
|
||||||
|
window.location.href = '?module=locations&action=list';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
alert('Loeschen fehlgeschlagen');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -2,24 +2,18 @@
|
|||||||
/**
|
/**
|
||||||
* app/modules/locations/list.php
|
* app/modules/locations/list.php
|
||||||
*
|
*
|
||||||
* Übersicht aller Standorte
|
* Uebersicht aller Standorte inkl. Gebaeude, Stockwerke und Raeume.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// =========================
|
$search = trim((string)($_GET['search'] ?? ''));
|
||||||
// Filter einlesen
|
|
||||||
// =========================
|
|
||||||
$search = trim($_GET['search'] ?? '');
|
|
||||||
|
|
||||||
// =========================
|
|
||||||
// Standorte laden
|
|
||||||
// =========================
|
|
||||||
$where = '';
|
$where = '';
|
||||||
$types = '';
|
$types = '';
|
||||||
$params = [];
|
$params = [];
|
||||||
|
|
||||||
if ($search !== '') {
|
if ($search !== '') {
|
||||||
$where = "WHERE name LIKE ? OR comment LIKE ?";
|
$where = "WHERE l.name LIKE ? OR l.comment LIKE ?";
|
||||||
$types = "ss";
|
$types = 'ss';
|
||||||
$params = ["%$search%", "%$search%"];
|
$params = ["%$search%", "%$search%"];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,59 +32,77 @@ $buildings = $sql->get(
|
|||||||
"SELECT b.id, b.location_id, b.name, b.comment
|
"SELECT b.id, b.location_id, b.name, b.comment
|
||||||
FROM buildings b
|
FROM buildings b
|
||||||
ORDER BY b.location_id, b.name",
|
ORDER BY b.location_id, b.name",
|
||||||
"",
|
'',
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
$floors = $sql->get(
|
||||||
|
"SELECT f.id, f.building_id, f.name, f.level,
|
||||||
|
COUNT(r.id) AS room_count
|
||||||
|
FROM floors f
|
||||||
|
LEFT JOIN rooms r ON r.floor_id = f.id
|
||||||
|
GROUP BY f.id
|
||||||
|
ORDER BY f.building_id, f.level, f.name",
|
||||||
|
'',
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
$rooms = $sql->get(
|
||||||
|
"SELECT r.id, r.floor_id, r.name, r.number, r.comment,
|
||||||
|
COUNT(no.id) AS outlet_count
|
||||||
|
FROM rooms r
|
||||||
|
LEFT JOIN network_outlets no ON no.room_id = r.id
|
||||||
|
GROUP BY r.id
|
||||||
|
ORDER BY r.floor_id,
|
||||||
|
CASE WHEN r.number IS NULL OR r.number = '' THEN 1 ELSE 0 END,
|
||||||
|
r.number,
|
||||||
|
r.name",
|
||||||
|
'',
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
$buildingsByLocation = [];
|
$buildingsByLocation = [];
|
||||||
foreach ($buildings as $building) {
|
foreach ($buildings as $building) {
|
||||||
$buildingsByLocation[$building['location_id']][] = $building;
|
$buildingsByLocation[(int)$building['location_id']][] = $building;
|
||||||
}
|
}
|
||||||
|
|
||||||
$floors = $sql->get(
|
|
||||||
"SELECT f.id, f.building_id, f.name, f.level
|
|
||||||
FROM floors f
|
|
||||||
ORDER BY f.building_id, f.level",
|
|
||||||
"",
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
$floorsByBuilding = [];
|
$floorsByBuilding = [];
|
||||||
foreach ($floors as $floor) {
|
foreach ($floors as $floor) {
|
||||||
$floorsByBuilding[$floor['building_id']][] = $floor;
|
$floorsByBuilding[(int)$floor['building_id']][] = $floor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$roomsByFloor = [];
|
||||||
|
foreach ($rooms as $room) {
|
||||||
|
$roomsByFloor[(int)$room['floor_id']][] = $room;
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="locations-container">
|
<div class="locations-container">
|
||||||
|
<link rel="stylesheet" href="/assets/css/locations-list.css">
|
||||||
|
<script src="/assets/js/locations-list.js" defer></script>
|
||||||
|
|
||||||
<h1>Standorte</h1>
|
<h1>Standorte</h1>
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
Toolbar
|
|
||||||
========================= -->
|
|
||||||
<div class="filter-form">
|
<div class="filter-form">
|
||||||
<form method="GET" style="display: flex; gap: 10px; flex-wrap: wrap; align-items: center;">
|
<form method="GET" class="locations-filter-form">
|
||||||
<input type="hidden" name="module" value="locations">
|
<input type="hidden" name="module" value="locations">
|
||||||
<input type="hidden" name="action" value="list">
|
<input type="hidden" name="action" value="list">
|
||||||
|
|
||||||
<input type="text" name="search" placeholder="Suche nach Name…"
|
<input type="text" name="search" placeholder="Suche nach Name..."
|
||||||
value="<?php echo htmlspecialchars($search); ?>" class="search-input">
|
value="<?php echo htmlspecialchars($search); ?>" class="search-input">
|
||||||
|
|
||||||
<button type="submit" class="button">Filter</button>
|
<button type="submit" class="button">Filter</button>
|
||||||
<a href="?module=locations&action=list" class="button">Reset</a>
|
<a href="?module=locations&action=list" class="button">Reset</a>
|
||||||
<a href="?module=locations&action=edit" class="button button-primary" style="margin-left: auto;">+ Neuer Standort</a>
|
<a href="?module=locations&action=edit" class="button button-primary locations-filter-add">+ Neuer Standort</a>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
Standorte-Tabelle
|
|
||||||
========================= -->
|
|
||||||
<?php if (!empty($locations)): ?>
|
<?php if (!empty($locations)): ?>
|
||||||
<table class="locations-list">
|
<table class="locations-list">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Gebäude</th>
|
<th>Gebaeude</th>
|
||||||
<th>Beschreibung</th>
|
<th>Beschreibung</th>
|
||||||
<th>Aktionen</th>
|
<th>Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -98,325 +110,144 @@ foreach ($floors as $floor) {
|
|||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($locations as $location): ?>
|
<?php foreach ($locations as $location): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td><strong><?php echo htmlspecialchars((string)$location['name']); ?></strong></td>
|
||||||
<strong><?php echo htmlspecialchars($location['name']); ?></strong>
|
<td><?php echo (int)$location['building_count']; ?></td>
|
||||||
</td>
|
<td><small><?php echo htmlspecialchars((string)($location['comment'] ?? '')); ?></small></td>
|
||||||
|
|
||||||
<td>
|
|
||||||
<?php echo $location['building_count']; ?>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<small><?php echo htmlspecialchars($location['comment'] ?? ''); ?></small>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<a href="?module=locations&action=edit&id=<?php echo $location['id']; ?>" class="button button-small">Bearbeiten</a>
|
<a href="?module=locations&action=edit&id=<?php echo (int)$location['id']; ?>" class="button button-small">Bearbeiten</a>
|
||||||
<a href="#" class="button button-small button-danger" onclick="confirmDelete(<?php echo $location['id']; ?>)">Löschen</a>
|
<button type="button" class="button button-small button-danger js-delete-location" data-location-id="<?php echo (int)$location['id']; ?>">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 Standorte gefunden.</p>
|
<p>Keine Standorte gefunden.</p>
|
||||||
<p>
|
<p><a href="?module=locations&action=edit" class="button button-primary">Ersten Standort anlegen</a></p>
|
||||||
<a href="?module=locations&action=edit" class="button button-primary">
|
|
||||||
Ersten Standort anlegen
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
|
||||||
//TODO design schlecht, mach es hübscher
|
|
||||||
?>
|
|
||||||
|
|
||||||
<section class="hierarchy-section">
|
<section class="hierarchy-section">
|
||||||
<h2>Gebäude & Stockwerke nach Standorten</h2>
|
<h2>Gebaeude, Stockwerke und Raeume</h2>
|
||||||
|
|
||||||
<?php if (!empty($locations)): ?>
|
<?php if (!empty($locations)): ?>
|
||||||
<ul class="hierarchy-list">
|
<table class="hierarchy-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Standort</th>
|
||||||
|
<th>Gebaeude</th>
|
||||||
|
<th>Stockwerk</th>
|
||||||
|
<th>Raum</th>
|
||||||
|
<th>Details</th>
|
||||||
|
<th class="actions">Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
<?php foreach ($locations as $location): ?>
|
<?php foreach ($locations as $location): ?>
|
||||||
<li class="hierarchy-list__item">
|
<?php $locationBuildings = $buildingsByLocation[(int)$location['id']] ?? []; ?>
|
||||||
<div class="hierarchy-location">
|
<tr class="hierarchy-row hierarchy-row--location">
|
||||||
<div class="hierarchy-location__info">
|
<td class="hierarchy-cell hierarchy-cell--location" colspan="4">
|
||||||
<span class="hierarchy-location__name"><?php echo htmlspecialchars($location['name']); ?></span>
|
<strong><?php echo htmlspecialchars((string)$location['name']); ?></strong>
|
||||||
<span class="hierarchy-location__meta"><?php echo $location['building_count']; ?> Gebäude</span>
|
<span class="hierarchy-meta">(<?php echo (int)$location['building_count']; ?> Gebaeude)</span>
|
||||||
</div>
|
</td>
|
||||||
<div class="hierarchy-actions">
|
<td></td>
|
||||||
<a href="?module=buildings&action=edit&location_id=<?php echo $location['id']; ?>" class="button button-small">+ Gebäude</a>
|
<td class="actions hierarchy-actions">
|
||||||
</div>
|
<a href="?module=buildings&action=edit&location_id=<?php echo (int)$location['id']; ?>" class="button button-small">+ Gebaeude</a>
|
||||||
</div>
|
</td>
|
||||||
<?php $locationBuildings = $buildingsByLocation[$location['id']] ?? []; ?>
|
</tr>
|
||||||
<?php if (!empty($locationBuildings)): ?>
|
|
||||||
<ul class="hierarchy-sublist">
|
<?php if (!empty($locationBuildings)): ?>
|
||||||
<?php foreach ($locationBuildings as $building): ?>
|
<?php foreach ($locationBuildings as $building): ?>
|
||||||
<li class="hierarchy-sublist__item">
|
<?php $buildingFloors = $floorsByBuilding[(int)$building['id']] ?? []; ?>
|
||||||
<div class="hierarchy-building">
|
<tr class="hierarchy-row hierarchy-row--building">
|
||||||
<div class="hierarchy-building__info">
|
<td class="hierarchy-cell hierarchy-cell--empty"> </td>
|
||||||
<span class="hierarchy-building__name"><?php echo htmlspecialchars($building['name']); ?></span>
|
<td class="hierarchy-cell hierarchy-cell--building" colspan="3"><?php echo htmlspecialchars((string)$building['name']); ?></td>
|
||||||
<?php if (!empty($building['comment'])): ?>
|
<td>
|
||||||
<span class="hierarchy-building__meta"><?php echo htmlspecialchars($building['comment']); ?></span>
|
<?php if (!empty($building['comment'])): ?>
|
||||||
<?php endif; ?>
|
<span class="hierarchy-meta"><?php echo htmlspecialchars((string)$building['comment']); ?></span>
|
||||||
</div>
|
<?php else: ?>
|
||||||
<div class="hierarchy-actions">
|
<span class="hierarchy-meta hierarchy-meta--muted">Kein Kommentar</span>
|
||||||
<a href="?module=buildings&action=edit&id=<?php echo $building['id']; ?>" class="button button-small">Bearbeiten</a>
|
<?php endif; ?>
|
||||||
<a href="#" class="button button-small button-danger" onclick="confirmBuildingDelete(<?php echo $building['id']; ?>)">Löschen</a>
|
</td>
|
||||||
<a href="?module=floors&action=edit&building_id=<?php echo $building['id']; ?>" class="button button-small">+ Stockwerk</a>
|
<td class="actions hierarchy-actions">
|
||||||
</div>
|
<a href="?module=buildings&action=edit&id=<?php echo (int)$building['id']; ?>" class="button button-small">Bearbeiten</a>
|
||||||
</div>
|
<button type="button" class="button button-small button-danger js-delete-building" data-building-id="<?php echo (int)$building['id']; ?>">Loeschen</button>
|
||||||
<?php $buildingFloors = $floorsByBuilding[$building['id']] ?? []; ?>
|
<a href="?module=floors&action=edit&building_id=<?php echo (int)$building['id']; ?>" class="button button-small">+ Stockwerk</a>
|
||||||
<?php if (!empty($buildingFloors)): ?>
|
</td>
|
||||||
<ul class="hierarchy-sublist hierarchy-sublist--nested">
|
</tr>
|
||||||
<?php foreach ($buildingFloors as $floor): ?>
|
|
||||||
<li>
|
<?php if (!empty($buildingFloors)): ?>
|
||||||
<div class="hierarchy-floor">
|
<?php foreach ($buildingFloors as $floor): ?>
|
||||||
<div class="hierarchy-floor__info">
|
<?php $floorRooms = $roomsByFloor[(int)$floor['id']] ?? []; ?>
|
||||||
<span class="hierarchy-floor__name"><?php echo htmlspecialchars($floor['name']); ?></span>
|
<tr class="hierarchy-row hierarchy-row--floor">
|
||||||
<?php if ($floor['level'] !== null): ?>
|
<td class="hierarchy-cell hierarchy-cell--empty"> </td>
|
||||||
<span class="hierarchy-floor__meta">Ebene <?php echo htmlspecialchars($floor['level']); ?></span>
|
<td class="hierarchy-cell hierarchy-cell--empty"> </td>
|
||||||
<?php endif; ?>
|
<td class="hierarchy-cell hierarchy-cell--floor" colspan="2"><?php echo htmlspecialchars((string)$floor['name']); ?></td>
|
||||||
</div>
|
<td>
|
||||||
<div class="hierarchy-actions">
|
<?php if ($floor['level'] !== null): ?>
|
||||||
<a href="?module=floors&action=edit&id=<?php echo $floor['id']; ?>" class="button button-small">Bearbeiten</a>
|
<span class="hierarchy-meta">Ebene <?php echo htmlspecialchars((string)$floor['level']); ?></span>
|
||||||
<a href="#" class="button button-small button-danger" onclick="confirmFloorDelete(<?php echo $floor['id']; ?>)">Löschen</a>
|
<?php else: ?>
|
||||||
</div>
|
<span class="hierarchy-meta hierarchy-meta--muted">Keine Ebene</span>
|
||||||
</div>
|
<?php endif; ?>
|
||||||
</li>
|
<span class="hierarchy-meta"> | <?php echo (int)$floor['room_count']; ?> Raeume</span>
|
||||||
<?php endforeach; ?>
|
</td>
|
||||||
</ul>
|
<td class="actions hierarchy-actions">
|
||||||
<?php else: ?>
|
<a href="?module=floors&action=edit&id=<?php echo (int)$floor['id']; ?>" class="button button-small">Bearbeiten</a>
|
||||||
<p class="hierarchy-empty">Keine Stockwerke für dieses Gebäude.</p>
|
<button type="button" class="button button-small button-danger js-delete-floor" data-floor-id="<?php echo (int)$floor['id']; ?>">Loeschen</button>
|
||||||
<?php endif; ?>
|
<a href="?module=rooms&action=edit&floor_id=<?php echo (int)$floor['id']; ?>" class="button button-small">+ Raum</a>
|
||||||
</li>
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<?php if (!empty($floorRooms)): ?>
|
||||||
|
<?php foreach ($floorRooms as $room): ?>
|
||||||
|
<tr class="hierarchy-row hierarchy-row--room">
|
||||||
|
<td class="hierarchy-cell hierarchy-cell--empty"> </td>
|
||||||
|
<td class="hierarchy-cell hierarchy-cell--empty"> </td>
|
||||||
|
<td class="hierarchy-cell hierarchy-cell--empty"> </td>
|
||||||
|
<td class="hierarchy-cell hierarchy-cell--room">
|
||||||
|
<?php echo htmlspecialchars((string)$room['name']); ?>
|
||||||
|
<?php if (!empty($room['number'])): ?>
|
||||||
|
<span class="hierarchy-meta">(<?php echo htmlspecialchars((string)$room['number']); ?>)</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="hierarchy-meta"><?php echo (int)$room['outlet_count']; ?> Dosen</span>
|
||||||
|
<?php if (!empty($room['comment'])): ?>
|
||||||
|
<span class="hierarchy-meta"> | <?php echo htmlspecialchars((string)$room['comment']); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td class="actions hierarchy-actions">
|
||||||
|
<a href="?module=rooms&action=edit&id=<?php echo (int)$room['id']; ?>" class="button button-small">Bearbeiten</a>
|
||||||
|
<button type="button" class="button button-small button-danger js-delete-room" data-room-id="<?php echo (int)$room['id']; ?>">Loeschen</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<tr class="hierarchy-row hierarchy-row--empty">
|
||||||
|
<td class="hierarchy-cell hierarchy-cell--empty"> </td>
|
||||||
|
<td class="hierarchy-cell hierarchy-cell--empty"> </td>
|
||||||
|
<td class="hierarchy-cell hierarchy-cell--room hierarchy-meta hierarchy-meta--muted" colspan="2">Keine Raeume</td>
|
||||||
|
<td><span class="hierarchy-meta hierarchy-meta--muted">Fuer dieses Stockwerk sind noch keine Raeume angelegt.</span></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</ul>
|
<?php endif; ?>
|
||||||
<?php else: ?>
|
<?php endforeach; ?>
|
||||||
<p class="hierarchy-empty">Für diesen Standort sind noch keine Gebäude hinterlegt.</p>
|
<?php else: ?>
|
||||||
<?php endif; ?>
|
<tr class="hierarchy-row hierarchy-row--empty">
|
||||||
</li>
|
<td class="hierarchy-cell hierarchy-cell--empty"> </td>
|
||||||
|
<td class="hierarchy-cell hierarchy-cell--empty" colspan="3">Keine Gebaeude</td>
|
||||||
|
<td><span class="hierarchy-meta hierarchy-meta--muted">Fuer diesen Standort sind noch keine Gebaeude vorhanden.</span></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</ul>
|
</tbody>
|
||||||
|
</table>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<p>Keine Standorte gefunden.</p>
|
<p>Keine Standorte gefunden.</p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<?php
|
|
||||||
//TODO style in css file
|
|
||||||
?>
|
|
||||||
<style>
|
|
||||||
.locations-container {
|
|
||||||
padding: 20px;
|
|
||||||
max-width: 1000px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-form {
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-form form {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-form input {
|
|
||||||
padding: 8px 12px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 250px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.locations-list {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin: 15px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.locations-list th {
|
|
||||||
background: #f5f5f5;
|
|
||||||
padding: 12px;
|
|
||||||
text-align: left;
|
|
||||||
border-bottom: 2px solid #ddd;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.locations-list td {
|
|
||||||
padding: 12px;
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.locations-list tr:hover {
|
|
||||||
background: #f9f9f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 8px 12px;
|
|
||||||
background: #007bff;
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:hover {
|
|
||||||
background: #0056b3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-primary {
|
|
||||||
background: #28a745;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-primary:hover {
|
|
||||||
background: #218838;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-small {
|
|
||||||
padding: 4px 8px;
|
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-danger {
|
|
||||||
background: #dc3545;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-danger:hover {
|
|
||||||
background: #c82333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
text-align: center;
|
|
||||||
padding: 40px 20px;
|
|
||||||
background: #f9f9f9;
|
|
||||||
border: 1px solid #eee;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hierarchy-section {
|
|
||||||
padding: 20px;
|
|
||||||
max-width: 1000px;
|
|
||||||
margin: 30px auto;
|
|
||||||
border-top: 1px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hierarchy-section h2 {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
font-size: 1.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hierarchy-list {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hierarchy-list__item {
|
|
||||||
padding: 15px;
|
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #fafafa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hierarchy-location,
|
|
||||||
.hierarchy-building,
|
|
||||||
.hierarchy-floor {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hierarchy-location__info,
|
|
||||||
.hierarchy-building__info,
|
|
||||||
.hierarchy-floor__info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hierarchy-location__name,
|
|
||||||
.hierarchy-building__name,
|
|
||||||
.hierarchy-floor__name {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hierarchy-location__meta,
|
|
||||||
.hierarchy-building__meta,
|
|
||||||
.hierarchy-floor__meta {
|
|
||||||
color: #666;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hierarchy-sublist {
|
|
||||||
list-style: none;
|
|
||||||
margin: 10px 0 0 20px;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hierarchy-sublist--nested {
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hierarchy-empty {
|
|
||||||
margin: 10px 0 0 20px;
|
|
||||||
color: #888;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
.hierarchy-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 6px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hierarchy-actions .button {
|
|
||||||
padding: 4px 10px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hierarchy-actions .button-danger {
|
|
||||||
background: #dc3545;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function confirmDelete(id) {
|
|
||||||
if (confirm('Diesen Standort wirklich löschen?')) {
|
|
||||||
// TODO: AJAX-Delete implementieren
|
|
||||||
alert('Löschen noch nicht implementiert');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmBuildingDelete(id) {
|
|
||||||
if (confirm('Dieses Gebäude wirklich löschen? Alle Stockwerke werden gelöscht.')) {
|
|
||||||
alert('Löschen noch nicht implementiert');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmFloorDelete(id) {
|
|
||||||
if (confirm('Dieses Stockwerk wirklich löschen? Alle Räume und Racks werden gelöscht.')) {
|
|
||||||
alert('Löschen noch nicht implementiert');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
36
app/modules/racks/delete.php
Normal file
36
app/modules/racks/delete.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* app/modules/racks/delete.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID fehlt']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$exists = $sql->single("SELECT id FROM racks WHERE id = ?", "i", [$id]);
|
||||||
|
if (!$exists) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Rack nicht gefunden']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = $sql->set("DELETE FROM racks WHERE id = ?", "i", [$id]);
|
||||||
|
if ($rows === false) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Loeschen fehlgeschlagen']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Rack geloescht']);
|
||||||
|
|
||||||
@@ -1,16 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* app/modules/racks/edit.php
|
* app/modules/racks/edit.php
|
||||||
*
|
|
||||||
* Rack anlegen oder bearbeiten
|
|
||||||
* - Name, Beschreibung
|
|
||||||
* - Zugehöriges Stockwerk (Floor)
|
|
||||||
* - Höhe in Höheneinheiten (HE)
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// =========================
|
|
||||||
// Kontext bestimmen
|
|
||||||
// =========================
|
|
||||||
$rackId = (int)($_GET['id'] ?? 0);
|
$rackId = (int)($_GET['id'] ?? 0);
|
||||||
$rack = null;
|
$rack = null;
|
||||||
|
|
||||||
@@ -23,83 +15,61 @@ if ($rackId > 0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$isEdit = !empty($rack);
|
$isEdit = !empty($rack);
|
||||||
$pageTitle = $isEdit ? "Rack bearbeiten: " . htmlspecialchars($rack['name']) : "Neues Rack";
|
$pageTitle = $isEdit ? 'Rack bearbeiten: ' . htmlspecialchars((string)$rack['name']) : 'Neues Rack';
|
||||||
|
$floors = $sql->get("SELECT id, name FROM floors ORDER BY name", '', []);
|
||||||
// =========================
|
|
||||||
// Floors laden
|
|
||||||
// =========================
|
|
||||||
$floors = $sql->get("SELECT id, name FROM floors ORDER BY name", "", []);
|
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="rack-edit">
|
<div class="rack-edit">
|
||||||
<h1><?php echo $pageTitle; ?></h1>
|
<h1><?php echo $pageTitle; ?></h1>
|
||||||
|
|
||||||
<form method="post" action="?module=racks&action=save" class="edit-form">
|
<form method="post" action="?module=racks&action=save" class="edit-form">
|
||||||
|
|
||||||
<?php if ($isEdit): ?>
|
<?php if ($isEdit): ?>
|
||||||
<input type="hidden" name="id" value="<?php echo $rackId; ?>">
|
<input type="hidden" name="id" value="<?php echo (int)$rackId; ?>">
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
Basisdaten
|
|
||||||
========================= -->
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Allgemein</legend>
|
<legend>Allgemein</legend>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="name">Name <span class="required">*</span></label>
|
<label for="name">Name <span class="required">*</span></label>
|
||||||
<input type="text" id="name" name="name" required
|
<input type="text" id="name" name="name" required value="<?php echo htmlspecialchars((string)($rack['name'] ?? '')); ?>" placeholder="z.B. Rack A1">
|
||||||
value="<?php echo htmlspecialchars($rack['name'] ?? ''); ?>"
|
|
||||||
placeholder="z.B. Rack A1">
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="comment">Beschreibung</label>
|
<label for="comment">Beschreibung</label>
|
||||||
<textarea id="comment" name="comment" rows="3"
|
<textarea id="comment" name="comment" rows="3" placeholder="z.B. Standort, Besonderheiten"><?php echo htmlspecialchars((string)($rack['comment'] ?? '')); ?></textarea>
|
||||||
placeholder="z.B. Standort, Besonderheiten"><?php echo htmlspecialchars($rack['comment'] ?? ''); ?></textarea>
|
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
Standort & Höhe
|
|
||||||
========================= -->
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Standort & Größe</legend>
|
<legend>Standort und Groesse</legend>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="floor_id">Stockwerk <span class="required">*</span></label>
|
<label for="floor_id">Stockwerk <span class="required">*</span></label>
|
||||||
<select id="floor_id" name="floor_id" required>
|
<select id="floor_id" name="floor_id" required>
|
||||||
<option value="">- Wählen -</option>
|
<option value="">- Waehlen -</option>
|
||||||
<?php foreach ($floors as $floor): ?>
|
<?php foreach ($floors as $floor): ?>
|
||||||
<option value="<?php echo $floor['id']; ?>"
|
<option value="<?php echo (int)$floor['id']; ?>" <?php echo ((int)($rack['floor_id'] ?? 0) === (int)$floor['id']) ? 'selected' : ''; ?>>
|
||||||
<?php echo ($rack['floor_id'] ?? 0) == $floor['id'] ? 'selected' : ''; ?>>
|
<?php echo htmlspecialchars((string)$floor['name']); ?>
|
||||||
<?php echo htmlspecialchars($floor['name']); ?>
|
</option>
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="height_he">Höhe in Höheneinheiten (HE) <span class="required">*</span></label>
|
<label for="height_he">Hoehe in Hoeheneinheiten (HE) <span class="required">*</span></label>
|
||||||
<input type="number" id="height_he" name="height_he" required min="1" max="100"
|
<input type="number" id="height_he" name="height_he" required min="1" max="100" value="<?php echo (int)($rack['height_he'] ?? 42); ?>">
|
||||||
value="<?php echo htmlspecialchars($rack['height_he'] ?? '42'); ?>"
|
<small>Standard: 42 HE</small>
|
||||||
placeholder="z.B. 42">
|
|
||||||
<small>Standard: 42 HE (ca. 2 Meter)</small>
|
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
Aktionen
|
|
||||||
========================= -->
|
|
||||||
<fieldset class="form-actions">
|
<fieldset class="form-actions">
|
||||||
<button type="submit" class="button button-primary">Speichern</button>
|
<button type="submit" class="button button-primary">Speichern</button>
|
||||||
<a href="?module=racks&action=list" class="button">Abbrechen</a>
|
<a href="?module=racks&action=list" class="button">Abbrechen</a>
|
||||||
<?php if ($isEdit): ?>
|
<?php if ($isEdit): ?>
|
||||||
<a href="#" class="button button-danger" onclick="confirmDelete(<?php echo $rackId; ?>)">Löschen</a>
|
<a href="#" class="button button-danger" onclick="return confirmDelete(<?php echo (int)$rackId; ?>)">Loeschen</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -197,60 +167,25 @@ $floors = $sql->get("SELECT id, name FROM floors ORDER BY name", "", []);
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
function confirmDelete(id) {
|
function confirmDelete(id) {
|
||||||
if (confirm('Dieses Rack wirklich löschen? Alle Geräte werden aus dem Rack entfernt.')) {
|
if (!confirm('Dieses Rack wirklich loeschen? Alle Geraete werden aus dem Rack entfernt.')) {
|
||||||
// TODO: AJAX-Delete implementieren
|
return false;
|
||||||
alert('Löschen noch nicht implementiert');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fetch('?module=racks&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.href = '?module=racks&action=list';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
|
||||||
|
})
|
||||||
|
.catch(() => alert('Loeschen fehlgeschlagen'));
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
Rack-SVG / Gerätepositionen
|
|
||||||
========================= -->
|
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
<legend>Rack-Layout</legend>
|
|
||||||
|
|
||||||
<div class="svg-editor-container">
|
|
||||||
<svg
|
|
||||||
id="rack-svg"
|
|
||||||
viewBox="0 0 200 1000"
|
|
||||||
width="100%"
|
|
||||||
height="600"
|
|
||||||
>
|
|
||||||
<!-- TODO: Rack-SVG laden -->
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="hint">
|
|
||||||
Geräte per Drag & Drop im Rack positionieren.
|
|
||||||
</p>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
Aktionen
|
|
||||||
========================= -->
|
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
<button type="submit">Speichern</button>
|
|
||||||
<button type="button" onclick="history.back()">Abbrechen</button>
|
|
||||||
<!-- TODO: Löschen, falls edit -->
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
JS-Konfiguration
|
|
||||||
========================= -->
|
|
||||||
|
|
||||||
<script>
|
|
||||||
/**
|
|
||||||
* Konfiguration für Rack-SVG-Editor
|
|
||||||
*/
|
|
||||||
|
|
||||||
// TODO: Rack-ID aus PHP setzen
|
|
||||||
// window.RACK_ID = <?= (int)$rackId ?>;
|
|
||||||
|
|
||||||
// TODO: Gerätepositionen an JS übergeben
|
|
||||||
// window.RACK_DEVICES = <?= json_encode($rackDevices ?? []) ?>;
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,42 +2,30 @@
|
|||||||
/**
|
/**
|
||||||
* app/modules/racks/list.php
|
* app/modules/racks/list.php
|
||||||
*
|
*
|
||||||
* Übersicht aller Racks
|
* Uebersicht aller Racks.
|
||||||
* - Anzeigen, Bearbeiten, Löschen
|
|
||||||
* - Zugehöriges Stockwerk anzeigen
|
|
||||||
* - Gerätecount
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// =========================
|
$search = trim((string)($_GET['search'] ?? ''));
|
||||||
// Filter einlesen
|
|
||||||
// =========================
|
|
||||||
$search = trim($_GET['search'] ?? '');
|
|
||||||
$floorId = (int)($_GET['floor_id'] ?? 0);
|
$floorId = (int)($_GET['floor_id'] ?? 0);
|
||||||
|
|
||||||
// =========================
|
|
||||||
// WHERE-Clause bauen
|
|
||||||
// =========================
|
|
||||||
$where = [];
|
$where = [];
|
||||||
$types = '';
|
$types = '';
|
||||||
$params = [];
|
$params = [];
|
||||||
|
|
||||||
if ($search !== '') {
|
if ($search !== '') {
|
||||||
$where[] = "r.name LIKE ?";
|
$where[] = 'r.name LIKE ?';
|
||||||
$types .= "s";
|
$types .= 's';
|
||||||
$params[] = "%$search%";
|
$params[] = "%$search%";
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($floorId > 0) {
|
if ($floorId > 0) {
|
||||||
$where[] = "r.floor_id = ?";
|
$where[] = 'r.floor_id = ?';
|
||||||
$types .= "i";
|
$types .= 'i';
|
||||||
$params[] = $floorId;
|
$params[] = $floorId;
|
||||||
}
|
}
|
||||||
|
|
||||||
$whereSql = $where ? "WHERE " . implode(" AND ", $where) : "";
|
$whereSql = $where ? ('WHERE ' . implode(' AND ', $where)) : '';
|
||||||
|
|
||||||
// =========================
|
|
||||||
// Racks laden
|
|
||||||
// =========================
|
|
||||||
$racks = $sql->get(
|
$racks = $sql->get(
|
||||||
"SELECT r.*, f.name AS floor_name, COUNT(d.id) AS device_count
|
"SELECT r.*, f.name AS floor_name, COUNT(d.id) AS device_count
|
||||||
FROM racks r
|
FROM racks r
|
||||||
@@ -50,212 +38,111 @@ $racks = $sql->get(
|
|||||||
$params
|
$params
|
||||||
);
|
);
|
||||||
|
|
||||||
// =========================
|
$floors = $sql->get('SELECT id, name FROM floors ORDER BY name', '', []);
|
||||||
// Filter-Daten laden
|
|
||||||
// =========================
|
|
||||||
$floors = $sql->get("SELECT id, name FROM floors ORDER BY name", "", []);
|
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="racks-container">
|
<div class="racks-container">
|
||||||
<h1>Racks</h1>
|
<h1>Racks</h1>
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
Toolbar
|
|
||||||
========================= -->
|
|
||||||
<div class="filter-form">
|
<div class="filter-form">
|
||||||
<form method="GET" style="display: flex; gap: 10px; flex-wrap: wrap; align-items: center;">
|
<form method="GET" style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;">
|
||||||
<input type="hidden" name="module" value="racks">
|
<input type="hidden" name="module" value="racks">
|
||||||
<input type="hidden" name="action" value="list">
|
<input type="hidden" name="action" value="list">
|
||||||
|
|
||||||
<input type="text" name="search" placeholder="Suche nach Name…"
|
<input type="text" name="search" placeholder="Suche nach Name..." value="<?php echo htmlspecialchars($search); ?>" class="search-input">
|
||||||
value="<?php echo htmlspecialchars($search); ?>" class="search-input">
|
|
||||||
|
|
||||||
<select name="floor_id">
|
<select name="floor_id">
|
||||||
<option value="">- Alle Stockwerke -</option>
|
<option value="">- Alle Stockwerke -</option>
|
||||||
<?php foreach ($floors as $floor): ?>
|
<?php foreach ($floors as $floor): ?>
|
||||||
<option value="<?php echo $floor['id']; ?>"
|
<option value="<?php echo (int)$floor['id']; ?>" <?php echo ((int)$floor['id'] === $floorId) ? 'selected' : ''; ?>>
|
||||||
<?php echo $floor['id'] === $floorId ? 'selected' : ''; ?>>
|
<?php echo htmlspecialchars((string)$floor['name']); ?>
|
||||||
<?php echo htmlspecialchars($floor['name']); ?>
|
</option>
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<button type="submit" class="button">Filter</button>
|
<button type="submit" class="button">Filter</button>
|
||||||
<a href="?module=racks&action=list" class="button">Reset</a>
|
<a href="?module=racks&action=list" class="button">Reset</a>
|
||||||
<a href="?module=racks&action=edit" class="button button-primary" style="margin-left: auto;">+ Neues Rack</a>
|
<a href="?module=racks&action=edit" class="button button-primary" style="margin-left:auto;">+ Neues Rack</a>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
Racks-Tabelle
|
|
||||||
========================= -->
|
|
||||||
<?php if (!empty($racks)): ?>
|
<?php if (!empty($racks)): ?>
|
||||||
<table class="rack-list">
|
<table class="rack-list">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Stockwerk</th>
|
<th>Stockwerk</th>
|
||||||
<th>Höhe (HE)</th>
|
<th>Hoehe (HE)</th>
|
||||||
<th>Geräte</th>
|
<th>Geraete</th>
|
||||||
<th>Beschreibung</th>
|
<th>Beschreibung</th>
|
||||||
<th>Aktionen</th>
|
<th>Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($racks as $rack): ?>
|
<?php foreach ($racks as $rack): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td><strong><?php echo htmlspecialchars((string)$rack['name']); ?></strong></td>
|
||||||
<strong><?php echo htmlspecialchars($rack['name']); ?></strong>
|
<td><?php echo htmlspecialchars((string)($rack['floor_name'] ?? '-')); ?></td>
|
||||||
</td>
|
<td><?php echo (int)$rack['height_he']; ?> HE</td>
|
||||||
|
<td><?php echo (int)$rack['device_count']; ?></td>
|
||||||
<td>
|
<td><small><?php echo htmlspecialchars((string)($rack['comment'] ?? '')); ?></small></td>
|
||||||
<?php echo htmlspecialchars($rack['floor_name'] ?? '—'); ?>
|
<td class="actions">
|
||||||
</td>
|
<a href="?module=racks&action=edit&id=<?php echo (int)$rack['id']; ?>" class="button button-small">Bearbeiten</a>
|
||||||
|
<a href="#" class="button button-small button-danger" onclick="return confirmDelete(<?php echo (int)$rack['id']; ?>)">Loeschen</a>
|
||||||
<td>
|
</td>
|
||||||
<?php echo $rack['height_he']; ?> HE
|
</tr>
|
||||||
</td>
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
<td>
|
</table>
|
||||||
<?php echo $rack['device_count']; ?>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<small><?php echo htmlspecialchars($rack['comment'] ?? ''); ?></small>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class="actions">
|
|
||||||
<a href="?module=racks&action=edit&id=<?php echo $rack['id']; ?>" class="button button-small">Bearbeiten</a>
|
|
||||||
<a href="#" class="button button-small button-danger" onclick="confirmDelete(<?php echo $rack['id']; ?>)">Löschen</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<p>Keine Racks gefunden.</p>
|
<p>Keine Racks gefunden.</p>
|
||||||
<p>
|
<p><a href="?module=racks&action=edit" class="button button-primary">Erstes Rack anlegen</a></p>
|
||||||
<a href="?module=racks&action=edit" class="button button-primary">
|
</div>
|
||||||
Erstes Rack anlegen
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.racks-container {
|
.racks-container { padding: 20px; max-width: 1000px; margin: 0 auto; }
|
||||||
padding: 20px;
|
.filter-form { margin: 20px 0; }
|
||||||
max-width: 1000px;
|
.filter-form input, .filter-form select { padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; }
|
||||||
margin: 0 auto;
|
.search-input { flex: 1; min-width: 250px; }
|
||||||
}
|
.rack-list { width: 100%; border-collapse: collapse; margin: 15px 0; }
|
||||||
|
.rack-list th { background: #f5f5f5; padding: 12px; text-align: left; border-bottom: 2px solid #ddd; font-weight: bold; }
|
||||||
.filter-form {
|
.rack-list td { padding: 12px; border-bottom: 1px solid #ddd; }
|
||||||
margin: 20px 0;
|
.rack-list tr:hover { background: #f9f9f9; }
|
||||||
}
|
.actions { white-space: nowrap; }
|
||||||
|
.button { display: inline-block; padding: 8px 12px; background: #007bff; color: #fff; text-decoration: none; border-radius: 4px; border: none; cursor: pointer; font-size: 0.9em; }
|
||||||
.filter-form form {
|
.button:hover { background: #0056b3; }
|
||||||
display: flex;
|
.button-primary { background: #28a745; }
|
||||||
gap: 10px;
|
.button-primary:hover { background: #218838; }
|
||||||
flex-wrap: wrap;
|
.button-small { padding: 4px 8px; font-size: 0.85em; }
|
||||||
align-items: center;
|
.button-danger { background: #dc3545; }
|
||||||
}
|
.button-danger:hover { background: #c82333; }
|
||||||
|
.empty-state { text-align: center; padding: 40px 20px; background: #f9f9f9; border: 1px solid #eee; border-radius: 8px; }
|
||||||
.filter-form input,
|
|
||||||
.filter-form select {
|
|
||||||
padding: 8px 12px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 250px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rack-list {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin: 15px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rack-list th {
|
|
||||||
background: #f5f5f5;
|
|
||||||
padding: 12px;
|
|
||||||
text-align: left;
|
|
||||||
border-bottom: 2px solid #ddd;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rack-list td {
|
|
||||||
padding: 12px;
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rack-list tr:hover {
|
|
||||||
background: #f9f9f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 8px 12px;
|
|
||||||
background: #007bff;
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:hover {
|
|
||||||
background: #0056b3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-primary {
|
|
||||||
background: #28a745;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-primary:hover {
|
|
||||||
background: #218838;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-small {
|
|
||||||
padding: 4px 8px;
|
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-danger {
|
|
||||||
background: #dc3545;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-danger:hover {
|
|
||||||
background: #c82333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
text-align: center;
|
|
||||||
padding: 40px 20px;
|
|
||||||
background: #f9f9f9;
|
|
||||||
border: 1px solid #eee;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function confirmDelete(id) {
|
function confirmDelete(id) {
|
||||||
if (confirm('Dieses Rack wirklich löschen?')) {
|
if (!confirm('Dieses Rack wirklich loeschen?')) {
|
||||||
// TODO: AJAX-Delete implementieren
|
return false;
|
||||||
alert('Löschen noch nicht implementiert');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fetch('?module=racks&action=delete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||||
|
body: 'id=' + encodeURIComponent(id)
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data && data.success) {
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
|
||||||
|
})
|
||||||
|
.catch(() => alert('Loeschen fehlgeschlagen'));
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</div>
|
|
||||||
<?php /* endif; */ ?>
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
64
app/modules/rooms/delete.php
Normal file
64
app/modules/rooms/delete.php
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* app/modules/rooms/delete.php
|
||||||
|
*
|
||||||
|
* Loescht einen Raum.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$roomId = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($roomId <= 0) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Ungueltige Raum-ID'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$room = $sql->single(
|
||||||
|
"SELECT id, name FROM rooms WHERE id = ?",
|
||||||
|
"i",
|
||||||
|
[$roomId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$room) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Raum nicht gefunden'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$deleted = $sql->set(
|
||||||
|
"DELETE FROM rooms WHERE id = ?",
|
||||||
|
"i",
|
||||||
|
[$roomId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($deleted <= 0) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Raum konnte nicht geloescht werden'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Raum geloescht'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
234
app/modules/rooms/edit.php
Normal file
234
app/modules/rooms/edit.php
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* app/modules/rooms/edit.php
|
||||||
|
*
|
||||||
|
* Raum anlegen oder bearbeiten.
|
||||||
|
* Optional kann ein Raum-Polygon auf der Stockwerkskarte gezeichnet werden.
|
||||||
|
*/
|
||||||
|
|
||||||
|
$roomId = (int)($_GET['id'] ?? 0);
|
||||||
|
$room = null;
|
||||||
|
|
||||||
|
if ($roomId > 0) {
|
||||||
|
$room = $sql->single(
|
||||||
|
"SELECT * FROM rooms WHERE id = ?",
|
||||||
|
"i",
|
||||||
|
[$roomId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$isEdit = !empty($room);
|
||||||
|
$prefillFloorId = (int)($_GET['floor_id'] ?? 0);
|
||||||
|
$selectedFloorId = (int)($room['floor_id'] ?? $prefillFloorId);
|
||||||
|
|
||||||
|
$floors = $sql->get(
|
||||||
|
"SELECT f.id, f.name, f.level, f.svg_path, b.name AS building_name, l.name AS location_name
|
||||||
|
FROM floors f
|
||||||
|
LEFT JOIN buildings b ON b.id = f.building_id
|
||||||
|
LEFT JOIN locations l ON l.id = b.location_id
|
||||||
|
ORDER BY l.name, b.name, f.level, f.name",
|
||||||
|
"",
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($floors as &$floor) {
|
||||||
|
$svgPath = trim((string)($floor['svg_path'] ?? ''));
|
||||||
|
$floor['svg_url'] = $svgPath !== '' ? '/' . ltrim($svgPath, "/\\") : '';
|
||||||
|
}
|
||||||
|
unset($floor);
|
||||||
|
|
||||||
|
$existingPolygon = trim((string)($room['polygon_points'] ?? ''));
|
||||||
|
$pageTitle = $isEdit ? "Raum bearbeiten: " . htmlspecialchars((string)$room['name']) : "Neuer Raum";
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="room-edit">
|
||||||
|
<h1><?php echo $pageTitle; ?></h1>
|
||||||
|
|
||||||
|
<form method="post" action="?module=rooms&action=save" class="edit-form">
|
||||||
|
<?php if ($isEdit): ?>
|
||||||
|
<input type="hidden" name="id" value="<?php echo (int)$room['id']; ?>">
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Allgemein</legend>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="room-name">Name <span class="required">*</span></label>
|
||||||
|
<input type="text" id="room-name" name="name" required value="<?php echo htmlspecialchars((string)($room['name'] ?? '')); ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="room-number">Raum-Nr.</label>
|
||||||
|
<input type="text" id="room-number" name="number" value="<?php echo htmlspecialchars((string)($room['number'] ?? '')); ?>" placeholder="z.B. EG-101">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="room-floor-id">Stockwerk <span class="required">*</span></label>
|
||||||
|
<select id="room-floor-id" name="floor_id" required>
|
||||||
|
<option value="">- Stockwerk waehlen -</option>
|
||||||
|
<?php foreach ($floors as $floor): ?>
|
||||||
|
<option value="<?php echo (int)$floor['id']; ?>"
|
||||||
|
data-svg-url="<?php echo htmlspecialchars((string)$floor['svg_url']); ?>"
|
||||||
|
<?php echo ((int)$floor['id'] === $selectedFloorId) ? 'selected' : ''; ?>>
|
||||||
|
<?php
|
||||||
|
$label = trim((string)$floor['location_name']) . ' / '
|
||||||
|
. trim((string)$floor['building_name']) . ' / '
|
||||||
|
. trim((string)$floor['name']);
|
||||||
|
if ($floor['level'] !== null) {
|
||||||
|
$label .= ' (Ebene ' . (string)$floor['level'] . ')';
|
||||||
|
}
|
||||||
|
echo htmlspecialchars($label);
|
||||||
|
?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="room-comment">Kommentar</label>
|
||||||
|
<textarea id="room-comment" name="comment" rows="3"><?php echo htmlspecialchars((string)($room['comment'] ?? '')); ?></textarea>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Polygon auf Stockwerkskarte (optional)</legend>
|
||||||
|
|
||||||
|
<input type="hidden" id="room-polygon-points" name="polygon_points" value="<?php echo htmlspecialchars($existingPolygon); ?>">
|
||||||
|
|
||||||
|
<div class="room-polygon-toolbar">
|
||||||
|
<label class="inline-checkbox">
|
||||||
|
<input type="checkbox" id="room-snap-walls" checked>
|
||||||
|
An Waenden snappen
|
||||||
|
</label>
|
||||||
|
<button type="button" class="button" id="room-undo-point">Letzten Punkt entfernen</button>
|
||||||
|
<button type="button" class="button button-danger" id="room-clear-polygon">Polygon loeschen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="room-polygon-wrap">
|
||||||
|
<svg id="room-polygon-canvas" viewBox="0 0 2000 1000" role="img" aria-label="Raumpolygon auf Stockwerkskarte"></svg>
|
||||||
|
<p class="hint" id="room-map-hint">Waehle zuerst ein Stockwerk mit Karte. Dann Punkte per Klick setzen, Punkte per Drag verschieben.</p>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="form-actions">
|
||||||
|
<button type="submit" class="button button-primary">Speichern</button>
|
||||||
|
<a href="?module=locations&action=list" class="button">Abbrechen</a>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.room-edit {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 20px auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-form {
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-form fieldset {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-form legend {
|
||||||
|
padding: 0 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input[type="text"],
|
||||||
|
.form-group select,
|
||||||
|
.form-group textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-polygon-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-polygon-wrap {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#room-polygon-canvas {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 560px;
|
||||||
|
display: block;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
background: #fafafa;
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #666;
|
||||||
|
margin: 8px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-checkbox {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 15px;
|
||||||
|
background: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-primary {
|
||||||
|
background: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-danger {
|
||||||
|
background: #dc3545;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script src="/assets/js/room-polygon-editor.js" defer></script>
|
||||||
12
app/modules/rooms/list.php
Normal file
12
app/modules/rooms/list.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* app/modules/rooms/list.php
|
||||||
|
*
|
||||||
|
* Raumverwaltung ist in die Standorte-Liste integriert.
|
||||||
|
*/
|
||||||
|
?>
|
||||||
|
<div class="connections-container">
|
||||||
|
<h1>Raeume</h1>
|
||||||
|
<p>Die Raumverwaltung befindet sich unter Standorte.</p>
|
||||||
|
<p><a class="button" href="?module=locations&action=list">Zu Standorte wechseln</a></p>
|
||||||
|
</div>
|
||||||
125
app/modules/rooms/save.php
Normal file
125
app/modules/rooms/save.php
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* app/modules/rooms/save.php
|
||||||
|
*
|
||||||
|
* Speichert oder aktualisiert einen Raum.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
header('Location: ?module=locations&action=list');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$roomId = (int)($_POST['id'] ?? 0);
|
||||||
|
$name = trim((string)($_POST['name'] ?? ''));
|
||||||
|
$number = trim((string)($_POST['number'] ?? ''));
|
||||||
|
$floorId = (int)($_POST['floor_id'] ?? 0);
|
||||||
|
$comment = trim((string)($_POST['comment'] ?? ''));
|
||||||
|
$rawPolygon = trim((string)($_POST['polygon_points'] ?? ''));
|
||||||
|
|
||||||
|
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";
|
||||||
|
header("Location: $redirect");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$polygonPoints = [];
|
||||||
|
$polygonJson = null;
|
||||||
|
$x = null;
|
||||||
|
$y = null;
|
||||||
|
$width = null;
|
||||||
|
$height = null;
|
||||||
|
|
||||||
|
if ($rawPolygon !== '') {
|
||||||
|
$decoded = json_decode($rawPolygon, true);
|
||||||
|
if (is_array($decoded)) {
|
||||||
|
foreach ($decoded as $point) {
|
||||||
|
if (!is_array($point)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$px = isset($point['x']) ? (float)$point['x'] : null;
|
||||||
|
$py = isset($point['y']) ? (float)$point['y'] : null;
|
||||||
|
if (!is_finite($px) || !is_finite($py)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$polygonPoints[] = [
|
||||||
|
'x' => (int)round($px),
|
||||||
|
'y' => (int)round($py),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($polygonPoints) >= 3) {
|
||||||
|
$xs = array_column($polygonPoints, 'x');
|
||||||
|
$ys = array_column($polygonPoints, 'y');
|
||||||
|
$minX = min($xs);
|
||||||
|
$maxX = max($xs);
|
||||||
|
$minY = min($ys);
|
||||||
|
$maxY = max($ys);
|
||||||
|
$x = (int)$minX;
|
||||||
|
$y = (int)$minY;
|
||||||
|
$width = (int)max(1, $maxX - $minX);
|
||||||
|
$height = (int)max(1, $maxY - $minY);
|
||||||
|
$polygonJson = json_encode($polygonPoints, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roomsHasPolygonColumn($sql)) {
|
||||||
|
if ($roomId > 0) {
|
||||||
|
$sql->set(
|
||||||
|
"UPDATE rooms
|
||||||
|
SET floor_id = ?, name = ?, number = ?, x = ?, y = ?, width = ?, height = ?, polygon_points = ?, comment = ?
|
||||||
|
WHERE id = ?",
|
||||||
|
"issiiiissi",
|
||||||
|
[$floorId, $name, $number, $x, $y, $width, $height, $polygonJson, $comment, $roomId]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$sql->set(
|
||||||
|
"INSERT INTO rooms (floor_id, name, number, x, y, width, height, polygon_points, comment)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
"issiiiiss",
|
||||||
|
[$floorId, $name, $number, $x, $y, $width, $height, $polygonJson, $comment]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($roomId > 0) {
|
||||||
|
$sql->set(
|
||||||
|
"UPDATE rooms
|
||||||
|
SET floor_id = ?, name = ?, number = ?, x = ?, y = ?, width = ?, height = ?, comment = ?
|
||||||
|
WHERE id = ?",
|
||||||
|
"issiiiisi",
|
||||||
|
[$floorId, $name, $number, $x, $y, $width, $height, $comment, $roomId]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$sql->set(
|
||||||
|
"INSERT INTO rooms (floor_id, name, number, x, y, width, height, comment)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
"issiiiis",
|
||||||
|
[$floorId, $name, $number, $x, $y, $width, $height, $comment]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION['success'] = $roomId > 0 ? 'Raum gespeichert' : 'Raum erstellt';
|
||||||
|
header('Location: ?module=locations&action=list');
|
||||||
|
exit;
|
||||||
|
|
||||||
|
function roomsHasPolygonColumn($sql)
|
||||||
|
{
|
||||||
|
static $hasColumn = null;
|
||||||
|
if ($hasColumn !== null) {
|
||||||
|
return $hasColumn;
|
||||||
|
}
|
||||||
|
$col = $sql->single("SHOW COLUMNS FROM rooms LIKE 'polygon_points'", "", []);
|
||||||
|
$hasColumn = !empty($col);
|
||||||
|
return $hasColumn;
|
||||||
|
}
|
||||||
@@ -1,19 +1,16 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* footer.php
|
* footer.php
|
||||||
*
|
|
||||||
* HTML-Footer, Scripts, evtl. Modale oder Notifications
|
|
||||||
* Wird am Ende jeder Seite eingebunden
|
|
||||||
*/
|
*/
|
||||||
?>
|
?>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>© <?php echo date('Y'); ?> Troy Grunt - NDT</p>
|
<p>© <?php echo date('Y'); ?> Troy Grunt - NDT</p>
|
||||||
|
<p class="footer-meta">
|
||||||
<!-- TODO: Optional: Statusanzeige, Debug-Info, Session-Hinweis -->
|
Umgebung: <?php echo defined('APP_ENV') ? htmlspecialchars(APP_ENV) : 'unknown'; ?>
|
||||||
|
| Session: <?php echo session_id() !== '' ? 'aktiv' : 'inaktiv'; ?>
|
||||||
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<!-- TODO: evtl. JS für modale Fenster oder Flash Messages -->
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -10,7 +10,10 @@
|
|||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="description" content="Netwatch - Netzwerk-Dokumentation und Verkabelungsverwaltung">
|
||||||
<title>Netzwerk-Dokumentation</title>
|
<title>Netzwerk-Dokumentation</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/icons/favicon.svg">
|
||||||
|
|
||||||
<!-- CSS -->
|
<!-- CSS -->
|
||||||
<link rel="stylesheet" href="/assets/css/app.css">
|
<link rel="stylesheet" href="/assets/css/app.css">
|
||||||
@@ -18,10 +21,10 @@
|
|||||||
|
|
||||||
<!-- 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>
|
||||||
|
|
||||||
<!-- TODO: Meta-Tags, Favicon -->
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
@@ -61,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; ?>
|
||||||
|
|||||||
@@ -2,25 +2,18 @@
|
|||||||
/**
|
/**
|
||||||
* layout.php
|
* layout.php
|
||||||
*
|
*
|
||||||
* Grundlayout: Header + Content + Footer
|
* Basislayout fuer Header + Content + Footer.
|
||||||
* Kann als Basis-Template dienen, falls Module HTML ausgeben
|
|
||||||
*
|
|
||||||
* Beispiel-Aufruf in Modul:
|
|
||||||
* include __DIR__ . '/../templates/layout.php';
|
|
||||||
*
|
|
||||||
* TODO: In Zukunft: zentrales Template-System (z.B. mit $content)
|
|
||||||
*/
|
*/
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<?php include __DIR__ . '/header.php'; ?>
|
<?php include __DIR__ . '/header.php'; ?>
|
||||||
|
|
||||||
<div class="content-wrapper">
|
<div class="content-wrapper">
|
||||||
<!-- TODO: Dynamischen Content hier einfügen -->
|
|
||||||
<?php
|
<?php
|
||||||
if (isset($content)) {
|
if (isset($content)) {
|
||||||
echo $content;
|
echo $content;
|
||||||
} else {
|
} else {
|
||||||
echo "<p>Inhalt fehlt</p>";
|
echo '<p>Kein Inhalt uebergeben.</p>';
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ Ein Raum innerhalb eines Stockwerks.
|
|||||||
|
|
||||||
**Grafische Attribute**
|
**Grafische Attribute**
|
||||||
- `x`, `y`, `width`, `height` zur visuellen Darstellung im Stockwerks-SVG
|
- `x`, `y`, `width`, `height` zur visuellen Darstellung im Stockwerks-SVG
|
||||||
|
- `polygon_points` (JSON) fuer optionale Freiform-Polygone auf Basis der Stockwerkskarte
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -123,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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
17
init.sql
17
init.sql
@@ -44,9 +44,9 @@ INSERT INTO `buildings` (`id`, `location_id`, `name`, `comment`) VALUES
|
|||||||
CREATE TABLE `connections` (
|
CREATE TABLE `connections` (
|
||||||
`id` int(11) NOT NULL,
|
`id` int(11) NOT NULL,
|
||||||
`connection_type_id` int(11) NOT NULL,
|
`connection_type_id` int(11) NOT NULL,
|
||||||
`port_a_type` enum('device','module','outlet') NOT NULL,
|
`port_a_type` enum('device','module','outlet','patchpanel') NOT NULL,
|
||||||
`port_a_id` int(11) NOT NULL,
|
`port_a_id` int(11) NOT NULL,
|
||||||
`port_b_type` enum('device','module','outlet') NOT NULL,
|
`port_b_type` enum('device','module','outlet','patchpanel') NOT NULL,
|
||||||
`port_b_id` int(11) NOT NULL,
|
`port_b_id` int(11) NOT NULL,
|
||||||
`vlan_config` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`vlan_config`)),
|
`vlan_config` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`vlan_config`)),
|
||||||
`mode` varchar(50) DEFAULT NULL,
|
`mode` varchar(50) DEFAULT NULL,
|
||||||
@@ -70,6 +70,13 @@ CREATE TABLE `connection_types` (
|
|||||||
`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;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Daten fuer Tabelle `connection_types`
|
||||||
|
--
|
||||||
|
|
||||||
|
INSERT INTO `connection_types` (`id`, `name`, `medium`, `duplex`, `max_speed`, `color`, `line_style`, `comment`) VALUES
|
||||||
|
(1, 'Default', 'copper', 'custom', NULL, NULL, 'solid', 'Auto-created default type');
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
--
|
--
|
||||||
@@ -369,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.
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
@@ -428,6 +438,7 @@ CREATE TABLE `rooms` (
|
|||||||
`y` int(11) DEFAULT NULL,
|
`y` int(11) DEFAULT NULL,
|
||||||
`width` int(11) DEFAULT NULL,
|
`width` int(11) DEFAULT NULL,
|
||||||
`height` int(11) DEFAULT NULL,
|
`height` int(11) DEFAULT NULL,
|
||||||
|
`polygon_points` longtext DEFAULT NULL,
|
||||||
`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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user