Compare commits

..

5 Commits

Author SHA1 Message Date
96f885efde Direktverbindung fuer unverbundene Ports zum Patchfeld anbieten
closes #25
2026-02-19 11:09:36 +01:00
dbe977f62c Erlaube feste Verdrahtung plus Patchkabel fuer Outlet/Patchpanel
closes #26
2026-02-19 11:00:48 +01:00
b973d2857b Bearbeite Issues #22 bis #24
closes #22

closes #23

closes #24
2026-02-19 10:31:53 +01:00
0642a3b6ef Behebe Dashboard-, Loesch- und Infrastruktur-Issues
closes #20

closes #19

closes #18

closes #17
2026-02-19 10:20:04 +01:00
4214ac45d9 next 2026-02-19 10:09:04 +01:00
9 changed files with 567 additions and 84 deletions

View File

@@ -1,6 +1,15 @@
# NEXT_STEPS # NEXT_STEPS
## Aktive Aufgaben (priorisiert) ## 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) ## 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

@@ -90,6 +90,87 @@ function isTopologyPairAllowed(string $typeA, string $typeB): bool
return true; return true;
} }
function buildEndpointUsageMap($sql, int $excludeConnectionId = 0): array
{
$usage = [
'device' => [],
'module' => [],
'outlet' => [],
'patchpanel' => [],
];
$rows = $sql->get(
"SELECT id, port_a_type, port_a_id, port_b_type, port_b_id
FROM connections
WHERE id <> ?",
'i',
[$excludeConnectionId]
);
$track = static function (string $endpointType, int $endpointId, string $otherType) use (&$usage): void {
if ($endpointId <= 0 || !isset($usage[$endpointType])) {
return;
}
if (!isset($usage[$endpointType][$endpointId])) {
$usage[$endpointType][$endpointId] = [
'total' => 0,
'fixed' => 0,
'patch' => 0,
];
}
$usage[$endpointType][$endpointId]['total']++;
if (in_array($endpointType, ['outlet', 'patchpanel'], true)) {
if (in_array($otherType, ['outlet', 'patchpanel'], true)) {
$usage[$endpointType][$endpointId]['fixed']++;
} elseif (in_array($otherType, ['device', 'module'], true)) {
$usage[$endpointType][$endpointId]['patch']++;
}
}
};
foreach ((array)$rows as $row) {
$typeA = normalizeEndpointType((string)($row['port_a_type'] ?? ''));
$typeB = normalizeEndpointType((string)($row['port_b_type'] ?? ''));
$idA = (int)($row['port_a_id'] ?? 0);
$idB = (int)($row['port_b_id'] ?? 0);
if ($typeA === null || $typeB === null) {
continue;
}
$track($typeA, $idA, $typeB);
$track($typeB, $idB, $typeA);
}
return $usage;
}
function validateEndpointCapacity(array $usage, string $endpointType, int $endpointId, string $otherType, string $label): ?string
{
if ($endpointId <= 0) {
return null;
}
$stats = $usage[$endpointType][$endpointId] ?? ['total' => 0, 'fixed' => 0, 'patch' => 0];
if ((int)$stats['total'] <= 0) {
return null;
}
if (in_array($endpointType, ['outlet', 'patchpanel'], true)) {
if ((int)$stats['total'] >= 2) {
return $label . ' hat bereits die maximale Anzahl von 2 Verbindungen';
}
if (in_array($otherType, ['outlet', 'patchpanel'], true) && (int)$stats['fixed'] >= 1) {
return $label . ' hat bereits eine feste Verdrahtung';
}
if (in_array($otherType, ['device', 'module'], true) && (int)$stats['patch'] >= 1) {
return $label . ' hat bereits ein Patchkabel';
}
return null;
}
return $label . ' ist bereits in Verwendung';
}
function loadConnections($sql): void function loadConnections($sql): void
{ {
$contextType = strtolower(trim((string)($_GET['context_type'] ?? 'all'))); $contextType = strtolower(trim((string)($_GET['context_type'] ?? 'all')));
@@ -226,9 +307,20 @@ function saveConnection($sql): void
$mode = isset($data['mode']) ? (string)$data['mode'] : null; $mode = isset($data['mode']) ? (string)$data['mode'] : null;
$comment = isset($data['comment']) ? (string)$data['comment'] : null; $comment = isset($data['comment']) ? (string)$data['comment'] : null;
if (!empty($data['id'])) { $connectionId = !empty($data['id']) ? (int)$data['id'] : 0;
$id = (int)$data['id']; $usage = buildEndpointUsageMap($sql, $connectionId);
$existing = $sql->single('SELECT id FROM connections WHERE id = ?', 'i', [$id]); $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) { if (!$existing) {
jsonError('Verbindung existiert nicht', 404); jsonError('Verbindung existiert nicht', 404);
} }

View File

@@ -44,17 +44,6 @@
cursor: crosshair; cursor: crosshair;
overflow: hidden; overflow: hidden;
} }
.floor-plan-svg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
pointer-events: none;
z-index: 0;
opacity: 0.75;
border-radius: 6px;
}
.floor-plan-overlay { .floor-plan-overlay {
position: absolute; position: absolute;
inset: 0; inset: 0;
@@ -63,6 +52,10 @@
z-index: 2; z-index: 2;
touch-action: none; touch-action: none;
} }
.floor-plan-overlay .floor-plan-background {
opacity: 0.75;
pointer-events: none;
}
.floor-plan-overlay .active-marker { .floor-plan-overlay .active-marker {
cursor: move; cursor: move;
} }

