Verhalten im Infrastruktur-Modul ist jetzt wie gewünscht:
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): <script src="/assets/js/floor-infrastructure-edit.js" defer></script> 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.
This commit is contained in:
294
app/assets/js/floor-infrastructure-edit.js
Normal file
294
app/assets/js/floor-infrastructure-edit.js
Normal file
@@ -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();
|
||||
});
|
||||
@@ -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",
|
||||
"",
|
||||
[]
|
||||
);
|
||||
?>
|
||||
|
||||
<div class="floor-infra-edit">
|
||||
@@ -188,12 +204,15 @@ $markerHeight = $type === 'patchpanel' ? $panel['height'] : $defaultOutletSize;
|
||||
data-marker-height="<?php echo $markerHeight; ?>"
|
||||
data-marker-type="patchpanel"
|
||||
data-x-field="pos_x"
|
||||
data-y-field="pos_y">
|
||||
data-y-field="pos_y"
|
||||
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-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">
|
||||
<div id="floor-plan-marker" class="floor-plan-marker panel-marker"
|
||||
style="--marker-width: <?php echo $markerWidth; ?>px; --marker-height: <?php echo $markerHeight; ?>px;"></div>
|
||||
</div>
|
||||
<p class="floor-plan-hint">Ziehe das Patchpanel oder klicke auf den Plan, um die Position zu setzen.</p>
|
||||
<p class="floor-plan-hint">Nur das aktuell bearbeitete Patchpanel ist verschiebbar. Andere Objekte werden als Referenz halbtransparent angezeigt.</p>
|
||||
<p class="floor-plan-position">Koordinate: <span id="floor-plan-position"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -204,7 +223,7 @@ $markerHeight = $type === 'patchpanel' ? $panel['height'] : $defaultOutletSize;
|
||||
</div>
|
||||
|
||||
<p id="panel-floor-missing-hint" class="info" <?php echo $showPanelPlacementFields ? 'hidden' : ''; ?>>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -253,12 +272,15 @@ $markerHeight = $type === 'patchpanel' ? $panel['height'] : $defaultOutletSize;
|
||||
data-marker-height="<?php echo $markerHeight; ?>"
|
||||
data-marker-type="outlet"
|
||||
data-x-field="x"
|
||||
data-y-field="y">
|
||||
data-y-field="y"
|
||||
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-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">
|
||||
<div id="floor-plan-marker" class="floor-plan-marker outlet-marker"
|
||||
style="--marker-width: <?php echo $markerWidth; ?>px; --marker-height: <?php echo $markerHeight; ?>px;"></div>
|
||||
</div>
|
||||
<p class="floor-plan-hint">Klicke oder ziehe die Wandbuchse auf dem Plan. Die Größe bleibt quadratisch.</p>
|
||||
<p class="floor-plan-hint">Nur die aktuell bearbeitete Wandbuchse ist verschiebbar. Andere Objekte werden als Referenz halbtransparent angezeigt.</p>
|
||||
<p class="floor-plan-position">Koordinate: <span id="floor-plan-position"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
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 clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
||||
|
||||
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 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;
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
});
|
||||
</script>
|
||||
<script src="/assets/js/floor-infrastructure-edit.js" defer></script>
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
<div class="toolbar">
|
||||
<a href="?module=floor_infrastructure&action=edit&type=patchpanel" class="button button-primary">
|
||||
+ Patchpanel hinzufuegen
|
||||
+ Patchpanel hinzufügen
|
||||
</a>
|
||||
<a href="?module=floor_infrastructure&action=edit&type=outlet" class="button">
|
||||
+ Wandbuchse hinzufuegen
|
||||
+ Wandbuchse hinzufügen
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -122,7 +122,7 @@ if ($editorFloorId > 0) {
|
||||
</select>
|
||||
|
||||
<button class="button">Filter</button>
|
||||
<a href="?module=floor_infrastructure&action=list" class="button">Zuruecksetzen</a>
|
||||
<a href="?module=floor_infrastructure&action=list" class="button">Zurücksetzen</a>
|
||||
</form>
|
||||
|
||||
<section class="infra-section">
|
||||
@@ -134,7 +134,7 @@ if ($editorFloorId > 0) {
|
||||
<th>Name</th>
|
||||
<th>Stockwerk</th>
|
||||
<th>Position</th>
|
||||
<th>Groesse</th>
|
||||
<th>Größe</th>
|
||||
<th>Ports</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
@@ -196,18 +196,18 @@ if ($editorFloorId > 0) {
|
||||
<section class="infra-plan">
|
||||
<h2>Stockwerksplan-Verortung</h2>
|
||||
<?php if (!$editorFloor): ?>
|
||||
<p class="empty-state">Kein Stockwerk fuer die Planansicht verfuegbar.</p>
|
||||
<p class="empty-state">Kein Stockwerk für die Planansicht verfügbar.</p>
|
||||
<?php elseif (($editorFloor['svg_url'] ?? '') === ''): ?>
|
||||
<p class="empty-state">Das gewaehlte Stockwerk hat noch keinen SVG-Plan hinterlegt.</p>
|
||||
<p class="empty-state">Das gewählte Stockwerk hat noch keinen SVG-Plan hinterlegt.</p>
|
||||
<?php else: ?>
|
||||
<p>Drag & Drop ist aktiv fuer <strong><?php echo htmlspecialchars($editorFloor['name']); ?></strong>. Aenderungen werden direkt gespeichert.</p>
|
||||
<p>Vorschau für <strong><?php echo htmlspecialchars($editorFloor['name']); ?></strong>. Positionen bearbeitest du über den jeweiligen „Bearbeiten“-Button.</p>
|
||||
<div id="infra-floor-canvas"
|
||||
class="infra-floor-canvas"
|
||||
data-save-url="?module=floor_infrastructure&action=save"
|
||||
data-floor-id="<?php echo (int)$editorFloor['id']; ?>">
|
||||
<img src="<?php echo htmlspecialchars($editorFloor['svg_url']); ?>" class="infra-floor-svg" alt="Stockwerksplan">
|
||||
</div>
|
||||
<p class="floor-plan-hint">Patchpanels: blau, Wandbuchsen: gruen. Ziehen und loslassen zum Speichern.</p>
|
||||
<p class="floor-plan-hint">Patchpanels: blau, Wandbuchsen: grün. In dieser Ansicht sind Marker nicht verschiebbar.</p>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
</div>
|
||||
@@ -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 = <?php echo json_encode($editorPatchPanels, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
|
||||
const outlets = <?php echo json_encode($editorOutlets, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
|
||||
|
||||
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'));
|
||||
|
||||
Reference in New Issue
Block a user