drag drop update
This commit is contained in:
8
BUGS.md
8
BUGS.md
@@ -1,4 +1,6 @@
|
|||||||
# gefundene bugs
|
# gefundene bugs
|
||||||
- device löschen geht nicht
|
- [?] device löschen geht nicht
|
||||||
- device_types svg modul malen
|
- [?] device_types svg modul malen
|
||||||
- ports drag n drop funktioniert nicht
|
- [?] ports drag n drop funktioniert nicht
|
||||||
|
- device _type soll schon aus dem 19zoll und he größe einen initialees rechteck erzeugen, welches als device grundgerüst funktionieren soll.
|
||||||
|
- beim dev typ machen, klick auf obj typ button, dann durch drag and drop die diagonale ziehen mit loslassen fixieren
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
(() => {
|
(() => {
|
||||||
const SVG_NS = 'http://www.w3.org/2000/svg';
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||||
|
const MIN_DRAW_SIZE = 4;
|
||||||
|
|
||||||
function initEditor() {
|
function initEditor() {
|
||||||
const editor = document.getElementById('device-type-shape-editor');
|
const editor = document.getElementById('device-type-shape-editor');
|
||||||
@@ -38,7 +39,13 @@
|
|||||||
font_size: editor.querySelector('[data-field="font_size"]')
|
font_size: editor.querySelector('[data-field="font_size"]')
|
||||||
};
|
};
|
||||||
|
|
||||||
let draggedTemplate = null;
|
let activeToolType = null;
|
||||||
|
let drawState = {
|
||||||
|
active: false,
|
||||||
|
shapeId: null,
|
||||||
|
startX: 0,
|
||||||
|
startY: 0
|
||||||
|
};
|
||||||
let dragState = {
|
let dragState = {
|
||||||
active: false,
|
active: false,
|
||||||
shapeId: null,
|
shapeId: null,
|
||||||
@@ -48,63 +55,21 @@
|
|||||||
let selectedShapeId = null;
|
let selectedShapeId = null;
|
||||||
let shapes = normalizeShapeList(readJson(hiddenInput.value));
|
let shapes = normalizeShapeList(readJson(hiddenInput.value));
|
||||||
|
|
||||||
bindToolbarDragEvents(editor);
|
bindToolbarEvents(editor);
|
||||||
bindCanvasDropEvents(svg);
|
|
||||||
bindCanvasPointerEvents(svg);
|
bindCanvasPointerEvents(svg);
|
||||||
bindOverlayEvents(overlay);
|
bindOverlayEvents(overlay);
|
||||||
render();
|
render();
|
||||||
|
|
||||||
function bindToolbarDragEvents(root) {
|
function bindToolbarEvents(root) {
|
||||||
const tools = root.querySelectorAll('.shape-tool[data-shape-template]');
|
const tools = root.querySelectorAll('.shape-tool[data-shape-template]');
|
||||||
tools.forEach((tool) => {
|
tools.forEach((tool) => {
|
||||||
tool.addEventListener('dragstart', (event) => {
|
tool.addEventListener('click', () => {
|
||||||
draggedTemplate = String(tool.dataset.shapeTemplate || '').trim();
|
const toolType = String(tool.dataset.shapeTemplate || '').trim();
|
||||||
if (event.dataTransfer) {
|
activeToolType = activeToolType === toolType ? null : toolType;
|
||||||
event.dataTransfer.effectAllowed = 'copy';
|
drawState.active = false;
|
||||||
event.dataTransfer.setData('text/plain', draggedTemplate);
|
drawState.shapeId = null;
|
||||||
}
|
renderToolState();
|
||||||
});
|
});
|
||||||
|
|
||||||
tool.addEventListener('dragend', () => {
|
|
||||||
draggedTemplate = null;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function bindCanvasDropEvents(canvas) {
|
|
||||||
const canvasWrap = canvas.closest('.shape-editor-canvas');
|
|
||||||
if (!canvasWrap) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
canvasWrap.addEventListener('dragover', (event) => {
|
|
||||||
if (!draggedTemplate) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
canvasWrap.classList.add('drag-over');
|
|
||||||
if (event.dataTransfer) {
|
|
||||||
event.dataTransfer.dropEffect = 'copy';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
canvasWrap.addEventListener('dragleave', () => {
|
|
||||||
canvasWrap.classList.remove('drag-over');
|
|
||||||
});
|
|
||||||
|
|
||||||
canvasWrap.addEventListener('drop', (event) => {
|
|
||||||
if (!draggedTemplate) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
canvasWrap.classList.remove('drag-over');
|
|
||||||
|
|
||||||
const point = toSvgPoint(event, canvas);
|
|
||||||
const shape = createDefaultShape(draggedTemplate, point.x, point.y);
|
|
||||||
shapes.push(shape);
|
|
||||||
selectedShapeId = shape.id;
|
|
||||||
persist();
|
|
||||||
render();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +79,34 @@
|
|||||||
if (!(target instanceof SVGElement)) {
|
if (!(target instanceof SVGElement)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const point = toSvgPoint(event, canvas);
|
||||||
|
|
||||||
|
if (activeToolType) {
|
||||||
|
const shape = createDefaultShape(activeToolType, point.x, point.y);
|
||||||
|
shapes.push(shape);
|
||||||
|
selectedShapeId = shape.id;
|
||||||
|
|
||||||
|
if (activeToolType === 'text') {
|
||||||
|
drawState.active = false;
|
||||||
|
drawState.shapeId = null;
|
||||||
|
persist();
|
||||||
|
render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawState = {
|
||||||
|
active: true,
|
||||||
|
shapeId: shape.id,
|
||||||
|
startX: Math.round(point.x),
|
||||||
|
startY: Math.round(point.y)
|
||||||
|
};
|
||||||
|
|
||||||
|
const shapeElement = target.closest('[data-shape-id]') || canvas;
|
||||||
|
shapeElement.setPointerCapture(event.pointerId);
|
||||||
|
persist();
|
||||||
|
render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const shapeElement = target.closest('[data-shape-id]');
|
const shapeElement = target.closest('[data-shape-id]');
|
||||||
if (!shapeElement) {
|
if (!shapeElement) {
|
||||||
@@ -131,7 +124,6 @@
|
|||||||
selectedShapeId = shape.id;
|
selectedShapeId = shape.id;
|
||||||
renderOverlay();
|
renderOverlay();
|
||||||
|
|
||||||
const point = toSvgPoint(event, canvas);
|
|
||||||
const anchor = getShapeAnchor(shape);
|
const anchor = getShapeAnchor(shape);
|
||||||
dragState = {
|
dragState = {
|
||||||
active: true,
|
active: true,
|
||||||
@@ -144,6 +136,18 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
canvas.addEventListener('pointermove', (event) => {
|
canvas.addEventListener('pointermove', (event) => {
|
||||||
|
if (drawState.active && drawState.shapeId) {
|
||||||
|
const shape = findShape(drawState.shapeId);
|
||||||
|
if (!shape) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const point = toSvgPoint(event, canvas);
|
||||||
|
updateShapeFromDiagonal(shape, drawState.startX, drawState.startY, point.x, point.y);
|
||||||
|
persist();
|
||||||
|
render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!dragState.active || !dragState.shapeId) {
|
if (!dragState.active || !dragState.shapeId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -162,13 +166,31 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
canvas.addEventListener('pointerup', () => {
|
canvas.addEventListener('pointerup', () => {
|
||||||
|
if (drawState.active && drawState.shapeId) {
|
||||||
|
const shape = findShape(drawState.shapeId);
|
||||||
|
if (shape && isBelowMinDrawSize(shape)) {
|
||||||
|
shapes = shapes.filter((candidate) => candidate.id !== shape.id);
|
||||||
|
if (selectedShapeId === shape.id) {
|
||||||
|
selectedShapeId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drawState.active = false;
|
||||||
|
drawState.shapeId = null;
|
||||||
|
}
|
||||||
|
|
||||||
dragState.active = false;
|
dragState.active = false;
|
||||||
dragState.shapeId = null;
|
dragState.shapeId = null;
|
||||||
|
persist();
|
||||||
|
render();
|
||||||
});
|
});
|
||||||
|
|
||||||
canvas.addEventListener('pointercancel', () => {
|
canvas.addEventListener('pointercancel', () => {
|
||||||
|
drawState.active = false;
|
||||||
|
drawState.shapeId = null;
|
||||||
dragState.active = false;
|
dragState.active = false;
|
||||||
dragState.shapeId = null;
|
dragState.shapeId = null;
|
||||||
|
persist();
|
||||||
|
render();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,10 +247,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
|
renderToolState();
|
||||||
renderCanvas();
|
renderCanvas();
|
||||||
renderOverlay();
|
renderOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderToolState() {
|
||||||
|
editor.querySelectorAll('.shape-tool[data-shape-template]').forEach((tool) => {
|
||||||
|
const type = String(tool.dataset.shapeTemplate || '').trim();
|
||||||
|
tool.classList.toggle('is-active', activeToolType === type);
|
||||||
|
});
|
||||||
|
svg.classList.toggle('shape-tool-active', !!activeToolType);
|
||||||
|
}
|
||||||
|
|
||||||
function renderCanvas() {
|
function renderCanvas() {
|
||||||
svg.querySelectorAll('.shape-object').forEach((el) => el.remove());
|
svg.querySelectorAll('.shape-object').forEach((el) => el.remove());
|
||||||
|
|
||||||
@@ -299,6 +330,38 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateShapeFromDiagonal(shape, x1, y1, x2, y2) {
|
||||||
|
const left = Math.min(x1, x2);
|
||||||
|
const top = Math.min(y1, y2);
|
||||||
|
const width = Math.abs(x2 - x1);
|
||||||
|
const height = Math.abs(y2 - y1);
|
||||||
|
|
||||||
|
if (shape.type === 'rect') {
|
||||||
|
shape.x = Math.round(left);
|
||||||
|
shape.y = Math.round(top);
|
||||||
|
shape.width = Math.round(width);
|
||||||
|
shape.height = Math.round(height);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shape.type === 'circle') {
|
||||||
|
const radius = Math.round(Math.min(width, height) / 2);
|
||||||
|
shape.radius = radius;
|
||||||
|
shape.x = Math.round(left + radius);
|
||||||
|
shape.y = Math.round(top + radius);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBelowMinDrawSize(shape) {
|
||||||
|
if (shape.type === 'rect') {
|
||||||
|
return (shape.width ?? 0) < MIN_DRAW_SIZE || (shape.height ?? 0) < MIN_DRAW_SIZE;
|
||||||
|
}
|
||||||
|
if (shape.type === 'circle') {
|
||||||
|
return (shape.radius ?? 0) < (MIN_DRAW_SIZE / 2);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function createSvgShapeElement(shape) {
|
function createSvgShapeElement(shape) {
|
||||||
const fill = normalizeColor(shape.fill, '#cccccc');
|
const fill = normalizeColor(shape.fill, '#cccccc');
|
||||||
const stroke = normalizeColor(shape.stroke, '#333333');
|
const stroke = normalizeColor(shape.stroke, '#333333');
|
||||||
@@ -381,7 +444,7 @@
|
|||||||
y: Math.round(y),
|
y: Math.round(y),
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
radius: 26,
|
radius: 1,
|
||||||
text: '',
|
text: '',
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fill: '#cfe6ff',
|
fill: '#cfe6ff',
|
||||||
@@ -416,8 +479,8 @@
|
|||||||
type: 'rect',
|
type: 'rect',
|
||||||
x: Math.round(x),
|
x: Math.round(x),
|
||||||
y: Math.round(y),
|
y: Math.round(y),
|
||||||
width: 120,
|
width: 1,
|
||||||
height: 60,
|
height: 1,
|
||||||
radius: 0,
|
radius: 0,
|
||||||
text: '',
|
text: '',
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
|||||||
471
app/assets/js/floor-svg-editor.js
Normal file
471
app/assets/js/floor-svg-editor.js
Normal file
@@ -0,0 +1,471 @@
|
|||||||
|
(() => {
|
||||||
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||||
|
const VIEWBOX_WIDTH = 2000;
|
||||||
|
const VIEWBOX_HEIGHT = 1000;
|
||||||
|
const SNAP_TOLERANCE = 12;
|
||||||
|
|
||||||
|
function initFloorSvgEditor() {
|
||||||
|
const editor = document.getElementById('floor-svg-editor');
|
||||||
|
const svg = document.getElementById('floor-svg-canvas');
|
||||||
|
const hiddenInput = document.getElementById('floor-svg-content');
|
||||||
|
if (!editor || !svg || !hiddenInput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const controls = {
|
||||||
|
startPolyline: document.getElementById('floor-start-polyline'),
|
||||||
|
finishPolyline: document.getElementById('floor-finish-polyline'),
|
||||||
|
deletePolyline: document.getElementById('floor-delete-polyline'),
|
||||||
|
clearDrawing: document.getElementById('floor-clear-drawing'),
|
||||||
|
lock45: document.getElementById('floor-lock-45'),
|
||||||
|
snapGuides: document.getElementById('floor-snap-guides'),
|
||||||
|
addGuide: document.getElementById('floor-add-guide'),
|
||||||
|
guideOrientation: document.getElementById('floor-guide-orientation'),
|
||||||
|
guidePosition: document.getElementById('floor-guide-position'),
|
||||||
|
guideList: document.getElementById('floor-guide-list')
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
polylines: [],
|
||||||
|
guides: [],
|
||||||
|
selectedPolylineId: null,
|
||||||
|
activePolylineId: null,
|
||||||
|
draggingVertex: null
|
||||||
|
};
|
||||||
|
|
||||||
|
loadFromExistingSvg(hiddenInput.value, state);
|
||||||
|
bindControlEvents(controls, state, svg, hiddenInput);
|
||||||
|
bindCanvasEvents(svg, controls, state, hiddenInput);
|
||||||
|
render(svg, controls, state, hiddenInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindControlEvents(controls, state, svg, hiddenInput) {
|
||||||
|
controls.startPolyline.addEventListener('click', () => {
|
||||||
|
const id = createId('poly');
|
||||||
|
state.polylines.push({
|
||||||
|
id,
|
||||||
|
points: []
|
||||||
|
});
|
||||||
|
state.activePolylineId = id;
|
||||||
|
state.selectedPolylineId = id;
|
||||||
|
render(svg, controls, state, hiddenInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
controls.finishPolyline.addEventListener('click', () => {
|
||||||
|
finishActivePolyline(state);
|
||||||
|
render(svg, controls, state, hiddenInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
controls.deletePolyline.addEventListener('click', () => {
|
||||||
|
if (!state.selectedPolylineId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.polylines = state.polylines.filter((line) => line.id !== state.selectedPolylineId);
|
||||||
|
if (state.activePolylineId === state.selectedPolylineId) {
|
||||||
|
state.activePolylineId = null;
|
||||||
|
}
|
||||||
|
state.selectedPolylineId = null;
|
||||||
|
render(svg, controls, state, hiddenInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
controls.clearDrawing.addEventListener('click', () => {
|
||||||
|
state.polylines = [];
|
||||||
|
state.guides = [];
|
||||||
|
state.selectedPolylineId = null;
|
||||||
|
state.activePolylineId = null;
|
||||||
|
state.draggingVertex = null;
|
||||||
|
render(svg, controls, state, hiddenInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
controls.addGuide.addEventListener('click', () => {
|
||||||
|
const orientation = controls.guideOrientation.value === 'horizontal' ? 'horizontal' : 'vertical';
|
||||||
|
const position = Number(controls.guidePosition.value);
|
||||||
|
if (!Number.isFinite(position)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.guides.push({
|
||||||
|
id: createId('guide'),
|
||||||
|
orientation,
|
||||||
|
position: Math.round(position)
|
||||||
|
});
|
||||||
|
controls.guidePosition.value = '';
|
||||||
|
render(svg, controls, state, hiddenInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
controls.guideList.addEventListener('click', (event) => {
|
||||||
|
const target = event.target;
|
||||||
|
if (!(target instanceof HTMLElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = target.getAttribute('data-remove-guide');
|
||||||
|
if (!id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.guides = state.guides.filter((guide) => guide.id !== id);
|
||||||
|
render(svg, controls, state, hiddenInput);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindCanvasEvents(svg, controls, state, hiddenInput) {
|
||||||
|
svg.addEventListener('pointerdown', (event) => {
|
||||||
|
const target = event.target;
|
||||||
|
if (!(target instanceof SVGElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vertex = target.closest('[data-vertex-index]');
|
||||||
|
if (vertex) {
|
||||||
|
const polylineId = vertex.getAttribute('data-polyline-id');
|
||||||
|
const vertexIndex = Number(vertex.getAttribute('data-vertex-index'));
|
||||||
|
if (polylineId && Number.isInteger(vertexIndex)) {
|
||||||
|
state.selectedPolylineId = polylineId;
|
||||||
|
state.draggingVertex = { polylineId, vertexIndex };
|
||||||
|
vertex.setPointerCapture(event.pointerId);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const polylineEl = target.closest('[data-polyline-id]');
|
||||||
|
if (polylineEl) {
|
||||||
|
const polylineId = polylineEl.getAttribute('data-polyline-id');
|
||||||
|
if (polylineId) {
|
||||||
|
state.selectedPolylineId = polylineId;
|
||||||
|
if (!state.activePolylineId) {
|
||||||
|
state.activePolylineId = polylineId;
|
||||||
|
}
|
||||||
|
render(svg, controls, state, hiddenInput);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const point = toSvgPoint(svg, event);
|
||||||
|
if (!point) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.activePolylineId) {
|
||||||
|
const id = createId('poly');
|
||||||
|
state.polylines.push({ id, points: [] });
|
||||||
|
state.activePolylineId = id;
|
||||||
|
state.selectedPolylineId = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeLine = state.polylines.find((line) => line.id === state.activePolylineId);
|
||||||
|
if (!activeLine) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextPoint = point;
|
||||||
|
if (controls.lock45.checked && activeLine.points.length > 0) {
|
||||||
|
nextPoint = lockTo45(activeLine.points[activeLine.points.length - 1], nextPoint);
|
||||||
|
}
|
||||||
|
if (controls.snapGuides.checked) {
|
||||||
|
nextPoint = snapPointToGuides(nextPoint, state.guides, SNAP_TOLERANCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
activeLine.points.push({
|
||||||
|
x: Math.round(nextPoint.x),
|
||||||
|
y: Math.round(nextPoint.y)
|
||||||
|
});
|
||||||
|
render(svg, controls, state, hiddenInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
svg.addEventListener('pointermove', (event) => {
|
||||||
|
if (!state.draggingVertex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const point = toSvgPoint(svg, event);
|
||||||
|
if (!point) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const line = state.polylines.find((item) => item.id === state.draggingVertex.polylineId);
|
||||||
|
if (!line) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const index = state.draggingVertex.vertexIndex;
|
||||||
|
if (!line.points[index]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextPoint = point;
|
||||||
|
if (controls.lock45.checked && index > 0 && line.points[index - 1]) {
|
||||||
|
nextPoint = lockTo45(line.points[index - 1], nextPoint);
|
||||||
|
}
|
||||||
|
if (controls.snapGuides.checked) {
|
||||||
|
nextPoint = snapPointToGuides(nextPoint, state.guides, SNAP_TOLERANCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
line.points[index] = {
|
||||||
|
x: Math.round(nextPoint.x),
|
||||||
|
y: Math.round(nextPoint.y)
|
||||||
|
};
|
||||||
|
render(svg, controls, state, hiddenInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
svg.addEventListener('pointerup', () => {
|
||||||
|
state.draggingVertex = null;
|
||||||
|
});
|
||||||
|
svg.addEventListener('pointercancel', () => {
|
||||||
|
state.draggingVertex = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(svg, controls, state, hiddenInput) {
|
||||||
|
const selected = state.polylines.find((line) => line.id === state.selectedPolylineId) || null;
|
||||||
|
|
||||||
|
svg.innerHTML = '';
|
||||||
|
svg.setAttribute('viewBox', `0 0 ${VIEWBOX_WIDTH} ${VIEWBOX_HEIGHT}`);
|
||||||
|
|
||||||
|
const background = createSvgElement('rect');
|
||||||
|
background.setAttribute('x', '0');
|
||||||
|
background.setAttribute('y', '0');
|
||||||
|
background.setAttribute('width', String(VIEWBOX_WIDTH));
|
||||||
|
background.setAttribute('height', String(VIEWBOX_HEIGHT));
|
||||||
|
background.setAttribute('fill', '#fafafa');
|
||||||
|
background.setAttribute('stroke', '#e1e1e1');
|
||||||
|
background.setAttribute('stroke-width', '1');
|
||||||
|
svg.appendChild(background);
|
||||||
|
|
||||||
|
state.guides.forEach((guide) => {
|
||||||
|
const line = createSvgElement('line');
|
||||||
|
if (guide.orientation === 'horizontal') {
|
||||||
|
line.setAttribute('x1', '0');
|
||||||
|
line.setAttribute('y1', String(guide.position));
|
||||||
|
line.setAttribute('x2', String(VIEWBOX_WIDTH));
|
||||||
|
line.setAttribute('y2', String(guide.position));
|
||||||
|
} else {
|
||||||
|
line.setAttribute('x1', String(guide.position));
|
||||||
|
line.setAttribute('y1', '0');
|
||||||
|
line.setAttribute('x2', String(guide.position));
|
||||||
|
line.setAttribute('y2', String(VIEWBOX_HEIGHT));
|
||||||
|
}
|
||||||
|
line.setAttribute('stroke', '#8f8f8f');
|
||||||
|
line.setAttribute('stroke-width', '1');
|
||||||
|
line.setAttribute('stroke-dasharray', '8 6');
|
||||||
|
line.setAttribute('opacity', '0.8');
|
||||||
|
line.setAttribute('data-guide-id', guide.id);
|
||||||
|
svg.appendChild(line);
|
||||||
|
});
|
||||||
|
|
||||||
|
state.polylines.forEach((polyline) => {
|
||||||
|
const line = createSvgElement('polyline');
|
||||||
|
line.setAttribute('fill', 'none');
|
||||||
|
line.setAttribute('stroke', polyline.id === state.selectedPolylineId ? '#007bff' : '#1f2937');
|
||||||
|
line.setAttribute('stroke-width', polyline.id === state.selectedPolylineId ? '5' : '3');
|
||||||
|
line.setAttribute('points', polyline.points.map((point) => `${point.x},${point.y}`).join(' '));
|
||||||
|
line.setAttribute('data-polyline-id', polyline.id);
|
||||||
|
svg.appendChild(line);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
selected.points.forEach((point, index) => {
|
||||||
|
const vertex = createSvgElement('circle');
|
||||||
|
vertex.setAttribute('cx', String(point.x));
|
||||||
|
vertex.setAttribute('cy', String(point.y));
|
||||||
|
vertex.setAttribute('r', '8');
|
||||||
|
vertex.setAttribute('fill', '#ffffff');
|
||||||
|
vertex.setAttribute('stroke', '#dc3545');
|
||||||
|
vertex.setAttribute('stroke-width', '3');
|
||||||
|
vertex.setAttribute('data-polyline-id', selected.id);
|
||||||
|
vertex.setAttribute('data-vertex-index', String(index));
|
||||||
|
svg.appendChild(vertex);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
controls.guideList.innerHTML = '';
|
||||||
|
state.guides.forEach((guide) => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
const label = guide.orientation === 'horizontal' ? 'Horizontal' : 'Vertikal';
|
||||||
|
li.innerHTML = `
|
||||||
|
<span>${label}: ${Math.round(guide.position)}</span>
|
||||||
|
<button type="button" class="button button-danger" data-remove-guide="${guide.id}">X</button>
|
||||||
|
`;
|
||||||
|
controls.guideList.appendChild(li);
|
||||||
|
});
|
||||||
|
|
||||||
|
hiddenInput.value = buildSvgMarkup(state.polylines, state.guides);
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishActivePolyline(state) {
|
||||||
|
const active = state.polylines.find((line) => line.id === state.activePolylineId);
|
||||||
|
if (active && active.points.length < 2) {
|
||||||
|
state.polylines = state.polylines.filter((line) => line.id !== active.id);
|
||||||
|
if (state.selectedPolylineId === active.id) {
|
||||||
|
state.selectedPolylineId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.activePolylineId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadFromExistingSvg(raw, state) {
|
||||||
|
const content = String(raw || '').trim();
|
||||||
|
if (!content) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(content, 'image/svg+xml');
|
||||||
|
const root = doc.documentElement;
|
||||||
|
if (!root || root.nodeName.toLowerCase() === 'parsererror') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.querySelectorAll('line[data-guide="1"], line.floor-guide').forEach((line) => {
|
||||||
|
const orientation = line.getAttribute('data-orientation') === 'horizontal' ? 'horizontal' : 'vertical';
|
||||||
|
const position = orientation === 'horizontal'
|
||||||
|
? Number(line.getAttribute('y1'))
|
||||||
|
: Number(line.getAttribute('x1'));
|
||||||
|
if (Number.isFinite(position)) {
|
||||||
|
state.guides.push({
|
||||||
|
id: createId('guide'),
|
||||||
|
orientation,
|
||||||
|
position: Math.round(position)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
root.querySelectorAll('polyline').forEach((polyline) => {
|
||||||
|
const pointsAttr = polyline.getAttribute('points') || '';
|
||||||
|
const points = parsePoints(pointsAttr);
|
||||||
|
if (points.length < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.polylines.push({
|
||||||
|
id: createId('poly'),
|
||||||
|
points
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (state.polylines.length > 0) {
|
||||||
|
state.selectedPolylineId = state.polylines[0].id;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// ignore invalid svg content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePoints(pointsAttr) {
|
||||||
|
return pointsAttr
|
||||||
|
.trim()
|
||||||
|
.split(/\s+/)
|
||||||
|
.map((pair) => {
|
||||||
|
const [x, y] = pair.split(',').map((value) => Number(value));
|
||||||
|
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { x: Math.round(x), y: Math.round(y) };
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSvgMarkup(polylines, guides) {
|
||||||
|
const svg = createSvgElement('svg');
|
||||||
|
svg.setAttribute('xmlns', SVG_NS);
|
||||||
|
svg.setAttribute('viewBox', `0 0 ${VIEWBOX_WIDTH} ${VIEWBOX_HEIGHT}`);
|
||||||
|
|
||||||
|
const background = createSvgElement('rect');
|
||||||
|
background.setAttribute('x', '0');
|
||||||
|
background.setAttribute('y', '0');
|
||||||
|
background.setAttribute('width', String(VIEWBOX_WIDTH));
|
||||||
|
background.setAttribute('height', String(VIEWBOX_HEIGHT));
|
||||||
|
background.setAttribute('fill', '#fafafa');
|
||||||
|
background.setAttribute('stroke', '#e1e1e1');
|
||||||
|
background.setAttribute('stroke-width', '1');
|
||||||
|
svg.appendChild(background);
|
||||||
|
|
||||||
|
guides.forEach((guide) => {
|
||||||
|
const line = createSvgElement('line');
|
||||||
|
if (guide.orientation === 'horizontal') {
|
||||||
|
line.setAttribute('x1', '0');
|
||||||
|
line.setAttribute('y1', String(guide.position));
|
||||||
|
line.setAttribute('x2', String(VIEWBOX_WIDTH));
|
||||||
|
line.setAttribute('y2', String(guide.position));
|
||||||
|
} else {
|
||||||
|
line.setAttribute('x1', String(guide.position));
|
||||||
|
line.setAttribute('y1', '0');
|
||||||
|
line.setAttribute('x2', String(guide.position));
|
||||||
|
line.setAttribute('y2', String(VIEWBOX_HEIGHT));
|
||||||
|
}
|
||||||
|
line.setAttribute('stroke', '#8f8f8f');
|
||||||
|
line.setAttribute('stroke-width', '1');
|
||||||
|
line.setAttribute('stroke-dasharray', '8 6');
|
||||||
|
line.setAttribute('class', 'floor-guide');
|
||||||
|
line.setAttribute('data-guide', '1');
|
||||||
|
line.setAttribute('data-orientation', guide.orientation);
|
||||||
|
svg.appendChild(line);
|
||||||
|
});
|
||||||
|
|
||||||
|
polylines.forEach((polyline) => {
|
||||||
|
if (polyline.points.length < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const line = createSvgElement('polyline');
|
||||||
|
line.setAttribute('fill', 'none');
|
||||||
|
line.setAttribute('stroke', '#1f2937');
|
||||||
|
line.setAttribute('stroke-width', '3');
|
||||||
|
line.setAttribute('class', 'floor-polyline');
|
||||||
|
line.setAttribute('points', polyline.points.map((point) => `${point.x},${point.y}`).join(' '));
|
||||||
|
svg.appendChild(line);
|
||||||
|
});
|
||||||
|
|
||||||
|
return new XMLSerializer().serializeToString(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lockTo45(origin, point) {
|
||||||
|
const dx = point.x - origin.x;
|
||||||
|
const dy = point.y - origin.y;
|
||||||
|
const length = Math.sqrt((dx * dx) + (dy * dy));
|
||||||
|
if (length === 0) {
|
||||||
|
return { x: origin.x, y: origin.y };
|
||||||
|
}
|
||||||
|
|
||||||
|
const angle = Math.atan2(dy, dx);
|
||||||
|
const step = Math.PI / 4;
|
||||||
|
const snapped = Math.round(angle / step) * step;
|
||||||
|
return {
|
||||||
|
x: origin.x + Math.cos(snapped) * length,
|
||||||
|
y: origin.y + Math.sin(snapped) * length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapPointToGuides(point, guides, tolerance) {
|
||||||
|
let next = { ...point };
|
||||||
|
guides.forEach((guide) => {
|
||||||
|
if (guide.orientation === 'vertical') {
|
||||||
|
if (Math.abs(next.x - guide.position) <= tolerance) {
|
||||||
|
next.x = guide.position;
|
||||||
|
}
|
||||||
|
} else if (Math.abs(next.y - guide.position) <= tolerance) {
|
||||||
|
next.y = guide.position;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toSvgPoint(svg, event) {
|
||||||
|
const pt = svg.createSVGPoint();
|
||||||
|
pt.x = event.clientX;
|
||||||
|
pt.y = event.clientY;
|
||||||
|
const ctm = svg.getScreenCTM();
|
||||||
|
if (!ctm) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const transformed = pt.matrixTransform(ctm.inverse());
|
||||||
|
return {
|
||||||
|
x: transformed.x,
|
||||||
|
y: transformed.y
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSvgElement(name) {
|
||||||
|
return document.createElementNS(SVG_NS, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createId(prefix) {
|
||||||
|
return `${prefix}_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', initFloorSvgEditor);
|
||||||
|
})();
|
||||||
@@ -35,6 +35,12 @@ $shapeDefinition = $deviceType['shape_definition'] ?? '[]';
|
|||||||
if (trim($shapeDefinition) === '') {
|
if (trim($shapeDefinition) === '') {
|
||||||
$shapeDefinition = '[]';
|
$shapeDefinition = '[]';
|
||||||
}
|
}
|
||||||
|
$portTypes = $sql->get(
|
||||||
|
"SELECT id, name FROM port_types ORDER BY name",
|
||||||
|
"",
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
$defaultPortTypeSelected = (int)($_POST['default_port_type_id'] ?? 0);
|
||||||
|
|
||||||
$isEdit = !empty($deviceType);
|
$isEdit = !empty($deviceType);
|
||||||
$pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType['name']) : "Neuer Gerätetyp";
|
$pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType['name']) : "Neuer Gerätetyp";
|
||||||
@@ -86,6 +92,19 @@ $pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType[
|
|||||||
placeholder="z.B. 48">
|
placeholder="z.B. 48">
|
||||||
<small>Beim Speichern werden bis zu dieser Zahl Platzhalter-Ports erstellt, bestehende Einträge bleiben erhalten.</small>
|
<small>Beim Speichern werden bis zu dieser Zahl Platzhalter-Ports erstellt, bestehende Einträge bleiben erhalten.</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="default_port_type_id">Standard-Porttyp für neue Ports</label>
|
||||||
|
<select id="default_port_type_id" name="default_port_type_id">
|
||||||
|
<option value="">- Kein Standard -</option>
|
||||||
|
<?php foreach ($portTypes as $portType): ?>
|
||||||
|
<option value="<?php echo (int)$portType['id']; ?>" <?php echo ($defaultPortTypeSelected === (int)$portType['id']) ? 'selected' : ''; ?>>
|
||||||
|
<?php echo htmlspecialchars($portType['name']); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<small>Wird beim automatischen Erstellen neuer Ports als Startwert gesetzt.</small>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<!-- =========================
|
<!-- =========================
|
||||||
@@ -117,7 +136,7 @@ $pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType[
|
|||||||
<div class="shape-editor" id="device-type-shape-editor">
|
<div class="shape-editor" id="device-type-shape-editor">
|
||||||
<div class="shape-toolbox">
|
<div class="shape-toolbox">
|
||||||
<h4>Werkzeuge</h4>
|
<h4>Werkzeuge</h4>
|
||||||
<p class="hint">Element in die Zeichenfläche ziehen, dann per Klick auswählen und bearbeiten.</p>
|
<p class="hint">Objekttyp anklicken, im Canvas diagonal ziehen und mit Loslassen fixieren.</p>
|
||||||
<div class="shape-tool-list">
|
<div class="shape-tool-list">
|
||||||
<button type="button" class="shape-tool" draggable="true" data-shape-template="rect">Rechteck</button>
|
<button type="button" class="shape-tool" draggable="true" data-shape-template="rect">Rechteck</button>
|
||||||
<button type="button" class="shape-tool" draggable="true" data-shape-template="circle">Kreis</button>
|
<button type="button" class="shape-tool" draggable="true" data-shape-template="circle">Kreis</button>
|
||||||
@@ -216,32 +235,50 @@ $pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType[
|
|||||||
<table class="port-definition-table">
|
<table class="port-definition-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Typ</th>
|
<th>Port-Typ</th>
|
||||||
<th></th>
|
<th>Löschen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody id="port-definition-body">
|
||||||
<?php if (!empty($ports)): ?>
|
<?php if (!empty($ports)): ?>
|
||||||
<?php foreach ($ports as $port): ?>
|
<?php foreach ($ports as $idx => $port): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?php echo htmlspecialchars($port['name']); ?></td>
|
<td><?php echo (int)($idx + 1); ?></td>
|
||||||
<td><?php echo htmlspecialchars($port['port_type_id'] ?? '-'); ?></td>
|
<td>
|
||||||
<td><a href="#" class="button button-small button-danger">Entfernen</a></td>
|
<input type="hidden" name="port_rows[<?php echo (int)$idx; ?>][id]" value="<?php echo (int)$port['id']; ?>">
|
||||||
|
<input type="text" name="port_rows[<?php echo (int)$idx; ?>][name]" value="<?php echo htmlspecialchars($port['name']); ?>" placeholder="z.B. Gi1/0/1">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select name="port_rows[<?php echo (int)$idx; ?>][port_type_id]">
|
||||||
|
<option value="">- Kein Typ -</option>
|
||||||
|
<?php foreach ($portTypes as $portType): ?>
|
||||||
|
<option value="<?php echo (int)$portType['id']; ?>" <?php echo ((int)($port['port_type_id'] ?? 0) === (int)$portType['id']) ? 'selected' : ''; ?>>
|
||||||
|
<?php echo htmlspecialchars($portType['name']); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<label class="inline-checkbox">
|
||||||
|
<input type="checkbox" name="port_rows[<?php echo (int)$idx; ?>][delete]" value="1">
|
||||||
|
entfernen
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="3"><em>Noch keine Ports definiert.</em></td>
|
<td colspan="4"><em>Noch keine Ports definiert.</em></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div style="margin-top: 15px;">
|
<div style="margin-top: 15px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
|
||||||
<input type="text" id="port_name" placeholder="Port-Name (z.B. GigabitEthernet 1/1)">
|
<button type="button" class="button" id="add-port-row">+ Port hinzufügen</button>
|
||||||
<input type="text" id="port_type" placeholder="Port-Typ (z.B. RJ45)">
|
<small>Ports können nach dem ersten Speichern jederzeit einzeln bearbeitet und gelöscht werden.</small>
|
||||||
<button type="button" class="button" onclick="addPortRow()">+ Port hinzufügen</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -333,12 +370,22 @@ $pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType[
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid #ddd;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.port-definition-table th {
|
.port-definition-table th {
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.port-definition-table input[type="text"],
|
||||||
|
.port-definition-table select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
.form-actions {
|
.form-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -400,6 +447,12 @@ $pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType[
|
|||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shape-tool.is-active {
|
||||||
|
background: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
.shape-editor-canvas.drag-over {
|
.shape-editor-canvas.drag-over {
|
||||||
outline: 2px dashed #007bff;
|
outline: 2px dashed #007bff;
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
@@ -409,6 +462,10 @@ $pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType[
|
|||||||
cursor: move;
|
cursor: move;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#shape-canvas.shape-tool-active {
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
|
||||||
.shape-object.is-selected {
|
.shape-object.is-selected {
|
||||||
filter: drop-shadow(0 0 5px rgba(0, 123, 255, 0.7));
|
filter: drop-shadow(0 0 5px rgba(0, 123, 255, 0.7));
|
||||||
}
|
}
|
||||||
@@ -500,19 +557,48 @@ $pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType[
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
function addPortRow() {
|
function addPortRow() {
|
||||||
const name = document.getElementById('port_name').value;
|
const body = document.getElementById('port-definition-body');
|
||||||
const type = document.getElementById('port_type').value;
|
if (!body) {
|
||||||
|
|
||||||
if (!name.trim()) {
|
|
||||||
alert('Port-Name erforderlich');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Neue Reihe zur Tabelle hinzufügen
|
const emptyRow = body.querySelector('tr td em');
|
||||||
alert('Port hinzufügen funktioniert noch nicht');
|
if (emptyRow) {
|
||||||
|
emptyRow.closest('tr').remove();
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('port_name').value = '';
|
const rowCount = body.querySelectorAll('tr').length;
|
||||||
document.getElementById('port_type').value = '';
|
const index = rowCount;
|
||||||
|
const number = rowCount + 1;
|
||||||
|
|
||||||
|
const portTypeOptions = `<?php
|
||||||
|
$optionHtml = '<option value="">- Kein Typ -</option>';
|
||||||
|
foreach ($portTypes as $pt) {
|
||||||
|
$optionHtml .= '<option value="' . (int)$pt['id'] . '">' . htmlspecialchars($pt['name'], ENT_QUOTES) . '</option>';
|
||||||
|
}
|
||||||
|
echo str_replace('`', '\`', $optionHtml);
|
||||||
|
?>`;
|
||||||
|
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.innerHTML = `
|
||||||
|
<td>${number}</td>
|
||||||
|
<td>
|
||||||
|
<input type="hidden" name="port_rows[${index}][id]" value="">
|
||||||
|
<input type="text" name="port_rows[${index}][name]" value="" placeholder="z.B. Gi1/0/1">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select name="port_rows[${index}][port_type_id]">
|
||||||
|
${portTypeOptions}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<label class="inline-checkbox">
|
||||||
|
<input type="checkbox" name="port_rows[${index}][delete]" value="1">
|
||||||
|
entfernen
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
body.appendChild(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmDelete(id) {
|
function confirmDelete(id) {
|
||||||
@@ -521,6 +607,13 @@ function confirmDelete(id) {
|
|||||||
alert('Löschen noch nicht implementiert');
|
alert('Löschen noch nicht implementiert');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const addButton = document.getElementById('add-port-row');
|
||||||
|
if (addButton) {
|
||||||
|
addButton.addEventListener('click', addPortRow);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="/assets/js/device-type-shape-editor.js" defer></script>
|
<script src="/assets/js/device-type-shape-editor.js" defer></script>
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ $name = trim($_POST['name'] ?? '');
|
|||||||
$category = $_POST['category'] ?? 'other';
|
$category = $_POST['category'] ?? 'other';
|
||||||
$comment = trim($_POST['comment'] ?? '');
|
$comment = trim($_POST['comment'] ?? '');
|
||||||
$seedPortCount = max(0, (int)($_POST['seed_ports'] ?? 0));
|
$seedPortCount = max(0, (int)($_POST['seed_ports'] ?? 0));
|
||||||
|
$defaultPortTypeId = normalizeNullableInt($_POST['default_port_type_id'] ?? null);
|
||||||
|
$portRows = is_array($_POST['port_rows'] ?? null) ? $_POST['port_rows'] : [];
|
||||||
$rawShapes = trim($_POST['shape_definition'] ?? '');
|
$rawShapes = trim($_POST['shape_definition'] ?? '');
|
||||||
$shapeDefinition = '[]';
|
$shapeDefinition = '[]';
|
||||||
if ($rawShapes !== '') {
|
if ($rawShapes !== '') {
|
||||||
@@ -126,7 +128,8 @@ if ($deviceTypeId > 0) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
seedDeviceTypePorts($sql, $deviceTypeId, $seedPortCount);
|
seedDeviceTypePorts($sql, $deviceTypeId, $seedPortCount, $defaultPortTypeId);
|
||||||
|
syncDeviceTypePorts($sql, $deviceTypeId, $portRows);
|
||||||
|
|
||||||
$_SESSION['success'] = $deviceTypeId ? "Gerätetyp gespeichert" : "Fehler beim Speichern";
|
$_SESSION['success'] = $deviceTypeId ? "Gerätetyp gespeichert" : "Fehler beim Speichern";
|
||||||
|
|
||||||
@@ -136,7 +139,7 @@ $_SESSION['success'] = $deviceTypeId ? "Gerätetyp gespeichert" : "Fehler beim S
|
|||||||
header('Location: ?module=device_types&action=list');
|
header('Location: ?module=device_types&action=list');
|
||||||
exit;
|
exit;
|
||||||
|
|
||||||
function seedDeviceTypePorts($sql, $deviceTypeId, $targetCount)
|
function seedDeviceTypePorts($sql, $deviceTypeId, $targetCount, $defaultPortTypeId = null)
|
||||||
{
|
{
|
||||||
if ($deviceTypeId <= 0 || $targetCount <= 0) {
|
if ($deviceTypeId <= 0 || $targetCount <= 0) {
|
||||||
return;
|
return;
|
||||||
@@ -153,6 +156,13 @@ function seedDeviceTypePorts($sql, $deviceTypeId, $targetCount)
|
|||||||
|
|
||||||
for ($i = 1; $i <= $toCreate; $i++) {
|
for ($i = 1; $i <= $toCreate; $i++) {
|
||||||
$index = $existing + $i;
|
$index = $existing + $i;
|
||||||
|
if ($defaultPortTypeId) {
|
||||||
|
$sql->set(
|
||||||
|
"INSERT INTO device_type_ports (device_type_id, name, port_type_id, x, y) VALUES (?, ?, ?, 0, 0)",
|
||||||
|
"isi",
|
||||||
|
[$deviceTypeId, "Port $index", $defaultPortTypeId]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
$sql->set(
|
$sql->set(
|
||||||
"INSERT INTO device_type_ports (device_type_id, name, port_type_id, x, y) VALUES (?, ?, NULL, 0, 0)",
|
"INSERT INTO device_type_ports (device_type_id, name, port_type_id, x, y) VALUES (?, ?, NULL, 0, 0)",
|
||||||
"is",
|
"is",
|
||||||
@@ -160,4 +170,87 @@ function seedDeviceTypePorts($sql, $deviceTypeId, $targetCount)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncDeviceTypePorts($sql, $deviceTypeId, array $rows)
|
||||||
|
{
|
||||||
|
if ($deviceTypeId <= 0 || empty($rows)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$existingPorts = $sql->get(
|
||||||
|
"SELECT id FROM device_type_ports WHERE device_type_id = ?",
|
||||||
|
"i",
|
||||||
|
[$deviceTypeId]
|
||||||
|
);
|
||||||
|
$existingIds = [];
|
||||||
|
foreach ($existingPorts as $existingPort) {
|
||||||
|
$existingIds[(int)$existingPort['id']] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$rowId = (int)($row['id'] ?? 0);
|
||||||
|
$name = trim($row['name'] ?? '');
|
||||||
|
$portTypeId = normalizeNullableInt($row['port_type_id'] ?? null);
|
||||||
|
$delete = (int)($row['delete'] ?? 0) === 1;
|
||||||
|
|
||||||
|
if ($rowId > 0 && !isset($existingIds[$rowId])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($delete) {
|
||||||
|
if ($rowId > 0) {
|
||||||
|
$sql->set(
|
||||||
|
"DELETE FROM device_type_ports WHERE id = ? AND device_type_id = ?",
|
||||||
|
"ii",
|
||||||
|
[$rowId, $deviceTypeId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($name === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($rowId > 0) {
|
||||||
|
if ($portTypeId) {
|
||||||
|
$sql->set(
|
||||||
|
"UPDATE device_type_ports SET name = ?, port_type_id = ? WHERE id = ? AND device_type_id = ?",
|
||||||
|
"siii",
|
||||||
|
[$name, $portTypeId, $rowId, $deviceTypeId]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$sql->set(
|
||||||
|
"UPDATE device_type_ports SET name = ?, port_type_id = NULL WHERE id = ? AND device_type_id = ?",
|
||||||
|
"sii",
|
||||||
|
[$name, $rowId, $deviceTypeId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($portTypeId) {
|
||||||
|
$sql->set(
|
||||||
|
"INSERT INTO device_type_ports (device_type_id, name, port_type_id, x, y) VALUES (?, ?, ?, 0, 0)",
|
||||||
|
"isi",
|
||||||
|
[$deviceTypeId, $name, $portTypeId]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$sql->set(
|
||||||
|
"INSERT INTO device_type_ports (device_type_id, name, port_type_id, x, y) VALUES (?, ?, NULL, 0, 0)",
|
||||||
|
"is",
|
||||||
|
[$deviceTypeId, $name]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeNullableInt($value)
|
||||||
|
{
|
||||||
|
if ($value === null || $value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$intValue = (int)$value;
|
||||||
|
return $intValue > 0 ? $intValue : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -119,6 +119,9 @@ function copyDevicePortsFromType($sql, $deviceId, $deviceTypeId)
|
|||||||
}
|
}
|
||||||
|
|
||||||
$portTypeId = isset($port['port_type_id']) ? (int)$port['port_type_id'] : null;
|
$portTypeId = isset($port['port_type_id']) ? (int)$port['port_type_id'] : null;
|
||||||
|
if ($portTypeId !== null && $portTypeId <= 0) {
|
||||||
|
$portTypeId = null;
|
||||||
|
}
|
||||||
|
|
||||||
if ($portTypeId === null) {
|
if ($portTypeId === null) {
|
||||||
$sql->set(
|
$sql->set(
|
||||||
|
|||||||
@@ -5,12 +5,9 @@
|
|||||||
* Floor / Stockwerk anlegen oder bearbeiten
|
* Floor / Stockwerk anlegen oder bearbeiten
|
||||||
* - Name, Ebene, Beschreibung
|
* - Name, Ebene, Beschreibung
|
||||||
* - Zugehöriges Gebäude
|
* - Zugehöriges Gebäude
|
||||||
* - SVG-Grundriss (optional)
|
* - SVG-Grundriss mit Linien-Editor (Polylinien)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// =========================
|
|
||||||
// Kontext bestimmen
|
|
||||||
// =========================
|
|
||||||
$floorId = (int)($_GET['id'] ?? 0);
|
$floorId = (int)($_GET['id'] ?? 0);
|
||||||
$floor = null;
|
$floor = null;
|
||||||
|
|
||||||
@@ -24,26 +21,26 @@ if ($floorId > 0) {
|
|||||||
|
|
||||||
$isEdit = !empty($floor);
|
$isEdit = !empty($floor);
|
||||||
$pageTitle = $isEdit ? "Stockwerk bearbeiten: " . htmlspecialchars($floor['name']) : "Neues Stockwerk";
|
$pageTitle = $isEdit ? "Stockwerk bearbeiten: " . htmlspecialchars($floor['name']) : "Neues Stockwerk";
|
||||||
|
|
||||||
// =========================
|
|
||||||
// Gebäude laden
|
|
||||||
// =========================
|
|
||||||
$buildings = $sql->get("SELECT id, name FROM buildings ORDER BY name", "", []);
|
$buildings = $sql->get("SELECT id, name FROM buildings ORDER BY name", "", []);
|
||||||
|
|
||||||
|
$existingSvgContent = '';
|
||||||
|
if (!empty($floor['svg_path'])) {
|
||||||
|
$relativePath = ltrim((string)$floor['svg_path'], "/\\");
|
||||||
|
$absolutePath = __DIR__ . '/../../' . $relativePath;
|
||||||
|
if (is_file($absolutePath) && is_readable($absolutePath)) {
|
||||||
|
$existingSvgContent = file_get_contents($absolutePath) ?: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="floor-edit">
|
<div class="floor-edit">
|
||||||
<h1><?php echo $pageTitle; ?></h1>
|
<h1><?php echo $pageTitle; ?></h1>
|
||||||
|
|
||||||
<form method="post" action="?module=floors&action=save" enctype="multipart/form-data" class="edit-form">
|
<form method="post" action="?module=floors&action=save" enctype="multipart/form-data" class="edit-form">
|
||||||
|
|
||||||
<?php if ($isEdit): ?>
|
<?php if ($isEdit): ?>
|
||||||
<input type="hidden" name="id" value="<?php echo $floorId; ?>">
|
<input type="hidden" name="id" value="<?php echo $floorId; ?>">
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
Basisdaten
|
|
||||||
========================= -->
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Allgemein</legend>
|
<legend>Allgemein</legend>
|
||||||
|
|
||||||
@@ -58,8 +55,7 @@ $buildings = $sql->get("SELECT id, name FROM buildings ORDER BY name", "", []);
|
|||||||
<label for="level">Ebene</label>
|
<label for="level">Ebene</label>
|
||||||
<input type="number" id="level" name="level"
|
<input type="number" id="level" name="level"
|
||||||
value="<?php echo htmlspecialchars($floor['level'] ?? '0'); ?>"
|
value="<?php echo htmlspecialchars($floor['level'] ?? '0'); ?>"
|
||||||
placeholder="z.B. 0 für Erdgeschoss, 1 für 1. OG">
|
placeholder="z.B. 0 für EG, 1 für 1. OG">
|
||||||
<small>Dient zur Sortierung</small>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -69,19 +65,15 @@ $buildings = $sql->get("SELECT id, name FROM buildings ORDER BY name", "", []);
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
Gebäude & Standort
|
|
||||||
========================= -->
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Standort</legend>
|
<legend>Standort</legend>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="building_id">Gebäude <span class="required">*</span></label>
|
<label for="building_id">Gebäude <span class="required">*</span></label>
|
||||||
<select id="building_id" name="building_id" required>
|
<select id="building_id" name="building_id" required>
|
||||||
<option value="">- Wählen -</option>
|
<option value="">- Wählen -</option>
|
||||||
<?php foreach ($buildings as $building): ?>
|
<?php foreach ($buildings as $building): ?>
|
||||||
<option value="<?php echo $building['id']; ?>"
|
<option value="<?php echo (int)$building['id']; ?>"
|
||||||
<?php echo ($floor['building_id'] ?? 0) == $building['id'] ? 'selected' : ''; ?>>
|
<?php echo ((int)($floor['building_id'] ?? 0) === (int)$building['id']) ? 'selected' : ''; ?>>
|
||||||
<?php echo htmlspecialchars($building['name']); ?>
|
<?php echo htmlspecialchars($building['name']); ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -89,43 +81,65 @@ $buildings = $sql->get("SELECT id, name FROM buildings ORDER BY name", "", []);
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
SVG-Grundriss (optional)
|
|
||||||
========================= -->
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Grundriss (SVG)</legend>
|
<legend>Grundriss (SVG)</legend>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="svg_file">SVG-Datei hochladen</label>
|
<label for="svg_file">SVG-Datei hochladen</label>
|
||||||
<input type="file" id="svg_file" name="svg_file" accept=".svg">
|
<input type="file" id="svg_file" name="svg_file" accept=".svg">
|
||||||
<small>Optionales Floorplan-SVG. Kann später im Editor bearbeitet werden.</small>
|
<small>Optional. Wenn ein Zeichnungsinhalt im Editor erstellt wird, wird dieser beim Speichern bevorzugt.</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if ($isEdit && $floor['svg_path']): ?>
|
<textarea id="floor-svg-content" name="floor_svg_content" hidden><?php echo htmlspecialchars($existingSvgContent); ?></textarea>
|
||||||
<div class="form-group">
|
|
||||||
<label>Aktueller Grundriss:</label>
|
<div id="floor-svg-editor" class="floor-svg-editor">
|
||||||
<p><small><?php echo htmlspecialchars($floor['svg_path']); ?></small></p>
|
<div class="floor-svg-tools">
|
||||||
|
<h4>Zeichenwerkzeug</h4>
|
||||||
|
<button type="button" class="button" id="floor-start-polyline">Neue Linie starten</button>
|
||||||
|
<button type="button" class="button" id="floor-finish-polyline">Linie beenden</button>
|
||||||
|
<button type="button" class="button button-danger" id="floor-delete-polyline">Ausgewählte Linie löschen</button>
|
||||||
|
<button type="button" class="button button-danger" id="floor-clear-drawing">Alles löschen</button>
|
||||||
|
|
||||||
|
<label class="inline-checkbox">
|
||||||
|
<input type="checkbox" id="floor-lock-45">
|
||||||
|
Winkel auf 45°-Vielfache begrenzen
|
||||||
|
</label>
|
||||||
|
<label class="inline-checkbox">
|
||||||
|
<input type="checkbox" id="floor-snap-guides" checked>
|
||||||
|
Ecken an Hilfslinien snappen
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="floor-svg-canvas-wrap">
|
||||||
|
<svg id="floor-svg-canvas" viewBox="0 0 2000 1000" role="img" aria-label="Stockwerk-Zeichnung"></svg>
|
||||||
|
<p class="hint">Linien müssen nicht geschlossen sein. Klick auf freie Fläche fügt Punkte hinzu, Punkte sind per Drag verschiebbar.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="floor-guides">
|
||||||
|
<h4>Hilfslinien</h4>
|
||||||
|
<div class="floor-guide-form">
|
||||||
|
<select id="floor-guide-orientation">
|
||||||
|
<option value="vertical">Vertikal</option>
|
||||||
|
<option value="horizontal">Horizontal</option>
|
||||||
|
</select>
|
||||||
|
<input type="number" id="floor-guide-position" placeholder="Position (SVG-Koordinate)">
|
||||||
|
<button type="button" class="button" id="floor-add-guide">Hilfslinie setzen</button>
|
||||||
|
</div>
|
||||||
|
<ul id="floor-guide-list" class="guide-list"></ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
Aktionen
|
|
||||||
========================= -->
|
|
||||||
<fieldset class="form-actions">
|
<fieldset class="form-actions">
|
||||||
<button type="submit" class="button button-primary">Speichern</button>
|
<button type="submit" class="button button-primary">Speichern</button>
|
||||||
<a href="?module=floors&action=list" class="button">Abbrechen</a>
|
<a href="?module=floors&action=list" class="button">Abbrechen</a>
|
||||||
<?php if ($isEdit): ?>
|
|
||||||
<a href="#" class="button button-danger" onclick="confirmDelete(<?php echo $floorId; ?>)">Löschen</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.floor-edit {
|
.floor-edit {
|
||||||
max-width: 800px;
|
max-width: 1200px;
|
||||||
margin: 20px auto;
|
margin: 20px auto;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
@@ -147,7 +161,6 @@ $buildings = $sql->get("SELECT id, name FROM buildings ORDER BY name", "", []);
|
|||||||
.edit-form legend {
|
.edit-form legend {
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 1.1em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
@@ -172,14 +185,68 @@ $buildings = $sql->get("SELECT id, name FROM buildings ORDER BY name", "", []);
|
|||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group textarea {
|
.floor-svg-editor {
|
||||||
resize: vertical;
|
display: grid;
|
||||||
|
grid-template-columns: 240px 1fr 280px;
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group small {
|
.floor-svg-tools,
|
||||||
|
.floor-guides,
|
||||||
|
.floor-svg-canvas-wrap {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-svg-tools {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-svg-canvas-wrap svg {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 560px;
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 5px;
|
border: 1px solid #eee;
|
||||||
|
background: #fafafa;
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-guide-form {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-list {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-list li {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 0;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-checkbox {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-weight: normal !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
font-size: 0.82rem;
|
||||||
color: #666;
|
color: #666;
|
||||||
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.required {
|
.required {
|
||||||
@@ -189,7 +256,7 @@ $buildings = $sql->get("SELECT id, name FROM buildings ORDER BY name", "", []);
|
|||||||
.form-actions {
|
.form-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-top: 30px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
@@ -211,64 +278,11 @@ $buildings = $sql->get("SELECT id, name FROM buildings ORDER BY name", "", []);
|
|||||||
background: #dc3545;
|
background: #dc3545;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:hover {
|
@media (max-width: 1200px) {
|
||||||
opacity: 0.8;
|
.floor-svg-editor {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script src="/assets/js/floor-svg-editor.js" defer></script>
|
||||||
function confirmDelete(id) {
|
|
||||||
if (confirm('Dieses Stockwerk wirklich löschen? Alle Räume und Racks werden gelöscht.')) {
|
|
||||||
// TODO: AJAX-Delete implementieren
|
|
||||||
alert('Löschen noch nicht implementiert');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
========================= -->
|
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
<legend>Grundriss / Floorplan</legend>
|
|
||||||
|
|
||||||
<div class="svg-editor-container">
|
|
||||||
<svg
|
|
||||||
id="floor-svg"
|
|
||||||
viewBox="0 0 2000 1000"
|
|
||||||
width="100%"
|
|
||||||
height="600"
|
|
||||||
>
|
|
||||||
<!-- TODO: Floorplan SVG laden -->
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="hint">
|
|
||||||
Räume und Netzwerkdosen per Drag & Drop platzieren. Nummerierung und Bezeichnungen editierbar.
|
|
||||||
</p>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
Aktionen
|
|
||||||
========================= -->
|
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
<button type="submit">Speichern</button>
|
|
||||||
<button type="button" onclick="history.back()">Abbrechen</button>
|
|
||||||
<!-- TODO: Löschen, falls edit -->
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- =========================
|
|
||||||
JS-Konfiguration
|
|
||||||
========================= -->
|
|
||||||
|
|
||||||
<script>
|
|
||||||
/**
|
|
||||||
* Konfiguration für Floorplan SVG-Editor
|
|
||||||
*/
|
|
||||||
|
|
||||||
// TODO: Floor-ID aus PHP setzen
|
|
||||||
// window.FLOOR_ID = <?= (int)$floorId ?>;
|
|
||||||
|
|
||||||
// TODO: Räume / Dosen an JS übergeben
|
|
||||||
// window.ROOMS = <?= json_encode($rooms) ?>;
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ $name = trim($_POST['name'] ?? '');
|
|||||||
$buildingId = (int)($_POST['building_id'] ?? 0);
|
$buildingId = (int)($_POST['building_id'] ?? 0);
|
||||||
$level = isset($_POST['level']) ? (int)$_POST['level'] : null;
|
$level = isset($_POST['level']) ? (int)$_POST['level'] : null;
|
||||||
$comment = trim($_POST['comment'] ?? '');
|
$comment = trim($_POST['comment'] ?? '');
|
||||||
|
$floorSvgContent = trim($_POST['floor_svg_content'] ?? '');
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// Validierung
|
// Validierung
|
||||||
@@ -79,6 +80,17 @@ if (!empty($_FILES['svg_file']['name'])) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($floorSvgContent !== '') {
|
||||||
|
$storedSvgPath = storeSvgEditorContent($sql, $floorId, $floorSvgContent);
|
||||||
|
if ($storedSvgPath === false) {
|
||||||
|
$_SESSION['error'] = "SVG aus dem Editor konnte nicht gespeichert werden";
|
||||||
|
$redirectUrl = $floorId ? "?module=floors&action=edit&id=$floorId" : "?module=floors&action=edit";
|
||||||
|
header("Location: $redirectUrl");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$svgPath = $storedSvgPath;
|
||||||
|
}
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// In DB speichern
|
// In DB speichern
|
||||||
// =========================
|
// =========================
|
||||||
@@ -113,3 +125,41 @@ $_SESSION['success'] = "Stockwerk gespeichert";
|
|||||||
// =========================
|
// =========================
|
||||||
header('Location: ?module=floors&action=list');
|
header('Location: ?module=floors&action=list');
|
||||||
exit;
|
exit;
|
||||||
|
|
||||||
|
function storeSvgEditorContent($sql, $floorId, $content)
|
||||||
|
{
|
||||||
|
$normalized = trim($content);
|
||||||
|
if ($normalized === '' || stripos($normalized, '<svg') === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$relativePath = null;
|
||||||
|
if ($floorId > 0) {
|
||||||
|
$existing = $sql->single(
|
||||||
|
"SELECT svg_path FROM floors WHERE id = ?",
|
||||||
|
"i",
|
||||||
|
[$floorId]
|
||||||
|
);
|
||||||
|
$candidate = trim((string)($existing['svg_path'] ?? ''));
|
||||||
|
if ($candidate !== '') {
|
||||||
|
$relativePath = ltrim($candidate, "/\\");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$relativePath) {
|
||||||
|
$relativePath = 'uploads/floorplans/' . uniqid('floor_') . '.svg';
|
||||||
|
}
|
||||||
|
|
||||||
|
$absolutePath = __DIR__ . '/../../' . $relativePath;
|
||||||
|
$targetDir = dirname($absolutePath);
|
||||||
|
if (!is_dir($targetDir)) {
|
||||||
|
mkdir($targetDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$written = file_put_contents($absolutePath, $normalized);
|
||||||
|
if ($written === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $relativePath;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user