From 3bc5a2ca04fbe947c24a977cb495df568cdd143c Mon Sep 17 00:00:00 2001 From: fixclean Date: Mon, 16 Feb 2026 10:04:12 +0100 Subject: [PATCH] =?UTF-8?q?Verhalten=20im=20Infrastruktur-Modul=20ist=20je?= =?UTF-8?q?tzt=20wie=20gew=C3=BCnscht:?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nur das aktiv bearbeitete Objekt ist verschiebbar. Alle anderen Objekte auf derselben Etage werden als halbtransparente Referenz angezeigt. In der Listenansicht sind Marker nur noch Vorschau (nicht verschiebbar). Geänderte Dateien: edit.php Referenzdaten für Patchpanels und Wandbuchsen geladen. Diese Daten als data-* am Canvas hinterlegt. Hinweise im UI angepasst. Styles ergänzt: .floor-plan-marker.is-active .floor-plan-reference (+ Varianten für Panel/Outlet) Inline-Script entfernt und auf externe JS-Datei umgestellt (CSP-sicher): floor-infrastructure-edit.js (neu) Drag/Drop nur für den aktiven Marker. Referenzmarker je Stockwerk rendern (halbtransparent, nicht interaktiv). Aktiven Marker aus Referenzmenge ausschließen. Referenzen bei Stockwerk-/Raumwechsel neu rendern. list.php Kartenansicht auf reine Vorschau umgestellt. Drag/Save-Logik entfernt. Marker per CSS nicht interaktiv (pointer-events: none, cursor: default). Hinweistext entsprechend angepasst. --- app/assets/js/floor-infrastructure-edit.js | 294 +++++++++++++++++++++ app/modules/floor_infrastructure/edit.php | 294 ++++----------------- app/modules/floor_infrastructure/list.php | 111 +------- 3 files changed, 357 insertions(+), 342 deletions(-) create mode 100644 app/assets/js/floor-infrastructure-edit.js diff --git a/app/assets/js/floor-infrastructure-edit.js b/app/assets/js/floor-infrastructure-edit.js new file mode 100644 index 0000000..ec91310 --- /dev/null +++ b/app/assets/js/floor-infrastructure-edit.js @@ -0,0 +1,294 @@ +document.addEventListener('DOMContentLoaded', () => { + const canvas = document.getElementById('floor-plan-canvas'); + const marker = document.getElementById('floor-plan-marker'); + const positionLabel = document.getElementById('floor-plan-position'); + if (!canvas || !marker) { + return; + } + + const xFieldName = canvas.dataset.xField; + const yFieldName = canvas.dataset.yField; + const xField = xFieldName ? document.querySelector(`input[name="${xFieldName}"]`) : null; + const yField = yFieldName ? document.querySelector(`input[name="${yFieldName}"]`) : null; + if (!xField || !yField) { + return; + } + + const markerWidth = Math.max(1, Number(canvas.dataset.markerWidth) || marker.offsetWidth); + const markerHeight = Math.max(1, Number(canvas.dataset.markerHeight) || marker.offsetHeight); + const markerType = canvas.dataset.markerType || ''; + const activeId = Number(canvas.dataset.activeId || 0); + const panelReferences = JSON.parse(canvas.dataset.referencePanels || '[]'); + const outletReferences = JSON.parse(canvas.dataset.referenceOutlets || '[]'); + + const clamp = (value, min, max) => Math.min(max, Math.max(min, value)); + + marker.classList.add('is-active'); + + const updatePositionLabel = (x, y) => { + if (positionLabel) { + positionLabel.textContent = `${Math.round(x)} x ${Math.round(y)}`; + } + }; + + const setMarkerPosition = (rawX, rawY) => { + const rect = canvas.getBoundingClientRect(); + const maxX = Math.max(0, rect.width - markerWidth); + const maxY = Math.max(0, rect.height - markerHeight); + const left = clamp(rawX, 0, maxX); + const top = clamp(rawY, 0, maxY); + marker.style.left = `${left}px`; + marker.style.top = `${top}px`; + xField.value = Math.round(left); + yField.value = Math.round(top); + updatePositionLabel(left, top); + }; + + const updateFromInputs = () => { + setMarkerPosition(Number(xField.value) || 0, Number(yField.value) || 0); + }; + + updateFromInputs(); + + let dragging = false; + let offsetX = 0; + let offsetY = 0; + + const startDrag = (clientX, clientY) => { + const markerRect = marker.getBoundingClientRect(); + offsetX = clientX - markerRect.left; + offsetY = clientY - markerRect.top; + }; + + marker.addEventListener('pointerdown', (event) => { + event.preventDefault(); + dragging = true; + startDrag(event.clientX, event.clientY); + marker.setPointerCapture(event.pointerId); + }); + + marker.addEventListener('pointermove', (event) => { + if (!dragging) { + return; + } + const rect = canvas.getBoundingClientRect(); + setMarkerPosition(event.clientX - rect.left - offsetX, event.clientY - rect.top - offsetY); + }); + + const stopDrag = (event) => { + if (!dragging) { + return; + } + dragging = false; + if (marker.hasPointerCapture(event.pointerId)) { + marker.releasePointerCapture(event.pointerId); + } + }; + + ['pointerup', 'pointercancel', 'pointerleave'].forEach((evt) => { + marker.addEventListener(evt, stopDrag); + }); + + canvas.addEventListener('pointerdown', (event) => { + if (event.target !== canvas) { + return; + } + const rect = canvas.getBoundingClientRect(); + setMarkerPosition(event.clientX - rect.left - markerWidth / 2, event.clientY - rect.top - markerHeight / 2); + }); + + [xField, yField].forEach((input) => { + input.addEventListener('input', () => { + updateFromInputs(); + }); + }); + + window.addEventListener('resize', () => { + updateFromInputs(); + }); + + const panelLocationSelect = document.getElementById('panel-location-select'); + const panelBuildingSelect = document.getElementById('panel-building-select'); + const panelFloorSelect = document.getElementById('panel-floor-select'); + const outletRoomSelect = document.getElementById('outlet-room-select'); + const floorPlanSvg = document.getElementById('floor-plan-svg'); + const panelPlacementFields = document.getElementById('panel-placement-fields'); + const panelFloorPlanGroup = document.getElementById('panel-floor-plan-group'); + const panelFloorMissingHint = document.getElementById('panel-floor-missing-hint'); + + const buildingOptions = panelBuildingSelect ? Array.from(panelBuildingSelect.options).filter((option) => option.value !== '') : []; + const floorOptions = panelFloorSelect ? Array.from(panelFloorSelect.options).filter((option) => option.value !== '') : []; + + const getCurrentFloorId = () => { + const floorOption = panelFloorSelect?.selectedOptions?.[0]; + if (floorOption?.value) { + return Number(floorOption.value); + } + const roomOption = outletRoomSelect?.selectedOptions?.[0]; + return Number(roomOption?.dataset?.floorId || 0); + }; + + const renderReferenceMarkers = () => { + canvas.querySelectorAll('.floor-plan-reference').forEach((node) => node.remove()); + const currentFloorId = getCurrentFloorId(); + if (!currentFloorId) { + return; + } + + const appendReference = (entry, cssClass, width, height) => { + const markerRef = document.createElement('div'); + markerRef.className = `floor-plan-reference ${cssClass}`; + markerRef.style.left = `${Number(entry.x || entry.pos_x || 0)}px`; + markerRef.style.top = `${Number(entry.y || entry.pos_y || 0)}px`; + if (width > 0) { + markerRef.style.width = `${width}px`; + } + if (height > 0) { + markerRef.style.height = `${height}px`; + } + markerRef.title = entry.name || ''; + canvas.appendChild(markerRef); + }; + + panelReferences.forEach((entry) => { + if (Number(entry.floor_id) !== currentFloorId) { + return; + } + if (markerType === 'patchpanel' && Number(entry.id) === activeId) { + return; + } + appendReference(entry, 'panel-marker', Math.max(1, Number(entry.width) || 140), Math.max(1, Number(entry.height) || 40)); + }); + + outletReferences.forEach((entry) => { + if (Number(entry.floor_id) !== currentFloorId) { + return; + } + if (markerType === 'outlet' && Number(entry.id) === activeId) { + return; + } + appendReference(entry, 'outlet-marker', 32, 32); + }); + }; + + const updateFloorPlanImage = () => { + if (!floorPlanSvg) { + return; + } + + const floorOption = panelFloorSelect?.selectedOptions?.[0]; + const roomOption = outletRoomSelect?.selectedOptions?.[0]; + const svgUrl = floorOption?.dataset?.svgUrl || roomOption?.dataset?.floorSvgUrl || ''; + + if (svgUrl) { + floorPlanSvg.src = svgUrl; + floorPlanSvg.hidden = false; + } else { + floorPlanSvg.removeAttribute('src'); + floorPlanSvg.hidden = true; + } + renderReferenceMarkers(); + }; + + if (floorPlanSvg) { + floorPlanSvg.addEventListener('error', () => { + floorPlanSvg.removeAttribute('src'); + floorPlanSvg.hidden = true; + }); + } + + const updatePanelPlacementVisibility = () => { + if (!panelFloorSelect || !panelPlacementFields || !panelFloorPlanGroup) { + return; + } + + const hasFloor = !!panelFloorSelect.value; + panelPlacementFields.hidden = !hasFloor; + panelFloorPlanGroup.hidden = !hasFloor; + if (panelFloorMissingHint) { + panelFloorMissingHint.hidden = hasFloor; + } + }; + + const filterFloorOptions = () => { + if (!panelFloorSelect) { + return; + } + const buildingValue = panelBuildingSelect?.value || ''; + let firstMatch = ''; + floorOptions.forEach((option) => { + const matches = !buildingValue || option.dataset.buildingId === buildingValue; + option.hidden = !matches; + option.disabled = !matches; + if (matches && !firstMatch) { + firstMatch = option.value; + } + }); + + const selectedOption = panelFloorSelect.querySelector(`option[value="${panelFloorSelect.value}"]`); + const isSelectedHidden = selectedOption ? selectedOption.hidden : true; + if (buildingValue && (!panelFloorSelect.value || isSelectedHidden) && firstMatch) { + panelFloorSelect.value = firstMatch; + } + + updatePanelPlacementVisibility(); + updateFloorPlanImage(); + }; + + const filterBuildingOptions = () => { + if (!panelBuildingSelect) { + return; + } + const locationValue = panelLocationSelect?.value || ''; + let firstMatch = ''; + buildingOptions.forEach((option) => { + const matches = !locationValue || option.dataset.locationId === locationValue; + option.hidden = !matches; + option.disabled = !matches; + if (matches && !firstMatch) { + firstMatch = option.value; + } + }); + + const selectedOption = panelBuildingSelect.querySelector(`option[value="${panelBuildingSelect.value}"]`); + const isSelectedHidden = selectedOption ? selectedOption.hidden : true; + if (locationValue && (!panelBuildingSelect.value || isSelectedHidden) && firstMatch) { + panelBuildingSelect.value = firstMatch; + } + }; + + if (panelLocationSelect) { + panelLocationSelect.addEventListener('change', () => { + filterBuildingOptions(); + filterFloorOptions(); + }); + } + + if (panelBuildingSelect) { + panelBuildingSelect.addEventListener('change', () => { + filterFloorOptions(); + }); + } + + if (panelFloorSelect) { + panelFloorSelect.addEventListener('change', () => { + updatePanelPlacementVisibility(); + updateFloorPlanImage(); + }); + } + + if (outletRoomSelect) { + outletRoomSelect.addEventListener('change', () => { + updateFloorPlanImage(); + }); + } + + if (panelLocationSelect) { + filterBuildingOptions(); + filterFloorOptions(); + } else { + updateFloorPlanImage(); + } + + updatePanelPlacementVisibility(); +}); diff --git a/app/modules/floor_infrastructure/edit.php b/app/modules/floor_infrastructure/edit.php index 709cbb5..ea66230 100644 --- a/app/modules/floor_infrastructure/edit.php +++ b/app/modules/floor_infrastructure/edit.php @@ -103,6 +103,22 @@ if ($type === 'patchpanel') { $markerWidth = $type === 'patchpanel' ? $panel['width'] : $defaultOutletSize; $markerHeight = $type === 'patchpanel' ? $panel['height'] : $defaultOutletSize; + +$mapPatchPanels = $sql->get( + "SELECT id, floor_id, name, pos_x, pos_y, width, height + FROM floor_patchpanels + ORDER BY floor_id, name", + "", + [] +); +$mapOutlets = $sql->get( + "SELECT o.id, r.floor_id, o.name, o.x, o.y + FROM network_outlets o + JOIN rooms r ON r.id = o.room_id + ORDER BY r.floor_id, o.name", + "", + [] +); ?>
@@ -188,12 +204,15 @@ $markerHeight = $type === 'patchpanel' ? $panel['height'] : $defaultOutletSize; data-marker-height="" data-marker-type="patchpanel" data-x-field="pos_x" - data-y-field="pos_y"> + data-y-field="pos_y" + data-active-id="" + data-reference-panels="" + data-reference-outlets=""> Stockwerksplan
-

Ziehe das Patchpanel oder klicke auf den Plan, um die Position zu setzen.

+

Nur das aktuell bearbeitete Patchpanel ist verschiebbar. Andere Objekte werden als Referenz halbtransparent angezeigt.

Koordinate:

@@ -204,7 +223,7 @@ $markerHeight = $type === 'patchpanel' ? $panel['height'] : $defaultOutletSize;

> - Position, Groesse und Stockwerkskarte werden erst angezeigt, sobald ein Stockwerk ausgewaehlt ist. + Position, Größe und Stockwerkskarte werden erst angezeigt, sobald ein Stockwerk ausgewählt ist.

@@ -253,12 +272,15 @@ $markerHeight = $type === 'patchpanel' ? $panel['height'] : $defaultOutletSize; data-marker-height="" data-marker-type="outlet" data-x-field="x" - data-y-field="y"> + data-y-field="y" + data-active-id="" + data-reference-panels="" + data-reference-outlets=""> Stockwerksplan
-

Klicke oder ziehe die Wandbuchse auf dem Plan. Die Größe bleibt quadratisch.

+

Nur die aktuell bearbeitete Wandbuchse ist verschiebbar. Andere Objekte werden als Referenz halbtransparent angezeigt.

Koordinate:

@@ -350,6 +372,10 @@ $markerHeight = $type === 'patchpanel' ? $panel['height'] : $defaultOutletSize; touch-action: none; z-index: 2; } +.floor-plan-marker.is-active { + cursor: move; + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.8), 0 0 0 4px rgba(13, 110, 253, 0.35); +} .floor-plan-marker.panel-marker { background: rgba(13, 110, 253, 0.25); border: 2px solid #0d6efd; @@ -360,6 +386,26 @@ $markerHeight = $type === 'patchpanel' ? $panel['height'] : $defaultOutletSize; border: 2px solid #198754; border-radius: 4px; } +.floor-plan-reference { + position: absolute; + top: 0; + left: 0; + pointer-events: none; + opacity: 0.35; + z-index: 1; +} +.floor-plan-reference.panel-marker { + background: rgba(13, 110, 253, 0.22); + border: 2px solid rgba(13, 110, 253, 0.7); + border-radius: 6px; +} +.floor-plan-reference.outlet-marker { + width: 32px; + height: 32px; + background: rgba(25, 135, 84, 0.22); + border: 2px solid rgba(25, 135, 84, 0.7); + border-radius: 4px; +} .floor-plan-hint { font-size: 0.85em; color: #444; @@ -372,241 +418,5 @@ $markerHeight = $type === 'patchpanel' ? $panel['height'] : $defaultOutletSize; } - + diff --git a/app/modules/floor_infrastructure/list.php b/app/modules/floor_infrastructure/list.php index b4c1117..58a50c2 100644 --- a/app/modules/floor_infrastructure/list.php +++ b/app/modules/floor_infrastructure/list.php @@ -2,7 +2,7 @@ /** * app/modules/floor_infrastructure/list.php * - * Uebersicht ueber Patchpanels und Netzwerkbuchsen auf Stockwerken + * Übersicht über Patchpanels und Netzwerkbuchsen auf Stockwerken */ $floorId = (int)($_GET['floor_id'] ?? 0); @@ -101,10 +101,10 @@ if ($editorFloorId > 0) {
- + Patchpanel hinzufuegen + + Patchpanel hinzufügen - + Wandbuchse hinzufuegen + + Wandbuchse hinzufügen
@@ -122,7 +122,7 @@ if ($editorFloorId > 0) { - Zuruecksetzen + Zurücksetzen
@@ -134,7 +134,7 @@ if ($editorFloorId > 0) { Name Stockwerk Position - Groesse + Größe Ports Aktionen @@ -196,18 +196,18 @@ if ($editorFloorId > 0) {

Stockwerksplan-Verortung

-

Kein Stockwerk fuer die Planansicht verfuegbar.

+

Kein Stockwerk für die Planansicht verfügbar.

-

Das gewaehlte Stockwerk hat noch keinen SVG-Plan hinterlegt.

+

Das gewählte Stockwerk hat noch keinen SVG-Plan hinterlegt.

-

Drag & Drop ist aktiv fuer . Aenderungen werden direkt gespeichert.

+

Vorschau für . Positionen bearbeitest du über den jeweiligen „Bearbeiten“-Button.

Stockwerksplan
-

Patchpanels: blau, Wandbuchsen: gruen. Ziehen und loslassen zum Speichern.

+

Patchpanels: blau, Wandbuchsen: grün. In dieser Ansicht sind Marker nicht verschiebbar.

@@ -277,8 +277,9 @@ if ($editorFloorId > 0) { left: 0; user-select: none; touch-action: none; - cursor: move; + cursor: default; z-index: 2; + pointer-events: none; } .infra-marker.patchpanel { background: rgba(13, 110, 253, 0.25); @@ -292,9 +293,6 @@ if ($editorFloorId > 0) { border: 2px solid #198754; border-radius: 4px; } -.infra-marker.is-saving { - opacity: 0.65; -} .floor-plan-hint { margin: 8px 0 0; font-size: 0.85em; @@ -319,95 +317,10 @@ document.addEventListener('DOMContentLoaded', () => { return; } - const saveUrl = canvas.dataset.saveUrl; - const floorId = Number(canvas.dataset.floorId || 0); const outletSize = 32; const patchPanels = ; const outlets = ; - const clamp = (value, min, max) => Math.min(max, Math.max(min, value)); - - const savePosition = async (entry, type, marker) => { - const form = new FormData(); - - if (type === 'patchpanel') { - form.append('type', 'patchpanel'); - form.append('id', String(entry.id)); - form.append('name', entry.name || ''); - form.append('floor_id', String(floorId)); - form.append('pos_x', String(entry.x)); - form.append('pos_y', String(entry.y)); - form.append('width', String(entry.width)); - form.append('height', String(entry.height)); - form.append('port_count', String(entry.port_count || 0)); - form.append('comment', entry.comment || ''); - } else { - form.append('type', 'outlet'); - form.append('id', String(entry.id)); - form.append('name', entry.name || ''); - form.append('room_id', String(entry.room_id || 0)); - form.append('x', String(entry.x)); - form.append('y', String(entry.y)); - form.append('comment', entry.comment || ''); - } - - marker.classList.add('is-saving'); - try { - await fetch(saveUrl, { - method: 'POST', - body: form, - credentials: 'same-origin' - }); - } finally { - marker.classList.remove('is-saving'); - } - }; - - const bindDrag = (marker, entry, type) => { - let dragging = false; - let offsetX = 0; - let offsetY = 0; - - marker.addEventListener('pointerdown', (event) => { - dragging = true; - const rect = marker.getBoundingClientRect(); - offsetX = event.clientX - rect.left; - offsetY = event.clientY - rect.top; - marker.setPointerCapture(event.pointerId); - }); - - marker.addEventListener('pointermove', (event) => { - if (!dragging) { - return; - } - - const rect = canvas.getBoundingClientRect(); - const width = type === 'patchpanel' ? entry.width : outletSize; - const height = type === 'patchpanel' ? entry.height : outletSize; - const maxX = Math.max(0, rect.width - width); - const maxY = Math.max(0, rect.height - height); - - entry.x = Math.round(clamp(event.clientX - rect.left - offsetX, 0, maxX)); - entry.y = Math.round(clamp(event.clientY - rect.top - offsetY, 0, maxY)); - marker.style.left = `${entry.x}px`; - marker.style.top = `${entry.y}px`; - }); - - const stopDrag = (event) => { - if (!dragging) { - return; - } - dragging = false; - if (marker.hasPointerCapture(event.pointerId)) { - marker.releasePointerCapture(event.pointerId); - } - savePosition(entry, type, marker); - }; - - marker.addEventListener('pointerup', stopDrag); - marker.addEventListener('pointercancel', stopDrag); - }; - const createMarker = (entry, type) => { const marker = document.createElement('div'); marker.className = `infra-marker ${type}`; @@ -423,8 +336,6 @@ document.addEventListener('DOMContentLoaded', () => { marker.style.left = `${entry.x}px`; marker.style.top = `${entry.y}px`; canvas.appendChild(marker); - - bindDrag(marker, entry, type); }; patchPanels.forEach((entry) => createMarker(entry, 'patchpanel'));