Behebe Dashboard-, Loesch- und Infrastruktur-Issues

closes #20

closes #19

closes #18

closes #17
This commit is contained in:
2026-02-19 10:20:04 +01:00
parent 4214ac45d9
commit 0642a3b6ef
6 changed files with 387 additions and 24 deletions

View File

@@ -1,10 +1,10 @@
# NEXT_STEPS # NEXT_STEPS
## Aktive Aufgaben (priorisiert) ## Aktive Aufgaben (priorisiert)
- [ ] [#20] //TODO Gesamt-Topologie-Wand im dashboard ist schwarze - [x] [#20] Gesamt-Topologie-Wand im dashboard ist schwarze
- [ ] [#19] //TODO gerät nicht löschbar wegen ports, ports sind aber nicht löschbar - [x] [#19] gerät nicht löschbar wegen ports, ports sind aber nicht löschbar
- [ ] [#18] //TODO wandbuchsen direkt beim erstellen schon an patchpanel bindfen - [x] [#18] wandbuchsen direkt beim erstellen schon an patchpanel bindfen
- [ ] [#17] //TODO infrastruktur karten zoombar, um objekte besser positionieren zu können, steps soll aber immernoch 1 bleiben - [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) ## Verifikation (Status unklar, nicht als erledigt markieren ohne Reproduktion + Commit)
- [x] [#15] Neue Verbindung: Netzwerkdose auswählbar (Regressionstest in UI durchgeführt) - [x] [#15] Neue Verbindung: Netzwerkdose auswählbar (Regressionstest in UI durchgeführt)

View File

@@ -30,6 +30,10 @@
flex-direction: column; flex-direction: column;
gap: 6px; gap: 6px;
} }
.floor-plan-toolbar {
display: flex;
gap: 6px;
}
.floor-plan-canvas { .floor-plan-canvas {
position: relative; position: relative;
width: 100%; width: 100%;

View File

@@ -29,8 +29,14 @@ document.addEventListener('DOMContentLoaded', () => {
let markerX = 0; let markerX = 0;
let markerY = 0; let markerY = 0;
let dragging = false; let dragging = false;
let panning = false;
let panStart = null;
let dragOffsetX = 0; let dragOffsetX = 0;
let dragOffsetY = 0; let dragOffsetY = 0;
let viewX = 0;
let viewY = 0;
let viewWidth = DEFAULT_PLAN_SIZE.width;
let viewHeight = DEFAULT_PLAN_SIZE.height;
const activeMarker = document.createElementNS(SVG_NS, 'rect'); const activeMarker = document.createElementNS(SVG_NS, 'rect');
activeMarker.classList.add('active-marker'); activeMarker.classList.add('active-marker');
@@ -45,7 +51,7 @@ document.addEventListener('DOMContentLoaded', () => {
const planSize = { ...DEFAULT_PLAN_SIZE }; const planSize = { ...DEFAULT_PLAN_SIZE };
const updateOverlayViewBox = () => { const updateOverlayViewBox = () => {
overlay.setAttribute('viewBox', `0 0 ${planSize.width} ${planSize.height}`); overlay.setAttribute('viewBox', `${viewX} ${viewY} ${viewWidth} ${viewHeight}`);
}; };
const updatePositionLabel = (x, y) => { const updatePositionLabel = (x, y) => {
@@ -72,17 +78,57 @@ document.addEventListener('DOMContentLoaded', () => {
}; };
const toOverlayPoint = (clientX, clientY) => { const toOverlayPoint = (clientX, clientY) => {
const pt = overlay.createSVGPoint(); const rect = overlay.getBoundingClientRect();
pt.x = clientX; if (rect.width <= 0 || rect.height <= 0) {
pt.y = clientY;
const ctm = overlay.getScreenCTM();
if (!ctm) {
return null; return null;
} }
const transformed = pt.matrixTransform(ctm.inverse()); const ratioX = (clientX - rect.left) / rect.width;
const ratioY = (clientY - rect.top) / rect.height;
const transformed = {
x: viewX + (ratioX * viewWidth),
y: viewY + (ratioY * viewHeight)
};
return { x: transformed.x, y: transformed.y }; return { x: transformed.x, y: transformed.y };
}; };
const clampView = () => {
const minWidth = Math.max(30, planSize.width * 0.1);
const minHeight = Math.max(30, planSize.height * 0.1);
viewWidth = clamp(viewWidth, minWidth, planSize.width);
viewHeight = clamp(viewHeight, minHeight, planSize.height);
viewX = clamp(viewX, 0, Math.max(0, planSize.width - viewWidth));
viewY = clamp(viewY, 0, Math.max(0, planSize.height - viewHeight));
};
const applyView = () => {
clampView();
updateOverlayViewBox();
};
const zoomAt = (clientX, clientY, factor) => {
const point = toOverlayPoint(clientX, clientY);
if (!point) {
return;
}
const ratioX = (point.x - viewX) / viewWidth;
const ratioY = (point.y - viewY) / viewHeight;
const nextWidth = viewWidth * factor;
const nextHeight = viewHeight * factor;
viewX = point.x - (ratioX * nextWidth);
viewY = point.y - (ratioY * nextHeight);
viewWidth = nextWidth;
viewHeight = nextHeight;
applyView();
};
const resetView = () => {
viewX = 0;
viewY = 0;
viewWidth = planSize.width;
viewHeight = planSize.height;
applyView();
};
const updateFromInputs = () => { const updateFromInputs = () => {
setMarkerPosition(Number(xField.value) || 0, Number(yField.value) || 0); setMarkerPosition(Number(xField.value) || 0, Number(yField.value) || 0);
}; };
@@ -195,6 +241,7 @@ document.addEventListener('DOMContentLoaded', () => {
const panelPlacementFields = document.getElementById('panel-placement-fields'); const panelPlacementFields = document.getElementById('panel-placement-fields');
const panelFloorPlanGroup = document.getElementById('panel-floor-plan-group'); const panelFloorPlanGroup = document.getElementById('panel-floor-plan-group');
const panelFloorMissingHint = document.getElementById('panel-floor-missing-hint'); const panelFloorMissingHint = document.getElementById('panel-floor-missing-hint');
const outletBindPatchpanelSelect = document.getElementById('outlet-bind-patchpanel-port-id');
const buildingOptions = panelBuildingSelect ? Array.from(panelBuildingSelect.options).filter((option) => option.value !== '') : []; const buildingOptions = panelBuildingSelect ? Array.from(panelBuildingSelect.options).filter((option) => option.value !== '') : [];
const floorOptions = panelFloorSelect ? Array.from(panelFloorSelect.options).filter((option) => option.value !== '') : []; const floorOptions = panelFloorSelect ? Array.from(panelFloorSelect.options).filter((option) => option.value !== '') : [];
@@ -208,6 +255,32 @@ document.addEventListener('DOMContentLoaded', () => {
return Number(roomOption?.dataset?.floorId || 0); return Number(roomOption?.dataset?.floorId || 0);
}; };
const filterPatchpanelBindOptions = () => {
if (!outletBindPatchpanelSelect) {
return;
}
const currentFloorId = getCurrentFloorId();
const options = Array.from(outletBindPatchpanelSelect.options).filter((option) => option.value !== '');
let firstMatch = '';
let selectedStillVisible = false;
options.forEach((option) => {
const optionFloorId = Number(option.dataset.floorId || 0);
const matchesFloor = !currentFloorId || optionFloorId === currentFloorId;
option.hidden = !matchesFloor;
if (matchesFloor && !option.disabled && !firstMatch) {
firstMatch = option.value;
}
if (matchesFloor && option.selected) {
selectedStillVisible = true;
}
});
if (!selectedStillVisible && firstMatch && !outletBindPatchpanelSelect.value) {
outletBindPatchpanelSelect.value = firstMatch;
}
};
const renderReferenceMarkers = () => { const renderReferenceMarkers = () => {
clearRoomHighlight(); clearRoomHighlight();
clearReferenceMarkers(); clearReferenceMarkers();
@@ -256,9 +329,10 @@ document.addEventListener('DOMContentLoaded', () => {
floorPlanSvg.hidden = true; floorPlanSvg.hidden = true;
planSize.width = DEFAULT_PLAN_SIZE.width; planSize.width = DEFAULT_PLAN_SIZE.width;
planSize.height = DEFAULT_PLAN_SIZE.height; planSize.height = DEFAULT_PLAN_SIZE.height;
updateOverlayViewBox(); resetView();
} }
renderReferenceMarkers(); renderReferenceMarkers();
filterPatchpanelBindOptions();
}; };
if (floorPlanSvg) { if (floorPlanSvg) {
@@ -291,7 +365,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (parts.length === 4 && parts.every((value) => Number.isFinite(value))) { if (parts.length === 4 && parts.every((value) => Number.isFinite(value))) {
planSize.width = Math.max(1, parts[2]); planSize.width = Math.max(1, parts[2]);
planSize.height = Math.max(1, parts[3]); planSize.height = Math.max(1, parts[3]);
updateOverlayViewBox(); resetView();
renderReferenceMarkers(); renderReferenceMarkers();
updateFromInputs(); updateFromInputs();
return; return;
@@ -307,13 +381,13 @@ document.addEventListener('DOMContentLoaded', () => {
planSize.width = DEFAULT_PLAN_SIZE.width; planSize.width = DEFAULT_PLAN_SIZE.width;
planSize.height = DEFAULT_PLAN_SIZE.height; planSize.height = DEFAULT_PLAN_SIZE.height;
} }
updateOverlayViewBox(); resetView();
renderReferenceMarkers(); renderReferenceMarkers();
updateFromInputs(); updateFromInputs();
} catch (error) { } catch (error) {
planSize.width = DEFAULT_PLAN_SIZE.width; planSize.width = DEFAULT_PLAN_SIZE.width;
planSize.height = DEFAULT_PLAN_SIZE.height; planSize.height = DEFAULT_PLAN_SIZE.height;
updateOverlayViewBox(); resetView();
renderReferenceMarkers(); renderReferenceMarkers();
updateFromInputs(); updateFromInputs();
} }
@@ -382,6 +456,7 @@ document.addEventListener('DOMContentLoaded', () => {
activeMarker.addEventListener('pointerdown', (event) => { activeMarker.addEventListener('pointerdown', (event) => {
event.preventDefault(); event.preventDefault();
dragging = true; dragging = true;
panning = false;
const point = toOverlayPoint(event.clientX, event.clientY); const point = toOverlayPoint(event.clientX, event.clientY);
if (!point) { if (!point) {
return; return;
@@ -403,13 +478,19 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
const stopDrag = (event) => { const stopDrag = (event) => {
if (!dragging) { if (dragging) {
return;
}
dragging = false; dragging = false;
if (activeMarker.hasPointerCapture(event.pointerId)) { if (activeMarker.hasPointerCapture(event.pointerId)) {
activeMarker.releasePointerCapture(event.pointerId); activeMarker.releasePointerCapture(event.pointerId);
} }
}
if (panning) {
panning = false;
panStart = null;
if (overlay.hasPointerCapture(event.pointerId)) {
overlay.releasePointerCapture(event.pointerId);
}
}
}; };
['pointerup', 'pointercancel', 'pointerleave'].forEach((evt) => { ['pointerup', 'pointercancel', 'pointerleave'].forEach((evt) => {
@@ -417,6 +498,19 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
overlay.addEventListener('pointerdown', (event) => { overlay.addEventListener('pointerdown', (event) => {
if (event.shiftKey || event.button === 1) {
event.preventDefault();
panning = true;
dragging = false;
panStart = {
clientX: event.clientX,
clientY: event.clientY,
viewX,
viewY
};
overlay.setPointerCapture(event.pointerId);
return;
}
if (event.target !== overlay) { if (event.target !== overlay) {
return; return;
} }
@@ -427,6 +521,32 @@ document.addEventListener('DOMContentLoaded', () => {
setMarkerPosition(point.x - markerWidth / 2, point.y - markerHeight / 2); setMarkerPosition(point.x - markerWidth / 2, point.y - markerHeight / 2);
}); });
overlay.addEventListener('pointermove', (event) => {
if (!panning || !panStart) {
return;
}
const rect = overlay.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0) {
return;
}
const scaleX = viewWidth / rect.width;
const scaleY = viewHeight / rect.height;
const dx = (event.clientX - panStart.clientX) * scaleX;
const dy = (event.clientY - panStart.clientY) * scaleY;
viewX = panStart.viewX - dx;
viewY = panStart.viewY - dy;
applyView();
});
overlay.addEventListener('pointerup', stopDrag);
overlay.addEventListener('pointercancel', stopDrag);
overlay.addEventListener('wheel', (event) => {
event.preventDefault();
const factor = event.deltaY < 0 ? 0.9 : 1.1;
zoomAt(event.clientX, event.clientY, factor);
}, { passive: false });
[xField, yField].forEach((input) => { [xField, yField].forEach((input) => {
input.addEventListener('input', () => { input.addEventListener('input', () => {
updateFromInputs(); updateFromInputs();
@@ -464,8 +584,26 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
} }
document.querySelectorAll('[data-floor-plan-zoom]').forEach((button) => {
button.addEventListener('click', () => {
const action = button.getAttribute('data-floor-plan-zoom');
if (action === 'in') {
const rect = overlay.getBoundingClientRect();
zoomAt(rect.left + (rect.width / 2), rect.top + (rect.height / 2), 0.85);
return;
}
if (action === 'out') {
const rect = overlay.getBoundingClientRect();
zoomAt(rect.left + (rect.width / 2), rect.top + (rect.height / 2), 1.15);
return;
}
resetView();
});
});
updateOverlayViewBox(); updateOverlayViewBox();
updateFromInputs(); updateFromInputs();
filterPatchpanelBindOptions();
if (panelLocationSelect) { if (panelLocationSelect) {
filterBuildingOptions(); filterBuildingOptions();

View File

@@ -245,7 +245,7 @@ foreach ($rackLinksByKey as $entry) {
<p class="topology-wall__hint">Hierarchie: Standort → Gebaeude → Stockwerk → Rack → Geraet. Linien zeigen Rack-Verbindungen (dicker = mehr Links).</p> <p class="topology-wall__hint">Hierarchie: Standort → Gebaeude → Stockwerk → Rack → Geraet. Linien zeigen Rack-Verbindungen (dicker = mehr Links).</p>
<svg id="dashboard-topology-svg" viewBox="0 0 2400 1400" role="img" aria-label="Topologie-Wand"> <svg id="dashboard-topology-svg" viewBox="0 0 2400 1400" role="img" aria-label="Topologie-Wand">
<rect id="dashboard-topology-bg" x="0" y="0" width="2400" height="1400" class="topology-bg"></rect> <rect id="dashboard-topology-bg" x="0" y="0" width="2400" height="1400" class="topology-bg" fill="#f7faff"></rect>
<g id="dashboard-topology-grid"></g> <g id="dashboard-topology-grid"></g>
<g id="dashboard-topology-connections"></g> <g id="dashboard-topology-connections"></g>
<g id="dashboard-topology-layer"></g> <g id="dashboard-topology-layer"></g>
@@ -681,7 +681,7 @@ foreach ($rackLinksByKey as $entry) {
overlayDeviceLink.innerHTML = `<a href="?module=devices&action=edit&id=${item.device_id}">Geraet bearbeiten</a>`; overlayDeviceLink.innerHTML = `<a href="?module=devices&action=edit&id=${item.device_id}">Geraet bearbeiten</a>`;
overlayActions.innerHTML = ` overlayActions.innerHTML = `
<a class="button button-small" href="?module=devices&action=edit&id=${item.device_id}">Editieren</a> <a class="button button-small" href="?module=devices&action=edit&id=${item.device_id}">Editieren</a>
<a class="button button-small button-danger" href="?module=devices&action=delete&id=${item.device_id}" onclick="return confirm('Dieses Geraet wirklich loeschen?');">Entfernen</a> <a class="button button-small button-danger" href="?module=devices&action=delete&id=${item.device_id}&force=1" onclick="return confirm('Dieses Geraet wirklich loeschen?');">Entfernen</a>
`; `;
} }

View File

@@ -125,6 +125,58 @@ $mapOutlets = $sql->get(
"", "",
[] []
); );
$patchpanelPortOptions = $sql->get(
"SELECT
fpp.id,
fpp.name,
fp.name AS patchpanel_name,
fp.floor_id,
f.name AS floor_name,
EXISTS(
SELECT 1
FROM connections c
WHERE
((c.port_a_type = 'patchpanel' OR c.port_a_type = 'floor_patchpanel_ports') AND c.port_a_id = fpp.id)
OR
((c.port_b_type = 'patchpanel' OR c.port_b_type = 'floor_patchpanel_ports') AND c.port_b_id = fpp.id)
) AS is_occupied
FROM floor_patchpanel_ports fpp
JOIN floor_patchpanels fp ON fp.id = fpp.patchpanel_id
LEFT JOIN floors f ON f.id = fp.floor_id
ORDER BY f.name, fp.name, fpp.name",
"",
[]
);
$selectedBindPatchpanelPortId = 0;
if ($type === 'outlet' && $id > 0) {
$selectedBindPatchpanelPortId = (int)($sql->single(
"SELECT
CASE
WHEN (c.port_a_type = 'patchpanel' OR c.port_a_type = 'floor_patchpanel_ports') THEN c.port_a_id
WHEN (c.port_b_type = 'patchpanel' OR c.port_b_type = 'floor_patchpanel_ports') THEN c.port_b_id
ELSE 0
END AS patchpanel_port_id
FROM connections c
JOIN network_outlet_ports nop
ON (
((c.port_a_type = 'outlet' OR c.port_a_type = 'network_outlet_ports') AND c.port_a_id = nop.id)
OR
((c.port_b_type = 'outlet' OR c.port_b_type = 'network_outlet_ports') AND c.port_b_id = nop.id)
)
WHERE nop.outlet_id = ?
AND (
c.port_a_type = 'patchpanel' OR c.port_a_type = 'floor_patchpanel_ports'
OR
c.port_b_type = 'patchpanel' OR c.port_b_type = 'floor_patchpanel_ports'
)
ORDER BY c.id
LIMIT 1",
"i",
[$id]
)['patchpanel_port_id'] ?? 0);
}
?> ?>
<div class="floor-infra-edit"> <div class="floor-infra-edit">
@@ -196,6 +248,11 @@ $mapOutlets = $sql->get(
<div id="panel-floor-plan-group" class="form-group" <?php echo $showPanelPlacementFields ? '' : 'hidden'; ?>> <div id="panel-floor-plan-group" class="form-group" <?php echo $showPanelPlacementFields ? '' : 'hidden'; ?>>
<label>Stockwerkskarte</label> <label>Stockwerkskarte</label>
<div class="floor-plan-block"> <div class="floor-plan-block">
<div class="floor-plan-toolbar">
<button type="button" class="button button-small" data-floor-plan-zoom="in">+</button>
<button type="button" class="button button-small" data-floor-plan-zoom="out">-</button>
<button type="button" class="button button-small" data-floor-plan-zoom="reset">Reset</button>
</div>
<div id="floor-plan-canvas" class="floor-plan-canvas" <div id="floor-plan-canvas" class="floor-plan-canvas"
data-marker-width="<?php echo $markerWidth; ?>" data-marker-width="<?php echo $markerWidth; ?>"
data-marker-height="<?php echo $markerHeight; ?>" data-marker-height="<?php echo $markerHeight; ?>"
@@ -208,7 +265,7 @@ $mapOutlets = $sql->get(
<img id="floor-plan-svg" class="floor-plan-svg" alt="Stockwerksplan"> <img id="floor-plan-svg" class="floor-plan-svg" alt="Stockwerksplan">
<svg id="floor-plan-overlay" class="floor-plan-overlay" viewBox="0 0 1 1" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg> <svg id="floor-plan-overlay" class="floor-plan-overlay" viewBox="0 0 1 1" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
</div> </div>
<p class="floor-plan-hint">Nur das aktuell bearbeitete Patchpanel ist verschiebbar. Andere Objekte werden als Referenz halbtransparent angezeigt. Neue Objekte starten bei Position 30 x 30.</p> <p class="floor-plan-hint">Nur das aktuell bearbeitete Patchpanel ist verschiebbar. Andere Objekte werden als Referenz halbtransparent angezeigt. Neue Objekte starten bei Position 30 x 30. Zoom per Mausrad, verschieben mit Shift + Drag.</p>
<p class="floor-plan-position">Koordinate: <span id="floor-plan-position"></span></p> <p class="floor-plan-position">Koordinate: <span id="floor-plan-position"></span></p>
</div> </div>
</div> </div>
@@ -255,12 +312,49 @@ $mapOutlets = $sql->get(
</select> </select>
</div> </div>
<div class="form-group">
<label for="outlet-bind-patchpanel-port-id">Direkt mit Patchpanel-Port verbinden</label>
<select name="bind_patchpanel_port_id" id="outlet-bind-patchpanel-port-id">
<option value="">- Kein direkter Link -</option>
<?php foreach ($patchpanelPortOptions as $portOption): ?>
<?php
$portId = (int)($portOption['id'] ?? 0);
$isSelected = $selectedBindPatchpanelPortId === $portId;
$isOccupied = ((int)($portOption['is_occupied'] ?? 0) === 1);
$isDisabled = $isOccupied && !$isSelected;
$labelParts = array_filter([
(string)($portOption['floor_name'] ?? ''),
(string)($portOption['patchpanel_name'] ?? ''),
(string)($portOption['name'] ?? ''),
]);
$label = implode(' / ', $labelParts);
if ($isOccupied && !$isSelected) {
$label .= ' (belegt)';
}
?>
<option
value="<?php echo $portId; ?>"
data-floor-id="<?php echo (int)($portOption['floor_id'] ?? 0); ?>"
<?php echo $isSelected ? 'selected' : ''; ?>
<?php echo $isDisabled ? 'disabled' : ''; ?>>
<?php echo htmlspecialchars($label); ?>
</option>
<?php endforeach; ?>
</select>
<small>Nur Ports vom gewaehlten Stockwerk sind auswaehlbar. Beim Speichern wird die Verbindung automatisch erstellt.</small>
</div>
<input type="hidden" name="x" value="<?php echo (int)($outlet['x'] ?? 30); ?>"> <input type="hidden" name="x" value="<?php echo (int)($outlet['x'] ?? 30); ?>">
<input type="hidden" name="y" value="<?php echo (int)($outlet['y'] ?? 30); ?>"> <input type="hidden" name="y" value="<?php echo (int)($outlet['y'] ?? 30); ?>">
<div class="form-group"> <div class="form-group">
<label>Stockwerkskarte</label> <label>Stockwerkskarte</label>
<div class="floor-plan-block"> <div class="floor-plan-block">
<div class="floor-plan-toolbar">
<button type="button" class="button button-small" data-floor-plan-zoom="in">+</button>
<button type="button" class="button button-small" data-floor-plan-zoom="out">-</button>
<button type="button" class="button button-small" data-floor-plan-zoom="reset">Reset</button>
</div>
<div id="floor-plan-canvas" class="floor-plan-canvas" <div id="floor-plan-canvas" class="floor-plan-canvas"
data-marker-width="<?php echo $markerWidth; ?>" data-marker-width="<?php echo $markerWidth; ?>"
data-marker-height="<?php echo $markerHeight; ?>" data-marker-height="<?php echo $markerHeight; ?>"
@@ -273,7 +367,7 @@ $mapOutlets = $sql->get(
<img id="floor-plan-svg" class="floor-plan-svg" alt="Stockwerksplan"> <img id="floor-plan-svg" class="floor-plan-svg" alt="Stockwerksplan">
<svg id="floor-plan-overlay" class="floor-plan-overlay" viewBox="0 0 1 1" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg> <svg id="floor-plan-overlay" class="floor-plan-overlay" viewBox="0 0 1 1" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
</div> </div>
<p class="floor-plan-hint">Nur die aktuell bearbeitete Wandbuchse ist verschiebbar. Blau = Patchpanel, Gruen = Dosen-Referenz, Orange = gewaehlter Raum. Netzwerkdosen sind immer 10 x 10.</p> <p class="floor-plan-hint">Nur die aktuell bearbeitete Wandbuchse ist verschiebbar. Blau = Patchpanel, Gruen = Dosen-Referenz, Orange = gewaehlter Raum. Netzwerkdosen sind immer 10 x 10. Zoom per Mausrad, verschieben mit Shift + Drag.</p>
<p class="floor-plan-position">Koordinate: <span id="floor-plan-position"></span></p> <p class="floor-plan-position">Koordinate: <span id="floor-plan-position"></span></p>
</div> </div>
</div> </div>

View File

@@ -80,6 +80,7 @@ if ($type === 'patchpanel') {
$x = (int)($_POST['x'] ?? 0); $x = (int)($_POST['x'] ?? 0);
$y = (int)($_POST['y'] ?? 0); $y = (int)($_POST['y'] ?? 0);
$comment = trim($_POST['comment'] ?? ''); $comment = trim($_POST['comment'] ?? '');
$bindPatchpanelPortId = (int)($_POST['bind_patchpanel_port_id'] ?? 0);
$outletId = $id; $outletId = $id;
$errors = []; $errors = [];
@@ -126,6 +127,132 @@ if ($type === 'patchpanel') {
[$outletId] [$outletId]
); );
} }
if ($bindPatchpanelPortId > 0) {
$roomFloorId = (int)($sql->single(
"SELECT floor_id FROM rooms WHERE id = ?",
"i",
[$roomId]
)['floor_id'] ?? 0);
$patchpanelPort = $sql->single(
"SELECT
fpp.id,
fp.floor_id
FROM floor_patchpanel_ports fpp
JOIN floor_patchpanels fp ON fp.id = fpp.patchpanel_id
WHERE fpp.id = ?",
"i",
[$bindPatchpanelPortId]
);
if (!$patchpanelPort) {
$_SESSION['error'] = 'Gewaehlter Patchpanel-Port existiert nicht';
$_SESSION['validation_errors'] = ['Gewaehlter Patchpanel-Port existiert nicht'];
header('Location: ?module=floor_infrastructure&action=edit&type=outlet&id=' . $outletId);
exit;
}
if ($roomFloorId <= 0 || (int)$patchpanelPort['floor_id'] !== $roomFloorId) {
$_SESSION['error'] = 'Patchpanel-Port und Raum muessen auf demselben Stockwerk liegen';
$_SESSION['validation_errors'] = ['Patchpanel-Port und Raum muessen auf demselben Stockwerk liegen'];
header('Location: ?module=floor_infrastructure&action=edit&type=outlet&id=' . $outletId);
exit;
}
$outletPortId = (int)($sql->single(
"SELECT id
FROM network_outlet_ports
WHERE outlet_id = ?
ORDER BY id
LIMIT 1",
"i",
[$outletId]
)['id'] ?? 0);
if ($outletPortId <= 0) {
$_SESSION['error'] = 'Wandbuchsen-Port konnte nicht ermittelt werden';
$_SESSION['validation_errors'] = ['Wandbuchsen-Port konnte nicht ermittelt werden'];
header('Location: ?module=floor_infrastructure&action=edit&type=outlet&id=' . $outletId);
exit;
}
$existingPatchpanelUsage = $sql->single(
"SELECT
id,
port_a_type,
port_a_id,
port_b_type,
port_b_id
FROM connections
WHERE
((port_a_type = 'patchpanel' OR port_a_type = 'floor_patchpanel_ports') AND port_a_id = ?)
OR
((port_b_type = 'patchpanel' OR port_b_type = 'floor_patchpanel_ports') AND port_b_id = ?)
LIMIT 1",
"ii",
[$bindPatchpanelPortId, $bindPatchpanelPortId]
);
if ($existingPatchpanelUsage) {
$sameOutletConnection = (
(
(($existingPatchpanelUsage['port_a_type'] ?? '') === 'outlet' || ($existingPatchpanelUsage['port_a_type'] ?? '') === 'network_outlet_ports')
&& (int)($existingPatchpanelUsage['port_a_id'] ?? 0) === $outletPortId
)
||
(
(($existingPatchpanelUsage['port_b_type'] ?? '') === 'outlet' || ($existingPatchpanelUsage['port_b_type'] ?? '') === 'network_outlet_ports')
&& (int)($existingPatchpanelUsage['port_b_id'] ?? 0) === $outletPortId
)
);
if (!$sameOutletConnection) {
$_SESSION['error'] = 'Gewaehlter Patchpanel-Port ist bereits verbunden';
$_SESSION['validation_errors'] = ['Gewaehlter Patchpanel-Port ist bereits verbunden'];
header('Location: ?module=floor_infrastructure&action=edit&type=outlet&id=' . $outletId);
exit;
}
}
$sql->set(
"DELETE FROM connections
WHERE
((port_a_type = 'outlet' OR port_a_type = 'network_outlet_ports') AND port_a_id = ? AND (port_b_type = 'patchpanel' OR port_b_type = 'floor_patchpanel_ports'))
OR
((port_b_type = 'outlet' OR port_b_type = 'network_outlet_ports') AND port_b_id = ? AND (port_a_type = 'patchpanel' OR port_a_type = 'floor_patchpanel_ports'))",
"ii",
[$outletPortId, $outletPortId]
);
$connectionTypeId = (int)($sql->single(
"SELECT id FROM connection_types ORDER BY id LIMIT 1",
"",
[]
)['id'] ?? 0);
if ($connectionTypeId <= 0) {
$connectionTypeId = (int)$sql->set(
"INSERT INTO connection_types (name, medium, duplex, line_style, comment) VALUES (?, ?, ?, ?, ?)",
"sssss",
['Default', 'copper', 'custom', 'solid', 'Auto-created by floor_infrastructure/save'],
true
);
}
if ($connectionTypeId <= 0) {
$_SESSION['error'] = 'Kein Verbindungstyp fuer automatische Bindung verfuegbar';
$_SESSION['validation_errors'] = ['Kein Verbindungstyp fuer automatische Bindung verfuegbar'];
header('Location: ?module=floor_infrastructure&action=edit&type=outlet&id=' . $outletId);
exit;
}
$sql->set(
"INSERT INTO connections (connection_type_id, port_a_type, port_a_id, port_b_type, port_b_id, vlan_config, comment)
VALUES (?, 'outlet', ?, 'patchpanel', ?, NULL, ?)",
"iiis",
[$connectionTypeId, $outletPortId, $bindPatchpanelPortId, 'Auto-Link bei Wandbuchsen-Erstellung']
);
}
} }
$_SESSION['success'] = $id > 0 ? 'Wandbuchse gespeichert' : 'Wandbuchse erstellt'; $_SESSION['success'] = $id > 0 ? 'Wandbuchse gespeichert' : 'Wandbuchse erstellt';
} else { } else {