(() => { function postDelete(url) { return fetch(url, { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' } }).then((response) => response.json()); } function handleLocationDelete(id) { if (!confirm('Diesen Standort wirklich loeschen?')) { return; } postDelete('?module=locations&action=delete&id=' + encodeURIComponent(id)) .then((data) => { if (data && data.success) { window.location.reload(); return; } alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen'); }) .catch(() => alert('Loeschen fehlgeschlagen')); } function handleBuildingDelete(id) { if (!confirm('Dieses Gebaeude wirklich loeschen? Alle Stockwerke werden geloescht.')) { return; } postDelete('?module=buildings&action=delete&id=' + encodeURIComponent(id)) .then((data) => { if (data && (data.success || data.status === 'ok')) { window.location.reload(); return; } alert((data && (data.message || data.error)) ? (data.message || data.error) : 'Loeschen fehlgeschlagen'); }) .catch(() => alert('Loeschen fehlgeschlagen')); } function handleFloorDelete(id) { if (!confirm('Dieses Stockwerk wirklich loeschen? Alle Raeume und Racks werden geloescht.')) { return; } postDelete('?module=floors&action=delete&id=' + encodeURIComponent(id)) .then((data) => { if (data && data.success) { window.location.reload(); return; } alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen'); }) .catch(() => alert('Loeschen fehlgeschlagen')); } function handleRoomDelete(id) { if (!confirm('Diesen Raum wirklich loeschen? Zugeordnete Dosen werden mitgeloescht.')) { return; } postDelete('?module=rooms&action=delete&id=' + encodeURIComponent(id)) .then((data) => { if (data && data.success) { window.location.reload(); return; } alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen'); }) .catch(() => alert('Loeschen fehlgeschlagen')); } function bindDeleteButtons() { document.querySelectorAll('.js-delete-location').forEach((button) => { button.addEventListener('click', () => { handleLocationDelete(Number(button.dataset.locationId || 0)); }); }); document.querySelectorAll('.js-delete-building').forEach((button) => { button.addEventListener('click', () => { handleBuildingDelete(Number(button.dataset.buildingId || 0)); }); }); document.querySelectorAll('.js-delete-floor').forEach((button) => { button.addEventListener('click', () => { handleFloorDelete(Number(button.dataset.floorId || 0)); }); }); document.querySelectorAll('.js-delete-room').forEach((button) => { button.addEventListener('click', () => { handleRoomDelete(Number(button.dataset.roomId || 0)); }); }); } 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(); }); })();