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>
535 lines
24 KiB
JavaScript
535 lines
24 KiB
JavaScript
/* ==========================================================================
|
||
Reiter "Grundriss": Etagen, Räume und wo ihre Kacheln sitzen.
|
||
|
||
Vorher stand das alles in restricted/rooms.php, und eine Kachel setzte man,
|
||
indem man in tools/kachelpositionen.php die Stelle ablas und die Zahlen in
|
||
die Datei schrieb. Hier geschieht beides am Plan selbst: Kacheln werden
|
||
gezogen, ein Raum ohne Kachel mit "Platzieren" und einem Klick gesetzt.
|
||
Von dort übernommen sind Raster, Geisterkachel und die Umrechnung: gezeigt
|
||
wird auf die Mitte, gespeichert die linke obere Ecke.
|
||
|
||
Geprüft wird nichts, was der Server nicht auch prüft
|
||
(restricted/grundriss.php). Jede Aktion antwortet mit dem ganzen neuen
|
||
Stand; die Maske zeichnet ihn einfach neu.
|
||
|
||
Nutzt textSchuetzen() aus settings.js - beide Dateien stehen auf derselben
|
||
Seite, settings.js zuerst.
|
||
========================================================================== */
|
||
|
||
let grundrissModell = null; // Antwort von ?action=grundriss
|
||
const grundrissZustand = {
|
||
etage: null, // gewählte Etage (code) oder "" für "neue Etage"
|
||
raum: null, // gewählter Raum (nr), 0 = neuer Raum, null = keiner
|
||
platzieren: null, // nr des Raums, der gerade auf den Plan gesetzt wird
|
||
ziehen: null, // { nr, dx, dy, x, y, bewegt } während eines Zugs
|
||
};
|
||
|
||
/* --- Ableitungen --------------------------------------------------------- */
|
||
|
||
function grEtage(code) {
|
||
return (grundrissModell.etagen || []).find(e => e.code === code) || null;
|
||
}
|
||
function grRaum(nr) {
|
||
return (grundrissModell.raeume || []).find(r => r.nr === nr) || null;
|
||
}
|
||
function grRaeumeDerEtage(code) {
|
||
return (grundrissModell.raeume || []).filter(r => r.floor === code);
|
||
}
|
||
function grMass() {
|
||
return grundrissModell.mass;
|
||
}
|
||
|
||
/**
|
||
* Lange Namen brechen am letzten Leerzeichen um - wie auf der Startseite
|
||
* (restricted/home.php), damit die Kachel hier genauso breit wirkt wie dort.
|
||
*/
|
||
function grNamenszeilen(name) {
|
||
const luecke = name.lastIndexOf(" ");
|
||
return name.length > 8 && luecke > 0 ? [name.slice(0, luecke), name.slice(luecke + 1)] : [name];
|
||
}
|
||
|
||
/** Zeigerposition in Koordinaten der Zeichenfläche. */
|
||
function grPunkt(svg, ereignis) {
|
||
const p = svg.createSVGPoint();
|
||
p.x = ereignis.clientX;
|
||
p.y = ereignis.clientY;
|
||
return p.matrixTransform(svg.getScreenCTM().inverse());
|
||
}
|
||
|
||
/** Linke obere Ecke auf die Zeichenfläche begrenzen - wie der Server. */
|
||
function grBegrenzt(x, y) {
|
||
const m = grMass();
|
||
return {
|
||
x: Math.round(Math.max(0, Math.min(m.planBreit - m.kachelBreit, x))),
|
||
y: Math.round(Math.max(0, Math.min(m.planHoch - m.kachelHoch, y))),
|
||
};
|
||
}
|
||
|
||
/* --- Zeichnen ------------------------------------------------------------ */
|
||
|
||
function zeichneGrundriss() {
|
||
const ziel = document.getElementById("grundrissFlaeche");
|
||
if (!ziel || !grundrissModell) return;
|
||
|
||
const etagen = grundrissModell.etagen || [];
|
||
if (grundrissZustand.etage === null || (grundrissZustand.etage !== "" && !grEtage(grundrissZustand.etage))) {
|
||
grundrissZustand.etage = etagen.length ? (grundrissModell.standard || etagen[0].code) : "";
|
||
}
|
||
const code = grundrissZustand.etage;
|
||
const etage = grEtage(code);
|
||
|
||
let html = '<div class="gr-etagen">';
|
||
etagen.forEach(e => {
|
||
html += '<button type="button" class="btn btn-sm gr-etage' + (e.code === code ? " aktiv" : "") + '"'
|
||
+ ' data-code="' + textSchuetzen(e.code) + '" title="' + textSchuetzen(e.label) + '">'
|
||
+ '<span class="gr-etage-code">' + textSchuetzen(e.code) + "</span>"
|
||
+ '<span class="gr-etage-name">' + textSchuetzen(e.label) + "</span>"
|
||
+ (e.standard ? '<i class="bi bi-star-fill gr-etage-std" title="Standard-Etage"></i>' : "")
|
||
+ "</button>";
|
||
});
|
||
html += '<button type="button" class="btn btn-sm btn-outline-secondary gr-etage-neu' + (code === "" ? " active" : "") + '">'
|
||
+ '<i class="bi bi-plus-lg me-1"></i>Etage</button></div>';
|
||
|
||
if (!etagen.length && code === "") {
|
||
html += '<p class="small text-body-secondary mb-3">Noch keine Etage angelegt. Eine Etage '
|
||
+ "braucht ein Kürzel (so steht sie im Menü und in den Etagenschaltern), einen Namen "
|
||
+ "und später ein Grundrissbild.</p>";
|
||
}
|
||
|
||
html += '<div class="row g-3">'
|
||
+ '<div class="col-lg-8">' + (etage ? grPlan(etage) : grPlanLeer()) + "</div>"
|
||
+ '<div class="col-lg-4">' + grEtageFormular(etage)
|
||
+ (etage ? grRaumListe(etage) + grRaumFormular(etage) : "")
|
||
+ "</div></div>"
|
||
+ '<div class="gr-stand small mt-2"></div>';
|
||
|
||
ziel.innerHTML = html;
|
||
}
|
||
|
||
function grPlanLeer() {
|
||
return '<div class="gr-plan-huelle gr-plan-leer"><span>Erst die Etage speichern, dann '
|
||
+ "lässt sich ihr Grundriss hochladen und bestücken.</span></div>";
|
||
}
|
||
|
||
/** Der Plan einer Etage: Bild, Raster, Kacheln, Geisterkachel. */
|
||
function grPlan(etage) {
|
||
const m = grMass();
|
||
const raeume = grRaeumeDerEtage(etage.code).filter(r => r.x !== null);
|
||
let svg = '<div class="gr-plan-huelle' + (grundrissZustand.platzieren ? " platzieren" : "") + '">'
|
||
+ '<svg class="gr-plan" viewBox="0 0 ' + m.planBreit + " " + m.planHoch + '">'
|
||
+ '<defs><pattern id="grRaster" width="10" height="10" patternUnits="userSpaceOnUse">'
|
||
+ '<path d="M10 0H0V10" fill="none" stroke="#ffffff1c" stroke-width=".4"/></pattern></defs>';
|
||
if (etage.bild) {
|
||
// Dieselbe Einpassung wie auf der Startseite: das Bild füllt die
|
||
// Zeichenfläche, ohne verzerrt zu werden (preserveAspectRatio "meet").
|
||
svg += '<image href="' + textSchuetzen(etage.bild) + '" x="0" y="0" width="' + m.planBreit
|
||
+ '" height="' + m.planHoch + '"/>';
|
||
} else {
|
||
svg += '<text x="' + m.planBreit / 2 + '" y="' + m.planHoch / 2 + '" class="gr-ohne-bild">'
|
||
+ "Noch kein Grundrissbild</text>";
|
||
}
|
||
svg += '<rect width="' + m.planBreit + '" height="' + m.planHoch + '" fill="url(#grRaster)" pointer-events="none"/>';
|
||
|
||
raeume.forEach(r => {
|
||
const zeilen = grNamenszeilen(r.name);
|
||
const ys = zeilen.length === 2 ? [9, 15.5] : [11.5];
|
||
svg += '<g class="gr-kachel' + (r.nr === grundrissZustand.raum ? " gewaehlt" : "") + '"'
|
||
+ ' data-nr="' + r.nr + '" transform="translate(' + r.x + "," + r.y + ')">'
|
||
+ '<rect width="' + m.kachelBreit + '" height="' + m.kachelHoch + '" rx="5"/>'
|
||
+ zeilen.map((z, i) => '<text x="' + m.kachelBreit / 2 + '" y="' + ys[i] + '">'
|
||
+ textSchuetzen(z) + "</text>").join("")
|
||
+ "</g>";
|
||
});
|
||
|
||
svg += '<g class="gr-geist" style="display:none"><rect width="' + m.kachelBreit + '" height="'
|
||
+ m.kachelHoch + '" rx="5"/></g></svg></div>';
|
||
|
||
const hinweis = grundrissZustand.platzieren
|
||
? "Auf die Stelle klicken, an der die Mitte der Kachel „"
|
||
+ textSchuetzen((grRaum(grundrissZustand.platzieren) || {}).name || "") + "“ liegen soll. Esc bricht ab."
|
||
: "Kacheln lassen sich ziehen; ein Klick wählt den Raum. Die Zeichenfläche ist "
|
||
+ m.planBreit + " × " + m.planHoch + ", eine Kachel " + m.kachelBreit + " × " + m.kachelHoch + ".";
|
||
return svg + '<p class="small text-body-secondary mt-1 mb-0 gr-plan-hinweis">' + hinweis + "</p>";
|
||
}
|
||
|
||
/** Formular der gewählten Etage - oder einer neuen. */
|
||
function grEtageFormular(etage) {
|
||
const neu = !etage;
|
||
const etagen = grundrissModell.etagen || [];
|
||
const i = neu ? -1 : etagen.findIndex(e => e.code === etage.code);
|
||
let html = '<div class="gr-block" data-block="etage">'
|
||
+ '<div class="gr-block-titel">' + (neu ? "Neue Etage" : "Etage") + "</div>"
|
||
+ '<div class="row g-2">'
|
||
+ '<div class="col-4"><label class="form-label small mb-0">Kürzel</label>'
|
||
+ '<input type="text" class="form-control form-control-sm" data-feld="code" maxlength="4"'
|
||
+ ' value="' + (neu ? "" : textSchuetzen(etage.code)) + '"' + (neu ? ' placeholder="EG"' : " readonly") + ' autocomplete="off"></div>'
|
||
+ '<div class="col-8"><label class="form-label small mb-0">Name</label>'
|
||
+ '<input type="text" class="form-control form-control-sm" data-feld="label" maxlength="40"'
|
||
+ ' value="' + (neu ? "" : textSchuetzen(etage.label)) + '" placeholder="Erdgeschoss"></div>'
|
||
+ "</div>"
|
||
+ '<div class="form-check mt-2"><input class="form-check-input" type="checkbox" id="grStandard" data-feld="standard"'
|
||
+ (!neu && etage.standard ? " checked" : "") + '>'
|
||
+ '<label class="form-check-label small" for="grStandard">Öffnet, wenn keine Etage gewählt ist</label></div>'
|
||
+ '<div class="gr-knoepfe">'
|
||
+ '<button type="button" class="btn btn-primary btn-sm gr-etage-speichern">Speichern</button>';
|
||
if (!neu) {
|
||
html += '<button type="button" class="btn btn-outline-secondary btn-sm gr-etage-links" title="Nach vorn"'
|
||
+ (i <= 0 ? " disabled" : "") + '><i class="bi bi-arrow-left"></i></button>'
|
||
+ '<button type="button" class="btn btn-outline-secondary btn-sm gr-etage-rechts" title="Nach hinten"'
|
||
+ (i >= etagen.length - 1 ? " disabled" : "") + '><i class="bi bi-arrow-right"></i></button>'
|
||
+ '<label class="btn btn-outline-secondary btn-sm mb-0" title="PNG, JPEG oder WebP">'
|
||
+ '<i class="bi bi-image me-1"></i>' + (etage.bild ? "Bild tauschen" : "Bild hochladen")
|
||
+ '<input type="file" class="d-none gr-bild" accept="image/png,image/jpeg,image/webp"></label>'
|
||
+ '<button type="button" class="btn btn-sm gr-etage-weg" title="Etage löschen"><i class="bi bi-trash3"></i></button>';
|
||
}
|
||
html += "</div>";
|
||
if (!neu && etage.automatiken.length) {
|
||
html += '<p class="small text-body-secondary mb-0 mt-1">' + etage.automatiken.length
|
||
+ " Automatik(en) stehen unter dieser Etage.</p>";
|
||
}
|
||
return html + "</div>";
|
||
}
|
||
|
||
/** Die Räume der Etage, mit und ohne Kachel. */
|
||
function grRaumListe(etage) {
|
||
const raeume = grRaeumeDerEtage(etage.code);
|
||
let html = '<div class="gr-block" data-block="raeume"><div class="gr-block-titel">Räume</div>';
|
||
if (!raeume.length) {
|
||
html += '<p class="small text-body-secondary mb-2">Noch keine Räume auf dieser Etage.</p>';
|
||
}
|
||
html += '<div class="gr-raeume">';
|
||
raeume.forEach(r => {
|
||
const aktiv = r.nr === grundrissZustand.raum;
|
||
html += '<div class="gr-raum' + (aktiv ? " gewaehlt" : "") + '" data-nr="' + r.nr + '">'
|
||
+ '<button type="button" class="gr-raum-name">' + textSchuetzen(r.name)
|
||
+ (r.thermostat ? ' <i class="bi bi-thermometer-half text-body-secondary" title="' + textSchuetzen(r.thermostat) + '"></i>' : "")
|
||
+ "</button>"
|
||
+ (r.x !== null
|
||
? '<span class="gr-raum-stand" title="Kachel bei ' + r.x + " / " + r.y + '"><i class="bi bi-geo-alt-fill"></i></span>'
|
||
: '<button type="button" class="btn btn-outline-secondary btn-sm gr-platzieren'
|
||
+ (grundrissZustand.platzieren === r.nr ? " active" : "") + '">Platzieren</button>')
|
||
+ "</div>";
|
||
});
|
||
html += '</div><button type="button" class="btn btn-sm btn-outline-secondary mt-2 gr-raum-neu'
|
||
+ (grundrissZustand.raum === 0 ? " active" : "") + '"><i class="bi bi-plus-lg me-1"></i>Raum</button></div>';
|
||
return html;
|
||
}
|
||
|
||
/** Formular des gewählten Raums, sofern einer gewählt ist. */
|
||
function grRaumFormular(etage) {
|
||
const nr = grundrissZustand.raum;
|
||
if (nr === null) return "";
|
||
const r = nr === 0 ? { nr: 0, floor: etage.code, name: "", kuerzel: "", thermostat: "", x: null, geraete: 0 } : grRaum(nr);
|
||
if (!r) return "";
|
||
|
||
const etagenAuswahl = (grundrissModell.etagen || []).map(e =>
|
||
'<option value="' + textSchuetzen(e.code) + '"' + (e.code === r.floor ? " selected" : "") + ">"
|
||
+ textSchuetzen(e.code + " · " + e.label) + "</option>").join("");
|
||
const zweige = '<datalist id="grZweige">' + (grundrissModell.zweige || []).map(z =>
|
||
'<option value="' + textSchuetzen(z) + '">').join("") + "</datalist>";
|
||
|
||
let html = '<div class="gr-block" data-block="raum">'
|
||
+ '<div class="gr-block-titel">' + (nr === 0 ? "Neuer Raum" : "Raum") + "</div>"
|
||
+ '<label class="form-label small mb-0">Name</label>'
|
||
+ '<input type="text" class="form-control form-control-sm" data-feld="name" maxlength="40" value="'
|
||
+ textSchuetzen(r.name) + '" placeholder="Wohnzimmer">'
|
||
+ '<div class="row g-2 mt-0">'
|
||
+ '<div class="col-6"><label class="form-label small mb-0">Etage</label>'
|
||
+ '<select class="form-select form-select-sm" data-feld="floor">' + etagenAuswahl + "</select></div>"
|
||
+ '<div class="col-6"><label class="form-label small mb-0" title="Teil der Element-Ids auf der Startseite. Leer lassen: entsteht aus dem Namen.">Kürzel</label>'
|
||
+ '<input type="text" class="form-control form-control-sm" data-feld="kuerzel" maxlength="40" value="'
|
||
+ textSchuetzen(r.kuerzel) + '" placeholder="aus dem Namen" autocomplete="off"></div></div>'
|
||
+ '<label class="form-label small mb-0 mt-2">Thermostat-Zweig</label>'
|
||
+ '<input type="text" class="form-control form-control-sm" data-feld="thermostat" list="grZweige" value="'
|
||
+ textSchuetzen(r.thermostat || "") + '" placeholder="leer = kein Thermostat" autocomplete="off">' + zweige
|
||
+ '<div class="form-text small">Unter diesem MQTT-Zweig meldet das Thermostat, z. B. Raumtemp/EG/Bad. '
|
||
+ "Er trägt Heizsymbol, Temperaturregler und die Vorgabewerte der Kachel.</div>"
|
||
+ '<div class="gr-knoepfe">'
|
||
+ '<button type="button" class="btn btn-primary btn-sm gr-raum-speichern">Speichern</button>';
|
||
if (nr !== 0) {
|
||
if (r.x !== null) {
|
||
html += '<button type="button" class="btn btn-outline-secondary btn-sm gr-kachelwerte" title="Werte dieser Kachel bearbeiten">'
|
||
+ '<i class="bi bi-grid-1x2 me-1"></i>Werte</button>'
|
||
+ '<button type="button" class="btn btn-outline-secondary btn-sm gr-vom-plan">Vom Plan</button>';
|
||
}
|
||
html += '<button type="button" class="btn btn-sm gr-raum-weg" title="Raum löschen"><i class="bi bi-trash3"></i></button>';
|
||
}
|
||
html += "</div>";
|
||
if (nr !== 0 && r.geraete) {
|
||
html += '<p class="small text-body-secondary mb-0 mt-1">' + r.geraete + " Gerät(e) zugeordnet.</p>";
|
||
}
|
||
return html + "</div>";
|
||
}
|
||
|
||
/* --- Sprechen mit dem Endpunkt ------------------------------------------- */
|
||
|
||
function grStand(text, klasse) {
|
||
const ziel = document.querySelector("#grundrissFlaeche .gr-stand");
|
||
if (!ziel) return;
|
||
ziel.className = "gr-stand small mt-2 " + (klasse || "text-body-secondary");
|
||
ziel.textContent = text;
|
||
clearTimeout(grStand._uhr);
|
||
if (klasse !== "text-danger") {
|
||
grStand._uhr = setTimeout(() => { if (ziel.textContent === text) ziel.textContent = ""; }, 4000);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Eine Aktion schicken, den neuen Stand übernehmen, neu zeichnen.
|
||
*
|
||
* Danach wissen die anderen Reiter nicht mehr, was gilt: neue Räume fehlen in
|
||
* der Kachelliste und in der Raumauswahl der Geräte. Sie laden beim nächsten
|
||
* Öffnen nach (grundrissGeaendert, siehe settings.js).
|
||
*/
|
||
async function grSenden(action, koerper, meldung) {
|
||
try {
|
||
const optionen = koerper instanceof FormData
|
||
? { method: "POST", body: koerper }
|
||
: { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(koerper) };
|
||
const antwort = await fetch("ajax/settings.php?action=" + action, optionen);
|
||
const d = await antwort.json();
|
||
if (!antwort.ok || d.error) throw new Error(d.error || "HTTP " + antwort.status);
|
||
grundrissModell = d.stand;
|
||
window.grundrissGeaendert = true;
|
||
zeichneGrundriss();
|
||
if (meldung) grStand(meldung, "text-success");
|
||
return d;
|
||
} catch (e) {
|
||
grStand(e.message, "text-danger");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function grundrissHolen() {
|
||
const ziel = document.getElementById("grundrissFlaeche");
|
||
if (!ziel) return;
|
||
try {
|
||
const antwort = await fetch("ajax/settings.php?action=grundriss", { cache: "no-store" });
|
||
const d = await antwort.json();
|
||
if (!antwort.ok || d.error) throw new Error(d.error || "HTTP " + antwort.status);
|
||
grundrissModell = d;
|
||
zeichneGrundriss();
|
||
} catch (e) {
|
||
ziel.innerHTML = '<div class="alert alert-danger mb-0">Grundriss nicht abrufbar: '
|
||
+ textSchuetzen(e.message) + "</div>";
|
||
}
|
||
}
|
||
|
||
/** Die Felder eines Blocks als Objekt. */
|
||
function grFelder(block) {
|
||
const werte = {};
|
||
block.querySelectorAll("[data-feld]").forEach(f => {
|
||
werte[f.dataset.feld] = f.type === "checkbox" ? f.checked : f.value;
|
||
});
|
||
return werte;
|
||
}
|
||
|
||
/* --- Bedienung ----------------------------------------------------------- */
|
||
|
||
function grundrissBinden() {
|
||
const flaeche = document.getElementById("grundrissFlaeche");
|
||
if (!flaeche) return;
|
||
|
||
flaeche.addEventListener("click", async function (ereignis) {
|
||
if (!grundrissModell) return;
|
||
const t = ereignis.target;
|
||
|
||
const etagenKnopf = t.closest(".gr-etage");
|
||
if (etagenKnopf) {
|
||
grundrissZustand.etage = etagenKnopf.dataset.code;
|
||
grundrissZustand.raum = null;
|
||
grundrissZustand.platzieren = null;
|
||
zeichneGrundriss();
|
||
return;
|
||
}
|
||
if (t.closest(".gr-etage-neu")) {
|
||
grundrissZustand.etage = "";
|
||
grundrissZustand.raum = null;
|
||
grundrissZustand.platzieren = null;
|
||
zeichneGrundriss();
|
||
return;
|
||
}
|
||
if (t.closest(".gr-etage-speichern")) {
|
||
const werte = grFelder(t.closest('[data-block="etage"]'));
|
||
const neu = grundrissZustand.etage === "";
|
||
const d = await grSenden("etage-speichern",
|
||
{ code: werte.code, label: werte.label, standard: werte.standard, neu: neu }, "Etage gespeichert.");
|
||
if (d && neu) {
|
||
grundrissZustand.etage = String(werte.code).trim().toUpperCase();
|
||
zeichneGrundriss();
|
||
}
|
||
return;
|
||
}
|
||
if (t.closest(".gr-etage-links") || t.closest(".gr-etage-rechts")) {
|
||
const codes = grundrissModell.etagen.map(e => e.code);
|
||
const i = codes.indexOf(grundrissZustand.etage);
|
||
const j = i + (t.closest(".gr-etage-links") ? -1 : 1);
|
||
if (i < 0 || j < 0 || j >= codes.length) return;
|
||
[codes[i], codes[j]] = [codes[j], codes[i]];
|
||
grSenden("etagen-reihenfolge", { codes: codes }, "Reihenfolge gespeichert.");
|
||
return;
|
||
}
|
||
if (t.closest(".gr-etage-weg")) {
|
||
const e = grEtage(grundrissZustand.etage);
|
||
if (!e || !confirm("Etage „" + e.label + "“ löschen?")) return;
|
||
const d = await grSenden("etage-loeschen", { code: e.code }, "Etage gelöscht.");
|
||
if (d) {
|
||
grundrissZustand.etage = null;
|
||
zeichneGrundriss();
|
||
}
|
||
return;
|
||
}
|
||
|
||
const raumZeile = t.closest(".gr-raum");
|
||
if (t.closest(".gr-platzieren") && raumZeile) {
|
||
const nr = Number(raumZeile.dataset.nr);
|
||
grundrissZustand.platzieren = grundrissZustand.platzieren === nr ? null : nr;
|
||
grundrissZustand.raum = nr;
|
||
zeichneGrundriss();
|
||
return;
|
||
}
|
||
if (t.closest(".gr-raum-name") && raumZeile) {
|
||
const nr = Number(raumZeile.dataset.nr);
|
||
grundrissZustand.raum = grundrissZustand.raum === nr ? null : nr;
|
||
grundrissZustand.platzieren = null;
|
||
zeichneGrundriss();
|
||
return;
|
||
}
|
||
if (t.closest(".gr-raum-neu")) {
|
||
grundrissZustand.raum = 0;
|
||
grundrissZustand.platzieren = null;
|
||
zeichneGrundriss();
|
||
const feld = flaeche.querySelector('[data-block="raum"] [data-feld="name"]');
|
||
if (feld) feld.focus();
|
||
return;
|
||
}
|
||
if (t.closest(".gr-raum-speichern")) {
|
||
const werte = grFelder(t.closest('[data-block="raum"]'));
|
||
werte.nr = grundrissZustand.raum || 0;
|
||
const d = await grSenden("raum-speichern", werte, "Raum gespeichert.");
|
||
if (d) {
|
||
grundrissZustand.raum = d.nr;
|
||
// Hat der Raum die Etage gewechselt, geht der Blick mit.
|
||
const r = grRaum(d.nr);
|
||
if (r && r.floor !== grundrissZustand.etage) grundrissZustand.etage = r.floor;
|
||
zeichneGrundriss();
|
||
}
|
||
return;
|
||
}
|
||
if (t.closest(".gr-vom-plan")) {
|
||
grSenden("raum-position", { nr: grundrissZustand.raum, x: null, y: null }, "Kachel vom Plan genommen.");
|
||
return;
|
||
}
|
||
if (t.closest(".gr-raum-weg")) {
|
||
const r = grRaum(grundrissZustand.raum);
|
||
if (!r) return;
|
||
const frage = "Raum „" + r.name + "“ löschen?"
|
||
+ (r.geraete ? "\n\n" + r.geraete + " Gerät(e) stehen danach ohne Raum da." : "");
|
||
if (!confirm(frage)) return;
|
||
const d = await grSenden("raum-loeschen", { nr: r.nr }, "Raum gelöscht.");
|
||
if (d) {
|
||
grundrissZustand.raum = null;
|
||
zeichneGrundriss();
|
||
}
|
||
return;
|
||
}
|
||
if (t.closest(".gr-kachelwerte")) {
|
||
const r = grRaum(grundrissZustand.raum);
|
||
if (r && typeof kachelOeffnen === "function") kachelOeffnen(r.floor, r.kuerzel);
|
||
}
|
||
});
|
||
|
||
// Das Bild geht los, sobald eine Datei gewählt ist - ein zweiter Klick auf
|
||
// "Speichern" wäre hier nur eine Gelegenheit, es zu vergessen.
|
||
flaeche.addEventListener("change", function (ereignis) {
|
||
const feld = ereignis.target.closest(".gr-bild");
|
||
if (!feld || !feld.files.length) return;
|
||
const formular = new FormData();
|
||
formular.append("code", grundrissZustand.etage);
|
||
formular.append("bild", feld.files[0]);
|
||
grStand("Bild wird hochgeladen …");
|
||
grSenden("etage-bild", formular, "Grundriss übernommen.");
|
||
});
|
||
|
||
/* Ziehen und Platzieren. Pointer-Ereignisse, damit es mit Maus, Stift und
|
||
Finger gleich geht; die Kachel folgt dem Zeiger, gespeichert wird beim
|
||
Loslassen. */
|
||
flaeche.addEventListener("pointerdown", function (ereignis) {
|
||
const kachel = ereignis.target.closest(".gr-kachel");
|
||
const svg = ereignis.target.closest(".gr-plan");
|
||
if (!kachel || !svg || grundrissZustand.platzieren) return;
|
||
const r = grRaum(Number(kachel.dataset.nr));
|
||
if (!r) return;
|
||
const p = grPunkt(svg, ereignis);
|
||
grundrissZustand.ziehen = { nr: r.nr, dx: p.x - r.x, dy: p.y - r.y, x: r.x, y: r.y, bewegt: false, kachel: kachel, svg: svg };
|
||
kachel.setPointerCapture(ereignis.pointerId);
|
||
kachel.classList.add("zieht");
|
||
ereignis.preventDefault();
|
||
});
|
||
|
||
flaeche.addEventListener("pointermove", function (ereignis) {
|
||
const z = grundrissZustand.ziehen;
|
||
if (z) {
|
||
const p = grPunkt(z.svg, ereignis);
|
||
const pos = grBegrenzt(p.x - z.dx, p.y - z.dy);
|
||
if (pos.x !== z.x || pos.y !== z.y) z.bewegt = true;
|
||
z.x = pos.x;
|
||
z.y = pos.y;
|
||
z.kachel.setAttribute("transform", "translate(" + pos.x + "," + pos.y + ")");
|
||
grStand("x " + pos.x + " · y " + pos.y);
|
||
return;
|
||
}
|
||
if (grundrissZustand.platzieren) {
|
||
const svg = ereignis.target.closest(".gr-plan");
|
||
const geist = flaeche.querySelector(".gr-geist");
|
||
if (!svg || !geist) return;
|
||
const m = grMass();
|
||
const p = grPunkt(svg, ereignis);
|
||
const pos = grBegrenzt(p.x - m.kachelBreit / 2, p.y - m.kachelHoch / 2);
|
||
geist.style.display = "";
|
||
geist.setAttribute("transform", "translate(" + pos.x + "," + pos.y + ")");
|
||
}
|
||
});
|
||
|
||
flaeche.addEventListener("pointerup", async function (ereignis) {
|
||
const z = grundrissZustand.ziehen;
|
||
if (z) {
|
||
grundrissZustand.ziehen = null;
|
||
z.kachel.classList.remove("zieht");
|
||
if (!z.bewegt) {
|
||
// Nur angetippt: den Raum wählen statt ihn zu verschieben.
|
||
grundrissZustand.raum = grundrissZustand.raum === z.nr ? null : z.nr;
|
||
zeichneGrundriss();
|
||
return;
|
||
}
|
||
grSenden("raum-position", { nr: z.nr, x: z.x, y: z.y }, "Kachel gesetzt.");
|
||
return;
|
||
}
|
||
if (grundrissZustand.platzieren) {
|
||
const svg = ereignis.target.closest(".gr-plan");
|
||
if (!svg) return;
|
||
const m = grMass();
|
||
const p = grPunkt(svg, ereignis);
|
||
const pos = grBegrenzt(p.x - m.kachelBreit / 2, p.y - m.kachelHoch / 2);
|
||
const nr = grundrissZustand.platzieren;
|
||
grundrissZustand.platzieren = null;
|
||
grSenden("raum-position", { nr: nr, x: pos.x, y: pos.y }, "Kachel gesetzt.");
|
||
}
|
||
});
|
||
|
||
document.addEventListener("keydown", function (ereignis) {
|
||
if (ereignis.key === "Escape" && grundrissZustand.platzieren) {
|
||
grundrissZustand.platzieren = null;
|
||
zeichneGrundriss();
|
||
}
|
||
});
|
||
|
||
const neuLaden = document.getElementById("grundrissNeuLaden");
|
||
if (neuLaden) neuLaden.addEventListener("click", grundrissHolen);
|
||
}
|
||
|
||
document.addEventListener("DOMContentLoaded", function () {
|
||
grundrissBinden();
|
||
grundrissHolen();
|
||
});
|