feat: complete package 2 delete flows and package 3 port management

This commit is contained in:
2026-02-18 10:16:24 +01:00
parent f4ce7f360d
commit 77758f71d3
14 changed files with 754 additions and 141 deletions

View File

@@ -9,15 +9,15 @@
- [x] Validierungsfehler anzeigen - [x] Validierungsfehler anzeigen
### Package 2: Delete-Funktionen (1h) ### Package 2: Delete-Funktionen (1h)
- [ ] DELETE-Endpoints für alle Module - [x] DELETE-Endpoints für alle Module
- [ ] AJAX-Bestätigung vor Löschen - [x] AJAX-Bestätigung vor Löschen
- [ ] Kaskadierendes Löschen prüfen (z.B. Floor → Racks) - [x] Kaskadierendes Löschen prüfen (z.B. Floor → Racks)
### Package 3: Port-Management (2-3h) ### Package 3: Port-Management (2-3h)
- [ ] Ports zu Device-Types verwalten - [x] Ports zu Device-Types verwalten
- [ ] Ports zu Devices anzeigen - [x] Ports zu Devices anzeigen
- [ ] Port-Status (aktiv/inaktiv) - [x] Port-Status (aktiv/inaktiv)
- [ ] VLAN-Zuordnung zu Ports - [x] VLAN-Zuordnung zu Ports
### Package 4: SVG-Editor für Floorplans (4-5h) ### Package 4: SVG-Editor für Floorplans (4-5h)
- [ ] Interaktiver SVG-Editor für Rooms - [ ] Interaktiver SVG-Editor für Rooms

View File

@@ -2,12 +2,19 @@
/** /**
* app/modules/connections/delete.php * app/modules/connections/delete.php
* *
* Loescht eine Verbindung und leitet zur Liste zurueck. * Loescht eine Verbindung (AJAX-POST bevorzugt, GET-Fallback fuer Redirects).
*/ */
$connectionId = (int)($_GET['id'] ?? $_POST['id'] ?? 0); $isPost = ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST';
$connectionId = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
if ($connectionId <= 0) { if ($connectionId <= 0) {
if ($isPost) {
header('Content-Type: application/json; charset=utf-8');
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Ungueltige Verbindungs-ID']);
exit;
}
$_SESSION['error'] = 'Ungueltige Verbindungs-ID'; $_SESSION['error'] = 'Ungueltige Verbindungs-ID';
header('Location: ?module=connections&action=list'); header('Location: ?module=connections&action=list');
exit; exit;
@@ -20,6 +27,12 @@ $connection = $sql->single(
); );
if (!$connection) { if (!$connection) {
if ($isPost) {
header('Content-Type: application/json; charset=utf-8');
http_response_code(404);
echo json_encode(['success' => false, 'message' => 'Verbindung nicht gefunden']);
exit;
}
$_SESSION['error'] = 'Verbindung nicht gefunden'; $_SESSION['error'] = 'Verbindung nicht gefunden';
header('Location: ?module=connections&action=list'); header('Location: ?module=connections&action=list');
exit; exit;
@@ -31,6 +44,17 @@ $rows = $sql->set(
[$connectionId] [$connectionId]
); );
if ($isPost) {
header('Content-Type: application/json; charset=utf-8');
if ($rows > 0) {
echo json_encode(['success' => true, 'message' => 'Verbindung geloescht']);
} else {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Verbindung konnte nicht geloescht werden']);
}
exit;
}
if ($rows > 0) { if ($rows > 0) {
$_SESSION['success'] = 'Verbindung geloescht'; $_SESSION['success'] = 'Verbindung geloescht';
} else { } else {

View File

@@ -335,11 +335,12 @@ $buildListUrl = static function (array $extra = []) use ($search, $deviceId): st
<a href="<?php echo htmlspecialchars($buildListUrl(['connection_id' => $connId])); ?>" class="button button-small">Details</a> <a href="<?php echo htmlspecialchars($buildListUrl(['connection_id' => $connId])); ?>" class="button button-small">Details</a>
<a href="?module=connections&action=edit&id=<?php echo $connId; ?>" class="button button-small">Bearbeiten</a> <a href="?module=connections&action=edit&id=<?php echo $connId; ?>" class="button button-small">Bearbeiten</a>
<a href="?module=connections&action=swap&id=<?php echo $connId; ?>" class="button button-small" onclick="return confirm('Von/Nach fuer diese Verbindung vertauschen?');">Von/Nach tauschen</a> <a href="?module=connections&action=swap&id=<?php echo $connId; ?>" class="button button-small" onclick="return confirm('Von/Nach fuer diese Verbindung vertauschen?');">Von/Nach tauschen</a>
<a href="?module=connections&action=delete&id=<?php echo $connId; ?>" class="button button-small button-danger" <button
data-confirm-delete="true" type="button"
data-confirm-message="Diese Verbindung wirklich loeschen?"> class="button button-small button-danger js-connection-delete"
data-connection-id="<?php echo $connId; ?>">
Loeschen Loeschen
</a> </button>
</td> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
@@ -382,7 +383,12 @@ $buildListUrl = static function (array $extra = []) use ($search, $deviceId): st
<div class="sidebar-actions"> <div class="sidebar-actions">
<a href="?module=connections&action=edit&id=<?php echo $selectedConnId; ?>" class="button button-small">Bearbeiten</a> <a href="?module=connections&action=edit&id=<?php echo $selectedConnId; ?>" class="button button-small">Bearbeiten</a>
<a href="?module=connections&action=swap&id=<?php echo $selectedConnId; ?>" class="button button-small" onclick="return confirm('Von/Nach fuer diese Verbindung vertauschen?');">Tauschen</a> <a href="?module=connections&action=swap&id=<?php echo $selectedConnId; ?>" class="button button-small" onclick="return confirm('Von/Nach fuer diese Verbindung vertauschen?');">Tauschen</a>
<a href="?module=connections&action=delete&id=<?php echo $selectedConnId; ?>" class="button button-small button-danger" data-confirm-delete="true" data-confirm-message="Diese Verbindung wirklich loeschen?">Loeschen</a> <button
type="button"
class="button button-small button-danger js-connection-delete"
data-connection-id="<?php echo $selectedConnId; ?>">
Loeschen
</button>
</div> </div>
<?php else: ?> <?php else: ?>
<p><em>Keine Verbindung ausgewaehlt.</em></p> <p><em>Keine Verbindung ausgewaehlt.</em></p>
@@ -474,3 +480,35 @@ $buildListUrl = static function (array $extra = []) use ($search, $deviceId): st
} }
} }
</style> </style>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.js-connection-delete').forEach((button) => {
button.addEventListener('click', () => {
const id = Number(button.dataset.connectionId || '0');
if (id <= 0) {
return;
}
if (!confirm('Diese Verbindung wirklich loeschen?')) {
return;
}
fetch('?module=connections&action=delete', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: 'id=' + encodeURIComponent(id)
})
.then((response) => response.json())
.then((data) => {
if (data && data.success) {
window.location.reload();
return;
}
alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
})
.catch(() => alert('Loeschen fehlgeschlagen'));
});
});
});
</script>

