Einstellungen -> Grundriss: Etagen, Raeume und Kacheln am Plan pflegen

Neuer Reiter mit Plan-Editor: Etagen anlegen, umbenennen, sortieren,
loeschen und als Standard markieren; Grundrissbild hochladen; Raeume mit
Name, Kuerzel und Thermostat-Zweig anlegen und bearbeiten; Kacheln im Plan
ziehen oder einen Raum ohne Kachel mit "Platzieren" und einem Klick setzen.
Loest tools/kachelpositionen.php ab - Raster, Geisterkachel und die
Umrechnung Mitte -> linke obere Ecke sind von dort uebernommen.

- restricted/grundriss.php: alle Pruefungen und Schreibzugriffe. Eine
  Etage mit Raeumen oder Automatiken laesst sich nicht loeschen, die
  Ablehnung nennt die Namen. Raeume loeschen laesst ihre Geraete ohne Raum.
- Grundrissbilder nach tiles/grundriss/ (beschreibbar, nicht im Git),
  nur PNG/JPEG/WebP, am Inhalt geprueft; SVG bewusst nicht. Prüfsumme im
  Dateinamen, alte hochgeladene Bilder werden weggeraeumt.
- ajax/settings.php: ?action=grundriss und sieben POST-Aktionen, jede
  antwortet mit dem ganzen neuen Stand.
- "Werte" springt in den Reiter Home-Kacheln und oeffnet die Kachel;
  Home-Kacheln und Geraete laden nach einer Grundriss-Aenderung nach.
- Startseite ohne Etage mit platziertem Raum: Hinweis auf die
  Einstellungen statt einer leeren Zeichenflaeche.
- README: Grundriss als Inhalt statt Code, neue Dateien und Endpunkte,
  Reihenfolge der Schema-Dateien fuer ein neues Haus.

Backend mit einer Probe-Etage durchgespielt (auch Fehlerfaelle, Upload,
Aufraeumen); Oberflaeche auf einer Probeseite mit echten Daten und
Zeiger-Ereignissen geprueft: Ziehen, Antippen, Platzieren, neuer Raum.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 11:11:10 +02:00
co-authored by Claude Opus 5
parent 62a8af3fc6
commit 8d626abc6d
12 changed files with 1365 additions and 255 deletions
+61 -3
View File
@@ -8,15 +8,25 @@
* POST ?action=kacheln-save {"floor": "OG", "room": "bad",
* "werte": [ ... ] oder null}
* GET ?action=geraete Geraete mit Raum, Verwendung und Alter
* POST ?action=geraete-raeume {"rooms": {"12": "OG/Bad", ...}}
* POST ?action=geraete-raeume {"rooms": {"123": "11", ...}} Geraet -> Raumnummer
* POST ?action=geraet-loeschen {"id": 12}
* GET ?action=skoda-keys Stand der Fahrzeug-Schluessel (ohne Werte!)
* POST ?action=skoda-keys-save {"felder": {"API_KEY2": "..."},
* "loeschen": ["API_KEY3"]}
*
* GET ?action=grundriss Etagen, Raeume, Kachelpositionen
* POST ?action=etage-speichern {"code": "EG", "label": "...", "standard": true, "neu": false}
* POST ?action=etagen-reihenfolge {"codes": ["OG", "EG", ...]}
* POST ?action=etage-loeschen {"code": "EG"}
* POST ?action=etage-bild multipart: code, bild (Datei)
* POST ?action=raum-speichern {"nr": 0|12, "floor": "EG", "name": "...", "kuerzel": "", "thermostat": ""}
* POST ?action=raum-position {"nr": 12, "x": 120, "y": 80} (x/y null = vom Plan)
* POST ?action=raum-loeschen {"nr": 12}
* Die Grundriss-Aktionen antworten mit dem ganzen neuen Stand.
*
* Ein reiner Verteiler: geprueft und geschrieben wird in
* restricted/costs.php bzw. restricted/kacheln.php, damit Maske und Endpunkt
* dieselbe Wahrheit ueber einen Zeitraum oder eine Kachel haben.
* restricted/costs.php, kacheln.php bzw. grundriss.php, damit Maske und
* Endpunkt dieselbe Wahrheit ueber einen Zeitraum oder eine Kachel haben.
*/
require_once("../helper.php");
@@ -24,6 +34,7 @@ require_once("../restricted/costs.php");
require_once("../restricted/kacheln.php");
require_once("../restricted/automations.php");
require_once("../restricted/skodaKeys.php");
require_once("../restricted/grundriss.php");
if (!checkLogin()) {
http_response_code(403);
@@ -40,6 +51,20 @@ function jsonAntwort($data, $code = 200)
$action = $_GET["action"] ?? "list";
// Das Grundrissbild kommt als Formular mit Datei, nicht als JSON - deshalb
// vor dem JSON-Teil.
if ($_SERVER["REQUEST_METHOD"] === "POST" && $action === "etage-bild") {
try {
$bild = etageBildSpeichern($_POST["code"] ?? "", $_FILES["bild"] ?? null);
grundriss(true);
jsonAntwort(["bild" => $bild, "stand" => grundrissStand()]);
} catch (InvalidArgumentException $e) {
jsonAntwort(["error" => $e->getMessage()], 400);
} catch (Throwable $e) {
jsonAntwort(["error" => $e->getMessage()], 500);
}
}
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$body = json_decode(file_get_contents("php://input"), true);
if (!is_array($body)) {
@@ -90,6 +115,31 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
$name = geraetLoeschen($body["id"] ?? 0);
jsonAntwort(["geloescht" => $name, "geraete" => geraeteListe()]);
case "etage-speichern":
etageSpeichern($body);
grundriss(true);
jsonAntwort(["stand" => grundrissStand()]);
case "etagen-reihenfolge":
etagenReihenfolge($body["codes"] ?? null);
grundriss(true);
jsonAntwort(["stand" => grundrissStand()]);
case "etage-loeschen":
etageLoeschen($body["code"] ?? "");
grundriss(true);
jsonAntwort(["stand" => grundrissStand()]);
case "raum-speichern":
$nr = raumSpeichern($body);
grundriss(true);
jsonAntwort(["nr" => $nr, "stand" => grundrissStand()]);
case "raum-position":
$pos = raumPosition($body["nr"] ?? 0, $body["x"] ?? null, $body["y"] ?? null);
grundriss(true);
jsonAntwort(["position" => $pos, "stand" => grundrissStand()]);
case "raum-loeschen":
raumLoeschen($body["nr"] ?? 0);
grundriss(true);
jsonAntwort(["stand" => grundrissStand()]);
case "skoda-keys-save":
/*
* Neue Schluessel eintragen. Zurueck kommt der neue Stand -
@@ -130,6 +180,14 @@ if ($action === "kacheln") {
}
}
if ($action === "grundriss") {
try {
jsonAntwort(grundrissStand());
} catch (Throwable $e) {
jsonAntwort(["error" => "Grundriss nicht abrufbar: " . $e->getMessage()], 500);
}
}
if ($action === "skoda-keys") {
try {
jsonAntwort(skodaStand());