Compare commits
60 Commits
98fac55ffd
...
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 | |||
| a3799dd8f5 | |||
| b55b9729af | |||
| d3ae285aba | |||
| 444c802756 | |||
| 24b2980d76 | |||
| 09e568d4d1 | |||
| 443b9ece63 | |||
| 9054dffdd5 | |||
| be3bbf01f2 | |||
| 985fc05aa1 | |||
| 88c8a3fd58 | |||
| 2c1a8c2a1f | |||
| 365b1a7da4 | |||
| 84720db13d |
0
.gitignore
vendored
Normal file
0
.gitignore
vendored
Normal file
58
AGENTS.md
Normal file
58
AGENTS.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
Codex arbeitet pragmatisch bei Aufgaben aus `NEXT.md` und User-Requests.
|
||||||
|
Ein Gitea-Issue ist optional.
|
||||||
|
|
||||||
|
## Kernregeln
|
||||||
|
|
||||||
|
1. Ein Issue ist **nicht erforderlich**, um eine Aufgabe umzusetzen.
|
||||||
|
2. Skills duerfen jederzeit verwendet werden (z. B. `gitea-issues`).
|
||||||
|
3. Ein `NEXT.md`-Punkt darf erst auf erledigt (`[x]`) gesetzt werden, wenn die Umsetzung im Code erfolgt ist.
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
Beispiel mit mehreren Issues:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Kurzbeschreibung der Aenderung
|
||||||
|
|
||||||
|
closes #12
|
||||||
|
closes #18
|
||||||
|
```
|
||||||
|
|
||||||
|
## Format fuer NEXT.md
|
||||||
|
|
||||||
|
- Offen ohne Issue:
|
||||||
|
- `- [ ] //TODO Backup-Runbook erstellen`
|
||||||
|
- Offen mit Issue:
|
||||||
|
- `- [ ] [#42] //TODO Backup-Runbook erstellen`
|
||||||
|
- Erledigt mit Issue:
|
||||||
|
- `- [x] [#42] Backup-Runbook erstellen`
|
||||||
|
- Erledigt ohne Issue:
|
||||||
|
- `- [x] Backup-Runbook erstellen`
|
||||||
|
|
||||||
|
## Annahme
|
||||||
|
|
||||||
|
- Gitea ist so konfiguriert, dass `closes #<id>` in Commit-Messages das Issue schliesst.
|
||||||
6
BUGS.md
6
BUGS.md
@@ -1,6 +0,0 @@
|
|||||||
# gefundene bugs
|
|
||||||
- [?] device löschen geht nicht
|
|
||||||
- [?] device_types svg modul malen
|
|
||||||
- [?] ports drag n drop funktioniert nicht
|
|
||||||
- device _type soll schon aus dem 19zoll und he größe einen initialees rechteck erzeugen, welches als device grundgerüst funktionieren soll.
|
|
||||||
- beim dev typ machen, klick auf obj typ button, dann durch drag and drop die diagonale ziehen mit loslassen fixieren
|
|
||||||
@@ -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
|
|
||||||
|
|
||||||
18
README.md
18
README.md
@@ -232,8 +232,26 @@ Verbindungen werden:
|
|||||||
- Netzwerkdosen:
|
- Netzwerkdosen:
|
||||||
- anklickbar
|
- anklickbar
|
||||||
- Ports sichtbar
|
- Ports sichtbar
|
||||||
|
- Patchpanels:
|
||||||
|
- Fest installierte Patchfelder, die im Raum- bzw. Stockwerksplan als eigene Objekte auftauchen.
|
||||||
|
- Sie stammen nicht aus dem `devices`-Modul, sondern zeigen die permanente Verdrahtung zwischen Patchpanels und Anschlussdosen.
|
||||||
|
- 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
|
||||||
|
|
||||||
|
### Patchpanel-Infrastruktur (Status: 18. Februar 2026)
|
||||||
|
- [x] Floorplans erweitert: Patchpanels können als feste Infrastrukturobjekte (ohne Rack-Device) inkl. `x/y` und Größe verwaltet werden.
|
||||||
|
- [x] Backend + SVG-Editor angepasst: Patchpanel-Ports werden über `floor_patchpanel_ports` gepflegt.
|
||||||
|
- [x] Patchpanel ↔ Patchpanel und Patchpanel ↔ Netzwerkbuchse werden über `connections` verwaltet.
|
||||||
|
- [x] SQL-Tabellen `floor_patchpanels` / `floor_patchpanel_ports` sind im Schema enthalten.
|
||||||
|
- [ ] Floorplan-Filter/Legend und erweiterte Suche für Infrastrukturobjekte weiter ausbauen.
|
||||||
|
|
||||||
|
### Stockwerksinfrastruktur-Modul
|
||||||
|
- 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.
|
||||||
|
- 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.
|
||||||
|
- [x] SVG-Editor für diese Objekte ist mit Drag & Drop umgesetzt und direkt mit dem Modul verbunden.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Datenbank (konzeptionell)
|
## Datenbank (konzeptionell)
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
'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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!isset($usage[$endpointType][$endpointId])) {
|
||||||
|
$usage[$endpointType][$endpointId] = [
|
||||||
|
'total' => 0,
|
||||||
|
'fixed' => 0,
|
||||||
|
'patch' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Kontext definieren (Standort, Rack, Floor, gesamtes Netz)
|
$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'];
|
||||||
|
$exists = $sql->single('SELECT id FROM connection_types WHERE id = ?', 'i', [$requestedId]);
|
||||||
|
if (!$exists) {
|
||||||
|
jsonError('connection_type_id existiert nicht', 400);
|
||||||
|
}
|
||||||
|
return $requestedId;
|
||||||
|
}
|
||||||
|
|
||||||
if (!$data) {
|
$defaultType = $sql->single('SELECT id FROM connection_types ORDER BY id ASC LIMIT 1');
|
||||||
http_response_code(400);
|
if (!$defaultType) {
|
||||||
echo json_encode(['error' => 'Ungültige JSON-Daten']);
|
jsonError('Kein Verbindungstyp vorhanden', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)$defaultType['id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveConnection($sql): void
|
||||||
|
{
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
jsonError('Methode nicht erlaubt', 405);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
if (!is_array($data)) {
|
||||||
|
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: Validierung
|
|
||||||
// - port_a_id vorhanden
|
|
||||||
// - port_b_id vorhanden
|
|
||||||
// - 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(
|
$id = $sql->set(
|
||||||
"INSERT INTO connections
|
'INSERT INTO connections (connection_type_id, port_a_type, port_a_id, port_b_type, port_b_id, vlan_config, mode, comment)
|
||||||
(connection_type_id, port_a_id, port_b_id, vlan, mode, comment)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||||
VALUES (?, ?, ?, ?, ?, ?)",
|
'isisisss',
|
||||||
"iiiiss",
|
[$connectionTypeId, $portAType, $portAId, $portBType, $portBId, $vlanConfig, $mode, $comment],
|
||||||
[
|
|
||||||
$data['connection_type_id'],
|
|
||||||
$data['port_a_id'],
|
|
||||||
$data['port_b_id'],
|
|
||||||
$data['vlan'],
|
|
||||||
$data['mode'],
|
|
||||||
$data['comment']
|
|
||||||
],
|
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
echo json_encode([
|
if ($id === false) {
|
||||||
'status' => 'created',
|
jsonError('Insert fehlgeschlagen', 500);
|
||||||
'id' => $id
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
echo json_encode(['status' => 'created', 'id' => $id]);
|
||||||
* Löscht eine Verbindung
|
}
|
||||||
*/
|
|
||||||
function deleteConnection($sql)
|
function deleteConnection($sql): void
|
||||||
{
|
{
|
||||||
$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 Verbindung existiert
|
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||||
|
if ($id <= 0) {
|
||||||
$rows = $sql->set(
|
jsonError('ID fehlt', 400);
|
||||||
"DELETE FROM connections WHERE id = ?",
|
}
|
||||||
"i",
|
|
||||||
[$id]
|
$existing = $sql->single('SELECT id FROM connections WHERE id = ?', 'i', [$id]);
|
||||||
);
|
if (!$existing) {
|
||||||
|
jsonError('Verbindung existiert nicht', 404);
|
||||||
echo json_encode([
|
}
|
||||||
'status' => 'deleted',
|
|
||||||
'rows' => $rows
|
$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",
|
|
||||||
[
|
|
||||||
$deviceTypeId,
|
|
||||||
$port['name'],
|
|
||||||
$port['port_type_id'],
|
|
||||||
$port['x'],
|
|
||||||
$port['y'],
|
|
||||||
$port['comment']
|
|
||||||
],
|
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if ($ok === false) {
|
||||||
|
$sql->set('ROLLBACK');
|
||||||
|
jsonError('Insert fehlgeschlagen', 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1,284 @@
|
|||||||
/* Zentrales Stylesheet (Layout, Farben, Komponenten) */
|
/* Zentrales Stylesheet (Layout, Farben, Komponenten) */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
font-family: 'Inter', 'Segoe UI', system-ui, sans-serif;
|
||||||
|
color: #1f1f1f;
|
||||||
|
background-color: #f4f5f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background-color: #f4f5f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 18px 32px;
|
||||||
|
background: linear-gradient(90deg, #0a3d62, #1d6fa5);
|
||||||
|
color: white;
|
||||||
|
gap: 20px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
box-shadow: 0 4px 20px rgba(15, 26, 45, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header__brand {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header__eyebrow {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
color: rgba(255, 255, 255, 0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header__title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-nav__list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-nav__item {
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-nav__link {
|
||||||
|
display: block;
|
||||||
|
padding: 10px 16px;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-nav__item.active .main-nav__link,
|
||||||
|
.main-nav__link:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
padding: 24px 32px 48px;
|
||||||
|
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 -------------------------------------------------- */
|
||||||
|
.filter-form {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-form form {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-form input,
|
||||||
|
.filter-form select {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connections-container {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e0e6ef;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 12px 40px rgba(15, 26, 45, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connections-list {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connections-list th {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 2px solid #ddd;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connections-list td {
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connections-list tr:hover {
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-cell {
|
||||||
|
width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge-warning {
|
||||||
|
background: #fff4e5;
|
||||||
|
color: #bb4c26;
|
||||||
|
border: 1px solid #f9c8a3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge-ok {
|
||||||
|
background: #e6f4ea;
|
||||||
|
color: #1b7333;
|
||||||
|
border: 1px solid #a6d4b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
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) {
|
||||||
|
.connections-list th,
|
||||||
|
.connections-list td {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.connections-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connections-sidebar {
|
||||||
|
position: static;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.app-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-nav__list {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
footer>p {
|
||||||
|
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,125 +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"]');
|
||||||
|
bindFormSubmitButton('#save-device', 'form[action*="module=devices"][action*="save"]');
|
||||||
|
bindFormSubmitButton('#save-floor', 'form[action*="module=floors"][action*="save"]');
|
||||||
|
bindFormSubmitButton('#save-rack', 'form[action*="module=racks"][action*="save"]');
|
||||||
|
|
||||||
// TODO: Save-Button Device-Type
|
document.querySelectorAll('[data-confirm-delete]').forEach((btn) => {
|
||||||
const saveDeviceTypeBtn = document.querySelector('#save-device-type');
|
btn.addEventListener('click', (event) => {
|
||||||
if (saveDeviceTypeBtn) {
|
event.preventDefault();
|
||||||
saveDeviceTypeBtn.addEventListener('click', (e) => {
|
const message = btn.getAttribute('data-confirm-message') || 'Aktion ausfuehren?';
|
||||||
e.preventDefault();
|
if (confirm(message)) {
|
||||||
// TODO: Save Device-Type via AJAX
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Save-Button Device
|
function bindFormSubmitButton(buttonSelector, formSelector) {
|
||||||
const saveDeviceBtn = document.querySelector('#save-device');
|
const button = document.querySelector(buttonSelector);
|
||||||
if (saveDeviceBtn) {
|
if (!button) {
|
||||||
saveDeviceBtn.addEventListener('click', (e) => {
|
return;
|
||||||
e.preventDefault();
|
}
|
||||||
// TODO: Save Device via AJAX
|
|
||||||
|
button.addEventListener('click', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const form = button.closest('form') || document.querySelector(formSelector);
|
||||||
|
if (form) {
|
||||||
|
form.requestSubmit();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Save-Button Floor
|
function ajaxPost(url, data, callback, onError) {
|
||||||
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 (Löschen, Import, Export, Filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================
|
|
||||||
// Utility Functions
|
|
||||||
// =========================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AJAX Request Helper
|
|
||||||
* @param {string} url
|
|
||||||
* @param {object} data
|
|
||||||
* @param {function} callback
|
|
||||||
*/
|
|
||||||
function ajaxPost(url, data, callback) {
|
|
||||||
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) {
|
||||||
// TODO: Dashboard-Container ermitteln
|
renderModules(container);
|
||||||
// const container = document.querySelector('#dashboard');
|
|
||||||
|
|
||||||
// TODO: Module rendern
|
|
||||||
// renderModules(container);
|
|
||||||
|
|
||||||
// TODO: Optional: Status-Daten laden (Counts, Warnings)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================
|
loadStats();
|
||||||
// Render Functions
|
showWarnings();
|
||||||
// =========================
|
renderRecentChanges();
|
||||||
|
}
|
||||||
|
|
||||||
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'),
|
||||||
// TODO: loadStats() → Anzahl Geräte, offene Ports, unverbundene Dosen
|
outlets: countRows('.infra-table tbody tr')
|
||||||
// TODO: showWarnings() → unverbundene Ports, VLAN-Konflikte
|
|
||||||
// TODO: RecentChanges() → letzte Änderungen
|
|
||||||
|
|
||||||
// =========================
|
|
||||||
// Expose Public Methods
|
|
||||||
// =========================
|
|
||||||
|
|
||||||
return {
|
|
||||||
init,
|
|
||||||
// renderModules // optional öffentlich machen
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const target = document.querySelector('[data-dashboard-stats]');
|
||||||
|
if (!target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.textContent = `Geraete: ${stats.devices} | Verbindungen: ${stats.connections} | Infrastruktur-Eintraege: ${stats.outlets}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showWarnings() {
|
||||||
|
const target = document.querySelector('[data-dashboard-warnings]');
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
const controls = {
|
const controls = {
|
||||||
startPolyline: document.getElementById('floor-start-polyline'),
|
startPolyline: document.getElementById('floor-start-polyline'),
|
||||||
finishPolyline: document.getElementById('floor-finish-polyline'),
|
finishPolyline: document.getElementById('floor-finish-polyline'),
|
||||||
|
removeLastPoint: document.getElementById('floor-remove-last-point'),
|
||||||
deletePolyline: document.getElementById('floor-delete-polyline'),
|
deletePolyline: document.getElementById('floor-delete-polyline'),
|
||||||
clearDrawing: document.getElementById('floor-clear-drawing'),
|
clearDrawing: document.getElementById('floor-clear-drawing'),
|
||||||
lock45: document.getElementById('floor-lock-45'),
|
lock45: document.getElementById('floor-lock-45'),
|
||||||
@@ -56,6 +57,30 @@
|
|||||||
render(svg, controls, state, hiddenInput);
|
render(svg, controls, state, hiddenInput);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (controls.removeLastPoint) {
|
||||||
|
controls.removeLastPoint.addEventListener('click', () => {
|
||||||
|
const targetId = state.activePolylineId || state.selectedPolylineId;
|
||||||
|
if (!targetId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const targetLine = state.polylines.find((line) => line.id === targetId);
|
||||||
|
if (!targetLine || targetLine.points.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
targetLine.points.pop();
|
||||||
|
if (targetLine.points.length === 0) {
|
||||||
|
state.polylines = state.polylines.filter((line) => line.id !== targetLine.id);
|
||||||
|
if (state.activePolylineId === targetLine.id) {
|
||||||
|
state.activePolylineId = null;
|
||||||
|
}
|
||||||
|
if (state.selectedPolylineId === targetLine.id) {
|
||||||
|
state.selectedPolylineId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
render(svg, controls, state, hiddenInput);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
controls.deletePolyline.addEventListener('click', () => {
|
controls.deletePolyline.addEventListener('click', () => {
|
||||||
if (!state.selectedPolylineId) {
|
if (!state.selectedPolylineId) {
|
||||||
return;
|
return;
|
||||||
@@ -212,7 +237,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function render(svg, controls, state, hiddenInput) {
|
function render(svg, controls, state, hiddenInput) {
|
||||||
const selected = state.polylines.find((line) => line.id === state.selectedPolylineId) || null;
|
const activeLine = state.polylines.find((line) => line.id === state.activePolylineId) || null;
|
||||||
|
const selectedLine = state.polylines.find((line) => line.id === state.selectedPolylineId) || null;
|
||||||
|
const undoLine = state.polylines.find((line) => line.id === (state.activePolylineId || state.selectedPolylineId)) || null;
|
||||||
|
|
||||||
svg.innerHTML = '';
|
svg.innerHTML = '';
|
||||||
svg.setAttribute('viewBox', `0 0 ${VIEWBOX_WIDTH} ${VIEWBOX_HEIGHT}`);
|
svg.setAttribute('viewBox', `0 0 ${VIEWBOX_WIDTH} ${VIEWBOX_HEIGHT}`);
|
||||||
@@ -227,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') {
|
||||||
@@ -258,8 +289,8 @@
|
|||||||
svg.appendChild(line);
|
svg.appendChild(line);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (selected) {
|
if (selectedLine) {
|
||||||
selected.points.forEach((point, index) => {
|
selectedLine.points.forEach((point, index) => {
|
||||||
const vertex = createSvgElement('circle');
|
const vertex = createSvgElement('circle');
|
||||||
vertex.setAttribute('cx', String(point.x));
|
vertex.setAttribute('cx', String(point.x));
|
||||||
vertex.setAttribute('cy', String(point.y));
|
vertex.setAttribute('cy', String(point.y));
|
||||||
@@ -267,7 +298,7 @@
|
|||||||
vertex.setAttribute('fill', '#ffffff');
|
vertex.setAttribute('fill', '#ffffff');
|
||||||
vertex.setAttribute('stroke', '#dc3545');
|
vertex.setAttribute('stroke', '#dc3545');
|
||||||
vertex.setAttribute('stroke-width', '3');
|
vertex.setAttribute('stroke-width', '3');
|
||||||
vertex.setAttribute('data-polyline-id', selected.id);
|
vertex.setAttribute('data-polyline-id', selectedLine.id);
|
||||||
vertex.setAttribute('data-vertex-index', String(index));
|
vertex.setAttribute('data-vertex-index', String(index));
|
||||||
svg.appendChild(vertex);
|
svg.appendChild(vertex);
|
||||||
});
|
});
|
||||||
@@ -284,6 +315,16 @@
|
|||||||
controls.guideList.appendChild(li);
|
controls.guideList.appendChild(li);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (controls.finishPolyline) {
|
||||||
|
controls.finishPolyline.disabled = !(activeLine && activeLine.points.length >= 2);
|
||||||
|
}
|
||||||
|
if (controls.removeLastPoint) {
|
||||||
|
controls.removeLastPoint.disabled = !(undoLine && undoLine.points.length > 0);
|
||||||
|
}
|
||||||
|
if (controls.deletePolyline) {
|
||||||
|
controls.deletePolyline.disabled = !selectedLine;
|
||||||
|
}
|
||||||
|
|
||||||
hiddenInput.value = buildSvgMarkup(state.polylines, state.guides);
|
hiddenInput.value = buildSvgMarkup(state.polylines, state.guides);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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 };
|
||||||
|
|
||||||
/* =========================
|
|
||||||
* Initialisierung
|
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
svgElement = document.querySelector('#network-svg');
|
|
||||||
|
|
||||||
if (!svgElement) {
|
|
||||||
console.warn('Network View: #network-svg nicht gefunden');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bindSvgEvents();
|
bindSvgEvents();
|
||||||
loadNetwork();
|
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);
|
||||||
function loadNetwork() {
|
if (CONTEXT_TYPE !== 'all') {
|
||||||
if (!CONTEXT_ID) {
|
params.set('context_id', String(CONTEXT_ID));
|
||||||
console.warn('CONTEXT_ID nicht gesetzt');
|
}
|
||||||
return;
|
return '/api/connections.php?' + params.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(`${API_LOAD_NETWORK}&context_id=${CONTEXT_ID}`)
|
function loadNetwork() {
|
||||||
.then(res => res.json())
|
fetch(buildLoadUrl())
|
||||||
.then(data => {
|
.then((res) => res.json())
|
||||||
// TODO: Datenstruktur validieren
|
.then((data) => {
|
||||||
devices = data.devices || [];
|
if (!data || !Array.isArray(data.devices) || !Array.isArray(data.connections)) {
|
||||||
connections = data.connections || [];
|
throw new Error('Antwortformat ungueltig');
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
alert('Positionen gespeichert');
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
console.error('Fehler beim Speichern', 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 };
|
||||||
|
|
||||||
/* =========================
|
|
||||||
* Initialisierung
|
|
||||||
* ========================= */
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
svgElement = document.querySelector('#device-svg');
|
|
||||||
|
|
||||||
if (!svgElement) {
|
|
||||||
console.warn('SVG Editor: #device-svg nicht gefunden');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bindSvgEvents();
|
bindSvgEvents();
|
||||||
loadPorts();
|
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)
|
|
||||||
ports = ports.filter(p => p.id !== selectedPortId);
|
|
||||||
selectedPortId = null;
|
|
||||||
|
|
||||||
rerenderPorts();
|
|
||||||
|
|
||||||
// TODO: Sidebar zurücksetzen
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
if (!confirm('Ausgewaehlten Port loeschen?')) {
|
||||||
* Laden / Speichern
|
return;
|
||||||
* ========================= */
|
}
|
||||||
|
|
||||||
|
ports = ports.filter((port) => String(port.id) !== String(selectedPortId));
|
||||||
|
selectedPortId = null;
|
||||||
|
rerenderPorts();
|
||||||
|
resetSidebar();
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
}
|
||||||
|
alert('Ports gespeichert');
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
console.error('Fehler beim Speichern', 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,42 +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
|
}
|
||||||
// z.B. $_SESSION['user'] setzen oder Login erzwingen
|
|
||||||
|
if (!isset($_SESSION['validation_errors']) || !is_array($_SESSION['validation_errors'])) {
|
||||||
|
$_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';
|
||||||
// TODO: Globale Funktionen: escape, redirect, flash messages, etc.
|
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', 'connections'];
|
$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() {
|
||||||
|
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');
|
require_once ('secret.php');
|
||||||
|
|
||||||
$this->m = $_m;
|
$this->m = $_m;
|
||||||
|
}
|
||||||
|
|
||||||
$this->h = new mysqli ( $_m ['host'], $_m ['user'], $_m ['pass'], $_m ['data'] );
|
$this->h = new mysqli ( $this->m ['host'], $this->m ['user'], $this->m ['pass'], $this->m ['data'] );
|
||||||
if ($this->h->connect_errno) {
|
if ($this->h->connect_errno) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,11 @@
|
|||||||
* KEINE Business-Logik
|
* KEINE Business-Logik
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sitzungs-Keys
|
||||||
|
*/
|
||||||
|
const FLASH_SESSION_KEY = 'flash_messages';
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
* Output / Sicherheit
|
* Output / Sicherheit
|
||||||
* ========================= */
|
* ========================= */
|
||||||
@@ -24,10 +29,13 @@
|
|||||||
*/
|
*/
|
||||||
function e(?string $value): string
|
function e(?string $value): string
|
||||||
{
|
{
|
||||||
// TODO: htmlspecialchars mit ENT_QUOTES + UTF-8
|
if ($value === null) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
* Redirects
|
* Redirects
|
||||||
* ========================= */
|
* ========================= */
|
||||||
@@ -40,7 +48,10 @@ function e(?string $value): string
|
|||||||
*/
|
*/
|
||||||
function redirect(string $url, int $code = 302): void
|
function redirect(string $url, int $code = 302): void
|
||||||
{
|
{
|
||||||
// TODO: header("Location: ...")
|
if (!headers_sent()) {
|
||||||
|
header('Location: ' . $url, true, $code);
|
||||||
|
}
|
||||||
|
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +67,15 @@ function redirect(string $url, int $code = 302): void
|
|||||||
*/
|
*/
|
||||||
function flash(string $type, string $message): void
|
function flash(string $type, string $message): void
|
||||||
{
|
{
|
||||||
// TODO: In $_SESSION speichern
|
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($_SESSION[FLASH_SESSION_KEY])) {
|
||||||
|
$_SESSION[FLASH_SESSION_KEY] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION[FLASH_SESSION_KEY][] = ['type' => $type, 'message' => $message];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,8 +85,14 @@ function flash(string $type, string $message): void
|
|||||||
*/
|
*/
|
||||||
function getFlashes(): array
|
function getFlashes(): array
|
||||||
{
|
{
|
||||||
// TODO: Aus Session lesen und löschen
|
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||||
return [];
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
$messages = $_SESSION[FLASH_SESSION_KEY] ?? [];
|
||||||
|
unset($_SESSION[FLASH_SESSION_KEY]);
|
||||||
|
|
||||||
|
return $messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
@@ -83,8 +108,7 @@ function getFlashes(): array
|
|||||||
*/
|
*/
|
||||||
function post(string $key, $default = null)
|
function post(string $key, $default = null)
|
||||||
{
|
{
|
||||||
// TODO: $_POST prüfen
|
return $_POST[$key] ?? $default;
|
||||||
return $default;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -96,8 +120,7 @@ function post(string $key, $default = null)
|
|||||||
*/
|
*/
|
||||||
function get(string $key, $default = null)
|
function get(string $key, $default = null)
|
||||||
{
|
{
|
||||||
// TODO: $_GET prüfen
|
return $_GET[$key] ?? $default;
|
||||||
return $default;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -107,8 +130,7 @@ function get(string $key, $default = null)
|
|||||||
*/
|
*/
|
||||||
function isPost(): bool
|
function isPost(): bool
|
||||||
{
|
{
|
||||||
// TODO: $_SERVER['REQUEST_METHOD']
|
return ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST';
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
@@ -123,8 +145,11 @@ function isPost(): bool
|
|||||||
*/
|
*/
|
||||||
function isEmpty($value): bool
|
function isEmpty($value): bool
|
||||||
{
|
{
|
||||||
// TODO: trim + empty
|
if (is_string($value)) {
|
||||||
return false;
|
return trim($value) === '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return empty($value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
@@ -139,10 +164,31 @@ function isEmpty($value): bool
|
|||||||
*/
|
*/
|
||||||
function url(string $path = ''): string
|
function url(string $path = ''): string
|
||||||
{
|
{
|
||||||
// TODO: Base-URL aus config.php
|
if ($path === '') {
|
||||||
|
$path = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('~^([a-z]+:)?//~i', $path)) {
|
||||||
return $path;
|
return $path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$script = $_SERVER['SCRIPT_NAME'] ?? '';
|
||||||
|
$baseDir = rtrim(strtr(dirname($script), '\\\\', '/'), '/');
|
||||||
|
|
||||||
|
if ($baseDir === '.' || $baseDir === '\\\\') {
|
||||||
|
$baseDir = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$segment = ltrim($path, '/');
|
||||||
|
$prefix = $baseDir === '' ? '' : $baseDir;
|
||||||
|
|
||||||
|
if ($segment === '') {
|
||||||
|
return $prefix === '' ? '/' : $prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ($prefix === '' ? '' : $prefix) . '/' . $segment;
|
||||||
|
}
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
* Debug / Entwicklung
|
* Debug / Entwicklung
|
||||||
* ========================= */
|
* ========================= */
|
||||||
@@ -154,7 +200,96 @@ function url(string $path = ''): string
|
|||||||
*/
|
*/
|
||||||
function dd($value): void
|
function dd($value): void
|
||||||
{
|
{
|
||||||
// TODO: var_dump / print_r + exit
|
echo '<pre>';
|
||||||
|
var_dump($value);
|
||||||
|
echo '</pre>';
|
||||||
|
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;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,8 +297,83 @@ function dd($value): void
|
|||||||
* 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]);
|
||||||
@@ -19,11 +19,13 @@ if ($buildingId > 0) {
|
|||||||
|
|
||||||
$isEdit = !empty($building);
|
$isEdit = !empty($building);
|
||||||
$pageTitle = $isEdit ? "Gebäude bearbeiten: " . htmlspecialchars($building['name']) : "Neues Gebäude";
|
$pageTitle = $isEdit ? "Gebäude bearbeiten: " . htmlspecialchars($building['name']) : "Neues Gebäude";
|
||||||
|
$prefillLocationId = (int)($_GET['location_id'] ?? 0);
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// Standorte laden
|
// Standorte laden
|
||||||
// =========================
|
// =========================
|
||||||
$locations = $sql->get("SELECT id, name FROM locations ORDER BY name", "", []);
|
$locations = $sql->get("SELECT id, name FROM locations ORDER BY name", "", []);
|
||||||
|
$selectedLocationId = $building['location_id'] ?? $prefillLocationId;
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
@@ -45,7 +47,7 @@ $locations = $sql->get("SELECT id, name FROM locations ORDER BY name", "", []);
|
|||||||
<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>
|
||||||
|
|
||||||
@@ -55,7 +57,7 @@ $locations = $sql->get("SELECT id, name FROM locations ORDER BY name", "", []);
|
|||||||
<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 ($building['location_id'] ?? 0) == $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; ?>
|
||||||
@@ -65,7 +67,7 @@ $locations = $sql->get("SELECT id, name FROM locations ORDER BY name", "", []);
|
|||||||
<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>
|
||||||
|
|
||||||
@@ -170,9 +172,25 @@ $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? 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,133 +68,289 @@ 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-layout">
|
||||||
<div class="connections-container">
|
<div class="connections-container">
|
||||||
<h1>Netzwerkverbindungen</h1>
|
<h1>Netzwerkverbindungen</h1>
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
Filter-Toolbar
|
|
||||||
========================= -->
|
|
||||||
<div class="filter-form">
|
<div class="filter-form">
|
||||||
<form method="GET" style="display: flex; gap: 10px; flex-wrap: wrap; align-items: center;">
|
<form method="GET">
|
||||||
<input type="hidden" name="module" value="connections">
|
<input type="hidden" name="module" value="connections">
|
||||||
<input type="hidden" name="action" value="list">
|
<input type="hidden" name="action" value="list">
|
||||||
|
|
||||||
<input type="text" name="search" placeholder="Suche nach Gerät oder Port…"
|
<input type="text" name="search" placeholder="Suche nach Geraet oder Port..."
|
||||||
value="<?php echo htmlspecialchars($search); ?>" class="search-input">
|
value="<?php echo htmlspecialchars($search); ?>" class="search-input">
|
||||||
|
|
||||||
<select name="device_id">
|
<select name="device_id">
|
||||||
<option value="">- Alle Geräte -</option>
|
<option value="">- Alle Geraete -</option>
|
||||||
<?php foreach ($devices as $device): ?>
|
<?php foreach ($devices as $device): ?>
|
||||||
<option value="<?php echo $device['id']; ?>"
|
<option value="<?php echo (int)$device['id']; ?>"
|
||||||
<?php echo $device['id'] === $deviceId ? 'selected' : ''; ?>>
|
<?php echo ((int)$device['id'] === $deviceId) ? 'selected' : ''; ?>>
|
||||||
<?php echo htmlspecialchars($device['name']); ?>
|
<?php echo htmlspecialchars((string)$device['name']); ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<button type="submit" class="button">Filter</button>
|
<button type="submit" class="button">Filter</button>
|
||||||
<a href="?module=connections&action=list" class="button">Reset</a>
|
<a href="?module=connections&action=list" class="button">Reset</a>
|
||||||
<a href="?module=connections&action=save" class="button button-primary" style="margin-left: auto;">+ Neue Verbindung</a>
|
<a href="?module=connections&action=edit" class="button button-primary">+ Neue Verbindung</a>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
Verbindungs-Tabelle
|
|
||||||
========================= -->
|
|
||||||
<?php if (!empty($connections)): ?>
|
<?php if (!empty($connections)): ?>
|
||||||
<table class="connections-list">
|
<table class="connections-list">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Von (Gerät → Port)</th>
|
<th>Von (Geraet -> Port)</th>
|
||||||
<th>Nach (Gerät → Port)</th>
|
<th>Nach (Geraet -> Port)</th>
|
||||||
<th>VLANs</th>
|
<th>VLANs</th>
|
||||||
<th>Beschreibung</th>
|
<th>Beschreibung</th>
|
||||||
|
<th>Status</th>
|
||||||
<th>Aktionen</th>
|
<th>Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($connections as $conn): ?>
|
<?php foreach ($connections as $conn): ?>
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<strong><?php echo htmlspecialchars($conn['device_a_name'] ?? 'N/A'); ?></strong><br>
|
|
||||||
<small><?php echo htmlspecialchars($conn['port_a_name'] ?? '—'); ?></small>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<strong><?php echo htmlspecialchars($conn['device_b_name'] ?? 'N/A'); ?></strong><br>
|
|
||||||
<small><?php echo htmlspecialchars($conn['port_b_name'] ?? '—'); ?></small>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<small>
|
|
||||||
<?php
|
<?php
|
||||||
if ($conn['vlan_config']) {
|
$connId = (int)($conn['id'] ?? 0);
|
||||||
$vlan = json_decode($conn['vlan_config'], true);
|
$comment = trim((string)($conn['comment'] ?? ''));
|
||||||
echo htmlspecialchars(implode(', ', (array)$vlan));
|
$hasMissingInfo = empty($conn['endpoint_a_name']) || empty($conn['endpoint_b_name'])
|
||||||
} else {
|
|| empty($conn['port_a_name']) || empty($conn['port_b_name']);
|
||||||
echo '—';
|
$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);
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
</small>
|
<tr class="<?php echo $rowClass; ?>">
|
||||||
|
<td>
|
||||||
|
<strong><?php echo htmlspecialchars((string)($conn['endpoint_a_name'] ?? 'N/A')); ?></strong><br>
|
||||||
|
<small><?php echo htmlspecialchars((string)($conn['port_a_name'] ?? '-')); ?></small>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<small><?php echo htmlspecialchars($conn['comment'] ?? ''); ?></small>
|
<strong><?php echo htmlspecialchars((string)($conn['endpoint_b_name'] ?? 'N/A')); ?></strong><br>
|
||||||
|
<small><?php echo htmlspecialchars((string)($conn['port_b_name'] ?? '-')); ?></small>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<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>
|
||||||
|
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<a href="?module=connections&action=edit&id=<?php echo $conn['id']; ?>" class="button button-small">Bearbeiten</a>
|
<a href="<?php echo htmlspecialchars($buildListUrl(['connection_id' => $connId])); ?>" class="button button-small">Details</a>
|
||||||
<a href="#" class="button button-small button-danger" onclick="confirmDelete(<?php echo $conn['id']; ?>)">Löschen</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<p>Keine Verbindungen gefunden.</p>
|
<p>Keine Verbindungen gefunden.</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="?module=connections&action=save" class="button button-primary">
|
<a href="?module=connections&action=edit" class="button button-primary">
|
||||||
Erste Verbindung anlegen
|
Erste Verbindung anlegen
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
@@ -165,150 +358,102 @@ $devices = $sql->get("SELECT id, name FROM devices ORDER BY name", "", []);
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<aside class="connections-sidebar">
|
||||||
.connections-container {
|
<section class="sidebar-card">
|
||||||
padding: 20px;
|
<h3>Ausgewaehlte Verbindung</h3>
|
||||||
max-width: 1200px;
|
<?php if ($selectedConnection): ?>
|
||||||
margin: 0 auto;
|
<?php
|
||||||
|
$selectedConnId = (int)($selectedConnection['id'] ?? 0);
|
||||||
|
$selectedVlans = [];
|
||||||
|
if (!empty($selectedConnection['vlan_config'])) {
|
||||||
|
$selectedVlans = (array)json_decode((string)$selectedConnection['vlan_config'], true);
|
||||||
}
|
}
|
||||||
|
?>
|
||||||
|
<p><strong>ID:</strong> #<?php echo $selectedConnId; ?></p>
|
||||||
|
<p><strong>Von:</strong><br>
|
||||||
|
<?php echo htmlspecialchars((string)($selectedConnection['endpoint_a_name'] ?? 'N/A')); ?><br>
|
||||||
|
<small><?php echo htmlspecialchars((string)($selectedConnection['port_a_name'] ?? '-')); ?></small>
|
||||||
|
</p>
|
||||||
|
<p><strong>Nach:</strong><br>
|
||||||
|
<?php echo htmlspecialchars((string)($selectedConnection['endpoint_b_name'] ?? 'N/A')); ?><br>
|
||||||
|
<small><?php echo htmlspecialchars((string)($selectedConnection['port_b_name'] ?? '-')); ?></small>
|
||||||
|
</p>
|
||||||
|
<p><strong>VLANs:</strong> <?php echo !empty($selectedVlans) ? htmlspecialchars(implode(', ', $selectedVlans)) : '-'; ?></p>
|
||||||
|
<p><strong>Kommentar:</strong> <?php echo htmlspecialchars((string)($selectedConnection['comment'] ?? '-')); ?></p>
|
||||||
|
<div class="sidebar-actions">
|
||||||
|
<a href="?module=connections&action=edit&id=<?php echo $selectedConnId; ?>" class="button button-small">Bearbeiten</a>
|
||||||
|
<a href="?module=connections&action=swap&id=<?php echo $selectedConnId; ?>" class="button button-small" onclick="return confirm('Von/Nach fuer diese Verbindung vertauschen?');">Tauschen</a>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button button-small button-danger js-connection-delete"
|
||||||
|
data-connection-id="<?php echo $selectedConnId; ?>">
|
||||||
|
Loeschen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<p><em>Keine Verbindung ausgewaehlt.</em></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
|
|
||||||
.filter-form {
|
<section class="sidebar-card">
|
||||||
margin: 20px 0;
|
<?php if ($selectedDevice): ?>
|
||||||
}
|
<h3>Ausgewaehltes Geraet</h3>
|
||||||
|
<p><strong><?php echo htmlspecialchars((string)$selectedDevice['name']); ?></strong></p>
|
||||||
.filter-form form {
|
<p>Typ: <?php echo htmlspecialchars((string)($selectedDevice['type_name'] ?? '-')); ?></p>
|
||||||
display: flex;
|
<p>Ports: <?php echo (int)$selectedDevice['port_count']; ?></p>
|
||||||
gap: 10px;
|
<p>Verbindungen: <?php echo (int)$selectedDevice['connection_count']; ?></p>
|
||||||
flex-wrap: wrap;
|
<p>
|
||||||
align-items: center;
|
VLANs:
|
||||||
}
|
<?php if (!empty($selectedDeviceVlans)): ?>
|
||||||
|
<?php echo htmlspecialchars(implode(', ', $selectedDeviceVlans)); ?>
|
||||||
.filter-form input,
|
<?php else: ?>
|
||||||
.filter-form select {
|
-
|
||||||
padding: 8px 12px;
|
<?php endif; ?>
|
||||||
border: 1px solid #ddd;
|
</p>
|
||||||
border-radius: 4px;
|
<?php if (!empty($selectedDevicePorts)): ?>
|
||||||
}
|
<h4>Ports (max. 12)</h4>
|
||||||
|
<ul>
|
||||||
.search-input {
|
<?php foreach ($selectedDevicePorts as $port): ?>
|
||||||
flex: 1;
|
<li><?php echo htmlspecialchars((string)$port['name']); ?></li>
|
||||||
min-width: 250px;
|
<?php endforeach; ?>
|
||||||
}
|
</ul>
|
||||||
|
<?php endif; ?>
|
||||||
.connections-list {
|
<?php else: ?>
|
||||||
width: 100%;
|
<h3>Ausgewaehltes Geraet</h3>
|
||||||
border-collapse: collapse;
|
<p><em>Bitte ein Geraet im Filter auswaehlen.</em></p>
|
||||||
margin: 15px 0;
|
<?php endif; ?>
|
||||||
}
|
</section>
|
||||||
|
</aside>
|
||||||
.connections-list th {
|
|
||||||
background: #f5f5f5;
|
|
||||||
padding: 12px;
|
|
||||||
text-align: left;
|
|
||||||
border-bottom: 2px solid #ddd;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connections-list td {
|
|
||||||
padding: 12px;
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connections-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>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function confirmDelete(id) {
|
|
||||||
if (confirm('Diese Verbindung wirklich löschen?')) {
|
|
||||||
// TODO: AJAX-Delete implementieren
|
|
||||||
alert('Löschen noch nicht implementiert');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
>
|
|
||||||
<!-- wird komplett per JS gerendert -->
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
Sidebar / Details
|
|
||||||
========================= -->
|
|
||||||
|
|
||||||
<aside class="sidebar">
|
|
||||||
<!-- TODO: Details zum ausgewählten Gerät anzeigen -->
|
|
||||||
<!--
|
|
||||||
- Gerätename
|
|
||||||
- Gerätetyp
|
|
||||||
- Ports
|
|
||||||
- VLANs
|
|
||||||
- Verbindungen
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- TODO: Verbindung bearbeiten / löschen -->
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
JS-Konfiguration
|
|
||||||
========================= -->
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
/**
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
* Konfiguration für network-view.js
|
document.querySelectorAll('.js-connection-delete').forEach((button) => {
|
||||||
* Wird bewusst hier gesetzt, nicht im JS selbst
|
button.addEventListener('click', () => {
|
||||||
*/
|
const id = Number(button.dataset.connectionId || '0');
|
||||||
|
if (id <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Kontext-ID aus PHP setzen
|
if (!confirm('Diese Verbindung wirklich loeschen?')) {
|
||||||
// window.NETWORK_CONTEXT_ID = <?= (int)$contextId ?>;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('?module=connections&action=delete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||||
|
body: 'id=' + encodeURIComponent(id)
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data && data.success) {
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
|
||||||
|
})
|
||||||
|
.catch(() => alert('Loeschen fehlgeschlagen'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</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,6 +15,7 @@ $deviceTypeId = (int)($_GET['id'] ?? 0);
|
|||||||
$deviceType = null;
|
$deviceType = null;
|
||||||
$ports = [];
|
$ports = [];
|
||||||
|
|
||||||
|
|
||||||
if ($deviceTypeId > 0) {
|
if ($deviceTypeId > 0) {
|
||||||
$deviceType = $sql->single(
|
$deviceType = $sql->single(
|
||||||
"SELECT * FROM device_types WHERE id = ?",
|
"SELECT * FROM device_types WHERE id = ?",
|
||||||
@@ -85,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"
|
||||||
@@ -105,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>
|
||||||
|
|
||||||
<!-- =========================
|
<!-- =========================
|
||||||
@@ -302,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>
|
||||||
@@ -317,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>
|
||||||
<!-- =========================
|
|
||||||
Zurück / Kontext
|
|
||||||
========================= -->
|
|
||||||
|
|
||||||
<div class="breadcrumb">
|
<div class="breadcrumb">
|
||||||
<a href="/?page=device_types/list">Gerätetypen</a>
|
<a href="?module=device_types&action=list">Geraetetypen</a>
|
||||||
→
|
->
|
||||||
<a href="/?page=device_types/edit&id=<?= $deviceTypeId ?>">
|
<a href="?module=device_types&action=edit&id=<?php echo (int)$deviceType['id']; ?>"><?php echo htmlspecialchars((string)$deviceType['name']); ?></a>
|
||||||
<!-- TODO: Gerätetyp-Name -->
|
-> Ports
|
||||||
Gerätetyp
|
|
||||||
</a>
|
|
||||||
→
|
|
||||||
Ports
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
Toolbar
|
|
||||||
========================= -->
|
|
||||||
|
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<button id="add-port">
|
<a class="button" href="?module=device_types&action=edit&id=<?php echo (int)$deviceType['id']; ?>">Zurueck zum Geraetetyp</a>
|
||||||
+ Port hinzufügen
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- TODO: Port-Typen verwalten -->
|
|
||||||
<!-- TODO: Import / Export -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="port-form" class="port-form" aria-hidden="true">
|
<form method="post" class="port-form">
|
||||||
|
<input type="hidden" name="form_action" value="add_port">
|
||||||
<div>
|
<div>
|
||||||
<label for="port-name">Portname</label>
|
<label for="name">Portname</label>
|
||||||
<input id="port-name" name="name" required placeholder="z. B. Gi1/0/1">
|
<input id="name" name="name" required placeholder="z. B. Gi1/0/1">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="port-type">Port-Typ</label>
|
<label for="port_type_id">Port-Typ</label>
|
||||||
<input id="port-type" name="type" required placeholder="RJ45, SFP …">
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label for="port-medium">Medium</label>
|
<label for="x">X</label>
|
||||||
<input id="port-medium" name="medium" placeholder="Kupfer, LWL …">
|
<input id="x" name="x" type="number" value="0">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="port-mode">Modus</label>
|
<label for="y">Y</label>
|
||||||
<input id="port-mode" name="mode" placeholder="Access, Trunk …">
|
<input id="y" name="y" type="number" value="0">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<button type="submit" class="button button-primary">Port hinzufuegen</button>
|
||||||
<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>
|
</form>
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
Port-Liste
|
|
||||||
========================= -->
|
|
||||||
|
|
||||||
<table class="port-list">
|
<table class="port-list">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>#</th>
|
<th>#</th>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Typ</th>
|
<th>Typ</th>
|
||||||
<th>Medium</th>
|
<th>X</th>
|
||||||
<th>Modus</th>
|
<th>Y</th>
|
||||||
<th>VLAN</th>
|
|
||||||
<th>Aktionen</th>
|
<th>Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="port-list-body">
|
<tbody>
|
||||||
|
<?php foreach ($ports as $port): ?>
|
||||||
<?php /* foreach ($ports as $port): */ ?>
|
|
||||||
<tr>
|
<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>
|
<td>
|
||||||
<!-- TODO: Port-Nummer -->
|
<form method="post" onsubmit="return confirm('Port wirklich loeschen?');" style="display:inline;">
|
||||||
1
|
<input type="hidden" name="form_action" value="delete_port">
|
||||||
</td>
|
<input type="hidden" name="port_id" value="<?php echo (int)$port['id']; ?>">
|
||||||
<td>
|
<button type="submit" class="button button-small button-danger">Loeschen</button>
|
||||||
<!-- TODO: Port-Name -->
|
</form>
|
||||||
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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php /* endforeach; */ ?>
|
<?php endforeach; ?>
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- =========================
|
<?php if ($svgUrl !== '' && ($deviceType['image_type'] ?? '') === 'svg'): ?>
|
||||||
SVG-Port-Positionierung
|
|
||||||
========================= -->
|
|
||||||
|
|
||||||
<section class="svg-port-editor-section">
|
<section class="svg-port-editor-section">
|
||||||
<h3>Port-Positionen</h3>
|
<h3>SVG Vorschau</h3>
|
||||||
|
<img src="<?php echo htmlspecialchars($svgUrl); ?>" alt="Geraetetyp SVG" style="max-width:100%; height:auto; border:1px solid #ddd;">
|
||||||
<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>
|
</section>
|
||||||
|
<?php endif; ?>
|
||||||
<!-- =========================
|
</div>
|
||||||
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) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($isCreate) {
|
||||||
seedDeviceTypePorts($sql, $deviceTypeId, $seedPortCount, $defaultPortTypeId);
|
seedDeviceTypePorts($sql, $deviceTypeId, $seedPortCount, $defaultPortTypeId);
|
||||||
|
}
|
||||||
syncDeviceTypePorts($sql, $deviceTypeId, $portRows);
|
syncDeviceTypePorts($sql, $deviceTypeId, $portRows);
|
||||||
|
|
||||||
$_SESSION['success'] = $deviceTypeId ? "Gerätetyp gespeichert" : "Fehler beim Speichern";
|
$_SESSION['success'] = $deviceTypeId ? "Gerätetyp gespeichert" : "Fehler beim Speichern";
|
||||||
|
|||||||
@@ -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,34 +22,34 @@ $params = [];
|
|||||||
|
|
||||||
if ($search !== '') {
|
if ($search !== '') {
|
||||||
$where[] = "(d.name LIKE ? OR d.serial_number LIKE ? OR dt.name LIKE ?)";
|
$where[] = "(d.name LIKE ? OR d.serial_number LIKE ? OR dt.name LIKE ?)";
|
||||||
$types .= "sss";
|
$types .= 'sss';
|
||||||
$params[] = "%$search%";
|
$params[] = "%$search%";
|
||||||
$params[] = "%$search%";
|
$params[] = "%$search%";
|
||||||
$params[] = "%$search%";
|
$params[] = "%$search%";
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($typeId > 0) {
|
if ($typeId > 0) {
|
||||||
$where[] = "d.device_type_id = ?";
|
$where[] = 'd.device_type_id = ?';
|
||||||
$types .= "i";
|
$types .= 'i';
|
||||||
$params[] = $typeId;
|
$params[] = $typeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($floorId > 0) {
|
if ($floorId > 0) {
|
||||||
$where[] = "f.id = ?";
|
$where[] = 'f.id = ?';
|
||||||
$types .= "i";
|
$types .= 'i';
|
||||||
$params[] = $floorId;
|
$params[] = $floorId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($rackId > 0) {
|
if ($rackId > 0) {
|
||||||
$where[] = "d.rack_id = ?";
|
$where[] = 'd.rack_id = ?';
|
||||||
$types .= "i";
|
$types .= 'i';
|
||||||
$params[] = $rackId;
|
$params[] = $rackId;
|
||||||
}
|
}
|
||||||
|
|
||||||
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// Geräte laden
|
// Geraete laden
|
||||||
// =========================
|
// =========================
|
||||||
$devices = $sql->get(
|
$devices = $sql->get(
|
||||||
"
|
"
|
||||||
@@ -63,7 +63,47 @@ $whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
|||||||
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,13 +239,13 @@ $racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
|
|||||||
Webconfig
|
Webconfig
|
||||||
</a>
|
</a>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
—
|
-
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<a href="?module=devices&action=edit&id=<?php echo $d['id']; ?>" class="button button-small">Bearbeiten</a>
|
<a href="?module=devices&action=edit&id=<?php echo $d['id']; ?>" class="button button-small">Bearbeiten</a>
|
||||||
<a href="#" 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>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -210,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>
|
||||||
@@ -329,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;
|
||||||
382
app/modules/floor_infrastructure/edit.php
Normal file
382
app/modules/floor_infrastructure/edit.php
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* app/modules/floor_infrastructure/edit.php
|
||||||
|
*
|
||||||
|
* Formular zum Anlegen/Bearbeiten von Patchpanels und Wandbuchsen
|
||||||
|
*/
|
||||||
|
|
||||||
|
$type = $_GET['type'] ?? 'patchpanel';
|
||||||
|
$id = (int)($_GET['id'] ?? 0);
|
||||||
|
|
||||||
|
$locations = $sql->get("SELECT id, name FROM locations ORDER BY name", "", []);
|
||||||
|
$buildings = $sql->get("SELECT id, name, location_id FROM buildings ORDER BY name", "", []);
|
||||||
|
$floors = $sql->get(
|
||||||
|
"SELECT f.*, b.name AS building_name, b.location_id, 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",
|
||||||
|
"",
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
$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;
|
||||||
|
$outlet = null;
|
||||||
|
$pageTitle = $type === 'outlet' ? 'Wandbuchse bearbeiten' : 'Patchpanel bearbeiten';
|
||||||
|
|
||||||
|
if ($type === 'patchpanel' && $id > 0) {
|
||||||
|
$panel = $sql->single(
|
||||||
|
"SELECT * FROM floor_patchpanels WHERE id = ?",
|
||||||
|
"i",
|
||||||
|
[$id]
|
||||||
|
);
|
||||||
|
if ($panel) {
|
||||||
|
$pageTitle = "Patchpanel bearbeiten: " . htmlspecialchars($panel['name']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type === 'outlet' && $id > 0) {
|
||||||
|
$outlet = $sql->single(
|
||||||
|
"SELECT * FROM network_outlets WHERE id = ?",
|
||||||
|
"i",
|
||||||
|
[$id]
|
||||||
|
);
|
||||||
|
if ($outlet) {
|
||||||
|
$pageTitle = "Wandbuchse bearbeiten: " . htmlspecialchars($outlet['name']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$panel = $panel ?? [];
|
||||||
|
$outlet = $outlet ?? [];
|
||||||
|
|
||||||
|
$selectedLocationId = 0;
|
||||||
|
$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') {
|
||||||
|
$panel['width'] = $defaultPanelSize['width'];
|
||||||
|
$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;
|
||||||
|
$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">
|
||||||
|
<link rel="stylesheet" href="/assets/css/floor-infrastructure-edit.css">
|
||||||
|
<h1><?php echo $pageTitle; ?></h1>
|
||||||
|
|
||||||
|
<form method="post" action="?module=floor_infrastructure&action=save" class="infra-edit-form">
|
||||||
|
<input type="hidden" name="type" value="<?php echo htmlspecialchars($type); ?>">
|
||||||
|
<input type="hidden" name="id" value="<?php echo $type === 'patchpanel' ? ($panel['id'] ?? $id) : ($outlet['id'] ?? $id); ?>">
|
||||||
|
|
||||||
|
<?php if ($type === 'patchpanel'): ?>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Name</label>
|
||||||
|
<input type="text" name="name" value="<?php echo htmlspecialchars($panel['name'] ?? ''); ?>" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Location</label>
|
||||||
|
<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 class="form-group">
|
||||||
|
<label>Gebäude</label>
|
||||||
|
<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 class="form-group">
|
||||||
|
<label>Stockwerk</label>
|
||||||
|
<select id="panel-floor-select" name="floor_id" required>
|
||||||
|
<option value="">- Stockwerk wählen -</option>
|
||||||
|
<?php foreach ($floors as $floor): ?>
|
||||||
|
<option value="<?php echo $floor['id']; ?>"
|
||||||
|
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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<div class="floor-plan-block">
|
||||||
|
<div id="floor-plan-canvas" class="floor-plan-canvas"
|
||||||
|
data-marker-width="<?php echo $markerWidth; ?>"
|
||||||
|
data-marker-height="<?php echo $markerHeight; ?>"
|
||||||
|
data-marker-type="patchpanel"
|
||||||
|
data-x-field="pos_x"
|
||||||
|
data-y-field="pos_y"
|
||||||
|
data-active-id="<?php echo (int)($panel['id'] ?? 0); ?>"
|
||||||
|
data-reference-panels="<?php echo htmlspecialchars(json_encode($mapPatchPanels, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ENT_QUOTES, 'UTF-8'); ?>"
|
||||||
|
data-reference-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>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Port-Anzahl</label>
|
||||||
|
<input type="number" name="port_count" value="<?php echo $panel['port_count'] ?? 0; ?>" min="0">
|
||||||
|
</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">
|
||||||
|
<label>Kommentar</label>
|
||||||
|
<textarea name="comment"><?php echo htmlspecialchars($panel['comment'] ?? ''); ?></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="info">Position und Größe folgen dem Drag-&-Drop auf dem Plan, damit alle Patchpanels einheitlich bleiben.</p>
|
||||||
|
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Name</label>
|
||||||
|
<input type="text" name="name" value="<?php echo htmlspecialchars($outlet['name'] ?? ''); ?>" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Raum</label>
|
||||||
|
<select name="room_id" id="outlet-room-select" required>
|
||||||
|
<option value="">- Raum wählen -</option>
|
||||||
|
<?php foreach ($rooms as $room): ?>
|
||||||
|
<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']); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="outlet-bind-patchpanel-port-id">Direkt mit Patchpanel-Port verbinden</label>
|
||||||
|
<select name="bind_patchpanel_port_id" id="outlet-bind-patchpanel-port-id">
|
||||||
|
<option value="">- Kein direkter Link -</option>
|
||||||
|
<?php foreach ($patchpanelPortOptions as $portOption): ?>
|
||||||
|
<?php
|
||||||
|
$portId = (int)($portOption['id'] ?? 0);
|
||||||
|
$isSelected = $selectedBindPatchpanelPortId === $portId;
|
||||||
|
$isOccupied = ((int)($portOption['is_occupied'] ?? 0) === 1);
|
||||||
|
$isDisabled = $isOccupied && !$isSelected;
|
||||||
|
$labelParts = array_filter([
|
||||||
|
(string)($portOption['floor_name'] ?? ''),
|
||||||
|
(string)($portOption['patchpanel_name'] ?? ''),
|
||||||
|
(string)($portOption['name'] ?? ''),
|
||||||
|
]);
|
||||||
|
$label = implode(' / ', $labelParts);
|
||||||
|
if ($isOccupied && !$isSelected) {
|
||||||
|
$label .= ' (belegt)';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<option
|
||||||
|
value="<?php echo $portId; ?>"
|
||||||
|
data-floor-id="<?php echo (int)($portOption['floor_id'] ?? 0); ?>"
|
||||||
|
<?php echo $isSelected ? 'selected' : ''; ?>
|
||||||
|
<?php echo $isDisabled ? 'disabled' : ''; ?>>
|
||||||
|
<?php echo htmlspecialchars($label); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<small>Nur Ports vom gewaehlten Stockwerk sind auswaehlbar. Beim Speichern wird die Verbindung automatisch erstellt.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="x" value="<?php echo (int)($outlet['x'] ?? 30); ?>">
|
||||||
|
<input type="hidden" name="y" value="<?php echo (int)($outlet['y'] ?? 30); ?>">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Stockwerkskarte</label>
|
||||||
|
<div class="floor-plan-block">
|
||||||
|
<div id="floor-plan-canvas" class="floor-plan-canvas"
|
||||||
|
data-marker-width="<?php echo $markerWidth; ?>"
|
||||||
|
data-marker-height="<?php echo $markerHeight; ?>"
|
||||||
|
data-marker-type="outlet"
|
||||||
|
data-x-field="x"
|
||||||
|
data-y-field="y"
|
||||||
|
data-active-id="<?php echo (int)($outlet['id'] ?? 0); ?>"
|
||||||
|
data-reference-panels="<?php echo htmlspecialchars(json_encode($mapPatchPanels, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ENT_QUOTES, 'UTF-8'); ?>"
|
||||||
|
data-reference-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>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Kommentar</label>
|
||||||
|
<textarea name="comment"><?php echo htmlspecialchars($outlet['comment'] ?? ''); ?></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="info">Wandbuchsen bleiben quadratisch, ihre Position wird über die Karte festgelegt.</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="button button-primary">Speichern</button>
|
||||||
|
<a href="?module=floor_infrastructure&action=list" class="button">Abbrechen</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/assets/js/floor-infrastructure-edit.js" defer></script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
288
app/modules/floor_infrastructure/list.php
Normal file
288
app/modules/floor_infrastructure/list.php
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* app/modules/floor_infrastructure/list.php
|
||||||
|
*
|
||||||
|
* Uebersicht ueber Patchpanels und Netzwerkbuchsen auf Stockwerken.
|
||||||
|
*/
|
||||||
|
|
||||||
|
$floorId = (int)($_GET['floor_id'] ?? 0);
|
||||||
|
|
||||||
|
$floors = $sql->get(
|
||||||
|
"SELECT id, name, svg_path
|
||||||
|
FROM floors
|
||||||
|
ORDER BY name",
|
||||||
|
"",
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
$where = '';
|
||||||
|
$types = '';
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if ($floorId > 0) {
|
||||||
|
$where = "WHERE p.floor_id = ?";
|
||||||
|
$types = 'i';
|
||||||
|
$params[] = $floorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$patchPanels = $sql->get(
|
||||||
|
"SELECT p.*, f.name AS floor_name
|
||||||
|
FROM floor_patchpanels p
|
||||||
|
LEFT JOIN floors f ON f.id = p.floor_id
|
||||||
|
$where
|
||||||
|
ORDER BY f.name, p.name",
|
||||||
|
$types,
|
||||||
|
$params
|
||||||
|
);
|
||||||
|
|
||||||
|
$networkOutlets = $sql->get(
|
||||||
|
"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
|
||||||
|
LEFT JOIN rooms r ON r.id = o.room_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",
|
||||||
|
"",
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
$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">
|
||||||
|
<link rel="stylesheet" href="/assets/css/floor-infrastructure-list.css">
|
||||||
|
<script src="/assets/js/floor-infrastructure-list.js" defer></script>
|
||||||
|
|
||||||
|
<h1>Stockwerksinfrastruktur</h1>
|
||||||
|
|
||||||
|
<div class="toolbar">
|
||||||
|
<a href="?module=floor_infrastructure&action=edit&type=patchpanel" class="button button-primary">
|
||||||
|
+ Patchpanel hinzufuegen
|
||||||
|
</a>
|
||||||
|
<a href="?module=floor_infrastructure&action=edit&type=outlet" class="button">
|
||||||
|
+ Wandbuchse hinzufuegen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="get" class="filter-form" id="infra-filter-form">
|
||||||
|
<input type="hidden" name="module" value="floor_infrastructure">
|
||||||
|
<input type="hidden" name="action" value="list">
|
||||||
|
|
||||||
|
<select name="floor_id" id="infra-floor-select">
|
||||||
|
<option value="">- Stockwerk waehlen -</option>
|
||||||
|
<?php foreach ($floors as $floor): ?>
|
||||||
|
<option value="<?php echo (int)$floor['id']; ?>" <?php echo ((int)$floor['id'] === $floorId) ? 'selected' : ''; ?>>
|
||||||
|
<?php echo htmlspecialchars((string)$floor['name']); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button class="button" type="submit">Filter</button>
|
||||||
|
<a href="?module=floor_infrastructure&action=list" class="button">Zuruecksetzen</a>
|
||||||
|
</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">
|
||||||
|
<h2>Patchpanels</h2>
|
||||||
|
<?php if (!empty($patchPanels)): ?>
|
||||||
|
<table class="infra-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Stockwerk</th>
|
||||||
|
<th>Position</th>
|
||||||
|
<th>Groesse</th>
|
||||||
|
<th>Ports</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($patchPanels as $panel): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?php echo htmlspecialchars((string)$panel['name']); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars((string)($panel['floor_name'] ?? '-')); ?></td>
|
||||||
|
<td><?php echo (int)$panel['pos_x'] . ' x ' . (int)$panel['pos_y']; ?></td>
|
||||||
|
<td><?php echo (int)$panel['width'] . ' x ' . (int)$panel['height']; ?></td>
|
||||||
|
<td><?php echo (int)$panel['port_count']; ?></td>
|
||||||
|
<td class="actions">
|
||||||
|
<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>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php else: ?>
|
||||||
|
<p class="empty-state">Noch keine Patchpanels definiert.</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="infra-section">
|
||||||
|
<h2>Wandbuchsen</h2>
|
||||||
|
<?php if (!empty($networkOutlets)): ?>
|
||||||
|
<table class="infra-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Stockwerk</th>
|
||||||
|
<th>Raum</th>
|
||||||
|
<th>Koordinaten</th>
|
||||||
|
<th>Ports</th>
|
||||||
|
<th>Kommentar</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($networkOutlets as $outlet): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?php echo htmlspecialchars((string)$outlet['name']); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars((string)($outlet['floor_name'] ?? '-')); ?></td>
|
||||||
|
<td>
|
||||||
|
<?php
|
||||||
|
$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">
|
||||||
|
<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>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php else: ?>
|
||||||
|
<p class="empty-state">Noch keine Wandbuchsen angelegt.</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.querySelectorAll('.js-floor-infra-delete').forEach((button) => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
const id = Number(button.dataset.deleteId || '0');
|
||||||
|
const type = (button.dataset.deleteType || '').trim();
|
||||||
|
const label = button.dataset.deleteLabel || 'Objekt';
|
||||||
|
if (id <= 0 || (type !== 'patchpanel' && type !== 'outlet')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entityLabel = type === 'patchpanel' ? 'Patchpanel' : 'Wandbuchse';
|
||||||
|
if (!confirm(entityLabel + ' "' + label + '" wirklich loeschen?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('?module=floor_infrastructure&action=delete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||||
|
body: 'id=' + encodeURIComponent(id) + '&type=' + encodeURIComponent(type)
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data && data.success) {
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
|
||||||
|
})
|
||||||
|
.catch(() => alert('Loeschen fehlgeschlagen'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
264
app/modules/floor_infrastructure/save.php
Normal file
264
app/modules/floor_infrastructure/save.php
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* app/modules/floor_infrastructure/save.php
|
||||||
|
*
|
||||||
|
* Speichert Patchpanel- und Wandbuchsen-Einträge
|
||||||
|
*/
|
||||||
|
|
||||||
|
$type = $_POST['type'] ?? '';
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($type === 'patchpanel') {
|
||||||
|
$fixedPanelWidth = 20;
|
||||||
|
$fixedPanelHeight = 5;
|
||||||
|
|
||||||
|
$name = trim($_POST['name'] ?? '');
|
||||||
|
$floorId = (int)($_POST['floor_id'] ?? 0);
|
||||||
|
$posX = (int)($_POST['pos_x'] ?? 0);
|
||||||
|
$posY = (int)($_POST['pos_y'] ?? 0);
|
||||||
|
$width = $fixedPanelWidth;
|
||||||
|
$height = $fixedPanelHeight;
|
||||||
|
$portCount = (int)($_POST['port_count'] ?? 0);
|
||||||
|
$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) {
|
||||||
|
$sql->set(
|
||||||
|
"UPDATE floor_patchpanels SET name = ?, floor_id = ?, pos_x = ?, pos_y = ?, width = ?, height = ?, port_count = ?, comment = ? WHERE id = ?",
|
||||||
|
"siiiiisii",
|
||||||
|
[$name, $floorId, $posX, $posY, $width, $height, $portCount, $comment, $id]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$panelId = (int)$sql->set(
|
||||||
|
"INSERT INTO floor_patchpanels (name, floor_id, pos_x, pos_y, width, height, port_count, comment) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
"siiiiiss",
|
||||||
|
[$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') {
|
||||||
|
$name = trim($_POST['name'] ?? '');
|
||||||
|
$roomId = (int)($_POST['room_id'] ?? 0);
|
||||||
|
$x = (int)($_POST['x'] ?? 0);
|
||||||
|
$y = (int)($_POST['y'] ?? 0);
|
||||||
|
$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) {
|
||||||
|
$sql->set(
|
||||||
|
"UPDATE network_outlets SET name = ?, room_id = ?, x = ?, y = ?, comment = ? WHERE id = ?",
|
||||||
|
"siiisi",
|
||||||
|
[$name, $roomId, $x, $y, $comment, $id]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$outletId = (int)$sql->set(
|
||||||
|
"INSERT INTO network_outlets (name, room_id, x, y, comment) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
"siiis",
|
||||||
|
[$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');
|
||||||
|
exit;
|
||||||
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
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
@@ -22,6 +22,8 @@ if ($floorId > 0) {
|
|||||||
$isEdit = !empty($floor);
|
$isEdit = !empty($floor);
|
||||||
$pageTitle = $isEdit ? "Stockwerk bearbeiten: " . htmlspecialchars($floor['name']) : "Neues Stockwerk";
|
$pageTitle = $isEdit ? "Stockwerk bearbeiten: " . htmlspecialchars($floor['name']) : "Neues Stockwerk";
|
||||||
$buildings = $sql->get("SELECT id, name FROM buildings ORDER BY name", "", []);
|
$buildings = $sql->get("SELECT id, name FROM buildings ORDER BY name", "", []);
|
||||||
|
$prefillBuildingId = (int)($_GET['building_id'] ?? 0);
|
||||||
|
$selectedBuildingId = $floor['building_id'] ?? $prefillBuildingId;
|
||||||
|
|
||||||
$existingSvgContent = '';
|
$existingSvgContent = '';
|
||||||
if (!empty($floor['svg_path'])) {
|
if (!empty($floor['svg_path'])) {
|
||||||
@@ -36,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; ?>
|
||||||
@@ -73,7 +75,7 @@ if (!empty($floor['svg_path'])) {
|
|||||||
<option value="">- Wählen -</option>
|
<option value="">- Wählen -</option>
|
||||||
<?php foreach ($buildings as $building): ?>
|
<?php foreach ($buildings as $building): ?>
|
||||||
<option value="<?php echo (int)$building['id']; ?>"
|
<option value="<?php echo (int)$building['id']; ?>"
|
||||||
<?php echo ((int)($floor['building_id'] ?? 0) === (int)$building['id']) ? 'selected' : ''; ?>>
|
<?php echo ((int)$selectedBuildingId === (int)$building['id']) ? 'selected' : ''; ?>>
|
||||||
<?php echo htmlspecialchars($building['name']); ?>
|
<?php echo htmlspecialchars($building['name']); ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -84,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">
|
||||||
@@ -97,6 +93,7 @@ if (!empty($floor['svg_path'])) {
|
|||||||
<h4>Zeichenwerkzeug</h4>
|
<h4>Zeichenwerkzeug</h4>
|
||||||
<button type="button" class="button" id="floor-start-polyline">Neue Linie starten</button>
|
<button type="button" class="button" id="floor-start-polyline">Neue Linie starten</button>
|
||||||
<button type="button" class="button" id="floor-finish-polyline">Linie beenden</button>
|
<button type="button" class="button" id="floor-finish-polyline">Linie beenden</button>
|
||||||
|
<button type="button" class="button" id="floor-remove-last-point">Letzten Punkt entfernen</button>
|
||||||
<button type="button" class="button button-danger" id="floor-delete-polyline">Ausgewählte Linie löschen</button>
|
<button type="button" class="button button-danger" id="floor-delete-polyline">Ausgewählte Linie löschen</button>
|
||||||
<button type="button" class="button button-danger" id="floor-clear-drawing">Alles löschen</button>
|
<button type="button" class="button button-danger" id="floor-clear-drawing">Alles löschen</button>
|
||||||
|
|
||||||
@@ -175,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%"];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,37 +28,81 @@ $locations = $sql->get(
|
|||||||
$params
|
$params
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$buildings = $sql->get(
|
||||||
|
"SELECT b.id, b.location_id, b.name, b.comment
|
||||||
|
FROM buildings b
|
||||||
|
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 = [];
|
||||||
|
foreach ($buildings as $building) {
|
||||||
|
$buildingsByLocation[(int)$building['location_id']][] = $building;
|
||||||
|
}
|
||||||
|
|
||||||
|
$floorsByBuilding = [];
|
||||||
|
foreach ($floors as $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>
|
||||||
@@ -72,146 +110,144 @@ $locations = $sql->get(
|
|||||||
<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>
|
||||||
|
|
||||||
<style>
|
<section class="hierarchy-section">
|
||||||
.locations-container {
|
<h2>Gebaeude, Stockwerke und Raeume</h2>
|
||||||
padding: 20px;
|
|
||||||
max-width: 1000px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-form {
|
<?php if (!empty($locations)): ?>
|
||||||
margin: 20px 0;
|
<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 $locationBuildings = $buildingsByLocation[(int)$location['id']] ?? []; ?>
|
||||||
|
<tr class="hierarchy-row hierarchy-row--location">
|
||||||
|
<td class="hierarchy-cell hierarchy-cell--location" colspan="4">
|
||||||
|
<strong><?php echo htmlspecialchars((string)$location['name']); ?></strong>
|
||||||
|
<span class="hierarchy-meta">(<?php echo (int)$location['building_count']; ?> Gebaeude)</span>
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
<td class="actions hierarchy-actions">
|
||||||
|
<a href="?module=buildings&action=edit&location_id=<?php echo (int)$location['id']; ?>" class="button button-small">+ Gebaeude</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
.filter-form form {
|
<?php if (!empty($locationBuildings)): ?>
|
||||||
display: flex;
|
<?php foreach ($locationBuildings as $building): ?>
|
||||||
gap: 10px;
|
<?php $buildingFloors = $floorsByBuilding[(int)$building['id']] ?? []; ?>
|
||||||
flex-wrap: wrap;
|
<tr class="hierarchy-row hierarchy-row--building">
|
||||||
align-items: center;
|
<td class="hierarchy-cell hierarchy-cell--empty"> </td>
|
||||||
}
|
<td class="hierarchy-cell hierarchy-cell--building" colspan="3"><?php echo htmlspecialchars((string)$building['name']); ?></td>
|
||||||
|
<td>
|
||||||
|
<?php if (!empty($building['comment'])): ?>
|
||||||
|
<span class="hierarchy-meta"><?php echo htmlspecialchars((string)$building['comment']); ?></span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="hierarchy-meta hierarchy-meta--muted">Kein Kommentar</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td class="actions hierarchy-actions">
|
||||||
|
<a href="?module=buildings&action=edit&id=<?php echo (int)$building['id']; ?>" class="button button-small">Bearbeiten</a>
|
||||||
|
<button type="button" class="button button-small button-danger js-delete-building" data-building-id="<?php echo (int)$building['id']; ?>">Loeschen</button>
|
||||||
|
<a href="?module=floors&action=edit&building_id=<?php echo (int)$building['id']; ?>" class="button button-small">+ Stockwerk</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
.filter-form input {
|
<?php if (!empty($buildingFloors)): ?>
|
||||||
padding: 8px 12px;
|
<?php foreach ($buildingFloors as $floor): ?>
|
||||||
border: 1px solid #ddd;
|
<?php $floorRooms = $roomsByFloor[(int)$floor['id']] ?? []; ?>
|
||||||
border-radius: 4px;
|
<tr class="hierarchy-row hierarchy-row--floor">
|
||||||
}
|
<td class="hierarchy-cell hierarchy-cell--empty"> </td>
|
||||||
|
<td class="hierarchy-cell hierarchy-cell--empty"> </td>
|
||||||
|
<td class="hierarchy-cell hierarchy-cell--floor" colspan="2"><?php echo htmlspecialchars((string)$floor['name']); ?></td>
|
||||||
|
<td>
|
||||||
|
<?php if ($floor['level'] !== null): ?>
|
||||||
|
<span class="hierarchy-meta">Ebene <?php echo htmlspecialchars((string)$floor['level']); ?></span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="hierarchy-meta hierarchy-meta--muted">Keine Ebene</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<span class="hierarchy-meta"> | <?php echo (int)$floor['room_count']; ?> Raeume</span>
|
||||||
|
</td>
|
||||||
|
<td class="actions hierarchy-actions">
|
||||||
|
<a href="?module=floors&action=edit&id=<?php echo (int)$floor['id']; ?>" class="button button-small">Bearbeiten</a>
|
||||||
|
<button type="button" class="button button-small button-danger js-delete-floor" data-floor-id="<?php echo (int)$floor['id']; ?>">Loeschen</button>
|
||||||
|
<a href="?module=rooms&action=edit&floor_id=<?php echo (int)$floor['id']; ?>" class="button button-small">+ Raum</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
.search-input {
|
<?php if (!empty($floorRooms)): ?>
|
||||||
flex: 1;
|
<?php foreach ($floorRooms as $room): ?>
|
||||||
min-width: 250px;
|
<tr class="hierarchy-row hierarchy-row--room">
|
||||||
}
|
<td class="hierarchy-cell hierarchy-cell--empty"> </td>
|
||||||
|
<td class="hierarchy-cell hierarchy-cell--empty"> </td>
|
||||||
.locations-list {
|
<td class="hierarchy-cell hierarchy-cell--empty"> </td>
|
||||||
width: 100%;
|
<td class="hierarchy-cell hierarchy-cell--room">
|
||||||
border-collapse: collapse;
|
<?php echo htmlspecialchars((string)$room['name']); ?>
|
||||||
margin: 15px 0;
|
<?php if (!empty($room['number'])): ?>
|
||||||
}
|
<span class="hierarchy-meta">(<?php echo htmlspecialchars((string)$room['number']); ?>)</span>
|
||||||
|
<?php endif; ?>
|
||||||
.locations-list th {
|
</td>
|
||||||
background: #f5f5f5;
|
<td>
|
||||||
padding: 12px;
|
<span class="hierarchy-meta"><?php echo (int)$room['outlet_count']; ?> Dosen</span>
|
||||||
text-align: left;
|
<?php if (!empty($room['comment'])): ?>
|
||||||
border-bottom: 2px solid #ddd;
|
<span class="hierarchy-meta"> | <?php echo htmlspecialchars((string)$room['comment']); ?></span>
|
||||||
font-weight: bold;
|
<?php endif; ?>
|
||||||
}
|
</td>
|
||||||
|
<td class="actions hierarchy-actions">
|
||||||
.locations-list td {
|
<a href="?module=rooms&action=edit&id=<?php echo (int)$room['id']; ?>" class="button button-small">Bearbeiten</a>
|
||||||
padding: 12px;
|
<button type="button" class="button button-small button-danger js-delete-room" data-room-id="<?php echo (int)$room['id']; ?>">Loeschen</button>
|
||||||
border-bottom: 1px solid #ddd;
|
</td>
|
||||||
}
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
.locations-list tr:hover {
|
<?php else: ?>
|
||||||
background: #f9f9f9;
|
<tr class="hierarchy-row hierarchy-row--empty">
|
||||||
}
|
<td class="hierarchy-cell hierarchy-cell--empty"> </td>
|
||||||
|
<td class="hierarchy-cell hierarchy-cell--empty"> </td>
|
||||||
.actions {
|
<td class="hierarchy-cell hierarchy-cell--room hierarchy-meta hierarchy-meta--muted" colspan="2">Keine Raeume</td>
|
||||||
white-space: nowrap;
|
<td><span class="hierarchy-meta hierarchy-meta--muted">Fuer dieses Stockwerk sind noch keine Raeume angelegt.</span></td>
|
||||||
}
|
<td></td>
|
||||||
|
</tr>
|
||||||
.button {
|
<?php endif; ?>
|
||||||
display: inline-block;
|
<?php endforeach; ?>
|
||||||
padding: 8px 12px;
|
<?php endif; ?>
|
||||||
background: #007bff;
|
<?php endforeach; ?>
|
||||||
color: white;
|
<?php else: ?>
|
||||||
text-decoration: none;
|
<tr class="hierarchy-row hierarchy-row--empty">
|
||||||
border-radius: 4px;
|
<td class="hierarchy-cell hierarchy-cell--empty"> </td>
|
||||||
border: none;
|
<td class="hierarchy-cell hierarchy-cell--empty" colspan="3">Keine Gebaeude</td>
|
||||||
cursor: pointer;
|
<td><span class="hierarchy-meta hierarchy-meta--muted">Fuer diesen Standort sind noch keine Gebaeude vorhanden.</span></td>
|
||||||
font-size: 0.9em;
|
<td></td>
|
||||||
}
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
.button:hover {
|
<?php endforeach; ?>
|
||||||
background: #0056b3;
|
</tbody>
|
||||||
}
|
</table>
|
||||||
|
<?php else: ?>
|
||||||
.button-primary {
|
<p>Keine Standorte gefunden.</p>
|
||||||
background: #28a745;
|
<?php endif; ?>
|
||||||
}
|
</section>
|
||||||
|
|
||||||
.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>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function confirmDelete(id) {
|
|
||||||
if (confirm('Diesen Standort wirklich löschen?')) {
|
|
||||||
// TODO: AJAX-Delete implementieren
|
|
||||||
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;
|
||||||
151
app/modules/port_types/edit.php
Normal file
151
app/modules/port_types/edit.php
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* app/modules/port_types/edit.php
|
||||||
|
*
|
||||||
|
* Porttyp erstellen / bearbeiten
|
||||||
|
*/
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Kontext bestimmen
|
||||||
|
// =========================
|
||||||
|
$portTypeId = (int)($_GET['id'] ?? 0);
|
||||||
|
$portType = null;
|
||||||
|
|
||||||
|
if ($portTypeId > 0) {
|
||||||
|
$portType = $sql->single(
|
||||||
|
"SELECT * FROM port_types WHERE id = ?",
|
||||||
|
"i",
|
||||||
|
[$portTypeId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$isEdit = !empty($portType);
|
||||||
|
$pageTitle = $isEdit ? "Porttyp bearbeiten: " . htmlspecialchars($portType['name']) : "Neuen Porttyp anlegen";
|
||||||
|
$mediaOptions = ['copper' => 'Kupfer', 'fiber' => 'Lichtwelle', 'coax' => 'Koax', 'other' => 'Sonstiges'];
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="port-type-edit">
|
||||||
|
<h1><?php echo $pageTitle; ?></h1>
|
||||||
|
|
||||||
|
<form method="post" action="?module=port_types&action=save" class="edit-form">
|
||||||
|
|
||||||
|
<?php if ($isEdit): ?>
|
||||||
|
<input type="hidden" name="id" value="<?php echo $portTypeId; ?>">
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Details</legend>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Name <span class="required">*</span></label>
|
||||||
|
<input type="text" id="name" name="name" required
|
||||||
|
value="<?php echo htmlspecialchars($portType['name'] ?? ''); ?>"
|
||||||
|
placeholder="z.B. RJ45 Gbit">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="medium">Medium</label>
|
||||||
|
<select id="medium" name="medium">
|
||||||
|
<?php foreach ($mediaOptions as $value => $label): ?>
|
||||||
|
<option value="<?php echo $value; ?>" <?php echo (($portType['medium'] ?? '') === $value) ? 'selected' : ''; ?>>
|
||||||
|
<?php echo $label; ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="comment">Beschreibung</label>
|
||||||
|
<textarea id="comment" name="comment" rows="4"><?php echo htmlspecialchars($portType['comment'] ?? ''); ?></textarea>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="form-actions">
|
||||||
|
<button type="submit" class="button button-primary">Speichern</button>
|
||||||
|
<a href="?module=port_types&action=list" class="button">Zurück zur Liste</a>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.port-type-edit {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 20px auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-form {
|
||||||
|
background: white;
|
||||||
|
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;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
padding: 10px 15px;
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-primary {
|
||||||
|
background: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
213
app/modules/port_types/list.php
Normal file
213
app/modules/port_types/list.php
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* app/modules/port_types/list.php
|
||||||
|
*
|
||||||
|
* Liste aller verfügbaren Porttypen
|
||||||
|
*/
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Filter einlesen
|
||||||
|
// =========================
|
||||||
|
$search = trim($_GET['search'] ?? '');
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Filter anwenden
|
||||||
|
// =========================
|
||||||
|
$where = [];
|
||||||
|
$types = '';
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if ($search !== '') {
|
||||||
|
$where[] = "(name LIKE ? OR comment LIKE ?)";
|
||||||
|
$types .= "ss";
|
||||||
|
$params[] = "%$search%";
|
||||||
|
$params[] = "%$search%";
|
||||||
|
}
|
||||||
|
|
||||||
|
$whereSql = $where ? "WHERE " . implode(" AND ", $where) : "";
|
||||||
|
|
||||||
|
$portTypes = $sql->get(
|
||||||
|
"SELECT * FROM port_types
|
||||||
|
$whereSql
|
||||||
|
ORDER BY name",
|
||||||
|
$types,
|
||||||
|
$params
|
||||||
|
);
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="port-types-container">
|
||||||
|
<h1>Porttypen</h1>
|
||||||
|
|
||||||
|
<div class="toolbar">
|
||||||
|
<form method="GET" class="filter-form">
|
||||||
|
<input type="hidden" name="module" value="port_types">
|
||||||
|
<input type="hidden" name="action" value="list">
|
||||||
|
<input type="text" name="search" placeholder="Suche Porttyp…"
|
||||||
|
value="<?php echo htmlspecialchars($search); ?>" class="search-input">
|
||||||
|
<button type="submit" class="button">Filter</button>
|
||||||
|
<a href="?module=port_types&action=list" class="button">Reset</a>
|
||||||
|
<a href="?module=port_types&action=edit" class="button button-primary">+ Neuer Porttyp</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (!empty($portTypes)): ?>
|
||||||
|
<table class="port-types-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Medium</th>
|
||||||
|
<th>Beschreibung</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($portTypes as $pt): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?php echo htmlspecialchars($pt['name']); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($pt['medium']); ?></td>
|
||||||
|
<td><small><?php echo htmlspecialchars($pt['comment'] ?? ''); ?></small></td>
|
||||||
|
<td class="actions">
|
||||||
|
<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>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="empty-state">
|
||||||
|
<p>Keine Porttypen definiert.</p>
|
||||||
|
<a href="?module=port_types&action=edit" class="button button-primary">Porttyp hinzufügen</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.port-types-container {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-form {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-form input[type="text"] {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.port-types-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.port-types-table th,
|
||||||
|
.port-types-table td {
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.port-types-table th {
|
||||||
|
background: #f5f5f5;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
background: #f9f9f9;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</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>
|
||||||
55
app/modules/port_types/save.php
Normal file
55
app/modules/port_types/save.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* app/modules/port_types/save.php
|
||||||
|
*
|
||||||
|
* Speichert Porttyp-Daten
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
header('Location: ?module=port_types&action=list');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$portTypeId = (int)($_POST['id'] ?? 0);
|
||||||
|
$name = trim($_POST['name'] ?? '');
|
||||||
|
$medium = $_POST['medium'] ?? 'other';
|
||||||
|
$comment = trim($_POST['comment'] ?? '');
|
||||||
|
|
||||||
|
$allowedMediums = ['copper', 'fiber', 'coax', 'other'];
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
if ($name === '') {
|
||||||
|
$errors[] = "Name ist erforderlich";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($medium, $allowedMediums, true)) {
|
||||||
|
$errors[] = "Ungültiges Medium";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($errors)) {
|
||||||
|
$_SESSION['error'] = implode(', ', $errors);
|
||||||
|
$_SESSION['validation_errors'] = $errors;
|
||||||
|
$redirect = $portTypeId ? "?module=port_types&action=edit&id=$portTypeId" : "?module=port_types&action=edit";
|
||||||
|
header("Location: $redirect");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($portTypeId > 0) {
|
||||||
|
$sql->set(
|
||||||
|
"UPDATE port_types SET name = ?, medium = ?, comment = ? WHERE id = ?",
|
||||||
|
"sssi",
|
||||||
|
[$name, $medium, $comment, $portTypeId]
|
||||||
|
);
|
||||||
|
$_SESSION['success'] = "Porttyp aktualisiert";
|
||||||
|
} else {
|
||||||
|
$sql->set(
|
||||||
|
"INSERT INTO port_types (name, medium, comment) VALUES (?, ?, ?)",
|
||||||
|
"sss",
|
||||||
|
[$name, $medium, $comment]
|
||||||
|
);
|
||||||
|
$_SESSION['success'] = "Porttyp erstellt";
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Location: ?module=port_types&action=list');
|
||||||
|
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,33 +38,24 @@ $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>
|
||||||
@@ -87,17 +66,14 @@ $floors = $sql->get("SELECT id, name FROM floors ORDER BY name", "", []);
|
|||||||
</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>
|
||||||
@@ -105,157 +81,68 @@ $floors = $sql->get("SELECT id, name FROM floors ORDER BY name", "", []);
|
|||||||
<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>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<?php echo $rack['height_he']; ?> HE
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<?php echo $rack['device_count']; ?>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<small><?php echo htmlspecialchars($rack['comment'] ?? ''); ?></small>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<a href="?module=racks&action=edit&id=<?php echo $rack['id']; ?>" class="button button-small">Bearbeiten</a>
|
<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="confirmDelete(<?php echo $rack['id']; ?>)">Löschen</a>
|
<a href="#" class="button button-small button-danger" onclick="return confirmDelete(<?php echo (int)$rack['id']; ?>)">Loeschen</a>
|
||||||
</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 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">
|
|
||||||
Erstes Rack anlegen
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
</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'); ?> Meine Firma - Netzwerk Dokumentation</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,14 +21,17 @@
|
|||||||
|
|
||||||
<!-- 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>
|
<header class="app-header">
|
||||||
<h1>Netzwerk-Dokumentation</h1>
|
<div class="app-header__brand">
|
||||||
|
<span class="app-header__eyebrow">Netzwerk</span>
|
||||||
|
<h1 class="app-header__title">Dokumentation</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
$currentModule = $_GET['module'] ?? 'dashboard';
|
$currentModule = $_GET['module'] ?? 'dashboard';
|
||||||
@@ -33,23 +39,23 @@
|
|||||||
$navItems = [
|
$navItems = [
|
||||||
'dashboard' => 'Dashboard',
|
'dashboard' => 'Dashboard',
|
||||||
'locations' => 'Standorte',
|
'locations' => 'Standorte',
|
||||||
'buildings' => 'Gebäude',
|
|
||||||
'device_types' => 'Gerätetypen',
|
'device_types' => 'Gerätetypen',
|
||||||
|
'port_types' => 'Porttypen',
|
||||||
'devices' => 'Geräte',
|
'devices' => 'Geräte',
|
||||||
'racks' => 'Racks',
|
'racks' => 'Racks',
|
||||||
'floors' => 'Stockwerke',
|
'floor_infrastructure' => 'Infrastruktur',
|
||||||
'connections' => 'Verbindungen',
|
'connections' => 'Verbindungen',
|
||||||
];
|
];
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<nav class="main-nav">
|
<nav class="main-nav">
|
||||||
<ul>
|
<ul class="main-nav__list">
|
||||||
<?php foreach ($navItems as $navModule => $label): ?>
|
<?php foreach ($navItems as $navModule => $label): ?>
|
||||||
<?php
|
<?php
|
||||||
$active = ($currentModule === $navModule) ? 'active' : '';
|
$active = ($currentModule === $navModule) ? 'active' : '';
|
||||||
?>
|
?>
|
||||||
<li class="<?= $active ?>">
|
<li class="main-nav__item <?= $active ?>">
|
||||||
<a href="?module=<?= $navModule ?>&action=list">
|
<a href="?module=<?= $navModule ?>&action=list" class="main-nav__link">
|
||||||
<?= $label ?>
|
<?= $label ?>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -58,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>
|
||||||
|
|||||||
1
app/uploads/floorplans/floor_698d9e1faaebd.svg
Normal file
1
app/uploads/floorplans/floor_698d9e1faaebd.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.3 KiB |
@@ -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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -95,6 +96,41 @@ Einzelne Ports einer Netzwerkdose.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Patchpanel-Objekte im Floorplan
|
||||||
|
Patchpanels haben eine Sonderstellung: Sie sind fest verkabelte Bündel von Ports und werden **nicht** wie normale `devices` im Rack verschoben, sondern als eigene Floorplan-Objekte fest auf einem Stockwerk positioniert.
|
||||||
|
|
||||||
|
**Verwendung**
|
||||||
|
- Repräsentieren physische Patchfelder auf dem Stockwerksplan.
|
||||||
|
- Werden im SVG mit festen `x`/`y`-Koordinaten verankert und symbolisieren deren reale Position.
|
||||||
|
- Ermöglichen die Dokumentation der dauerhaften Verbindungen zu anderen Patchpanels, Netzwerkdosen oder Geräteports ohne Rack-Neuanlage.
|
||||||
|
|
||||||
|
**Grafische Attribute**
|
||||||
|
- `x`, `y`, `width`, `height` für die Visualisierung im Floorplan.
|
||||||
|
- `name`/`label` und `port_count` für die eindeutige Kennzeichnung.
|
||||||
|
- Optional: `connection_group` oder `cross_connect_id`, um zusammengehörige Patchfelder zu bündeln.
|
||||||
|
|
||||||
|
**Verbindungen**
|
||||||
|
- Jeder Patchpanel-Port wird über `connections` mit passenden Gegenstellen verbunden (andere Patchpanels, Dosen, Geräteports, Module).
|
||||||
|
- Da sie Teil der Floorplan-Grafik sind, lassen sich die permanenten Kabelverbindungen direkt auf der Stockwerkskarte darstellen.
|
||||||
|
|
||||||
|
### Tabellenstruktur `floor_patchpanels`
|
||||||
|
Die Bühne für Patchpanel-Objekte auf dem Stockwerkplan.
|
||||||
|
- `floor_id` referenziert das Stockwerk, in dem das Panel liegt.
|
||||||
|
- `pos_x`, `pos_y`, `width`, `height` definieren das feste Rechteck auf der SVG.
|
||||||
|
- `port_count` und `comment` beschreiben die Kapazität und zusätzliche Hinweise.
|
||||||
|
|
||||||
|
### Tabellenstruktur `floor_patchpanel_ports`
|
||||||
|
- Jeder Eintrag ist ein physischer Port eines Patchpanels.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
**Status (18. Februar 2026)**
|
||||||
|
- [x] Floorplan- und CRUD-Module wurden für Patchpanels als Floor-Objekte inkl. Port-Pflege erweitert (`floor_patchpanels`, `floor_patchpanel_ports`).
|
||||||
|
- [x] Verbindungen zwischen Patchpanel ↔ Patchpanel und Patchpanel ↔ Netzwerkbuchse sind in der `connections`-Logik abbildbar.
|
||||||
|
- [ ] UI/CSV/Export-Dokumentation weiter ausbauen, damit Planer Kabelverläufe direkt auswerten können.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 3. Racks & physische Infrastruktur
|
## 3. Racks & physische Infrastruktur
|
||||||
|
|
||||||
### `racks`
|
### `racks`
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ services:
|
|||||||
build: .
|
build: .
|
||||||
container_name: netdoc_web
|
container_name: netdoc_web
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "8001:80"
|
||||||
volumes:
|
volumes:
|
||||||
- ./app:/var/www/html
|
- ./app:/var/www/html
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -27,7 +27,7 @@ services:
|
|||||||
image: phpmyadmin/phpmyadmin
|
image: phpmyadmin/phpmyadmin
|
||||||
container_name: netdoc_phpmyadmin
|
container_name: netdoc_phpmyadmin
|
||||||
ports:
|
ports:
|
||||||
- "8080:80"
|
- "8081:80"
|
||||||
environment:
|
environment:
|
||||||
PMA_HOST: db
|
PMA_HOST: db
|
||||||
PMA_USER: netdoc
|
PMA_USER: netdoc
|
||||||
|
|||||||
40
docker-portainer.yml
Normal file
40
docker-portainer.yml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
container_name: netdoc_web
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
volumes:
|
||||||
|
- ./app:/var/www/html
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
restart: "no"
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: mariadb:11
|
||||||
|
container_name: netdoc_db
|
||||||
|
environment:
|
||||||
|
MARIADB_ROOT_PASSWORD: root
|
||||||
|
MARIADB_DATABASE: netdoc
|
||||||
|
MARIADB_USER: netdoc
|
||||||
|
MARIADB_PASSWORD: netdoc
|
||||||
|
volumes:
|
||||||
|
- db_data:/var/lib/mysql
|
||||||
|
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||||
|
restart: "no"
|
||||||
|
|
||||||
|
phpmyadmin:
|
||||||
|
image: phpmyadmin/phpmyadmin
|
||||||
|
container_name: netdoc_phpmyadmin
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
environment:
|
||||||
|
PMA_HOST: db
|
||||||
|
PMA_USER: netdoc
|
||||||
|
PMA_PASSWORD: netdoc
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
restart: "no"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db_data:
|
||||||
Reference in New Issue
Block a user