View File

@@ -3,12 +3,20 @@
* app/modules/devices/delete.php * app/modules/devices/delete.php
* *
* Loescht ein Geraet. Bei Abhaengigkeiten ist force=1 erforderlich. * Loescht ein Geraet. Bei Abhaengigkeiten ist force=1 erforderlich.
* Unterstuetzt GET-Redirects und AJAX-POST.
*/ */
$deviceId = (int)($_GET['id'] ?? 0); $isPost = ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST';
$forceDelete = (int)($_GET['force'] ?? 0) === 1; $deviceId = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
$forceDelete = (int)($_POST['force'] ?? $_GET['force'] ?? 0) === 1;
if ($deviceId <= 0) { if ($deviceId <= 0) {
if ($isPost) {
header('Content-Type: application/json; charset=utf-8');
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Ungueltige Geraete-ID']);
exit;
}
$_SESSION['error'] = "Ungueltige Geraete-ID"; $_SESSION['error'] = "Ungueltige Geraete-ID";
header('Location: ?module=devices&action=list'); header('Location: ?module=devices&action=list');
exit; exit;
@@ -21,6 +29,12 @@ $device = $sql->single(
); );
if (!$device) { if (!$device) {
if ($isPost) {
header('Content-Type: application/json; charset=utf-8');
http_response_code(404);
echo json_encode(['success' => false, 'message' => 'Geraet nicht gefunden']);
exit;
}
$_SESSION['error'] = "Geraet nicht gefunden"; $_SESSION['error'] = "Geraet nicht gefunden";
header('Location: ?module=devices&action=list'); header('Location: ?module=devices&action=list');
exit; exit;
@@ -70,7 +84,23 @@ if ($hasDependencies && !$forceDelete) {
$parts[] = $moduleCount . ' Port-Module'; $parts[] = $moduleCount . ' Port-Module';
} }
$_SESSION['error'] = "Geraet hat abhaengige Daten (" . implode(', ', $parts) . "). Loeschen bitte bestaetigen."; $dependencyMessage = "Geraet hat abhaengige Daten (" . implode(', ', $parts) . "). Loeschen bitte bestaetigen.";
if ($isPost) {
header('Content-Type: application/json; charset=utf-8');
http_response_code(409);
echo json_encode([
'success' => false,
'requires_force' => true,
'message' => $dependencyMessage,
'dependencies' => [
'connections' => $connectionCount,
'ports' => $portCount,
'modules' => $moduleCount
]
]);
exit;
}
$_SESSION['error'] = $dependencyMessage;
header('Location: ?module=devices&action=edit&id=' . urlencode((string)$deviceId)); header('Location: ?module=devices&action=edit&id=' . urlencode((string)$deviceId));
exit; exit;
} }
@@ -91,8 +121,19 @@ $deleted = $sql->set(
); );
if ($deleted > 0) { if ($deleted > 0) {
if ($isPost) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['success' => true, 'message' => "Geraet geloescht: " . $device['name']]);
exit;
}
$_SESSION['success'] = "Geraet geloescht: " . $device['name']; $_SESSION['success'] = "Geraet geloescht: " . $device['name'];
} else { } else {
if ($isPost) {
header('Content-Type: application/json; charset=utf-8');
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Geraet konnte nicht geloescht werden']);
exit;
}
$_SESSION['error'] = "Geraet konnte nicht geloescht werden"; $_SESSION['error'] = "Geraet konnte nicht geloescht werden";
} }

