closes #33
This commit is contained in:
@@ -71,6 +71,19 @@
|
|||||||
background: #218838;
|
background: #218838;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-secondary:hover {
|
||||||
|
background: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.button-small {
|
.button-small {
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
@@ -185,3 +198,98 @@
|
|||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.floor-preview-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(15, 23, 42, 0.85);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
|
z-index: 1100;
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.2s ease, visibility 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-preview-overlay.is-visible {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-preview-card {
|
||||||
|
width: min(960px, 100%);
|
||||||
|
max-height: calc(100vh - 32px);
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 25px 60px rgba(15, 23, 42, 0.35);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-preview-header,
|
||||||
|
.floor-preview-footer {
|
||||||
|
padding: 16px 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
border-bottom: 1px solid #edf2f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-preview-footer {
|
||||||
|
border-top: 1px solid #edf2f7;
|
||||||
|
border-bottom: none;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-preview-label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin: 0;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-preview-header h3 {
|
||||||
|
margin: 2px 0 0;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-preview-body {
|
||||||
|
padding: 16px 24px 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-preview-body canvas {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 900px;
|
||||||
|
background: #0a0c10;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-preview-empty {
|
||||||
|
margin: 0;
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-preview-close {
|
||||||
|
background: transparent;
|
||||||
|
color: #1f2a37;
|
||||||
|
border: 1px solid #dfe3ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-preview-close:hover {
|
||||||
|
background: #f4f5f7;
|
||||||
|
}
|
||||||
|
|||||||
@@ -92,5 +92,334 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', bindDeleteButtons);
|
function initFloorPreview() {
|
||||||
|
const overlay = document.querySelector('[data-floor-preview-overlay]');
|
||||||
|
if (!overlay) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const titleElement = overlay.querySelector('[data-floor-preview-title]');
|
||||||
|
const canvas = overlay.querySelector('[data-floor-preview-canvas]');
|
||||||
|
const messageElement = overlay.querySelector('[data-floor-preview-message]');
|
||||||
|
const closeButton = overlay.querySelector('[data-floor-preview-close]');
|
||||||
|
const downloadButton = overlay.querySelector('.js-floor-preview-download');
|
||||||
|
const ctx = canvas ? canvas.getContext('2d') : null;
|
||||||
|
const palette = ['#f94144', '#f3722c', '#f9844a', '#f9c74f', '#90be6d', '#43aa8b', '#577590', '#277da1', '#845ef7', '#ffb703', '#ff85c0'];
|
||||||
|
const DEFAULT_VIEWBOX = { x: 0, y: 0, width: 1200, height: 700 };
|
||||||
|
let currentFloorName = 'Stockwerk';
|
||||||
|
let latestRenderId = 0;
|
||||||
|
|
||||||
|
const parseRoomPayload = (payload) => {
|
||||||
|
if (!payload) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(payload);
|
||||||
|
if (!Array.isArray(parsed)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return parsed.map((room) => ({
|
||||||
|
number: typeof room.number === 'string' ? room.number : (room.number != null ? String(room.number) : ''),
|
||||||
|
name: typeof room.name === 'string' ? room.name : (room.name != null ? String(room.name) : ''),
|
||||||
|
polygon: Array.isArray(room.polygon) ? room.polygon
|
||||||
|
.map((point) => ({
|
||||||
|
x: Number(point && point.x),
|
||||||
|
y: Number(point && point.y)
|
||||||
|
}))
|
||||||
|
.filter((point) => Number.isFinite(point.x) && Number.isFinite(point.y))
|
||||||
|
: [],
|
||||||
|
bounds: {
|
||||||
|
x: Number(room.bounds && room.bounds.x),
|
||||||
|
y: Number(room.bounds && room.bounds.y),
|
||||||
|
width: Number(room.bounds && room.bounds.width),
|
||||||
|
height: Number(room.bounds && room.bounds.height)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const safeViewBox = (viewBox) => {
|
||||||
|
if (!viewBox || !Number.isFinite(viewBox.width) || !Number.isFinite(viewBox.height) || viewBox.width <= 0 || viewBox.height <= 0) {
|
||||||
|
return { ...DEFAULT_VIEWBOX };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
x: Number.isFinite(viewBox.x) ? viewBox.x : 0,
|
||||||
|
y: Number.isFinite(viewBox.y) ? viewBox.y : 0,
|
||||||
|
width: viewBox.width,
|
||||||
|
height: viewBox.height
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const readViewBox = (svgRoot) => {
|
||||||
|
const rawViewBox = (svgRoot.getAttribute('viewBox') || '').trim();
|
||||||
|
if (rawViewBox) {
|
||||||
|
const parts = rawViewBox.split(/\s+/).map(Number);
|
||||||
|
if (parts.length === 4 && parts.every(Number.isFinite)) {
|
||||||
|
return {
|
||||||
|
x: parts[0],
|
||||||
|
y: parts[1],
|
||||||
|
width: parts[2],
|
||||||
|
height: parts[3]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const width = Number(svgRoot.getAttribute('width'));
|
||||||
|
const height = Number(svgRoot.getAttribute('height'));
|
||||||
|
if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) {
|
||||||
|
return { x: 0, y: 0, width, height };
|
||||||
|
}
|
||||||
|
return { ...DEFAULT_VIEWBOX };
|
||||||
|
};
|
||||||
|
|
||||||
|
const computeMetrics = (viewBox, canvasWidth, canvasHeight) => {
|
||||||
|
const width = viewBox.width || 1;
|
||||||
|
const height = viewBox.height || 1;
|
||||||
|
const scale = Math.min(canvasWidth / width, canvasHeight / height);
|
||||||
|
const drawWidth = width * scale;
|
||||||
|
const drawHeight = height * scale;
|
||||||
|
return {
|
||||||
|
scale,
|
||||||
|
offsetX: (canvasWidth - drawWidth) / 2,
|
||||||
|
offsetY: (canvasHeight - drawHeight) / 2,
|
||||||
|
drawWidth,
|
||||||
|
drawHeight
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const toCanvasPoint = (point, viewBox, metrics) => ({
|
||||||
|
x: metrics.offsetX + (point.x - viewBox.x) * metrics.scale,
|
||||||
|
y: metrics.offsetY + (point.y - viewBox.y) * metrics.scale
|
||||||
|
});
|
||||||
|
|
||||||
|
const getRoomShape = (room) => {
|
||||||
|
if (Array.isArray(room.polygon) && room.polygon.length >= 3) {
|
||||||
|
return room.polygon;
|
||||||
|
}
|
||||||
|
const bounds = room.bounds || {};
|
||||||
|
if (!Number.isFinite(bounds.x) || !Number.isFinite(bounds.y) || !Number.isFinite(bounds.width) || !Number.isFinite(bounds.height)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{ x: bounds.x, y: bounds.y },
|
||||||
|
{ x: bounds.x + bounds.width, y: bounds.y },
|
||||||
|
{ x: bounds.x + bounds.width, y: bounds.y + bounds.height },
|
||||||
|
{ x: bounds.x, y: bounds.y + bounds.height }
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const computeBounds = (points) => {
|
||||||
|
if (!points.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let minX = points[0].x;
|
||||||
|
let minY = points[0].y;
|
||||||
|
points.forEach((point) => {
|
||||||
|
if (point.x < minX) {
|
||||||
|
minX = point.x;
|
||||||
|
}
|
||||||
|
if (point.y < minY) {
|
||||||
|
minY = point.y;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { minX, minY };
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawRooms = (rooms, viewBox, metrics) => {
|
||||||
|
rooms.forEach((room, index) => {
|
||||||
|
const shape = getRoomShape(room);
|
||||||
|
if (!shape.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = new Path2D();
|
||||||
|
shape.forEach((point, pointIndex) => {
|
||||||
|
const canvasPoint = toCanvasPoint(point, viewBox, metrics);
|
||||||
|
if (pointIndex === 0) {
|
||||||
|
path.moveTo(canvasPoint.x, canvasPoint.y);
|
||||||
|
} else {
|
||||||
|
path.lineTo(canvasPoint.x, canvasPoint.y);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
path.closePath();
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.globalAlpha = 0.78;
|
||||||
|
ctx.fillStyle = palette[index % palette.length];
|
||||||
|
ctx.fill(path);
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.85)';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.stroke(path);
|
||||||
|
|
||||||
|
const bounds = computeBounds(shape);
|
||||||
|
const labelPos = bounds ? toCanvasPoint({ x: bounds.minX, y: bounds.minY }, viewBox, metrics) : { x: metrics.offsetX, y: metrics.offsetY };
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
ctx.textBaseline = 'top';
|
||||||
|
ctx.font = '600 16px "Segoe UI", sans-serif';
|
||||||
|
const numberLabel = room.number ? `${room.number}` : '-';
|
||||||
|
ctx.fillText(numberLabel, labelPos.x + 6, labelPos.y + 6);
|
||||||
|
ctx.font = '14px "Segoe UI", sans-serif';
|
||||||
|
const nameLabel = room.name || '—';
|
||||||
|
ctx.fillText(nameLabel, labelPos.x + 6, labelPos.y + 26);
|
||||||
|
ctx.restore();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadImage = (src) => new Promise((resolve, reject) => {
|
||||||
|
const image = new Image();
|
||||||
|
image.onload = () => resolve(image);
|
||||||
|
image.onerror = reject;
|
||||||
|
image.src = src;
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadFloorSvg = async (url) => {
|
||||||
|
if (!url) {
|
||||||
|
throw new Error('Keine SVG-URL');
|
||||||
|
}
|
||||||
|
const response = await fetch(url, { credentials: 'same-origin' });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('SVG konnte nicht geladen werden');
|
||||||
|
}
|
||||||
|
const raw = await response.text();
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(raw, 'image/svg+xml');
|
||||||
|
const root = doc.documentElement;
|
||||||
|
if (!root || root.nodeName.toLowerCase() === 'parsererror') {
|
||||||
|
throw new Error('SVG ungültig');
|
||||||
|
}
|
||||||
|
const viewBox = safeViewBox(readViewBox(root));
|
||||||
|
const encodedSvg = encodeURIComponent(raw);
|
||||||
|
const dataUri = `data:image/svg+xml;charset=utf-8,${encodedSvg}`;
|
||||||
|
return { dataUri, viewBox };
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderPreview = async (rooms, floorSvgUrl) => {
|
||||||
|
if (!canvas || !ctx) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
latestRenderId += 1;
|
||||||
|
const renderId = latestRenderId;
|
||||||
|
|
||||||
|
const canvasWidth = 980;
|
||||||
|
const canvasHeight = 640;
|
||||||
|
canvas.width = canvasWidth;
|
||||||
|
canvas.height = canvasHeight;
|
||||||
|
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
||||||
|
ctx.fillStyle = '#05070d';
|
||||||
|
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
||||||
|
|
||||||
|
if (!rooms.length) {
|
||||||
|
canvas.hidden = true;
|
||||||
|
if (messageElement) {
|
||||||
|
messageElement.textContent = 'Keine Raeume vorhanden.';
|
||||||
|
messageElement.hidden = false;
|
||||||
|
}
|
||||||
|
downloadButton?.setAttribute('disabled', 'disabled');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.hidden = false;
|
||||||
|
if (messageElement) {
|
||||||
|
messageElement.hidden = true;
|
||||||
|
messageElement.textContent = '';
|
||||||
|
}
|
||||||
|
downloadButton?.setAttribute('disabled', 'disabled');
|
||||||
|
|
||||||
|
let viewBox = { ...DEFAULT_VIEWBOX };
|
||||||
|
let metrics = computeMetrics(viewBox, canvasWidth, canvasHeight);
|
||||||
|
|
||||||
|
if (floorSvgUrl) {
|
||||||
|
try {
|
||||||
|
const { dataUri, viewBox: svgViewBox } = await loadFloorSvg(floorSvgUrl);
|
||||||
|
if (renderId !== latestRenderId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
viewBox = svgViewBox;
|
||||||
|
metrics = computeMetrics(viewBox, canvasWidth, canvasHeight);
|
||||||
|
const floorImage = await loadImage(dataUri);
|
||||||
|
if (renderId !== latestRenderId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ctx.drawImage(floorImage, metrics.offsetX, metrics.offsetY, metrics.drawWidth, metrics.drawHeight);
|
||||||
|
} catch (error) {
|
||||||
|
if (renderId !== latestRenderId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (messageElement) {
|
||||||
|
messageElement.textContent = 'Stockwerkskarte konnte nicht geladen werden.';
|
||||||
|
messageElement.hidden = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawRooms(rooms, viewBox, metrics);
|
||||||
|
downloadButton?.removeAttribute('disabled');
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openPreview = async (floorName, rooms, floorSvgUrl) => {
|
||||||
|
currentFloorName = floorName || 'Stockwerk';
|
||||||
|
|
||||||
|
if (titleElement) {
|
||||||
|
titleElement.textContent = currentFloorName;
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay.classList.add('is-visible');
|
||||||
|
overlay.setAttribute('aria-hidden', 'false');
|
||||||
|
try {
|
||||||
|
await renderPreview(rooms, floorSvgUrl);
|
||||||
|
} catch (error) {
|
||||||
|
if (messageElement) {
|
||||||
|
messageElement.textContent = 'Vorschau konnte nicht geladen werden.';
|
||||||
|
messageElement.hidden = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closePreview = () => {
|
||||||
|
overlay.classList.remove('is-visible');
|
||||||
|
overlay.setAttribute('aria-hidden', 'true');
|
||||||
|
};
|
||||||
|
|
||||||
|
document.querySelectorAll('.js-preview-floor').forEach((button) => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
const rooms = parseRoomPayload(button.dataset.floorRooms);
|
||||||
|
const floorName = button.dataset.floorName || button.dataset.floorId || 'Stockwerk';
|
||||||
|
const floorSvgUrl = button.dataset.floorSvg || '';
|
||||||
|
void openPreview(floorName, rooms, floorSvgUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
overlay.addEventListener('click', (event) => {
|
||||||
|
if (event.target === overlay) {
|
||||||
|
closePreview();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
closeButton?.addEventListener('click', closePreview);
|
||||||
|
document.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
closePreview();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
downloadButton?.addEventListener('click', () => {
|
||||||
|
if (!canvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackName = currentFloorName.toLowerCase().replace(/[^a-z0-9]+/gi, '_').replace(/^_+|_+$/g, '') || 'stockwerk';
|
||||||
|
const anchor = document.createElement('a');
|
||||||
|
anchor.href = canvas.toDataURL('image/png');
|
||||||
|
anchor.download = `stockwerk-${fallbackName}.png`;
|
||||||
|
anchor.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
bindDeleteButtons();
|
||||||
|
initFloorPreview();
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ $buildings = $sql->get(
|
|||||||
);
|
);
|
||||||
|
|
||||||
$floors = $sql->get(
|
$floors = $sql->get(
|
||||||
"SELECT f.id, f.building_id, f.name, f.level,
|
"SELECT f.id, f.building_id, f.name, f.level, f.svg_path,
|
||||||
COUNT(r.id) AS room_count
|
COUNT(r.id) AS room_count
|
||||||
FROM floors f
|
FROM floors f
|
||||||
LEFT JOIN rooms r ON r.floor_id = f.id
|
LEFT JOIN rooms r ON r.floor_id = f.id
|
||||||
@@ -47,8 +47,15 @@ $floors = $sql->get(
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
foreach ($floors as &$floor) {
|
||||||
|
$rawPath = trim((string)($floor['svg_path'] ?? ''));
|
||||||
|
$floor['svg_url'] = $rawPath !== '' ? '/' . ltrim($rawPath, '/\\') : '';
|
||||||
|
}
|
||||||
|
unset($floor);
|
||||||
|
|
||||||
$rooms = $sql->get(
|
$rooms = $sql->get(
|
||||||
"SELECT r.id, r.floor_id, r.name, r.number, r.comment,
|
"SELECT r.id, r.floor_id, r.name, r.number, r.comment,
|
||||||
|
r.x, r.y, r.width, r.height, r.polygon_points,
|
||||||
COUNT(no.id) AS outlet_count
|
COUNT(no.id) AS outlet_count
|
||||||
FROM rooms r
|
FROM rooms r
|
||||||
LEFT JOIN network_outlets no ON no.room_id = r.id
|
LEFT JOIN network_outlets no ON no.room_id = r.id
|
||||||
@@ -193,7 +200,55 @@ foreach ($rooms as $room) {
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<span class="hierarchy-meta"> | <?php echo (int)$floor['room_count']; ?> Raeume</span>
|
<span class="hierarchy-meta"> | <?php echo (int)$floor['room_count']; ?> Raeume</span>
|
||||||
</td>
|
</td>
|
||||||
|
<?php
|
||||||
|
$roomPreviewData = [];
|
||||||
|
foreach ($floorRooms as $room) {
|
||||||
|
$rawPolygon = trim((string)($room['polygon_points'] ?? ''));
|
||||||
|
$polygonPoints = [];
|
||||||
|
if ($rawPolygon !== '') {
|
||||||
|
$decodedPoints = json_decode($rawPolygon, true);
|
||||||
|
if (is_array($decodedPoints)) {
|
||||||
|
foreach ($decodedPoints as $point) {
|
||||||
|
$x = $point['x'] ?? null;
|
||||||
|
$y = $point['y'] ?? null;
|
||||||
|
if (is_numeric($x) && is_numeric($y)) {
|
||||||
|
$polygonPoints[] = [
|
||||||
|
'x' => (float)$x,
|
||||||
|
'y' => (float)$y,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$roomPreviewData[] = [
|
||||||
|
'id' => (int)$room['id'],
|
||||||
|
'number' => (string)($room['number'] ?? ''),
|
||||||
|
'name' => (string)($room['name'] ?? ''),
|
||||||
|
'polygon' => $polygonPoints,
|
||||||
|
'bounds' => [
|
||||||
|
'x' => isset($room['x']) ? (float)$room['x'] : null,
|
||||||
|
'y' => isset($room['y']) ? (float)$room['y'] : null,
|
||||||
|
'width' => isset($room['width']) ? (float)$room['width'] : null,
|
||||||
|
'height' => isset($room['height']) ? (float)$room['height'] : null,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$roomPreviewPayload = json_encode($roomPreviewData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
$roomPreviewPayload = ($roomPreviewPayload === false) ? '[]' : $roomPreviewPayload;
|
||||||
|
$roomPreviewPayload = htmlspecialchars($roomPreviewPayload, ENT_QUOTES, 'UTF-8');
|
||||||
|
$hasRoomsForPreview = !empty($roomPreviewData);
|
||||||
|
?>
|
||||||
<td class="actions hierarchy-actions">
|
<td class="actions hierarchy-actions">
|
||||||
|
<button type="button"
|
||||||
|
class="button button-small button-secondary js-preview-floor"
|
||||||
|
data-floor-id="<?php echo (int)$floor['id']; ?>"
|
||||||
|
data-floor-name="<?php echo htmlspecialchars((string)$floor['name']); ?>"
|
||||||
|
data-floor-rooms="<?php echo $roomPreviewPayload; ?>"
|
||||||
|
data-floor-svg="<?php echo htmlspecialchars((string)($floor['svg_url'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>"
|
||||||
|
<?php if (!$hasRoomsForPreview): ?>disabled title="Keine Raeume vorhanden"<?php endif; ?>>
|
||||||
|
Stockwerk als PNG
|
||||||
|
</button>
|
||||||
<a href="?module=floors&action=edit&id=<?php echo (int)$floor['id']; ?>" class="button button-small">Bearbeiten</a>
|
<a href="?module=floors&action=edit&id=<?php echo (int)$floor['id']; ?>" class="button button-small">Bearbeiten</a>
|
||||||
<button type="button" class="button button-small button-danger js-delete-floor" data-floor-id="<?php echo (int)$floor['id']; ?>">Loeschen</button>
|
<button type="button" class="button button-small button-danger js-delete-floor" data-floor-id="<?php echo (int)$floor['id']; ?>">Loeschen</button>
|
||||||
<a href="?module=rooms&action=edit&floor_id=<?php echo (int)$floor['id']; ?>" class="button button-small">+ Raum</a>
|
<a href="?module=rooms&action=edit&floor_id=<?php echo (int)$floor['id']; ?>" class="button button-small">+ Raum</a>
|
||||||
@@ -251,3 +306,22 @@ foreach ($rooms as $room) {
|
|||||||
<p>Keine Standorte gefunden.</p>
|
<p>Keine Standorte gefunden.</p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<div class="floor-preview-overlay" data-floor-preview-overlay aria-hidden="true">
|
||||||
|
<div class="floor-preview-card">
|
||||||
|
<div class="floor-preview-header">
|
||||||
|
<div>
|
||||||
|
<p class="floor-preview-label">Stockwerk-Vorschau</p>
|
||||||
|
<h3 data-floor-preview-title>Stockwerk</h3>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="button button-small floor-preview-close" data-floor-preview-close>Schließen</button>
|
||||||
|
</div>
|
||||||
|
<div class="floor-preview-body">
|
||||||
|
<canvas data-floor-preview-canvas width="900" height="600" aria-label="Stockwerk mit Raeumen"></canvas>
|
||||||
|
<p class="floor-preview-empty" data-floor-preview-message hidden>Keine Raeume vorhanden.</p>
|
||||||
|
</div>
|
||||||
|
<div class="floor-preview-footer">
|
||||||
|
<button type="button" class="button js-floor-preview-download" disabled>Als PNG speichern</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user