Files
netwatch/app/assets/js/locations-list.js
2026-03-31 10:22:22 +02:00

426 lines
17 KiB
JavaScript

(() => {
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();
});
})();