View File

@@ -64,6 +64,19 @@ if ($isEdit) {
// ========================= // =========================
$deviceTypes = $sql->get("SELECT id, name, category FROM device_types ORDER BY name", "", []); $deviceTypes = $sql->get("SELECT id, name, category FROM device_types ORDER BY name", "", []);
$racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []); $racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
$devicePorts = [];
if ($isEdit) {
$devicePorts = $sql->get(
"SELECT dp.id, dp.name, dp.status, dp.mode, dp.vlan_config, pt.name AS port_type_name
FROM device_ports dp
LEFT JOIN port_types pt ON pt.id = dp.port_type_id
WHERE dp.device_id = ?
ORDER BY dp.id",
"i",
[$deviceId]
);
}
?> ?>
@@ -160,6 +173,67 @@ $racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
</div> </div>
</fieldset> </fieldset>
<fieldset>
<legend>Ports</legend>
<?php if ($isEdit): ?>
<?php if (!empty($devicePorts)): ?>
<p><small>Portstatus und VLAN-Zuordnung koennen hier direkt gepflegt werden (VLANs kommagetrennt, z. B. 10,20,30).</small></p>
<table class="device-port-table">
<thead>
<tr>
<th>Name</th>
<th>Port-Typ</th>
<th>Status</th>
<th>Modus</th>
<th>VLANs</th>
</tr>
</thead>
<tbody>
<?php foreach ($devicePorts as $port): ?>
<?php
$vlanValue = '';
if (!empty($port['vlan_config'])) {
$decodedVlans = json_decode((string)$port['vlan_config'], true);
if (is_array($decodedVlans)) {
$vlanValue = implode(', ', array_map('strval', $decodedVlans));
} else {
$vlanValue = (string)$port['vlan_config'];
}
}
$statusValue = (string)($port['status'] ?? 'active');
if (!in_array($statusValue, ['active', 'disabled'], true)) {
$statusValue = 'active';
}
?>
<tr>
<td>
<input type="text" name="device_ports[<?php echo (int)$port['id']; ?>][name]" value="<?php echo htmlspecialchars((string)$port['name']); ?>">
</td>
<td><?php echo htmlspecialchars((string)($port['port_type_name'] ?? '-')); ?></td>
<td>
<select name="device_ports[<?php echo (int)$port['id']; ?>][status]">
<option value="active" <?php echo $statusValue === 'active' ? 'selected' : ''; ?>>aktiv</option>
<option value="disabled" <?php echo $statusValue === 'disabled' ? 'selected' : ''; ?>>inaktiv</option>
</select>
</td>
<td>
<input type="text" name="device_ports[<?php echo (int)$port['id']; ?>][mode]" value="<?php echo htmlspecialchars((string)($port['mode'] ?? '')); ?>" placeholder="z. B. access/trunk">
</td>
<td>
<input type="text" name="device_ports[<?php echo (int)$port['id']; ?>][vlan_config]" value="<?php echo htmlspecialchars($vlanValue); ?>" placeholder="10,20,30">
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p><small>Zu diesem Geraet sind aktuell keine Ports vorhanden.</small></p>
<?php endif; ?>
<?php else: ?>
<p><small>Ports werden nach dem ersten Speichern automatisch aus dem Geraetetyp erzeugt und koennen dann hier gepflegt werden.</small></p>
<?php endif; ?>
</fieldset>
<!-- ========================= <!-- =========================
Aktionen Aktionen
========================= --> ========================= -->
@@ -264,34 +338,64 @@ $racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
.button:hover { .button:hover {
opacity: 0.8; opacity: 0.8;
} }
.device-port-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
.device-port-table th,
.device-port-table td {
border-bottom: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.device-port-table input,
.device-port-table select {
width: 100%;
min-width: 120px;
}
</style> </style>
<script> <script>
function confirmDelete(link, id, connectionCount, portCount, moduleCount) { function confirmDelete(link, id, connectionCount, portCount, moduleCount) {
if (confirm('Dieses Gerät wirklich löschen?')) { if (!confirm('Dieses Geraet wirklich loeschen?')) {
const hasDependencies = (connectionCount > 0) || (portCount > 0) || (moduleCount > 0);
if (hasDependencies) {
const details = [];
if (connectionCount > 0) {
details.push(connectionCount + ' Verbindungen');
}
if (portCount > 0) {
details.push(portCount + ' Ports');
}
if (moduleCount > 0) {
details.push(moduleCount + ' Port-Module');
}
const dependencyMessage = 'Es gibt abhängige Daten (' + details.join(', ') + '). Diese auch löschen?';
if (!confirm(dependencyMessage)) {
return false;
}
window.location.href = (link && link.href ? link.href : ('?module=devices&action=delete&id=' + encodeURIComponent(id))) + '&force=1';
return false; return false;
} }
return true; const requestDelete = (forceDelete) => {
const body = ['id=' + encodeURIComponent(id)];
if (forceDelete) {
body.push('force=1');
} }
fetch('?module=devices&action=delete', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: body.join('&')
})
.then((response) => response.json())
.then((data) => {
if (data && data.success) {
window.location.href = '?module=devices&action=list';
return;
}
if (data && data.requires_force) {
if (confirm(data.message || 'Es gibt abhaengige Daten. Trotzdem loeschen?')) {
requestDelete(true);
}
return;
}
alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
})
.catch(() => alert('Loeschen fehlgeschlagen'));
};
requestDelete(false);
return false; return false;
} }
</script> </script>

