diff --git a/app/assets/js/room-polygon-editor.js b/app/assets/js/room-polygon-editor.js
new file mode 100644
index 0000000..123cb71
--- /dev/null
+++ b/app/assets/js/room-polygon-editor.js
@@ -0,0 +1,377 @@
+(() => {
+ const SVG_NS = 'http://www.w3.org/2000/svg';
+ const DEFAULT_VIEWBOX = { x: 0, y: 0, width: 2000, height: 1000 };
+ const SNAP_TOLERANCE = 16;
+
+ function initRoomPolygonEditor() {
+ const floorSelect = document.getElementById('room-floor-id');
+ const canvas = document.getElementById('room-polygon-canvas');
+ const polygonInput = document.getElementById('room-polygon-points');
+ const snapWalls = document.getElementById('room-snap-walls');
+ const undoButton = document.getElementById('room-undo-point');
+ const clearButton = document.getElementById('room-clear-polygon');
+ const mapHint = document.getElementById('room-map-hint');
+
+ if (!floorSelect || !canvas || !polygonInput) {
+ return;
+ }
+
+ const state = {
+ viewBox: { ...DEFAULT_VIEWBOX },
+ wallPoints: [],
+ floorLayerNodes: [],
+ points: parsePoints(polygonInput.value),
+ draggingIndex: null
+ };
+
+ const updateInput = () => {
+ if (state.points.length >= 3) {
+ polygonInput.value = JSON.stringify(state.points.map((point) => ({
+ x: Math.round(point.x),
+ y: Math.round(point.y)
+ })));
+ } else {
+ polygonInput.value = '';
+ }
+ };
+
+ const render = () => {
+ canvas.innerHTML = '';
+ canvas.setAttribute(
+ 'viewBox',
+ `${state.viewBox.x} ${state.viewBox.y} ${state.viewBox.width} ${state.viewBox.height}`
+ );
+
+ const bg = createSvgElement('rect');
+ bg.setAttribute('x', String(state.viewBox.x));
+ bg.setAttribute('y', String(state.viewBox.y));
+ bg.setAttribute('width', String(state.viewBox.width));
+ bg.setAttribute('height', String(state.viewBox.height));
+ bg.setAttribute('fill', '#f7f7f7');
+ bg.setAttribute('stroke', '#e1e1e1');
+ canvas.appendChild(bg);
+
+ if (state.floorLayerNodes.length > 0) {
+ const floorLayer = createSvgElement('g');
+ floorLayer.setAttribute('opacity', '0.55');
+ floorLayer.setAttribute('pointer-events', 'none');
+ state.floorLayerNodes.forEach((node) => {
+ floorLayer.appendChild(node.cloneNode(true));
+ });
+ canvas.appendChild(floorLayer);
+ }
+
+ if (state.points.length >= 2) {
+ const polyline = createSvgElement('polyline');
+ polyline.setAttribute('fill', state.points.length >= 3 ? 'rgba(13, 110, 253, 0.16)' : 'none');
+ polyline.setAttribute('stroke', '#0d6efd');
+ polyline.setAttribute('stroke-width', '3');
+ polyline.setAttribute('points', buildPointString(state.points));
+ if (state.points.length >= 3) {
+ polyline.setAttribute('stroke-linejoin', 'round');
+ polyline.setAttribute('stroke-linecap', 'round');
+ polyline.setAttribute('points', `${buildPointString(state.points)} ${state.points[0].x},${state.points[0].y}`);
+ }
+ canvas.appendChild(polyline);
+ }
+
+ state.points.forEach((point, index) => {
+ const vertex = createSvgElement('circle');
+ vertex.setAttribute('cx', String(point.x));
+ vertex.setAttribute('cy', String(point.y));
+ vertex.setAttribute('r', '9');
+ vertex.setAttribute('fill', '#ffffff');
+ vertex.setAttribute('stroke', '#dc3545');
+ vertex.setAttribute('stroke-width', '3');
+ vertex.setAttribute('data-vertex-index', String(index));
+ canvas.appendChild(vertex);
+ });
+
+ updateInput();
+ };
+
+ const snapPoint = (point) => {
+ if (!snapWalls || !snapWalls.checked || state.wallPoints.length === 0) {
+ return point;
+ }
+ let nearest = null;
+ let nearestDist = Number.POSITIVE_INFINITY;
+ state.wallPoints.forEach((wallPoint) => {
+ const dx = wallPoint.x - point.x;
+ const dy = wallPoint.y - point.y;
+ const dist = Math.sqrt((dx * dx) + (dy * dy));
+ if (dist < nearestDist) {
+ nearestDist = dist;
+ nearest = wallPoint;
+ }
+ });
+ if (nearest && nearestDist <= SNAP_TOLERANCE) {
+ return { x: nearest.x, y: nearest.y };
+ }
+ return point;
+ };
+
+ const addPoint = (event) => {
+ const point = toSvgPoint(canvas, event);
+ if (!point) {
+ return;
+ }
+ state.points.push(snapPoint(point));
+ render();
+ };
+
+ const onPointerDown = (event) => {
+ const target = event.target;
+ if (!(target instanceof SVGElement)) {
+ return;
+ }
+ const vertex = target.closest('[data-vertex-index]');
+ if (vertex) {
+ const vertexIndex = Number(vertex.getAttribute('data-vertex-index'));
+ if (Number.isInteger(vertexIndex)) {
+ state.draggingIndex = vertexIndex;
+ vertex.setPointerCapture(event.pointerId);
+ }
+ return;
+ }
+ addPoint(event);
+ };
+
+ const onPointerMove = (event) => {
+ if (state.draggingIndex === null) {
+ return;
+ }
+ const point = toSvgPoint(canvas, event);
+ if (!point) {
+ return;
+ }
+ state.points[state.draggingIndex] = snapPoint(point);
+ render();
+ };
+
+ const stopDragging = () => {
+ state.draggingIndex = null;
+ };
+
+ const parseFloorSvg = (rawSvg) => {
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(rawSvg, 'image/svg+xml');
+ const root = doc.documentElement;
+ if (!root || root.nodeName.toLowerCase() === 'parsererror') {
+ return null;
+ }
+ return root;
+ };
+
+ const readViewBox = (svgRoot) => {
+ const vb = (svgRoot.getAttribute('viewBox') || '').trim();
+ if (vb) {
+ const parts = vb.split(/\s+/).map((value) => Number(value));
+ if (parts.length === 4 && parts.every((value) => Number.isFinite(value))) {
+ 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 collectSnapPoints = (svgRoot) => {
+ const points = [];
+
+ const addPointValue = (x, y) => {
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
+ return;
+ }
+ points.push({ x: Math.round(x), y: Math.round(y) });
+ };
+
+ svgRoot.querySelectorAll('line').forEach((line) => {
+ addPointValue(Number(line.getAttribute('x1')), Number(line.getAttribute('y1')));
+ addPointValue(Number(line.getAttribute('x2')), Number(line.getAttribute('y2')));
+ });
+
+ svgRoot.querySelectorAll('polyline, polygon').forEach((shape) => {
+ parsePointString(shape.getAttribute('points') || '').forEach((point) => addPointValue(point.x, point.y));
+ });
+
+ svgRoot.querySelectorAll('rect').forEach((rect) => {
+ const x = Number(rect.getAttribute('x'));
+ const y = Number(rect.getAttribute('y'));
+ const width = Number(rect.getAttribute('width'));
+ const height = Number(rect.getAttribute('height'));
+ addPointValue(x, y);
+ addPointValue(x + width, y);
+ addPointValue(x + width, y + height);
+ addPointValue(x, y + height);
+ });
+
+ svgRoot.querySelectorAll('path').forEach((path) => {
+ const d = path.getAttribute('d') || '';
+ const numbers = (d.match(/-?\d+(\.\d+)?/g) || []).map((value) => Number(value));
+ for (let i = 0; i < numbers.length - 1; i += 2) {
+ addPointValue(numbers[i], numbers[i + 1]);
+ }
+ });
+
+ return dedupePoints(points);
+ };
+
+ const loadFloor = async () => {
+ const selected = floorSelect.selectedOptions[0];
+ const svgUrl = selected ? (selected.dataset.svgUrl || '') : '';
+
+ state.viewBox = { ...DEFAULT_VIEWBOX };
+ state.wallPoints = [];
+ state.floorLayerNodes = [];
+
+ if (!svgUrl) {
+ if (mapHint) {
+ mapHint.textContent = 'Fuer dieses Stockwerk ist keine Karte hinterlegt. Polygon kann trotzdem frei gezeichnet werden.';
+ }
+ render();
+ return;
+ }
+
+ try {
+ const response = await fetch(svgUrl, { credentials: 'same-origin' });
+ if (!response.ok) {
+ throw new Error('SVG not available');
+ }
+ const raw = await response.text();
+ const root = parseFloorSvg(raw);
+ if (!root) {
+ throw new Error('Invalid SVG');
+ }
+
+ state.viewBox = readViewBox(root);
+ state.wallPoints = collectSnapPoints(root);
+ state.floorLayerNodes = Array.from(root.childNodes)
+ .filter((node) => node.nodeType === Node.ELEMENT_NODE)
+ .map((node) => node.cloneNode(true));
+
+ if (mapHint) {
+ mapHint.textContent = state.wallPoints.length > 0
+ ? 'Klick setzt Punkte. Punkte sind per Drag verschiebbar. Snap nutzt Wandpunkte aus der Stockwerkskarte.'
+ : 'Klick setzt Punkte. Punkte sind per Drag verschiebbar.';
+ }
+ } catch (error) {
+ if (mapHint) {
+ mapHint.textContent = 'Stockwerkskarte konnte nicht geladen werden. Polygon kann frei gezeichnet werden.';
+ }
+ }
+
+ render();
+ };
+
+ canvas.addEventListener('pointerdown', onPointerDown);
+ canvas.addEventListener('pointermove', onPointerMove);
+ canvas.addEventListener('pointerup', stopDragging);
+ canvas.addEventListener('pointercancel', stopDragging);
+ canvas.addEventListener('pointerleave', stopDragging);
+
+ floorSelect.addEventListener('change', () => {
+ loadFloor();
+ });
+
+ if (undoButton) {
+ undoButton.addEventListener('click', () => {
+ if (state.points.length === 0) {
+ return;
+ }
+ state.points.pop();
+ render();
+ });
+ }
+
+ if (clearButton) {
+ clearButton.addEventListener('click', () => {
+ state.points = [];
+ render();
+ });
+ }
+
+ render();
+ loadFloor();
+ }
+
+ function parsePoints(raw) {
+ if (!raw) {
+ return [];
+ }
+ try {
+ const parsed = JSON.parse(raw);
+ if (!Array.isArray(parsed)) {
+ return [];
+ }
+ return parsed
+ .map((point) => ({
+ x: Number(point && point.x),
+ y: Number(point && point.y)
+ }))
+ .filter((point) => Number.isFinite(point.x) && Number.isFinite(point.y));
+ } catch (error) {
+ return [];
+ }
+ }
+
+ function parsePointString(pointsAttr) {
+ return pointsAttr
+ .trim()
+ .split(/\s+/)
+ .map((pair) => {
+ const values = pair.split(',');
+ if (values.length !== 2) {
+ return null;
+ }
+ const x = Number(values[0]);
+ const y = Number(values[1]);
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
+ return null;
+ }
+ return { x, y };
+ })
+ .filter(Boolean);
+ }
+
+ function dedupePoints(points) {
+ const map = new Map();
+ points.forEach((point) => {
+ const key = `${Math.round(point.x)}:${Math.round(point.y)}`;
+ if (!map.has(key)) {
+ map.set(key, { x: Math.round(point.x), y: Math.round(point.y) });
+ }
+ });
+ return Array.from(map.values());
+ }
+
+ function buildPointString(points) {
+ return points.map((point) => `${Math.round(point.x)},${Math.round(point.y)}`).join(' ');
+ }
+
+ function createSvgElement(name) {
+ return document.createElementNS(SVG_NS, name);
+ }
+
+ 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 };
+ }
+
+ document.addEventListener('DOMContentLoaded', initRoomPolygonEditor);
+})();
diff --git a/app/index.php b/app/index.php
index a76aab9..c7fc06d 100644
--- a/app/index.php
+++ b/app/index.php
@@ -27,7 +27,7 @@ $module = $_GET['module'] ?? 'dashboard';
$action = $_GET['action'] ?? 'list';
// Whitelist der Module
-$validModules = ['dashboard', 'locations', 'buildings', 'device_types', 'devices', 'racks', 'floors', 'floor_infrastructure', 'connections', 'port_types'];
+$validModules = ['dashboard', 'locations', 'buildings', 'rooms', 'device_types', 'devices', 'racks', 'floors', 'floor_infrastructure', 'connections', 'port_types'];
// Whitelist der Aktionen
$validActions = ['list', 'edit', 'save', 'ports', 'delete'];
diff --git a/app/modules/locations/list.php b/app/modules/locations/list.php
index be0aea0..0283425 100644
--- a/app/modules/locations/list.php
+++ b/app/modules/locations/list.php
@@ -1,25 +1,19 @@
-get(
"SELECT b.id, b.location_id, b.name, b.comment
FROM buildings b
ORDER BY b.location_id, b.name",
- "",
+ '',
+ []
+);
+
+$floors = $sql->get(
+ "SELECT f.id, f.building_id, f.name, f.level,
+ COUNT(r.id) AS room_count
+ FROM floors f
+ LEFT JOIN rooms r ON r.floor_id = f.id
+ GROUP BY f.id
+ ORDER BY f.building_id, f.level, f.name",
+ '',
+ []
+);
+
+$rooms = $sql->get(
+ "SELECT r.id, r.floor_id, r.name, r.number, r.comment,
+ COUNT(no.id) AS outlet_count
+ FROM rooms r
+ LEFT JOIN network_outlets no ON no.room_id = r.id
+ GROUP BY r.id
+ ORDER BY r.floor_id, r.name",
+ '',
[]
);
$buildingsByLocation = [];
foreach ($buildings as $building) {
- $buildingsByLocation[$building['location_id']][] = $building;
+ $buildingsByLocation[(int)$building['location_id']][] = $building;
}
-$floors = $sql->get(
- "SELECT f.id, f.building_id, f.name, f.level
- FROM floors f
- ORDER BY f.building_id, f.level",
- "",
- []
-);
-
$floorsByBuilding = [];
foreach ($floors as $floor) {
- $floorsByBuilding[$floor['building_id']][] = $floor;
+ $floorsByBuilding[(int)$floor['building_id']][] = $floor;
}
+$roomsByFloor = [];
+foreach ($rooms as $room) {
+ $roomsByFloor[(int)$room['floor_id']][] = $room;
+}
?>
- Gebäude & Stockwerke nach Standorten
+ Gebaeude, Stockwerke und Raeume
+
-
-
-
- | Standort |
- Gebäude |
- Stockwerk |
- Details |
- Aktionen |
+
+
+
+ | Standort |
+ Gebaeude |
+ Stockwerk / Raum |
+ Details |
+ Aktionen |
+
+
+
+
+
+
+ |
+
+ ( Gebaeude)
+ |
+ |
+
+ + Gebaeude
+ |
-
-
-
-
-
- |
-
- ( Gebäude)
- |
- |
-
- + Gebäude
- |
-
-
-
-
-
- | |
- |
-
-
-
-
- Kein Kommentar
-
- |
-
- Bearbeiten
- Löschen
- + Stockwerk
- |
-
-
-
-
- | |
- |
- |
-
-
- Ebene
-
- Keine Ebene
-
- |
-
- Bearbeiten
- Löschen
- |
-
-
-
-
-
-
+
+
+
+
+
| |
- |
- Keine Gebäude |
- Für diesen Standort sind noch keine Gebäude vorhanden. |
- |
+ |
+
+
+
+
+ Kein Kommentar
+
+ |
+
+ Bearbeiten
+ Loeschen
+ + Stockwerk
+ |
-
-
-
-
+
+
+
+
+
+ | |
+ |
+ |
+
+
+ Ebene
+
+ Keine Ebene
+
+ | Raeume
+ |
+
+ Bearbeiten
+ Loeschen
+ + Raum
+ |
+
+
+
+
+
+ | |
+ |
+
+
+
+ ()
+
+ |
+
+ Dosen
+
+ |
+
+ |
+
+ Bearbeiten
+ Loeschen
+ |
+
+
+
+
+ | |
+ |
+ Keine Raeume |
+ Fuer dieses Stockwerk sind noch keine Raeume angelegt. |
+ |
+
+
+
+
+
+
+
+ | |
+ |
+ Keine Gebaeude |
+ Fuer diesen Standort sind noch keine Gebaeude vorhanden. |
+ |
+
+
+
+
+
Keine Standorte gefunden.
-
-
-
-
diff --git a/app/modules/rooms/delete.php b/app/modules/rooms/delete.php
new file mode 100644
index 0000000..86e7041
--- /dev/null
+++ b/app/modules/rooms/delete.php
@@ -0,0 +1,64 @@
+ false,
+ 'message' => 'Methode nicht erlaubt'
+ ]);
+ exit;
+}
+
+$roomId = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
+
+if ($roomId <= 0) {
+ http_response_code(400);
+ echo json_encode([
+ 'success' => false,
+ 'message' => 'Ungueltige Raum-ID'
+ ]);
+ exit;
+}
+
+$room = $sql->single(
+ "SELECT id, name FROM rooms WHERE id = ?",
+ "i",
+ [$roomId]
+);
+
+if (!$room) {
+ http_response_code(404);
+ echo json_encode([
+ 'success' => false,
+ 'message' => 'Raum nicht gefunden'
+ ]);
+ exit;
+}
+
+$deleted = $sql->set(
+ "DELETE FROM rooms WHERE id = ?",
+ "i",
+ [$roomId]
+);
+
+if ($deleted <= 0) {
+ http_response_code(500);
+ echo json_encode([
+ 'success' => false,
+ 'message' => 'Raum konnte nicht geloescht werden'
+ ]);
+ exit;
+}
+
+echo json_encode([
+ 'success' => true,
+ 'message' => 'Raum geloescht'
+]);
+exit;
diff --git a/app/modules/rooms/edit.php b/app/modules/rooms/edit.php
new file mode 100644
index 0000000..1004cfb
--- /dev/null
+++ b/app/modules/rooms/edit.php
@@ -0,0 +1,234 @@
+ 0) {
+ $room = $sql->single(
+ "SELECT * FROM rooms WHERE id = ?",
+ "i",
+ [$roomId]
+ );
+}
+
+$isEdit = !empty($room);
+$prefillFloorId = (int)($_GET['floor_id'] ?? 0);
+$selectedFloorId = (int)($room['floor_id'] ?? $prefillFloorId);
+
+$floors = $sql->get(
+ "SELECT f.id, f.name, f.level, f.svg_path, b.name AS building_name, l.name AS location_name
+ FROM floors f
+ LEFT JOIN buildings b ON b.id = f.building_id
+ LEFT JOIN locations l ON l.id = b.location_id
+ ORDER BY l.name, b.name, f.level, f.name",
+ "",
+ []
+);
+
+foreach ($floors as &$floor) {
+ $svgPath = trim((string)($floor['svg_path'] ?? ''));
+ $floor['svg_url'] = $svgPath !== '' ? '/' . ltrim($svgPath, "/\\") : '';
+}
+unset($floor);
+
+$existingPolygon = trim((string)($room['polygon_points'] ?? ''));
+$pageTitle = $isEdit ? "Raum bearbeiten: " . htmlspecialchars((string)$room['name']) : "Neuer Raum";
+?>
+
+
+
+
+
+
diff --git a/app/modules/rooms/list.php b/app/modules/rooms/list.php
new file mode 100644
index 0000000..f3435e3
--- /dev/null
+++ b/app/modules/rooms/list.php
@@ -0,0 +1,12 @@
+
+
diff --git a/app/modules/rooms/save.php b/app/modules/rooms/save.php
new file mode 100644
index 0000000..c4cf750
--- /dev/null
+++ b/app/modules/rooms/save.php
@@ -0,0 +1,115 @@
+ 0 ? "?module=rooms&action=edit&id=$roomId" : "?module=rooms&action=edit&floor_id=$floorId";
+ header("Location: $redirect");
+ exit;
+}
+
+$polygonPoints = [];
+$polygonJson = null;
+$x = null;
+$y = null;
+$width = null;
+$height = null;
+
+if ($rawPolygon !== '') {
+ $decoded = json_decode($rawPolygon, true);
+ if (is_array($decoded)) {
+ foreach ($decoded as $point) {
+ if (!is_array($point)) {
+ continue;
+ }
+ $px = isset($point['x']) ? (float)$point['x'] : null;
+ $py = isset($point['y']) ? (float)$point['y'] : null;
+ if (!is_finite($px) || !is_finite($py)) {
+ continue;
+ }
+ $polygonPoints[] = [
+ 'x' => (int)round($px),
+ 'y' => (int)round($py),
+ ];
+ }
+ }
+}
+
+if (count($polygonPoints) >= 3) {
+ $xs = array_column($polygonPoints, 'x');
+ $ys = array_column($polygonPoints, 'y');
+ $minX = min($xs);
+ $maxX = max($xs);
+ $minY = min($ys);
+ $maxY = max($ys);
+ $x = (int)$minX;
+ $y = (int)$minY;
+ $width = (int)max(1, $maxX - $minX);
+ $height = (int)max(1, $maxY - $minY);
+ $polygonJson = json_encode($polygonPoints, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+}
+
+if (roomsHasPolygonColumn($sql)) {
+ if ($roomId > 0) {
+ $sql->set(
+ "UPDATE rooms
+ SET floor_id = ?, name = ?, number = ?, x = ?, y = ?, width = ?, height = ?, polygon_points = ?, comment = ?
+ WHERE id = ?",
+ "issiiiissi",
+ [$floorId, $name, $number, $x, $y, $width, $height, $polygonJson, $comment, $roomId]
+ );
+ } else {
+ $sql->set(
+ "INSERT INTO rooms (floor_id, name, number, x, y, width, height, polygon_points, comment)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ "issiiiiss",
+ [$floorId, $name, $number, $x, $y, $width, $height, $polygonJson, $comment]
+ );
+ }
+} else {
+ if ($roomId > 0) {
+ $sql->set(
+ "UPDATE rooms
+ SET floor_id = ?, name = ?, number = ?, x = ?, y = ?, width = ?, height = ?, comment = ?
+ WHERE id = ?",
+ "issiiiisi",
+ [$floorId, $name, $number, $x, $y, $width, $height, $comment, $roomId]
+ );
+ } else {
+ $sql->set(
+ "INSERT INTO rooms (floor_id, name, number, x, y, width, height, comment)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
+ "issiiiis",
+ [$floorId, $name, $number, $x, $y, $width, $height, $comment]
+ );
+ }
+}
+
+header('Location: ?module=locations&action=list');
+exit;
+
+function roomsHasPolygonColumn($sql)
+{
+ static $hasColumn = null;
+ if ($hasColumn !== null) {
+ return $hasColumn;
+ }
+ $col = $sql->single("SHOW COLUMNS FROM rooms LIKE 'polygon_points'", "", []);
+ $hasColumn = !empty($col);
+ return $hasColumn;
+}
diff --git a/doc/DATABASE.md b/doc/DATABASE.md
index 1df743d..893cfe6 100644
--- a/doc/DATABASE.md
+++ b/doc/DATABASE.md
@@ -69,6 +69,7 @@ Ein Raum innerhalb eines Stockwerks.
**Grafische Attribute**
- `x`, `y`, `width`, `height` zur visuellen Darstellung im Stockwerks-SVG
+- `polygon_points` (JSON) fuer optionale Freiform-Polygone auf Basis der Stockwerkskarte
---
diff --git a/init.sql b/init.sql
index 87ad7ef..a78af85 100644
--- a/init.sql
+++ b/init.sql
@@ -428,6 +428,7 @@ CREATE TABLE `rooms` (
`y` int(11) DEFAULT NULL,
`width` int(11) DEFAULT NULL,
`height` int(11) DEFAULT NULL,
+ `polygon_points` longtext DEFAULT NULL,
`comment` text DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;