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

@@ -3,12 +3,20 @@
* app/modules/devices/delete.php
*
* Loescht ein Geraet. Bei Abhaengigkeiten ist force=1 erforderlich.
* Unterstuetzt GET-Redirects und AJAX-POST.
*/
$deviceId = (int)($_GET['id'] ?? 0);
$forceDelete = (int)($_GET['force'] ?? 0) === 1;
$isPost = ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST';
$deviceId = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
$forceDelete = (int)($_POST['force'] ?? $_GET['force'] ?? 0) === 1;
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";
header('Location: ?module=devices&action=list');
exit;
@@ -21,6 +29,12 @@ $device = $sql->single(
);
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";
header('Location: ?module=devices&action=list');
exit;
@@ -70,7 +84,23 @@ if ($hasDependencies && !$forceDelete) {
$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));
exit;
}
@@ -91,8 +121,19 @@ $deleted = $sql->set(
);
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'];
} 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";
}

View File

@@ -64,6 +64,19 @@ if ($isEdit) {
// =========================
$deviceTypes = $sql->get("SELECT id, name, category FROM device_types 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>
</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
========================= -->
@@ -264,34 +338,64 @@ $racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
.button:hover {
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>
<script>
function confirmDelete(link, id, connectionCount, portCount, moduleCount) {
if (confirm('Dieses Gerät wirklich löschen?')) {
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 true;
if (!confirm('Dieses Geraet wirklich loeschen?')) {
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>

View File

@@ -1,7 +1,7 @@
<?php
/**
* modules/devices/list.php
* Vollständige Geräteübersicht mit Filter
* Vollstaendige Geraeteuebersicht mit Filter
*/
// =========================
@@ -22,69 +22,88 @@ $params = [];
if ($search !== '') {
$where[] = "(d.name LIKE ? OR d.serial_number LIKE ? OR dt.name LIKE ?)";
$types .= "sss";
$types .= 'sss';
$params[] = "%$search%";
$params[] = "%$search%";
$params[] = "%$search%";
}
if ($typeId > 0) {
$where[] = "d.device_type_id = ?";
$types .= "i";
$where[] = 'd.device_type_id = ?';
$types .= 'i';
$params[] = $typeId;
}
if ($floorId > 0) {
$where[] = "f.id = ?";
$types .= "i";
$where[] = 'f.id = ?';
$types .= 'i';
$params[] = $floorId;
}
if ($rackId > 0) {
$where[] = "d.rack_id = ?";
$types .= "i";
$where[] = 'd.rack_id = ?';
$types .= 'i';
$params[] = $rackId;
}
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
// =========================
// Geräte laden
// Geraete laden
// =========================
$devices = $sql->get(
"
SELECT
d.id,
d.name,
d.serial_number,
d.rack_position_he,
d.rack_height_he,
d.web_config_url,
dt.name AS device_type,
dt.image_path,
f.name AS floor_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_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' 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' AND c.port_b_id IN (
SELECT dp4.id FROM device_ports dp4 WHERE dp4.device_id = d.id
))
) AS connection_count
$devices = $sql->get(
"
SELECT
d.id,
d.name,
d.serial_number,
d.rack_position_he,
d.rack_height_he,
d.web_config_url,
dt.name AS device_type,
dt.image_path,
f.name AS floor_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' 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' 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
JOIN device_types dt ON dt.id = d.device_type_id
LEFT JOIN racks r ON r.id = d.rack_id
@@ -99,22 +118,19 @@ $whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
// =========================
// Filter-Daten laden
// =========================
$deviceTypes = $sql->get("SELECT id, name FROM device_types 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", "", []);
$deviceTypes = $sql->get('SELECT id, name FROM device_types 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', '', []);
?>
<div class="devices-container">
<h1>Geräte</h1>
<h1>Geraete</h1>
<!-- =========================
Filter-Toolbar
========================= -->
<form method="get" class="filter-form">
<input type="hidden" name="module" value="devices">
<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">
<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=edit" class="button button-primary" style="margin-left: auto;">
+ Neues Gerät
+ Neues Geraet
</a>
</form>
<!-- =========================
Geräte-Liste
========================= -->
<?php if (!empty($devices)): ?>
<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>
<table class="device-list">
@@ -169,6 +182,7 @@ $racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
<th>Stockwerk</th>
<th>Rack</th>
<th>Position (HE)</th>
<th>Ports</th>
<th>Seriennummer</th>
<th>Webconfig</th>
<th>Aktionen</th>
@@ -186,28 +200,37 @@ $racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
</td>
<td>
<?php echo htmlspecialchars($d['floor_name'] ?? ''); ?>
<?php echo htmlspecialchars($d['floor_name'] ?? '-'); ?>
</td>
<td>
<?php echo htmlspecialchars($d['rack_name'] ?? ''); ?>
<?php echo htmlspecialchars($d['rack_name'] ?? '-'); ?>
</td>
<td>
<?php
if ($d['rack_position_he']) {
echo $d['rack_position_he'];
echo (int)$d['rack_position_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 {
echo "—";
echo '-';
}
?>
</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>
@@ -216,13 +239,13 @@ $racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
Webconfig
</a>
<?php else: ?>
-
<?php endif; ?>
</td>
<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=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>
</tr>
<?php endforeach; ?>
@@ -231,10 +254,10 @@ $racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
<?php else: ?>
<div class="empty-state">
<p>Keine Geräte gefunden.</p>
<p>Keine Geraete gefunden.</p>
<p>
<a href="?module=devices&action=edit" class="button button-primary">
Erstes Gerät anlegen
Erstes Geraet anlegen
</a>
</p>
</div>
@@ -351,33 +374,41 @@ $racks = $sql->get("SELECT id, name FROM racks ORDER BY name", "", []);
<script>
function confirmDelete(link, id, connectionCount, portCount, moduleCount) {
if (confirm('Dieses Gerät wirklich löschen?')) {
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 true;
if (!confirm('Dieses Geraet wirklich loeschen?')) {
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>

View File

@@ -25,6 +25,7 @@ $rackHeightHe = (int)($_POST['rack_height_he'] ?? 1);
$serialNumber = trim($_POST['serial_number'] ?? '');
$comment = trim($_POST['comment'] ?? '');
$webConfigUrl = trim($_POST['web_config_url'] ?? '');
$devicePortRows = is_array($_POST['device_ports'] ?? null) ? $_POST['device_ports'] : [];
if ($webConfigUrl === '') {
$webConfigUrl = null;
}
@@ -88,6 +89,9 @@ if ($isNewDevice) {
if ($isNewDevice && $deviceId > 0) {
copyDevicePortsFromType($sql, $deviceId, $deviceTypeId);
}
if ($deviceId > 0 && !$isNewDevice && !empty($devicePortRows)) {
syncDevicePorts($sql, $deviceId, $devicePortRows);
}
$_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);
}