View File

@@ -1,7 +1,7 @@
<?php <?php
/** /**
* modules/devices/list.php * modules/devices/list.php
* Vollständige Geräteübersicht mit Filter * Vollstaendige Geraeteuebersicht mit Filter
*/ */
// ========================= // =========================
@@ -22,34 +22,34 @@ $params = [];
if ($search !== '') { if ($search !== '') {
$where[] = "(d.name LIKE ? OR d.serial_number LIKE ? OR dt.name LIKE ?)"; $where[] = "(d.name LIKE ? OR d.serial_number LIKE ? OR dt.name LIKE ?)";
$types .= "sss"; $types .= 'sss';
$params[] = "%$search%"; $params[] = "%$search%";
$params[] = "%$search%"; $params[] = "%$search%";
$params[] = "%$search%"; $params[] = "%$search%";
} }
if ($typeId > 0) { if ($typeId > 0) {
$where[] = "d.device_type_id = ?"; $where[] = 'd.device_type_id = ?';
$types .= "i"; $types .= 'i';
$params[] = $typeId; $params[] = $typeId;
} }
if ($floorId > 0) { if ($floorId > 0) {
$where[] = "f.id = ?"; $where[] = 'f.id = ?';
$types .= "i"; $types .= 'i';
$params[] = $floorId; $params[] = $floorId;
} }
if ($rackId > 0) { if ($rackId > 0) {
$where[] = "d.rack_id = ?"; $where[] = 'd.rack_id = ?';
$types .= "i"; $types .= 'i';
$params[] = $rackId; $params[] = $rackId;
} }
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : ''; $whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
// ========================= // =========================
// Geräte laden // Geraete laden
// ========================= // =========================
$devices = $sql->get( $devices = $sql->get(
" "
@@ -69,6 +69,25 @@ $whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
FROM device_ports dp FROM device_ports dp
WHERE dp.device_id = d.id WHERE dp.device_id = d.id
) AS port_count, ) AS port_count,
(
SELECT COUNT(*)
FROM device_ports dp
WHERE dp.device_id = d.id
AND dp.status = 'active'
) AS active_port_count,
(
SELECT COUNT(*)
FROM device_ports dp
WHERE dp.device_id = d.id
AND dp.status = 'disabled'
) AS disabled_port_count,
(
SELECT COUNT(*)
FROM device_ports dp
WHERE dp.device_id = d.id
AND dp.vlan_config IS NOT NULL
AND dp.vlan_config <> '[]'
) AS vlan_port_count,
( (
SELECT COUNT(*) SELECT COUNT(*)
FROM device_port_modules dpm FROM device_port_modules dpm
@@ -99,22 +118,19 @@ $whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
// ========================= // =========================
// Filter-Daten laden // Filter-Daten laden
// ========================= // =========================
$deviceTypes = $sql->get("SELECT id, name FROM device_types ORDER BY name", "", []); $deviceTypes = $sql->get('SELECT id, name FROM device_types ORDER BY name', '', []);
$floors = $sql->get("SELECT id, name FROM floors ORDER BY name", "", []); $floors = $sql->get('SELECT id, name FROM floors ORDER BY name', '', []);
$racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []); $racks = $sql->get('SELECT id, name FROM racks ORDER BY name', '', []);
?> ?>
<div class="devices-container"> <div class="devices-container">
<h1>Geräte</h1> <h1>Geraete</h1>
<!-- =========================
Filter-Toolbar
========================= -->
<form method="get" class="filter-form"> <form method="get" class="filter-form">
<input type="hidden" name="module" value="devices"> <input type="hidden" name="module" value="devices">
<input type="hidden" name="action" value="list"> <input type="hidden" name="action" value="list">
<input type="text" name="search" placeholder="Suche nach Name oder Seriennummer" <input type="text" name="search" placeholder="Suche nach Name oder Seriennummer..."
value="<?php echo htmlspecialchars($search); ?>" class="search-input"> value="<?php echo htmlspecialchars($search); ?>" class="search-input">
<select name="type_id"> <select name="type_id">
@@ -149,16 +165,13 @@ $racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
<a href="?module=devices&action=list" class="button">Reset</a> <a href="?module=devices&action=list" class="button">Reset</a>
<a href="?module=devices&action=edit" class="button button-primary" style="margin-left: auto;"> <a href="?module=devices&action=edit" class="button button-primary" style="margin-left: auto;">
+ Neues Gerät + Neues Geraet
</a> </a>
</form> </form>
<!-- =========================
Geräte-Liste
========================= -->
<?php if (!empty($devices)): ?> <?php if (!empty($devices)): ?>
<div class="device-stats"> <div class="device-stats">
<p>Gefundene Geräte: <strong><?php echo count($devices); ?></strong></p> <p>Gefundene Geraete: <strong><?php echo count($devices); ?></strong></p>
</div> </div>
<table class="device-list"> <table class="device-list">
@@ -169,6 +182,7 @@ $racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
<th>Stockwerk</th> <th>Stockwerk</th>
<th>Rack</th> <th>Rack</th>
<th>Position (HE)</th> <th>Position (HE)</th>
<th>Ports</th>
<th>Seriennummer</th> <th>Seriennummer</th>
<th>Webconfig</th> <th>Webconfig</th>
<th>Aktionen</th> <th>Aktionen</th>
@@ -186,28 +200,37 @@ $racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
</td> </td>
<td> <td>
<?php echo htmlspecialchars($d['floor_name'] ?? ''); ?> <?php echo htmlspecialchars($d['floor_name'] ?? '-'); ?>
</td> </td>
<td> <td>
<?php echo htmlspecialchars($d['rack_name'] ?? ''); ?> <?php echo htmlspecialchars($d['rack_name'] ?? '-'); ?>
</td> </td>
<td> <td>
<?php <?php
if ($d['rack_position_he']) { if ($d['rack_position_he']) {
echo $d['rack_position_he']; echo (int)$d['rack_position_he'];
if ($d['rack_height_he']) { if ($d['rack_height_he']) {
echo "" . ($d['rack_position_he'] + $d['rack_height_he'] - 1); echo '-' . ((int)$d['rack_position_he'] + (int)$d['rack_height_he'] - 1);
} }
} else { } else {
echo "—"; echo '-';
} }
?> ?>
</td> </td>
<td> <td>
<small><?php echo htmlspecialchars($d['serial_number'] ?? '—'); ?></small> <small>
<?php echo (int)$d['port_count']; ?> gesamt<br>
<?php echo (int)$d['active_port_count']; ?> aktiv /
<?php echo (int)$d['disabled_port_count']; ?> inaktiv<br>
VLAN gesetzt: <?php echo (int)$d['vlan_port_count']; ?>
</small>
</td>
<td>
<small><?php echo htmlspecialchars($d['serial_number'] ?? '-'); ?></small>
</td> </td>
<td> <td>
@@ -216,13 +239,13 @@ $racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
Webconfig Webconfig
</a> </a>
<?php else: ?> <?php else: ?>
-
<?php endif; ?> <?php endif; ?>
</td> </td>
<td class="actions"> <td class="actions">
<a href="?module=devices&action=edit&id=<?php echo $d['id']; ?>" class="button button-small">Bearbeiten</a> <a href="?module=devices&action=edit&id=<?php echo $d['id']; ?>" class="button button-small">Bearbeiten</a>
<a href="?module=devices&action=delete&id=<?php echo (int)$d['id']; ?>" class="button button-small button-danger" onclick="return confirmDelete(this, <?php echo (int)$d['id']; ?>, <?php echo (int)$d['connection_count']; ?>, <?php echo (int)$d['port_count']; ?>, <?php echo (int)$d['module_count']; ?>)">Löschen</a> <a href="?module=devices&action=delete&id=<?php echo (int)$d['id']; ?>" class="button button-small button-danger" onclick="return confirmDelete(this, <?php echo (int)$d['id']; ?>, <?php echo (int)$d['connection_count']; ?>, <?php echo (int)$d['port_count']; ?>, <?php echo (int)$d['module_count']; ?>)">Loeschen</a>
</td> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
@@ -231,10 +254,10 @@ $racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
<?php else: ?> <?php else: ?>
<div class="empty-state"> <div class="empty-state">
<p>Keine Geräte gefunden.</p> <p>Keine Geraete gefunden.</p>
<p> <p>
<a href="?module=devices&action=edit" class="button button-primary"> <a href="?module=devices&action=edit" class="button button-primary">
Erstes Gerät anlegen Erstes Geraet anlegen
</a> </a>
</p> </p>
</div> </div>
@@ -351,33 +374,41 @@ $racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
<script> <script>
function confirmDelete(link, id, connectionCount, portCount, moduleCount) { function confirmDelete(link, id, connectionCount, portCount, moduleCount) {
if (confirm('Dieses Gerät wirklich löschen?')) { if (!confirm('Dieses Geraet wirklich loeschen?')) {
const hasDependencies = (connectionCount > 0) || (portCount > 0) || (moduleCount > 0);
if (hasDependencies) {
const details = [];
if (connectionCount > 0) {
details.push(connectionCount + ' Verbindungen');
}
if (portCount > 0) {
details.push(portCount + ' Ports');
}
if (moduleCount > 0) {
details.push(moduleCount + ' Port-Module');
}
const dependencyMessage = 'Es gibt abhängige Daten (' + details.join(', ') + '). Diese auch löschen?';
if (!confirm(dependencyMessage)) {
return false;
}
window.location.href = (link && link.href ? link.href : ('?module=devices&action=delete&id=' + encodeURIComponent(id))) + '&force=1';
return false; return false;
} }
return true; const requestDelete = (forceDelete) => {
const body = ['id=' + encodeURIComponent(id)];
if (forceDelete) {
body.push('force=1');
} }
fetch('?module=devices&action=delete', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: body.join('&')
})
.then((response) => response.json())
.then((data) => {
if (data && data.success) {
window.location.reload();
return;
}
if (data && data.requires_force) {
if (confirm(data.message || 'Es gibt abhaengige Daten. Trotzdem loeschen?')) {
requestDelete(true);
}
return;
}
alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
})
.catch(() => alert('Loeschen fehlgeschlagen'));
};
requestDelete(false);
return false; return false;
} }
</script> </script>

