From 12141485ae14b9c37e2cd4115a1839027c0e6398 Mon Sep 17 00:00:00 2001 From: fixclean Date: Mon, 16 Feb 2026 11:57:24 +0100 Subject: [PATCH] infrastruktur karte --- app/assets/css/floor-infrastructure-edit.css | 108 ++++++ app/assets/css/floor-infrastructure-list.css | 109 ++++++ app/assets/js/floor-infrastructure-edit.js | 346 ++++++++++++++----- app/assets/js/floor-infrastructure-list.js | 148 ++++++++ app/modules/floor_infrastructure/edit.php | 139 +------- app/modules/floor_infrastructure/list.php | 310 ++++++----------- app/modules/floor_infrastructure/save.php | 14 +- 7 files changed, 752 insertions(+), 422 deletions(-) create mode 100644 app/assets/css/floor-infrastructure-edit.css create mode 100644 app/assets/css/floor-infrastructure-list.css create mode 100644 app/assets/js/floor-infrastructure-list.js diff --git a/app/assets/css/floor-infrastructure-edit.css b/app/assets/css/floor-infrastructure-edit.css new file mode 100644 index 0000000..2b3a129 --- /dev/null +++ b/app/assets/css/floor-infrastructure-edit.css @@ -0,0 +1,108 @@ +.floor-infra-edit { + padding: 25px; + max-width: 1200px; +} +.infra-edit-form { + display: flex; + flex-direction: column; + gap: 15px; +} +.form-group { + display: flex; + flex-direction: column; + gap: 6px; +} +.form-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 12px; +} +.form-actions { + display: flex; + gap: 10px; +} +.info { + font-size: 0.9em; + color: #555; +} +.floor-plan-block { + display: flex; + flex-direction: column; + gap: 6px; +} +.floor-plan-canvas { + position: relative; + width: 100%; + min-height: 560px; + border: 1px solid #d4d4d4; + border-radius: 8px; + background-color: #fff; + background-image: + linear-gradient(90deg, rgba(0, 0, 0, 0.05) 1px, transparent 1px), + linear-gradient(rgba(0, 0, 0, 0.05) 1px, transparent 1px); + background-size: 40px 40px; + cursor: crosshair; + 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 { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + z-index: 2; + touch-action: none; +} +.floor-plan-overlay .active-marker { + cursor: move; +} +.floor-plan-overlay .panel-marker { + fill: rgba(13, 110, 253, 0.25); + stroke: #0d6efd; + stroke-width: 2; +} +.floor-plan-overlay .outlet-marker { + fill: rgba(25, 135, 84, 0.25); + stroke: #198754; + stroke-width: 2; +} +.floor-plan-overlay .reference-marker { + pointer-events: none; + opacity: 0.35; +} +.floor-plan-overlay .room-highlight { + pointer-events: none; + fill: rgba(255, 193, 7, 0.22); + stroke: #ff9800; + stroke-width: 2; +} +.floor-plan-overlay .reference-marker.panel-marker { + fill: rgba(13, 110, 253, 0.22); + stroke: rgba(13, 110, 253, 0.7); + stroke-width: 2; +} +.floor-plan-overlay .reference-marker.outlet-marker { + fill: rgba(25, 135, 84, 0.22); + stroke: rgba(25, 135, 84, 0.7); + stroke-width: 2; +} +.floor-plan-hint { + font-size: 0.85em; + color: #444; + margin: 0; +} +.floor-plan-position { + margin: 0; + font-size: 0.85em; + color: #666; +} diff --git a/app/assets/css/floor-infrastructure-list.css b/app/assets/css/floor-infrastructure-list.css new file mode 100644 index 0000000..45e8617 --- /dev/null +++ b/app/assets/css/floor-infrastructure-list.css @@ -0,0 +1,109 @@ +.floor-infra { + padding: 25px; +} + +.toolbar { + display: flex; + gap: 10px; + margin-bottom: 15px; +} + +.filter-form { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + margin-bottom: 18px; +} + +.filter-form select { + padding: 8px 10px; + border-radius: 4px; + border: 1px solid #ccc; +} + +.infra-plan { + padding: 15px; + margin-bottom: 28px; + background: #f7f7f7; + border: 1px dashed #ccc; + border-radius: 6px; +} + +.infra-floor-canvas { + position: relative; + margin-top: 12px; + width: 100%; + min-height: 560px; + border: 1px solid #d4d4d4; + border-radius: 8px; + background: #fff; + overflow: hidden; +} + +.infra-floor-svg { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: contain; + pointer-events: none; + opacity: 0.85; +} + +.infra-floor-overlay { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + z-index: 2; +} + +.infra-overlay-marker { + pointer-events: auto; +} + +.infra-overlay-marker.patchpanel { + fill: rgba(13, 110, 253, 0.25); + stroke: #0d6efd; + stroke-width: 2; +} + +.infra-overlay-marker.outlet { + fill: rgba(25, 135, 84, 0.25); + stroke: #198754; + stroke-width: 2; +} + +.infra-section { + margin-bottom: 30px; +} + +.infra-table { + width: 100%; + border-collapse: collapse; + margin-top: 10px; +} + +.infra-table th, +.infra-table td { + padding: 10px; + border-bottom: 1px solid #ddd; +} + +.floor-plan-hint { + margin: 8px 0 0; + font-size: 0.85em; + color: #444; +} + +.empty-state { + padding: 20px; + background: #fafafa; + border: 1px dashed #ccc; + border-radius: 6px; +} + +.actions .button-small { + margin-right: 6px; +} diff --git a/app/assets/js/floor-infrastructure-edit.js b/app/assets/js/floor-infrastructure-edit.js index 46241df..8db1d3f 100644 --- a/app/assets/js/floor-infrastructure-edit.js +++ b/app/assets/js/floor-infrastructure-edit.js @@ -1,8 +1,11 @@ document.addEventListener('DOMContentLoaded', () => { + const SVG_NS = 'http://www.w3.org/2000/svg'; + const DEFAULT_PLAN_SIZE = { width: 2000, height: 1000 }; + const canvas = document.getElementById('floor-plan-canvas'); - const marker = document.getElementById('floor-plan-marker'); + const overlay = document.getElementById('floor-plan-overlay'); const positionLabel = document.getElementById('floor-plan-position'); - if (!canvas || !marker) { + if (!canvas || !overlay) { return; } @@ -14,8 +17,8 @@ document.addEventListener('DOMContentLoaded', () => { return; } - const markerWidth = Math.max(1, Number(canvas.dataset.markerWidth) || marker.offsetWidth); - const markerHeight = Math.max(1, Number(canvas.dataset.markerHeight) || marker.offsetHeight); + const markerWidth = Math.max(1, Number(canvas.dataset.markerWidth) || 10); + const markerHeight = Math.max(1, Number(canvas.dataset.markerHeight) || 10); const markerType = canvas.dataset.markerType || ''; const activeId = Number(canvas.dataset.activeId || 0); const panelReferences = JSON.parse(canvas.dataset.referencePanels || '[]'); @@ -23,7 +26,27 @@ document.addEventListener('DOMContentLoaded', () => { const clamp = (value, min, max) => Math.min(max, Math.max(min, value)); - marker.classList.add('is-active'); + let markerX = 0; + let markerY = 0; + let dragging = false; + let dragOffsetX = 0; + let dragOffsetY = 0; + + const activeMarker = document.createElementNS(SVG_NS, 'rect'); + activeMarker.classList.add('active-marker'); + if (markerType === 'patchpanel') { + activeMarker.classList.add('panel-marker'); + } else { + activeMarker.classList.add('outlet-marker'); + } + activeMarker.setAttribute('width', String(markerWidth)); + activeMarker.setAttribute('height', String(markerHeight)); + overlay.appendChild(activeMarker); + + const planSize = { ...DEFAULT_PLAN_SIZE }; + const updateOverlayViewBox = () => { + overlay.setAttribute('viewBox', `0 0 ${planSize.width} ${planSize.height}`); + }; const updatePositionLabel = (x, y) => { if (positionLabel) { @@ -31,81 +54,138 @@ document.addEventListener('DOMContentLoaded', () => { } }; + const paintActiveMarker = () => { + activeMarker.setAttribute('x', String(Math.round(markerX))); + activeMarker.setAttribute('y', String(Math.round(markerY))); + }; + 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 maxX = Math.max(0, planSize.width - markerWidth); + const maxY = Math.max(0, planSize.height - markerHeight); + markerX = clamp(rawX, 0, maxX); + markerY = clamp(rawY, 0, maxY); + + paintActiveMarker(); + xField.value = Math.round(markerX); + yField.value = Math.round(markerY); + updatePositionLabel(markerX, markerY); + }; + + const toOverlayPoint = (clientX, clientY) => { + const pt = overlay.createSVGPoint(); + pt.x = clientX; + pt.y = clientY; + const ctm = overlay.getScreenCTM(); + if (!ctm) { + return null; + } + const transformed = pt.matrixTransform(ctm.inverse()); + return { x: transformed.x, y: transformed.y }; }; 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; + const clearReferenceMarkers = () => { + overlay.querySelectorAll('.reference-marker').forEach((node) => node.remove()); }; - 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); - } + const clearRoomHighlight = () => { + overlay.querySelectorAll('.room-highlight').forEach((node) => node.remove()); }; - ['pointerup', 'pointercancel', 'pointerleave'].forEach((evt) => { - marker.addEventListener(evt, stopDrag); - }); + const getNumericCoord = (value) => { + if (value === null || value === undefined || value === '') { + return null; + } + const num = Number(value); + return Number.isFinite(num) ? num : null; + }; - canvas.addEventListener('pointerdown', (event) => { - if (event.target !== canvas) { + const appendReference = (entry, cssClass, width, height) => { + const rawX = entry.x ?? entry.pos_x; + const rawY = entry.y ?? entry.pos_y; + const x = getNumericCoord(rawX); + const y = getNumericCoord(rawY); + if (x === null || y === null) { 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(); - }); - }); + const ref = document.createElementNS(SVG_NS, 'rect'); + ref.classList.add('reference-marker', cssClass); + ref.setAttribute('x', String(Math.round(x))); + ref.setAttribute('y', String(Math.round(y))); + ref.setAttribute('width', String(Math.max(1, Math.round(width)))); + ref.setAttribute('height', String(Math.max(1, Math.round(height)))); + if (entry.name) { + ref.setAttribute('aria-label', String(entry.name)); + } + overlay.insertBefore(ref, activeMarker); + }; - window.addEventListener('resize', () => { - updateFromInputs(); - }); + const appendRoomHighlight = () => { + if (!outletRoomSelect) { + return; + } + const selectedRoomOption = outletRoomSelect.selectedOptions?.[0]; + if (!selectedRoomOption || !selectedRoomOption.value) { + return; + } + + const currentFloorId = getCurrentFloorId(); + const roomFloorId = Number(selectedRoomOption.dataset.floorId || 0); + if (!currentFloorId || roomFloorId !== currentFloorId) { + return; + } + + const polygonRaw = String(selectedRoomOption.dataset.roomPolygon || '').trim(); + if (polygonRaw) { + try { + const parsed = JSON.parse(polygonRaw); + if (Array.isArray(parsed)) { + const points = parsed + .map((point) => ({ + x: Number(point && point.x), + y: Number(point && point.y) + })) + .filter((point) => Number.isFinite(point.x) && Number.isFinite(point.y)); + + if (points.length >= 3) { + const polygon = document.createElementNS(SVG_NS, 'polygon'); + polygon.classList.add('room-highlight'); + polygon.setAttribute( + 'points', + points.map((point) => `${Math.round(point.x)},${Math.round(point.y)}`).join(' ') + ); + overlay.insertBefore(polygon, activeMarker); + return; + } + } + } catch (error) { + // ignore invalid room polygon json + } + } + + const x = Number(selectedRoomOption.dataset.roomX || 0); + const y = Number(selectedRoomOption.dataset.roomY || 0); + const width = Number(selectedRoomOption.dataset.roomWidth || 0); + const height = Number(selectedRoomOption.dataset.roomHeight || 0); + if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(width) || !Number.isFinite(height)) { + return; + } + if (width <= 0 || height <= 0) { + return; + } + + const rect = document.createElementNS(SVG_NS, 'rect'); + rect.classList.add('room-highlight'); + rect.setAttribute('x', String(Math.round(x))); + rect.setAttribute('y', String(Math.round(y))); + rect.setAttribute('width', String(Math.round(width))); + rect.setAttribute('height', String(Math.round(height))); + overlay.insertBefore(rect, activeMarker); + }; const panelLocationSelect = document.getElementById('panel-location-select'); const panelBuildingSelect = document.getElementById('panel-building-select'); @@ -129,26 +209,13 @@ document.addEventListener('DOMContentLoaded', () => { }; const renderReferenceMarkers = () => { - canvas.querySelectorAll('.floor-plan-reference').forEach((node) => node.remove()); + clearRoomHighlight(); + clearReferenceMarkers(); 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); - }; + appendRoomHighlight(); panelReferences.forEach((entry) => { if (Number(entry.floor_id) !== currentFloorId) { @@ -157,7 +224,7 @@ document.addEventListener('DOMContentLoaded', () => { 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)); + appendReference(entry, 'panel-marker', Math.max(1, Number(entry.width) || 20), Math.max(1, Number(entry.height) || 5)); }); outletReferences.forEach((entry) => { @@ -183,9 +250,13 @@ document.addEventListener('DOMContentLoaded', () => { if (svgUrl) { floorPlanSvg.src = svgUrl; floorPlanSvg.hidden = false; + loadPlanDimensions(svgUrl); } else { floorPlanSvg.removeAttribute('src'); floorPlanSvg.hidden = true; + planSize.width = DEFAULT_PLAN_SIZE.width; + planSize.height = DEFAULT_PLAN_SIZE.height; + updateOverlayViewBox(); } renderReferenceMarkers(); }; @@ -197,6 +268,57 @@ document.addEventListener('DOMContentLoaded', () => { }); } + const loadPlanDimensions = async (svgUrl) => { + if (!svgUrl) { + return; + } + try { + const response = await fetch(svgUrl, { credentials: 'same-origin' }); + if (!response.ok) { + throw new Error('SVG not available'); + } + const raw = await response.text(); + const parser = new DOMParser(); + const doc = parser.parseFromString(raw, 'image/svg+xml'); + const root = doc.documentElement; + if (!root || root.nodeName.toLowerCase() === 'parsererror') { + throw new Error('Invalid SVG'); + } + + const vb = String(root.getAttribute('viewBox') || '').trim(); + if (vb) { + const parts = vb.split(/\s+/).map((value) => Number(value)); + if (parts.length === 4 && parts.every((value) => Number.isFinite(value))) { + planSize.width = Math.max(1, parts[2]); + planSize.height = Math.max(1, parts[3]); + updateOverlayViewBox(); + renderReferenceMarkers(); + updateFromInputs(); + return; + } + } + + const width = Number(root.getAttribute('width')); + const height = Number(root.getAttribute('height')); + if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) { + planSize.width = width; + planSize.height = height; + } else { + planSize.width = DEFAULT_PLAN_SIZE.width; + planSize.height = DEFAULT_PLAN_SIZE.height; + } + updateOverlayViewBox(); + renderReferenceMarkers(); + updateFromInputs(); + } catch (error) { + planSize.width = DEFAULT_PLAN_SIZE.width; + planSize.height = DEFAULT_PLAN_SIZE.height; + updateOverlayViewBox(); + renderReferenceMarkers(); + updateFromInputs(); + } + }; + const updatePanelPlacementVisibility = () => { if (!panelFloorSelect || !panelPlacementFields || !panelFloorPlanGroup) { return; @@ -257,6 +379,65 @@ document.addEventListener('DOMContentLoaded', () => { } }; + activeMarker.addEventListener('pointerdown', (event) => { + event.preventDefault(); + dragging = true; + const point = toOverlayPoint(event.clientX, event.clientY); + if (!point) { + return; + } + dragOffsetX = point.x - markerX; + dragOffsetY = point.y - markerY; + activeMarker.setPointerCapture(event.pointerId); + }); + + activeMarker.addEventListener('pointermove', (event) => { + if (!dragging) { + return; + } + const point = toOverlayPoint(event.clientX, event.clientY); + if (!point) { + return; + } + setMarkerPosition(point.x - dragOffsetX, point.y - dragOffsetY); + }); + + const stopDrag = (event) => { + if (!dragging) { + return; + } + dragging = false; + if (activeMarker.hasPointerCapture(event.pointerId)) { + activeMarker.releasePointerCapture(event.pointerId); + } + }; + + ['pointerup', 'pointercancel', 'pointerleave'].forEach((evt) => { + activeMarker.addEventListener(evt, stopDrag); + }); + + overlay.addEventListener('pointerdown', (event) => { + if (event.target !== overlay) { + return; + } + const point = toOverlayPoint(event.clientX, event.clientY); + if (!point) { + return; + } + setMarkerPosition(point.x - markerWidth / 2, point.y - markerHeight / 2); + }); + + [xField, yField].forEach((input) => { + input.addEventListener('input', () => { + updateFromInputs(); + }); + }); + + window.addEventListener('resize', () => { + updateFromInputs(); + renderReferenceMarkers(); + }); + if (panelLocationSelect) { panelLocationSelect.addEventListener('change', () => { filterBuildingOptions(); @@ -283,6 +464,9 @@ document.addEventListener('DOMContentLoaded', () => { }); } + updateOverlayViewBox(); + updateFromInputs(); + if (panelLocationSelect) { filterBuildingOptions(); filterFloorOptions(); diff --git a/app/assets/js/floor-infrastructure-list.js b/app/assets/js/floor-infrastructure-list.js new file mode 100644 index 0000000..e488af9 --- /dev/null +++ b/app/assets/js/floor-infrastructure-list.js @@ -0,0 +1,148 @@ +document.addEventListener('DOMContentLoaded', () => { + const SVG_NS = 'http://www.w3.org/2000/svg'; + const DEFAULT_PLAN_SIZE = { width: 2000, height: 1000 }; + + const filterForm = document.getElementById('infra-filter-form'); + const floorSelect = document.getElementById('infra-floor-select'); + if (filterForm && floorSelect) { + floorSelect.addEventListener('change', () => { + filterForm.submit(); + }); + } + + const canvas = document.getElementById('infra-floor-canvas'); + const overlay = document.getElementById('infra-floor-overlay'); + const floorSvg = canvas ? canvas.querySelector('.infra-floor-svg') : null; + if (!canvas || !overlay || !floorSvg) { + return; + } + + const patchPanels = safeJsonParse(canvas.dataset.patchpanels); + const outlets = safeJsonParse(canvas.dataset.outlets); + const planSize = { ...DEFAULT_PLAN_SIZE }; + + const updateOverlayViewBox = () => { + overlay.setAttribute('viewBox', `0 0 ${planSize.width} ${planSize.height}`); + }; + + const createMarker = (entry, type) => { + const x = Number(entry.x); + const y = Number(entry.y); + if (!Number.isFinite(x) || !Number.isFinite(y)) { + return; + } + + const marker = document.createElementNS(SVG_NS, 'rect'); + marker.classList.add('infra-overlay-marker', type); + marker.setAttribute('x', String(Math.round(x))); + marker.setAttribute('y', String(Math.round(y))); + + if (type === 'patchpanel') { + marker.setAttribute('width', String(Math.max(1, Number(entry.width) || 20))); + marker.setAttribute('height', String(Math.max(1, Number(entry.height) || 5))); + } else { + marker.setAttribute('width', '10'); + marker.setAttribute('height', '10'); + } + + const tooltipLines = buildTooltipLines(entry, type); + if (tooltipLines.length > 0) { + const titleNode = document.createElementNS(SVG_NS, 'title'); + titleNode.textContent = tooltipLines.join('\n'); + marker.appendChild(titleNode); + } + + overlay.appendChild(marker); + }; + + patchPanels.forEach((entry) => createMarker(entry, 'patchpanel')); + outlets.forEach((entry) => createMarker(entry, 'outlet')); + + const loadPlanDimensions = async (svgUrl) => { + if (!svgUrl) { + updateOverlayViewBox(); + return; + } + try { + const response = await fetch(svgUrl, { credentials: 'same-origin' }); + if (!response.ok) { + throw new Error('SVG not available'); + } + const raw = await response.text(); + const parser = new DOMParser(); + const doc = parser.parseFromString(raw, 'image/svg+xml'); + const root = doc.documentElement; + if (!root || root.nodeName.toLowerCase() === 'parsererror') { + throw new Error('Invalid SVG'); + } + + const vb = String(root.getAttribute('viewBox') || '').trim(); + if (vb) { + const parts = vb.split(/\s+/).map((value) => Number(value)); + if (parts.length === 4 && parts.every((value) => Number.isFinite(value))) { + planSize.width = Math.max(1, parts[2]); + planSize.height = Math.max(1, parts[3]); + updateOverlayViewBox(); + return; + } + } + + const width = Number(root.getAttribute('width')); + const height = Number(root.getAttribute('height')); + if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) { + planSize.width = width; + planSize.height = height; + } else { + planSize.width = DEFAULT_PLAN_SIZE.width; + planSize.height = DEFAULT_PLAN_SIZE.height; + } + updateOverlayViewBox(); + } catch (error) { + planSize.width = DEFAULT_PLAN_SIZE.width; + planSize.height = DEFAULT_PLAN_SIZE.height; + updateOverlayViewBox(); + } + }; + + loadPlanDimensions(floorSvg.getAttribute('src') || ''); +}); + +function safeJsonParse(value) { + if (!value) { + return []; + } + try { + const parsed = JSON.parse(value); + return Array.isArray(parsed) ? parsed : []; + } catch (error) { + return []; + } +} + +function buildTooltipLines(entry, type) { + if (type === 'patchpanel') { + const lines = [ + `Patchpanel: ${entry.name || '-'}`, + `Ports: ${Number(entry.port_count) || 0}`, + `Position: ${Number(entry.x) || 0} x ${Number(entry.y) || 0}` + ]; + if (entry.comment) { + lines.push(`Kommentar: ${entry.comment}`); + } + return lines; + } + + const roomLabel = `${entry.room_name || '-'}${entry.room_number ? ` (${entry.room_number})` : ''}`; + const lines = [ + `Wandbuchse: ${entry.name || '-'}`, + `Raum: ${roomLabel}`, + `Position: ${Number(entry.x) || 0} x ${Number(entry.y) || 0}` + ]; + if (entry.port_names) { + lines.push(`Ports: ${entry.port_names}`); + } + if (entry.comment) { + lines.push(`Kommentar: ${entry.comment}`); + } + return lines; +} diff --git a/app/modules/floor_infrastructure/edit.php b/app/modules/floor_infrastructure/edit.php index fca3b73..b67e2c0 100644 --- a/app/modules/floor_infrastructure/edit.php +++ b/app/modules/floor_infrastructure/edit.php @@ -20,7 +20,8 @@ $floors = $sql->get( [] ); $rooms = $sql->get( - "SELECT r.id, r.name, r.floor_id, f.name AS floor_name, f.svg_path, b.id AS building_id, l.id AS location_id + "SELECT r.id, r.name, r.floor_id, r.x, r.y, r.width, r.height, r.polygon_points, + f.name AS floor_name, f.svg_path, b.id AS building_id, l.id AS location_id FROM rooms r LEFT JOIN floors f ON f.id = r.floor_id LEFT JOIN buildings b ON b.id = f.building_id @@ -92,13 +93,13 @@ if ($type === 'patchpanel') { } } -$defaultPanelSize = ['width' => 140, 'height' => 40]; +$defaultPanelSize = ['width' => 20, 'height' => 5]; $defaultOutletSize = 10; $showPanelPlacementFields = $type === 'patchpanel' && $selectedFloorId > 0; if ($type === 'patchpanel') { - $panel['width'] = $panel['width'] ?? $defaultPanelSize['width']; - $panel['height'] = $panel['height'] ?? $defaultPanelSize['height']; + $panel['width'] = $defaultPanelSize['width']; + $panel['height'] = $defaultPanelSize['height']; $panel['pos_x'] = $panel['pos_x'] ?? 30; $panel['pos_y'] = $panel['pos_y'] ?? 30; } else { @@ -127,6 +128,7 @@ $mapOutlets = $sql->get( ?>
+

@@ -204,8 +206,7 @@ $mapOutlets = $sql->get( data-reference-panels="" data-reference-outlets=""> Stockwerksplan -
+

Nur das aktuell bearbeitete Patchpanel ist verschiebbar. Andere Objekte werden als Referenz halbtransparent angezeigt. Neue Objekte starten bei Position 30 x 30.

Koordinate:

@@ -242,6 +243,11 @@ $mapOutlets = $sql->get( @@ -265,10 +271,9 @@ $mapOutlets = $sql->get( data-reference-panels="" data-reference-outlets=""> Stockwerksplan -
+ -

Nur die aktuell bearbeitete Wandbuchse ist verschiebbar. Andere Objekte werden als Referenz halbtransparent angezeigt. Netzwerkdosen sind immer 10 x 10.

+

Nur die aktuell bearbeitete Wandbuchse ist verschiebbar. Blau = Patchpanel, Gruen = Dosen-Referenz, Orange = gewaehlter Raum. Netzwerkdosen sind immer 10 x 10.

Koordinate:

@@ -292,119 +297,7 @@ $mapOutlets = $sql->get( //TODO drag an drop auf der stockwerkskarte für die patchfelder und wandbuchsen. buchsen haben eine einheitliche größe, und sind quadratisch, patchfelder sind auch für sich einheitlich, sind rechteckig und breiter als hoch //TODO style in css files einsortieren ?> - - + + diff --git a/app/modules/floor_infrastructure/list.php b/app/modules/floor_infrastructure/list.php index bd4ed52..8b9cd48 100644 --- a/app/modules/floor_infrastructure/list.php +++ b/app/modules/floor_infrastructure/list.php @@ -2,12 +2,18 @@ /** * app/modules/floor_infrastructure/list.php * - * Übersicht über Patchpanels und Netzwerkbuchsen auf Stockwerken + * Uebersicht ueber Patchpanels und Netzwerkbuchsen auf Stockwerken. */ $floorId = (int)($_GET['floor_id'] ?? 0); -$floors = $sql->get("SELECT id, name, svg_path FROM floors ORDER BY name", "", []); +$floors = $sql->get( + "SELECT id, name, svg_path + FROM floors + ORDER BY name", + "", + [] +); $where = ''; $types = ''; @@ -15,7 +21,7 @@ $params = []; if ($floorId > 0) { $where = "WHERE p.floor_id = ?"; - $types = "i"; + $types = 'i'; $params[] = $floorId; } @@ -30,10 +36,15 @@ $patchPanels = $sql->get( ); $networkOutlets = $sql->get( - "SELECT o.*, r.name AS room_name, f.name AS floor_name, f.id AS floor_id + "SELECT o.id, o.room_id, o.name, o.x, o.y, o.comment, + r.name AS room_name, r.number AS room_number, + f.name AS floor_name, f.id AS floor_id, + GROUP_CONCAT(nop.name ORDER BY nop.name SEPARATOR ', ') AS port_names FROM network_outlets o LEFT JOIN rooms r ON r.id = o.room_id LEFT JOIN floors f ON f.id = r.floor_id + LEFT JOIN network_outlet_ports nop ON nop.outlet_id = o.id + GROUP BY o.id ORDER BY f.name, r.name, o.name", "", [] @@ -50,81 +61,101 @@ foreach ($floors as $floor) { ]; } -$editorFloorId = $floorId; -if ($editorFloorId <= 0) { - foreach ($floorMap as $candidate) { - if ($candidate['svg_url'] !== '') { - $editorFloorId = (int)$candidate['id']; - break; - } - } -} - -$editorFloor = $floorMap[$editorFloorId] ?? null; +$editorFloor = ($floorId > 0 && isset($floorMap[$floorId])) ? $floorMap[$floorId] : null; $editorPatchPanels = []; $editorOutlets = []; -if ($editorFloorId > 0) { +if ($editorFloor) { foreach ($patchPanels as $panel) { - if ((int)$panel['floor_id'] === $editorFloorId) { - $editorPatchPanels[] = [ - 'id' => (int)$panel['id'], - 'name' => (string)$panel['name'], - 'floor_id' => (int)$panel['floor_id'], - 'x' => (int)$panel['pos_x'], - 'y' => (int)$panel['pos_y'], - 'width' => max(1, (int)$panel['width']), - 'height' => max(1, (int)$panel['height']), - 'port_count' => (int)$panel['port_count'], - 'comment' => (string)($panel['comment'] ?? '') - ]; + if ((int)$panel['floor_id'] !== $floorId) { + continue; } + $editorPatchPanels[] = [ + 'id' => (int)$panel['id'], + 'name' => (string)$panel['name'], + 'x' => (int)$panel['pos_x'], + 'y' => (int)$panel['pos_y'], + 'width' => max(1, (int)$panel['width']), + 'height' => max(1, (int)$panel['height']), + 'port_count' => (int)$panel['port_count'], + 'comment' => (string)($panel['comment'] ?? '') + ]; } foreach ($networkOutlets as $outlet) { - if ((int)$outlet['floor_id'] === $editorFloorId) { - $editorOutlets[] = [ - 'id' => (int)$outlet['id'], - 'name' => (string)$outlet['name'], - 'room_id' => (int)$outlet['room_id'], - 'x' => (int)$outlet['x'], - 'y' => (int)$outlet['y'], - 'comment' => (string)($outlet['comment'] ?? '') - ]; + if ((int)$outlet['floor_id'] !== $floorId) { + continue; } + $editorOutlets[] = [ + 'id' => (int)$outlet['id'], + 'name' => (string)$outlet['name'], + 'x' => (int)$outlet['x'], + 'y' => (int)$outlet['y'], + 'room_name' => (string)($outlet['room_name'] ?? ''), + 'room_number' => (string)($outlet['room_number'] ?? ''), + 'port_names' => (string)($outlet['port_names'] ?? ''), + 'comment' => (string)($outlet['comment'] ?? '') + ]; } } ?>
+ + +

Stockwerksinfrastruktur

- + - + - - - Zurücksetzen + + Zuruecksetzen +
+

Stockwerkskarte

+ +

Bitte ein Stockwerk auswaehlen, um die Karte anzuzeigen.

+ +

Gewaehltes Stockwerk wurde nicht gefunden.

+ +

Das gewaehlte Stockwerk hat keinen SVG-Plan hinterlegt.

+ +

+ Read-only Vorschau fuer . + Alle Objekte werden angezeigt; Hover zeigt Details. +

+
+ Stockwerksplan + +
+

Blau: Patchpanel | Gruen: Wandbuchse. Hover zeigt Name, Raum und Ports (Browser-Tooltip).

+ +
+

Patchpanels

@@ -134,7 +165,7 @@ if ($editorFloorId > 0) { Name Stockwerk Position - Größe + Groesse Ports Aktionen @@ -142,13 +173,13 @@ if ($editorFloorId > 0) { - - - - - + + + + + - Bearbeiten + Bearbeiten @@ -169,6 +200,7 @@ if ($editorFloorId > 0) { Stockwerk Raum Koordinaten + Ports Kommentar Aktionen @@ -176,13 +208,23 @@ if ($editorFloorId > 0) { - - - - - + + + + + + + + - Bearbeiten + Bearbeiten @@ -192,154 +234,4 @@ if ($editorFloorId > 0) {

Noch keine Wandbuchsen angelegt.

- -
-

Stockwerksplan-Verortung

- -

Kein Stockwerk für die Planansicht verfügbar.

- -

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

- -

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

-
- Stockwerksplan -
-

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

- -
- - - - - - diff --git a/app/modules/floor_infrastructure/save.php b/app/modules/floor_infrastructure/save.php index 8d00d5d..bd44496 100644 --- a/app/modules/floor_infrastructure/save.php +++ b/app/modules/floor_infrastructure/save.php @@ -9,22 +9,18 @@ $type = $_POST['type'] ?? ''; $id = (int)($_POST['id'] ?? 0); if ($type === 'patchpanel') { + $fixedPanelWidth = 20; + $fixedPanelHeight = 5; + $name = trim($_POST['name'] ?? ''); $floorId = (int)($_POST['floor_id'] ?? 0); $posX = (int)($_POST['pos_x'] ?? 0); $posY = (int)($_POST['pos_y'] ?? 0); - $width = (int)($_POST['width'] ?? 0); - $height = (int)($_POST['height'] ?? 0); + $width = $fixedPanelWidth; + $height = $fixedPanelHeight; $portCount = (int)($_POST['port_count'] ?? 0); $comment = trim($_POST['comment'] ?? ''); - if ($width <= 0) { - $width = 140; - } - if ($height <= 0) { - $height = 40; - } - if ($id > 0) { $sql->set( "UPDATE floor_patchpanels SET name = ?, floor_id = ?, pos_x = ?, pos_y = ?, width = ?, height = ?, port_count = ?, comment = ? WHERE id = ?",