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", + "", + [] +); ?>
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.
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) { @@ -122,7 +122,7 @@ if ($editorFloorId > 0) { - Zuruecksetzen + ZurücksetzenKein 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.
Patchpanels: blau, Wandbuchsen: gruen. Ziehen und loslassen zum Speichern.
+Patchpanels: blau, Wandbuchsen: grün. In dieser Ansicht sind Marker nicht verschiebbar.