View File

@@ -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;
} }
@@ -88,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";
@@ -139,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);
}

View File

@@ -2,30 +2,48 @@
/** /**
* app/modules/floor_infrastructure/delete.php * app/modules/floor_infrastructure/delete.php
* *
* Loescht Patchpanels oder Wandbuchsen. * Loescht Patchpanels oder Wandbuchsen (AJAX-POST bevorzugt).
*/ */
$type = strtolower(trim((string)($_GET['type'] ?? ''))); $isPost = ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST';
$id = (int)($_GET['id'] ?? 0); $type = strtolower(trim((string)($_POST['type'] ?? $_GET['type'] ?? '')));
$id = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
if ($id <= 0 || !in_array($type, ['patchpanel', 'outlet'], true)) { if ($id <= 0 || !in_array($type, ['patchpanel', 'outlet'], true)) {
if ($isPost) {
header('Content-Type: application/json; charset=utf-8');
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Ungueltige Anfrage']);
exit;
}
header('Location: ?module=floor_infrastructure&action=list'); header('Location: ?module=floor_infrastructure&action=list');
exit; exit;
} }
if ($type === 'patchpanel') { if ($type === 'patchpanel') {
$sql->set( $rows = $sql->set(
"DELETE FROM floor_patchpanels WHERE id = ?", "DELETE FROM floor_patchpanels WHERE id = ?",
"i", "i",
[$id] [$id]
); );
} else { } else {
$sql->set( $rows = $sql->set(
"DELETE FROM network_outlets WHERE id = ?", "DELETE FROM network_outlets WHERE id = ?",
"i", "i",
[$id] [$id]
); );
} }
if ($isPost) {
header('Content-Type: application/json; charset=utf-8');
if ($rows > 0) {
echo json_encode(['success' => true, 'message' => 'Infrastrukturobjekt geloescht']);
} else {
http_response_code(404);
echo json_encode(['success' => false, 'message' => 'Infrastrukturobjekt nicht gefunden oder bereits geloescht']);
}
exit;
}
header('Location: ?module=floor_infrastructure&action=list'); header('Location: ?module=floor_infrastructure&action=list');
exit; exit;