View File

@@ -29,8 +29,26 @@ 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;
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'); const activeMarker = document.createElementNS(SVG_NS, 'rect');
activeMarker.classList.add('active-marker'); activeMarker.classList.add('active-marker');
@@ -45,7 +63,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 +90,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);
}; };
@@ -191,10 +249,10 @@ document.addEventListener('DOMContentLoaded', () => {
const panelBuildingSelect = document.getElementById('panel-building-select'); const panelBuildingSelect = document.getElementById('panel-building-select');
const panelFloorSelect = document.getElementById('panel-floor-select'); const panelFloorSelect = document.getElementById('panel-floor-select');
const outletRoomSelect = document.getElementById('outlet-room-select'); const outletRoomSelect = document.getElementById('outlet-room-select');
const floorPlanSvg = document.getElementById('floor-plan-svg');
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 +266,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();
@@ -239,35 +323,27 @@ document.addEventListener('DOMContentLoaded', () => {
}; };
const updateFloorPlanImage = () => { const updateFloorPlanImage = () => {
if (!floorPlanSvg) {
return;
}
const floorOption = panelFloorSelect?.selectedOptions?.[0]; const floorOption = panelFloorSelect?.selectedOptions?.[0];
const roomOption = outletRoomSelect?.selectedOptions?.[0]; const roomOption = outletRoomSelect?.selectedOptions?.[0];
const svgUrl = floorOption?.dataset?.svgUrl || roomOption?.dataset?.floorSvgUrl || ''; const svgUrl = floorOption?.dataset?.svgUrl || roomOption?.dataset?.floorSvgUrl || '';
if (svgUrl) { if (svgUrl) {
floorPlanSvg.src = svgUrl; backgroundImage.setAttribute('href', svgUrl);
floorPlanSvg.hidden = false; backgroundImage.setAttribute('width', String(planSize.width));
backgroundImage.setAttribute('height', String(planSize.height));
backgroundImage.setAttribute('display', 'block');
loadPlanDimensions(svgUrl); loadPlanDimensions(svgUrl);
} else { } else {
floorPlanSvg.removeAttribute('src'); backgroundImage.removeAttribute('href');
floorPlanSvg.hidden = true; backgroundImage.setAttribute('display', 'none');
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) {
floorPlanSvg.addEventListener('error', () => {
floorPlanSvg.removeAttribute('src');
floorPlanSvg.hidden = true;
});
}
const loadPlanDimensions = async (svgUrl) => { const loadPlanDimensions = async (svgUrl) => {
if (!svgUrl) { if (!svgUrl) {
return; return;
@@ -291,7 +367,9 @@ 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(); backgroundImage.setAttribute('width', String(planSize.width));
backgroundImage.setAttribute('height', String(planSize.height));
resetView();
renderReferenceMarkers(); renderReferenceMarkers();
updateFromInputs(); updateFromInputs();
return; return;
@@ -307,13 +385,17 @@ 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(); backgroundImage.setAttribute('width', String(planSize.width));
backgroundImage.setAttribute('height', String(planSize.height));
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(); backgroundImage.setAttribute('width', String(planSize.width));
backgroundImage.setAttribute('height', String(planSize.height));
resetView();
renderReferenceMarkers(); renderReferenceMarkers();
updateFromInputs(); updateFromInputs();
} }
@@ -382,6 +464,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,12 +486,18 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
const stopDrag = (event) => { const stopDrag = (event) => {
if (!dragging) { if (dragging) {
return; dragging = false;
if (activeMarker.hasPointerCapture(event.pointerId)) {
activeMarker.releasePointerCapture(event.pointerId);
}
} }
dragging = false; if (panning) {
if (activeMarker.hasPointerCapture(event.pointerId)) { panning = false;
activeMarker.releasePointerCapture(event.pointerId); panStart = null;
if (overlay.hasPointerCapture(event.pointerId)) {
overlay.releasePointerCapture(event.pointerId);
}
} }
}; };
@@ -417,6 +506,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 +529,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();
@@ -466,6 +594,7 @@ document.addEventListener('DOMContentLoaded', () => {
updateOverlayViewBox(); updateOverlayViewBox();
updateFromInputs(); updateFromInputs();
filterPatchpanelBindOptions();
if (panelLocationSelect) { if (panelLocationSelect) {
filterBuildingOptions(); filterBuildingOptions();

View File

@@ -39,6 +39,18 @@ $portBType = $normalizePortType((string)($connection['port_b_type'] ?? 'device')
$portAId = (int)($connection['port_a_id'] ?? 0); $portAId = (int)($connection['port_a_id'] ?? 0);
$portBId = (int)($connection['port_b_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 = [ $endpointOptions = [
'device' => [], 'device' => [],
'module' => [], 'module' => [],
@@ -46,7 +58,7 @@ $endpointOptions = [
'patchpanel' => [], 'patchpanel' => [],
]; ];
$occupiedByType = [ $occupiedStatsByType = [
'device' => [], 'device' => [],
'module' => [], 'module' => [],
'outlet' => [], 'outlet' => [],
@@ -62,31 +74,37 @@ $occupiedRows = $sql->get(
foreach ((array)$occupiedRows as $row) { foreach ((array)$occupiedRows as $row) {
$typeA = $normalizePortType((string)($row['port_a_type'] ?? '')); $typeA = $normalizePortType((string)($row['port_a_type'] ?? ''));
$idA = (int)($row['port_a_id'] ?? 0); $idA = (int)($row['port_a_id'] ?? 0);
if ($idA > 0 && isset($occupiedByType[$typeA])) { if ($idA > 0 && isset($occupiedStatsByType[$typeA])) {
$occupiedByType[$typeA][$idA] = true; if (!isset($occupiedStatsByType[$typeA][$idA])) {
$occupiedStatsByType[$typeA][$idA] = ['total' => 0];
}
$occupiedStatsByType[$typeA][$idA]['total']++;
} }
$typeB = $normalizePortType((string)($row['port_b_type'] ?? '')); $typeB = $normalizePortType((string)($row['port_b_type'] ?? ''));
$idB = (int)($row['port_b_id'] ?? 0); $idB = (int)($row['port_b_id'] ?? 0);
if ($idB > 0 && isset($occupiedByType[$typeB])) { if ($idB > 0 && isset($occupiedStatsByType[$typeB])) {
$occupiedByType[$typeB][$idB] = true; if (!isset($occupiedStatsByType[$typeB][$idB])) {
$occupiedStatsByType[$typeB][$idB] = ['total' => 0];
}
$occupiedStatsByType[$typeB][$idB]['total']++;
} }
} }
$isEndpointAllowed = static function (string $type, int $id) use ($occupiedByType, $portAType, $portAId, $portBType, $portBId): bool { $isEndpointAllowed = static function (string $type, int $id) use ($occupiedStatsByType, $portAType, $portAId, $portBType, $portBId): bool {
if ($id <= 0) { if ($id <= 0) {
return false; return false;
} }
if ($type === 'outlet') {
return true;
}
if ($type === $portAType && $id === $portAId) { if ($type === $portAType && $id === $portAId) {
return true; return true;
} }
if ($type === $portBType && $id === $portBId) { if ($type === $portBType && $id === $portBId) {
return true; return true;
} }
return empty($occupiedByType[$type][$id]);
$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. // Auto-heal: ensure each outlet has at least one selectable port.
@@ -173,7 +191,14 @@ foreach ($outletPorts as $row) {
if (!$isEndpointAllowed('outlet', $id)) { if (!$isEndpointAllowed('outlet', $id)) {
continue; continue;
} }
$parts = array_filter([(string)($row['floor_name'] ?? ''), (string)($row['room_name'] ?? ''), (string)$row['outlet_name'], (string)$row['name']]); $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'][] = [ $endpointOptions['outlet'][] = [
'id' => $id, 'id' => $id,
'label' => implode(' / ', $parts), 'label' => implode(' / ', $parts),

View File

@@ -82,16 +82,17 @@ $trackUsage = static function (string $endpointType, int $endpointId, string $ot
if (!isset($endpointUsage[$endpointType][$endpointId])) { if (!isset($endpointUsage[$endpointType][$endpointId])) {
$endpointUsage[$endpointType][$endpointId] = [ $endpointUsage[$endpointType][$endpointId] = [
'total' => 0, 'total' => 0,
'patchpanel' => 0, 'fixed' => 0,
'other' => 0, 'patch' => 0,
]; ];
} }
$endpointUsage[$endpointType][$endpointId]['total']++; $endpointUsage[$endpointType][$endpointId]['total']++;
if ($endpointType === 'outlet') {
if ($otherType === 'patchpanel') { if (in_array($endpointType, ['outlet', 'patchpanel'], true)) {
$endpointUsage[$endpointType][$endpointId]['patchpanel']++; if (in_array($otherType, ['outlet', 'patchpanel'], true)) {
} else { $endpointUsage[$endpointType][$endpointId]['fixed']++;
$endpointUsage[$endpointType][$endpointId]['other']++; } elseif (in_array($otherType, ['device', 'module'], true)) {
$endpointUsage[$endpointType][$endpointId]['patch']++;
} }
} }
}; };
@@ -111,26 +112,25 @@ $validateEndpointUsage = static function (string $endpointType, int $endpointId,
return null; return null;
} }
$stats = $endpointUsage[$endpointType][$endpointId] ?? ['total' => 0, 'patchpanel' => 0, 'other' => 0]; $stats = $endpointUsage[$endpointType][$endpointId] ?? ['total' => 0, 'fixed' => 0, 'patch' => 0];
if ((int)$stats['total'] <= 0) { if ((int)$stats['total'] <= 0) {
return null; return null;
} }
if ($endpointType !== 'outlet') { if (in_array($endpointType, ['outlet', 'patchpanel'], true)) {
return $label . " ist bereits in Verwendung"; if ((int)$stats['total'] >= 2) {
} return $label . " hat bereits die maximale Anzahl von 2 Verbindungen";
}
if ($otherType === 'patchpanel') { if (in_array($otherType, ['outlet', 'patchpanel'], true) && (int)$stats['fixed'] >= 1) {
if ((int)$stats['patchpanel'] > 0) { return $label . " hat bereits eine feste Verdrahtung";
return $label . " hat bereits eine Patchpanel-Verbindung"; }
if (in_array($otherType, ['device', 'module'], true) && (int)$stats['patch'] >= 1) {
return $label . " hat bereits ein Patchkabel";
} }
return null; return null;
} }
if ((int)$stats['other'] > 0) { return $label . " ist bereits in Verwendung";
return $label . " hat bereits eine Endgeraete-Verbindung";
}
return null;
}; };
$errorA = $validateEndpointUsage($portAType, $portAId, $portBType, 'Port an Endpunkt A'); $errorA = $validateEndpointUsage($portAType, $portAId, $portBType, 'Port an Endpunkt A');

View File

@@ -79,6 +79,26 @@ foreach ($sql->get(
} }
$devicePortPreviewByDevice = []; $devicePortPreviewByDevice = [];
$connectedDevicePorts = [];
foreach ($sql->get(
"SELECT port_a_type, port_a_id, port_b_type, port_b_id
FROM connections",
"",
[]
) as $row) {
$portAType = strtolower(trim((string)($row['port_a_type'] ?? '')));
$portBType = strtolower(trim((string)($row['port_b_type'] ?? '')));
$portAId = (int)($row['port_a_id'] ?? 0);
$portBId = (int)($row['port_b_id'] ?? 0);
if (($portAType === 'device' || $portAType === 'device_ports') && $portAId > 0) {
$connectedDevicePorts[$portAId] = true;
}
if (($portBType === 'device' || $portBType === 'device_ports') && $portBId > 0) {
$connectedDevicePorts[$portBId] = true;
}
}
foreach ($sql->get( foreach ($sql->get(
"SELECT id, device_id, name "SELECT id, device_id, name
FROM device_ports FROM device_ports
@@ -98,7 +118,8 @@ foreach ($sql->get(
} }
$devicePortPreviewByDevice[$deviceId][] = [ $devicePortPreviewByDevice[$deviceId][] = [
'id' => (int)($row['id'] ?? 0), 'id' => (int)($row['id'] ?? 0),
'name' => (string)($row['name'] ?? '') 'name' => (string)($row['name'] ?? ''),
'is_connected' => isset($connectedDevicePorts[(int)($row['id'] ?? 0)]),
]; ];
} }
@@ -245,7 +266,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>
@@ -658,7 +679,11 @@ foreach ($rackLinksByKey as $entry) {
overlayMeta.textContent = `Geraet: ${item.device_name} | Rack: ${item.rack_name} | Stockwerk: ${item.floor_name}`; overlayMeta.textContent = `Geraet: ${item.device_name} | Rack: ${item.rack_name} | Stockwerk: ${item.floor_name}`;
if (item.device_id > 0) { if (item.device_id > 0) {
overlayRackLink.innerHTML = `<a href="?module=devices&action=edit&id=${item.device_id}">Geraet bearbeiten</a>`; overlayRackLink.innerHTML = `<a href="?module=devices&action=edit&id=${item.device_id}">Geraet bearbeiten</a>`;
overlayDeviceLink.innerHTML = `<a href="?module=connections&action=edit">Verbindung fuer Port erstellen</a>`; if (item.port_id > 0 && !item.is_connected) {
overlayDeviceLink.innerHTML = `<a href="?module=connections&action=edit&port_a_type=device&port_a_id=${item.port_id}&port_b_type=patchpanel">Mit Patchfeld verbinden</a>`;
} else {
overlayDeviceLink.textContent = 'Port ist bereits verbunden';
}
} }
if (item.port_id > 0) { if (item.port_id > 0) {
overlayActions.innerHTML = `<a class="button button-small" href="?module=devices&action=edit&id=${item.device_id}">Port im Geraet aendern</a>`; overlayActions.innerHTML = `<a class="button button-small" href="?module=devices&action=edit&id=${item.device_id}">Port im Geraet aendern</a>`;
@@ -681,7 +706,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>
`; `;
} }
@@ -853,6 +878,7 @@ foreach ($rackLinksByKey as $entry) {
kind: 'port', kind: 'port',
port_id: Number(port.id || 0), port_id: Number(port.id || 0),
port_name: port.name || `Port ${portIndex + 1}`, port_name: port.name || `Port ${portIndex + 1}`,
is_connected: !!port.is_connected,
device_id: entry.device_id, device_id: entry.device_id,
device_name: entry.device_name, device_name: entry.device_name,
rack_name: entry.rack_name, rack_name: entry.rack_name,

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">
@@ -205,10 +257,9 @@ $mapOutlets = $sql->get(
data-active-id="<?php echo (int)($panel['id'] ?? 0); ?>" 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-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'); ?>"> data-reference-outlets="<?php echo htmlspecialchars(json_encode($mapOutlets, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ENT_QUOTES, 'UTF-8'); ?>">
<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 mit Mausrad, verschieben mit Shift + Drag.</p>
<p class="floor-plan-position">Koordinate: <span id="floor-plan-position"></span></p> <p class="floor-plan-position">Koordinate: <span id="floor-plan-position"></span></p>
</div> </div>
</div> </div>
@@ -255,6 +306,38 @@ $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); ?>">
@@ -270,10 +353,9 @@ $mapOutlets = $sql->get(
data-active-id="<?php echo (int)($outlet['id'] ?? 0); ?>" 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-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'); ?>"> data-reference-outlets="<?php echo htmlspecialchars(json_encode($mapOutlets, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ENT_QUOTES, 'UTF-8'); ?>">
<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 mit Mausrad, verschieben mit Shift + Drag.</p>
<p class="floor-plan-position">Koordinate: <span id="floor-plan-position"></span></p> <p class="floor-plan-position">Koordinate: <span id="floor-plan-position"></span></p>
</div> </div>
</div> </div>

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 {