View File

@@ -180,7 +180,14 @@ if ($editorFloor) {
<td><?php echo (int)$panel['port_count']; ?></td> <td><?php echo (int)$panel['port_count']; ?></td>
<td class="actions"> <td class="actions">
<a href="?module=floor_infrastructure&action=edit&type=patchpanel&id=<?php echo (int)$panel['id']; ?>" class="button button-small">Bearbeiten</a> <a href="?module=floor_infrastructure&action=edit&type=patchpanel&id=<?php echo (int)$panel['id']; ?>" class="button button-small">Bearbeiten</a>
<a href="?module=floor_infrastructure&action=delete&type=patchpanel&id=<?php echo (int)$panel['id']; ?>" class="button button-small button-danger" onclick="return confirm('Patchpanel wirklich loeschen?');">Loeschen</a> <button
type="button"
class="button button-small button-danger js-floor-infra-delete"
data-delete-id="<?php echo (int)$panel['id']; ?>"
data-delete-type="patchpanel"
data-delete-label="<?php echo htmlspecialchars((string)$panel['name'], ENT_QUOTES, 'UTF-8'); ?>">
Loeschen
</button>
</td> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
@@ -226,7 +233,14 @@ if ($editorFloor) {
<td><?php echo htmlspecialchars((string)($outlet['comment'] ?? '')); ?></td> <td><?php echo htmlspecialchars((string)($outlet['comment'] ?? '')); ?></td>
<td class="actions"> <td class="actions">
<a href="?module=floor_infrastructure&action=edit&type=outlet&id=<?php echo (int)$outlet['id']; ?>" class="button button-small">Bearbeiten</a> <a href="?module=floor_infrastructure&action=edit&type=outlet&id=<?php echo (int)$outlet['id']; ?>" class="button button-small">Bearbeiten</a>
<a href="?module=floor_infrastructure&action=delete&type=outlet&id=<?php echo (int)$outlet['id']; ?>" class="button button-small button-danger" onclick="return confirm('Wandbuchse wirklich loeschen?');">Loeschen</a> <button
type="button"
class="button button-small button-danger js-floor-infra-delete"
data-delete-id="<?php echo (int)$outlet['id']; ?>"
data-delete-type="outlet"
data-delete-label="<?php echo htmlspecialchars((string)$outlet['name'], ENT_QUOTES, 'UTF-8'); ?>">
Loeschen
</button>
</td> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
@@ -237,3 +251,38 @@ if ($editorFloor) {
<?php endif; ?> <?php endif; ?>
</section> </section>
</div> </div>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.js-floor-infra-delete').forEach((button) => {
button.addEventListener('click', () => {
const id = Number(button.dataset.deleteId || '0');
const type = (button.dataset.deleteType || '').trim();
const label = button.dataset.deleteLabel || 'Objekt';
if (id <= 0 || (type !== 'patchpanel' && type !== 'outlet')) {
return;
}
const entityLabel = type === 'patchpanel' ? 'Patchpanel' : 'Wandbuchse';
if (!confirm(entityLabel + ' "' + label + '" wirklich loeschen?')) {
return;
}
fetch('?module=floor_infrastructure&action=delete', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: 'id=' + encodeURIComponent(id) + '&type=' + encodeURIComponent(type)
})
.then((response) => response.json())
.then((data) => {
if (data && data.success) {
window.location.reload();
return;
}
alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
})
.catch(() => alert('Loeschen fehlgeschlagen'));
});
});
});
</script>

View File

@@ -25,6 +25,47 @@ if (!$exists) {
exit; exit;
} }
$forceDelete = (int)($_POST['force'] ?? $_GET['force'] ?? 0) === 1;
$dependencyCounts = $sql->single(
"SELECT
(SELECT COUNT(*) FROM rooms WHERE floor_id = ?) AS room_count,
(SELECT COUNT(*) FROM racks WHERE floor_id = ?) AS rack_count,
(SELECT COUNT(*) FROM floor_patchpanels WHERE floor_id = ?) AS patchpanel_count",
"iii",
[$id, $id, $id]
);
$roomCount = (int)($dependencyCounts['room_count'] ?? 0);
$rackCount = (int)($dependencyCounts['rack_count'] ?? 0);
$patchpanelCount = (int)($dependencyCounts['patchpanel_count'] ?? 0);
$hasDependencies = $roomCount > 0 || $rackCount > 0 || $patchpanelCount > 0;
if ($hasDependencies && !$forceDelete) {
$parts = [];
if ($roomCount > 0) {
$parts[] = $roomCount . ' Raeume';
}
if ($rackCount > 0) {
$parts[] = $rackCount . ' Racks';
}
if ($patchpanelCount > 0) {
$parts[] = $patchpanelCount . ' Patchpanels';
}
http_response_code(409);
echo json_encode([
'success' => false,
'requires_force' => true,
'message' => 'Beim Loeschen werden abhaengige Daten entfernt (' . implode(', ', $parts) . '). Fortfahren?',
'dependencies' => [
'rooms' => $roomCount,
'racks' => $rackCount,
'patchpanels' => $patchpanelCount
]
]);
exit;
}
$rows = $sql->set("DELETE FROM floors WHERE id = ?", "i", [$id]); $rows = $sql->set("DELETE FROM floors WHERE id = ?", "i", [$id]);
if ($rows === false) { if ($rows === false) {
http_response_code(500); http_response_code(500);
@@ -32,5 +73,13 @@ if ($rows === false) {
exit; exit;
} }
echo json_encode(['success' => true, 'message' => 'Stockwerk geloescht']); echo json_encode([
'success' => true,
'message' => 'Stockwerk geloescht',
'dependencies' => [
'rooms' => $roomCount,
'racks' => $rackCount,
'patchpanels' => $patchpanelCount
]
]);

View File

@@ -233,11 +233,20 @@ $buildings = $sql->get("SELECT id, name FROM buildings ORDER BY name", "", []);
<script> <script>
function confirmDelete(id) { function confirmDelete(id) {
if (confirm('Dieses Stockwerk wirklich loeschen? Alle Raeume und Racks werden geloescht.')) { if (!confirm('Dieses Stockwerk wirklich loeschen?')) {
return;
}
const requestDelete = (forceDelete) => {
const body = ['id=' + encodeURIComponent(id)];
if (forceDelete) {
body.push('force=1');
}
fetch('?module=floors&action=delete', { fetch('?module=floors&action=delete', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: 'id=' + encodeURIComponent(id) body: body.join('&')
}) })
.then((res) => res.json()) .then((res) => res.json())
.then((data) => { .then((data) => {
@@ -245,12 +254,22 @@ function confirmDelete(id) {
window.location.reload(); window.location.reload();
return; return;
} }
if (data && data.requires_force) {
if (confirm(data.message || 'Abhaengige Daten ebenfalls loeschen?')) {
requestDelete(true);
}
return;
}
alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen'); alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
}) })
.catch(() => { .catch(() => {
alert('Loeschen fehlgeschlagen'); alert('Loeschen fehlgeschlagen');
}); });
} };
requestDelete(false);
} }
</script> </script>

View File

@@ -7,6 +7,16 @@
header('Content-Type: application/json; charset=utf-8'); header('Content-Type: application/json; charset=utf-8');
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
if ($method !== 'POST') {
http_response_code(405);
echo json_encode([
'success' => false,
'message' => 'Methode nicht erlaubt'
]);
exit;
}
$locationId = (int)($_POST['id'] ?? $_GET['id'] ?? 0); $locationId = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
if ($locationId <= 0) { if ($locationId <= 0) {

View 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;

View File

@@ -69,6 +69,13 @@ $portTypes = $sql->get(
<td><small><?php echo htmlspecialchars($pt['comment'] ?? ''); ?></small></td> <td><small><?php echo htmlspecialchars($pt['comment'] ?? ''); ?></small></td>
<td class="actions"> <td class="actions">
<a href="?module=port_types&action=edit&id=<?php echo (int)$pt['id']; ?>" class="button button-small">Bearbeiten</a> <a href="?module=port_types&action=edit&id=<?php echo (int)$pt['id']; ?>" class="button button-small">Bearbeiten</a>
<button
type="button"
class="button button-small button-danger js-port-type-delete"
data-port-type-id="<?php echo (int)$pt['id']; ?>"
data-port-type-name="<?php echo htmlspecialchars((string)$pt['name'], ENT_QUOTES, 'UTF-8'); ?>">
Loeschen
</button>
</td> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
@@ -171,3 +178,36 @@ $portTypes = $sql->get(
white-space: nowrap; white-space: nowrap;
} }
</style> </style>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.js-port-type-delete').forEach((button) => {
button.addEventListener('click', () => {
const id = Number(button.dataset.portTypeId || '0');
const name = button.dataset.portTypeName || 'Porttyp';
if (id <= 0) {
return;
}
if (!confirm('Porttyp "' + name + '" wirklich loeschen?')) {
return;
}
fetch('?module=port_types&action=delete', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: 'id=' + encodeURIComponent(id)
})
.then((response) => response.json())
.then((data) => {
if (data && data.success) {
window.location.reload();
return;
}
alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
})
.catch(() => alert('Loeschen fehlgeschlagen'));
});
});
});
</script>