Die Auswahlliste im Editor bot ein "-" an. Eine Automatik ohne Etage taucht aber in keinem der drei Reiter auf und ist danach nur noch in der Datenbank zu finden - anlegen und sofort verlieren. Der Eintrag ist weg, und der Zustand ist jetzt an drei Stellen unmoeglich statt nur unwahrscheinlich: die Liste kennt nur noch OG, EG und UG, saveAutomation lehnt alles andere mit einer Meldung ab statt still auf "" zurueckzufallen, und im Enum der Spalte gibt es den leeren Wert nicht mehr. Die Reihenfolge in der Liste ist jetzt OG, EG, UG wie bei den Reitern - damit trifft der Rueckfall auf den ersten Eintrag dieselbe Etage wie die Startseite. Kennt die Liste die Etage eines Datensatzes nicht, faellt sie darauf zurueck, statt leer stehen zu bleiben und beim Speichern "" zu schicken. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
582 lines
21 KiB
JavaScript
582 lines
21 KiB
JavaScript
/**
|
||
* Editor und Uebersicht fuer die Automatiken.
|
||
*
|
||
* Der Editor arbeitet auf einem Modell und zeichnet daraus die Oberflaeche -
|
||
* nicht umgekehrt. Frueher stand die erste Bedingungszeile als festes HTML im
|
||
* PHP und alle weiteren wurden im JavaScript zusammengesetzt; beide Fassungen
|
||
* liefen auseinander, und beim Speichern musste der Server aus durchnummerierten
|
||
* Feldnamen wieder eine Struktur raten.
|
||
*
|
||
* Modell:
|
||
* autoModel.groups = [ [ {state_id, operator, value}, ... ], ... ]
|
||
* eine innere Liste ist ein UND-Block, die aeussere
|
||
* verknuepft die Bloecke mit ODER
|
||
* autoModel.actions = [ {command_id, params: {parameterID: wert}}, ... ]
|
||
*
|
||
* Der Geraetekatalog kommt zusammen mit dem Datensatz im selben Dokument
|
||
* (siehe ajax/AutoAction.php), es gibt also keine Nachladerei je Zeile.
|
||
*/
|
||
|
||
let autoModel = null;
|
||
let autoDevices = [];
|
||
let autoLockouts = [];
|
||
|
||
// --- Katalog-Helfer ------------------------------------------------------
|
||
|
||
function deviceById(id) {
|
||
return autoDevices.find(d => d.id === id) || null;
|
||
}
|
||
|
||
function stateById(id) {
|
||
for (const dev of autoDevices) {
|
||
const state = dev.states.find(s => s.id === id);
|
||
if (state) return { device: dev, state: state };
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function commandById(id) {
|
||
for (const dev of autoDevices) {
|
||
const cmd = dev.commands.find(c => c.id === id);
|
||
if (cmd) return { device: dev, command: cmd };
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function operatorLabel(state, op) {
|
||
const treffer = state.operators.find(o => o.value === op);
|
||
return treffer ? treffer.label : op;
|
||
}
|
||
|
||
/** Erster Messwert des ersten Geraets - Vorbelegung fuer eine neue Zeile. */
|
||
function ersterMesswert() {
|
||
const dev = autoDevices.find(d => d.states.length > 0);
|
||
return dev ? dev.states[0] : null;
|
||
}
|
||
|
||
function ersterBefehl() {
|
||
const dev = autoDevices.find(d => d.commands.length > 0);
|
||
return dev ? dev.commands[0] : null;
|
||
}
|
||
|
||
// --- Bausteine fuer die Oberflaeche --------------------------------------
|
||
|
||
function optionListe(select, eintraege, gewaehlt) {
|
||
select.innerHTML = "";
|
||
eintraege.forEach(e => {
|
||
const opt = document.createElement("option");
|
||
opt.value = e.value;
|
||
opt.text = e.text;
|
||
if (String(e.value) === String(gewaehlt)) opt.selected = true;
|
||
select.appendChild(opt);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Wertfeld passend zum Datentyp des Messwerts oder Parameters.
|
||
* Liefert das fertige Element; der Aufrufer haengt seinen Listener an.
|
||
*/
|
||
function wertFeld(spec, wert) {
|
||
let feld;
|
||
if (spec.input === "bool") {
|
||
feld = document.createElement("select");
|
||
feld.className = "form-select";
|
||
optionListe(feld, [{ value: "true", text: "JA" }, { value: "false", text: "NEIN" }],
|
||
wert === "" ? "true" : wert);
|
||
} else if (spec.input === "select") {
|
||
feld = document.createElement("select");
|
||
feld.className = "form-select";
|
||
optionListe(feld, spec.options.map(o => ({ value: o, text: o })), wert);
|
||
} else {
|
||
feld = document.createElement("input");
|
||
feld.className = "form-control";
|
||
feld.type = spec.input;
|
||
if (spec.min !== undefined && spec.min !== null) feld.min = spec.min;
|
||
if (spec.max !== undefined && spec.max !== null) feld.max = spec.max;
|
||
feld.value = wert !== "" ? wert
|
||
: spec.input === "time" ? "00:00"
|
||
: spec.input === "date" ? new Date().toISOString().split("T")[0]
|
||
: spec.input === "number" ? "0" : "";
|
||
}
|
||
return feld;
|
||
}
|
||
|
||
/** Standardwert, wenn eine Zeile auf einen anderen Messwert umgestellt wird. */
|
||
function standardWert(spec) {
|
||
if (spec.input === "bool") return "true";
|
||
if (spec.input === "select") return spec.options.length ? spec.options[0] : "";
|
||
if (spec.input === "time") return "00:00";
|
||
if (spec.input === "date") return new Date().toISOString().split("T")[0];
|
||
if (spec.input === "number") return "0";
|
||
return "";
|
||
}
|
||
|
||
// --- Ausloeser -----------------------------------------------------------
|
||
|
||
/**
|
||
* Zeichnet alle UND-Bloecke. Jeder Block hat sein eigenes "+ Bedingung", denn
|
||
* mit einem einzigen Knopf ganz unten kaeme man immer nur an den letzten
|
||
* Block heran und koennte eine bestehende Alternative nie mehr ergaenzen.
|
||
*/
|
||
function renderConditions() {
|
||
const ziel = document.getElementById("condBlocks");
|
||
ziel.innerHTML = "";
|
||
|
||
autoModel.groups.forEach((gruppe, gi) => {
|
||
if (gi > 0) {
|
||
const trenner = document.createElement("div");
|
||
trenner.className = "d-flex align-items-center my-2 text-body-secondary";
|
||
trenner.innerHTML = "<hr class='flex-grow-1 m-0'>"
|
||
+ "<span class='px-3 small fw-bold'>ODER</span>"
|
||
+ "<hr class='flex-grow-1 m-0'>";
|
||
ziel.appendChild(trenner);
|
||
}
|
||
|
||
const block = document.createElement("div");
|
||
block.className = "border rounded p-2";
|
||
|
||
gruppe.forEach((bed, bi) => {
|
||
if (bi > 0) {
|
||
const und = document.createElement("div");
|
||
und.className = "small fw-bold text-body-secondary ms-1 my-1";
|
||
und.textContent = "und";
|
||
block.appendChild(und);
|
||
}
|
||
block.appendChild(conditionRow(gi, bi, bed));
|
||
});
|
||
|
||
const add = document.createElement("button");
|
||
add.type = "button";
|
||
add.className = "btn btn-outline-success btn-sm mt-2";
|
||
add.innerHTML = "<i class='bi bi-plus-lg'></i> Bedingung";
|
||
add.onclick = () => {
|
||
const state = ersterMesswert();
|
||
if (!state) return;
|
||
gruppe.push({ state_id: state.id, operator: state.operators[0].value, value: standardWert(state) });
|
||
renderConditions();
|
||
};
|
||
block.appendChild(add);
|
||
|
||
ziel.appendChild(block);
|
||
});
|
||
|
||
renderSummary();
|
||
}
|
||
|
||
function conditionRow(gi, bi, bed) {
|
||
const zeile = document.createElement("div");
|
||
zeile.className = "input-group";
|
||
|
||
const treffer = stateById(bed.state_id);
|
||
const dev = treffer ? treffer.device : null;
|
||
const state = treffer ? treffer.state : null;
|
||
|
||
// Geraet
|
||
const geraet = document.createElement("select");
|
||
geraet.className = "form-select";
|
||
optionListe(geraet, autoDevices.filter(d => d.states.length)
|
||
.map(d => ({ value: d.id, text: d.name })),
|
||
dev ? dev.id : "");
|
||
geraet.onchange = () => {
|
||
const neu = deviceById(Number(geraet.value));
|
||
if (!neu || !neu.states.length) return;
|
||
bed.state_id = neu.states[0].id;
|
||
bed.operator = neu.states[0].operators[0].value;
|
||
bed.value = standardWert(neu.states[0]);
|
||
renderConditions();
|
||
};
|
||
zeile.appendChild(geraet);
|
||
|
||
// Messwert - nur zeigen, wenn das Geraet mehr als einen hat
|
||
if (dev && dev.states.length > 1) {
|
||
const messwert = document.createElement("select");
|
||
messwert.className = "form-select";
|
||
optionListe(messwert, dev.states.map(s => ({
|
||
value: s.id,
|
||
text: s.name + (s.value ? " (= " + s.value + ")" : "")
|
||
})), bed.state_id);
|
||
messwert.onchange = () => {
|
||
const neu = dev.states.find(s => s.id === Number(messwert.value));
|
||
bed.state_id = neu.id;
|
||
bed.operator = neu.operators[0].value;
|
||
bed.value = standardWert(neu);
|
||
renderConditions();
|
||
};
|
||
zeile.appendChild(messwert);
|
||
}
|
||
|
||
if (state) {
|
||
// Operator. Eine Auswahlliste statt des frueheren Umschalt-Knopfes: der
|
||
// zeigte nur seinen aktuellen Zustand, welche Vergleiche es sonst noch
|
||
// gibt, fand man erst durch Klicken heraus.
|
||
const op = document.createElement("select");
|
||
// Feste, schmale Breite: der Pfeil der Auswahlliste braucht Platz, sonst
|
||
// schiebt er das Zeichen aus dem sichtbaren Bereich.
|
||
op.className = "form-select flex-grow-0 text-center";
|
||
op.style.flex = "0 0 5.5rem";
|
||
optionListe(op, state.operators.map(o => ({ value: o.value, text: o.label })), bed.operator);
|
||
op.onchange = () => { bed.operator = op.value; renderSummary(); };
|
||
zeile.appendChild(op);
|
||
|
||
const wert = wertFeld(state, bed.value);
|
||
wert.onchange = () => { bed.value = wert.value; renderSummary(); };
|
||
zeile.appendChild(wert);
|
||
|
||
if (state.unit) {
|
||
const einheit = document.createElement("span");
|
||
einheit.className = "input-group-text";
|
||
einheit.textContent = state.unit;
|
||
zeile.appendChild(einheit);
|
||
}
|
||
}
|
||
|
||
const weg = document.createElement("button");
|
||
weg.type = "button";
|
||
weg.className = "btn btn-outline-danger";
|
||
weg.innerHTML = "<i class='bi bi-trash3'></i>";
|
||
weg.onclick = () => {
|
||
autoModel.groups[gi].splice(bi, 1);
|
||
// Ein leerer Block hat keine Bedeutung mehr und verschwindet mit.
|
||
if (!autoModel.groups[gi].length) autoModel.groups.splice(gi, 1);
|
||
renderConditions();
|
||
};
|
||
zeile.appendChild(weg);
|
||
|
||
return zeile;
|
||
}
|
||
|
||
/**
|
||
* Derselbe Satz, den auch die Uebersicht und der Runner benutzen. Er steht
|
||
* unter den Bloecken, damit man die Klammerung nicht aus der Optik erschliessen
|
||
* muss, sondern schwarz auf weiss liest.
|
||
*/
|
||
function conditionText() {
|
||
const teile = autoModel.groups.filter(g => g.length).map(gruppe => {
|
||
const text = gruppe.map(bed => {
|
||
const treffer = stateById(bed.state_id);
|
||
if (!treffer) return "?";
|
||
let s = treffer.device.name + ": " + treffer.state.name + " "
|
||
+ operatorLabel(treffer.state, bed.operator) + " " + bed.value;
|
||
if (treffer.state.unit) s += " " + treffer.state.unit;
|
||
return s;
|
||
}).join(" und ");
|
||
return (autoModel.groups.length > 1 && gruppe.length > 1) ? "(" + text + ")" : text;
|
||
});
|
||
return teile.join(" oder ");
|
||
}
|
||
|
||
function renderSummary() {
|
||
const text = conditionText();
|
||
document.getElementById("condSummary").innerHTML = text
|
||
? "Löst aus, wenn <strong>" + text.replace(/&/g, "&").replace(/</g, "<") + "</strong>."
|
||
: "Noch kein Auslöser – die Automatik würde nie starten.";
|
||
}
|
||
|
||
// --- Aktionen ------------------------------------------------------------
|
||
|
||
function renderActions() {
|
||
const ziel = document.getElementById("actionList");
|
||
ziel.innerHTML = "";
|
||
autoModel.actions.forEach((aktion, ai) => ziel.appendChild(actionRow(ai, aktion)));
|
||
}
|
||
|
||
function actionRow(ai, aktion) {
|
||
const zeile = document.createElement("div");
|
||
zeile.className = "input-group mb-2";
|
||
|
||
const treffer = commandById(aktion.command_id);
|
||
const dev = treffer ? treffer.device : null;
|
||
const cmd = treffer ? treffer.command : null;
|
||
|
||
const geraet = document.createElement("select");
|
||
geraet.className = "form-select";
|
||
optionListe(geraet, autoDevices.filter(d => d.commands.length)
|
||
.map(d => ({ value: d.id, text: d.name })),
|
||
dev ? dev.id : "");
|
||
geraet.onchange = () => {
|
||
const neu = deviceById(Number(geraet.value));
|
||
if (!neu || !neu.commands.length) return;
|
||
aktion.command_id = neu.commands[0].id;
|
||
aktion.params = standardParams(neu.commands[0]);
|
||
renderActions();
|
||
};
|
||
zeile.appendChild(geraet);
|
||
|
||
if (dev && dev.commands.length > 1) {
|
||
const befehl = document.createElement("select");
|
||
befehl.className = "form-select";
|
||
optionListe(befehl, dev.commands.map(c => ({ value: c.id, text: c.name })), aktion.command_id);
|
||
befehl.onchange = () => {
|
||
const neu = dev.commands.find(c => c.id === Number(befehl.value));
|
||
aktion.command_id = neu.id;
|
||
aktion.params = standardParams(neu);
|
||
renderActions();
|
||
};
|
||
zeile.appendChild(befehl);
|
||
}
|
||
|
||
// Je Parameter ein Feld - wie viele es sind, sagt das Geraet. Frueher gab es
|
||
// vier feste Felder "Wert 1" bis "Wert 4", die je nach Kommando versteckt
|
||
// wurden, ihren Inhalt aber trotzdem mitschickten.
|
||
if (cmd) {
|
||
cmd.params.forEach(p => {
|
||
const beschriftung = document.createElement("span");
|
||
beschriftung.className = "input-group-text";
|
||
beschriftung.textContent = p.name;
|
||
zeile.appendChild(beschriftung);
|
||
|
||
const wert = wertFeld(p, aktion.params[p.id] !== undefined ? aktion.params[p.id] : standardWert(p));
|
||
wert.onchange = () => { aktion.params[p.id] = wert.value; };
|
||
zeile.appendChild(wert);
|
||
});
|
||
}
|
||
|
||
const weg = document.createElement("button");
|
||
weg.type = "button";
|
||
weg.className = "btn btn-outline-danger";
|
||
weg.innerHTML = "<i class='bi bi-trash3'></i>";
|
||
weg.onclick = () => { autoModel.actions.splice(ai, 1); renderActions(); };
|
||
zeile.appendChild(weg);
|
||
|
||
return zeile;
|
||
}
|
||
|
||
function standardParams(cmd) {
|
||
const params = {};
|
||
cmd.params.forEach(p => { params[p.id] = standardWert(p); });
|
||
return params;
|
||
}
|
||
|
||
// --- Rahmenbedingungen ---------------------------------------------------
|
||
|
||
const WOCHENTAGE = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
||
|
||
function renderWeekdays() {
|
||
const ziel = document.getElementById("weekdayGroup");
|
||
ziel.innerHTML = "";
|
||
WOCHENTAGE.forEach((tag, i) => {
|
||
const feld = document.createElement("div");
|
||
feld.className = "input-group-text";
|
||
feld.innerHTML = "<input class='form-check-input mt-0 me-1' type='checkbox' id='day" + i + "'"
|
||
+ (autoModel.weekdays & (1 << i) ? " checked" : "") + ">"
|
||
+ "<label class='form-check-label' for='day" + i + "'>" + tag + "</label>";
|
||
feld.querySelector("input").onchange = e => {
|
||
if (e.target.checked) autoModel.weekdays |= (1 << i);
|
||
else autoModel.weekdays &= ~(1 << i);
|
||
};
|
||
ziel.appendChild(feld);
|
||
});
|
||
}
|
||
|
||
function fuelleFormular() {
|
||
const sperre = document.getElementById("lockoutSecs");
|
||
optionListe(sperre, autoLockouts.map(o => ({ value: o.value, text: o.label })),
|
||
autoModel.lockout_secs);
|
||
document.getElementById("autoName").value = autoModel.name;
|
||
// Kennt die Liste die Etage nicht, bleibt eine Auswahlliste sonst leer
|
||
// stehen und schickt beim Speichern "" mit.
|
||
const etage = document.getElementById("autoFloor");
|
||
etage.value = autoModel.floor;
|
||
if (etage.selectedIndex < 0) {
|
||
etage.selectedIndex = 0;
|
||
autoModel.floor = etage.value;
|
||
}
|
||
document.getElementById("tSpanFrom").value = autoModel.window_from;
|
||
document.getElementById("tSpanTo").value = autoModel.window_to;
|
||
document.getElementById("actFerien").checked = autoModel.on_vacation;
|
||
document.getElementById("actFeier").checked = autoModel.on_holiday;
|
||
document.getElementById("runOnce").checked = autoModel.force_once;
|
||
renderWeekdays();
|
||
renderConditions();
|
||
renderActions();
|
||
}
|
||
|
||
/** Die Felder, die direkt am Formular haengen, ins Modell zurueckholen. */
|
||
function leseFormular() {
|
||
autoModel.name = document.getElementById("autoName").value.trim();
|
||
autoModel.floor = document.getElementById("autoFloor").value;
|
||
autoModel.window_from = document.getElementById("tSpanFrom").value;
|
||
autoModel.window_to = document.getElementById("tSpanTo").value;
|
||
autoModel.on_vacation = document.getElementById("actFerien").checked;
|
||
autoModel.on_holiday = document.getElementById("actFeier").checked;
|
||
autoModel.force_once = document.getElementById("runOnce").checked;
|
||
autoModel.lockout_secs = Number(document.getElementById("lockoutSecs").value);
|
||
}
|
||
|
||
// --- Modal ---------------------------------------------------------------
|
||
|
||
function loadAutomatic() {
|
||
const daten = JSON.parse(document.getElementById("autoActionData").textContent);
|
||
autoDevices = daten.devices;
|
||
autoLockouts = daten.lockouts;
|
||
|
||
const a = daten.automation;
|
||
autoModel = {
|
||
id: a.id,
|
||
name: a.name,
|
||
floor: a.floor,
|
||
enabled: a.enabled,
|
||
window_from: a.window_from,
|
||
window_to: a.window_to,
|
||
weekdays: a.weekdays,
|
||
on_vacation: a.on_vacation,
|
||
on_holiday: a.on_holiday,
|
||
force_once: a.force_once,
|
||
lockout_secs: a.lockout_secs,
|
||
groups: [],
|
||
actions: a.actions.map(x => ({ command_id: x.command_id, params: Object.assign({}, x.params) }))
|
||
};
|
||
|
||
// Die flache Liste aus der Datenbank wieder in Bloecke fassen. Die
|
||
// Gruppennummern kommen aufsteigend und dicht, siehe saveAutomation().
|
||
const nachGruppe = new Map();
|
||
a.conditions.forEach(c => {
|
||
if (!nachGruppe.has(c.group)) nachGruppe.set(c.group, []);
|
||
nachGruppe.get(c.group).push({ state_id: c.state_id, operator: c.operator, value: c.value });
|
||
});
|
||
autoModel.groups = Array.from(nachGruppe.values());
|
||
|
||
// Eine neue Automatik startet mit einer leeren Bedingung und einer Aktion,
|
||
// sonst steht man vor einem leeren Formular ohne erkennbaren Anfang.
|
||
if (!autoModel.groups.length) {
|
||
const state = ersterMesswert();
|
||
if (state) {
|
||
autoModel.groups = [[{ state_id: state.id, operator: state.operators[0].value, value: standardWert(state) }]];
|
||
}
|
||
}
|
||
if (!autoModel.actions.length) {
|
||
const cmd = ersterBefehl();
|
||
if (cmd) autoModel.actions = [{ command_id: cmd.id, params: standardParams(cmd) }];
|
||
}
|
||
|
||
fuelleFormular();
|
||
}
|
||
|
||
function openAutoActionModal(params) {
|
||
const contentURL = "./ajax/AutoAction.php" + params;
|
||
document.getElementById("modal-title").innerHTML =
|
||
params.search("id=") > 0 ? "Automatik bearbeiten" : "Neue Automatik anlegen";
|
||
|
||
// Der Speichern-Knopf gehoert allen Modals gemeinsam. Ein Klon ohne
|
||
// Zuhoerer stellt sicher, dass nicht auch noch der Handler eines vorher
|
||
// geoeffneten Modals mitfeuert.
|
||
const alt = document.getElementById("modalSaveBtn");
|
||
const btn = alt.cloneNode(true);
|
||
alt.replaceWith(btn);
|
||
btn.addEventListener("click", submitAutoAction);
|
||
|
||
// Der Editor braucht mehr Breite als die Ladedialoge, gibt sie aber wieder
|
||
// her, damit die anderen Modals unveraendert aussehen.
|
||
const dialog = document.querySelector("#modalEV .modal-dialog");
|
||
dialog.classList.add("modal-xl");
|
||
document.getElementById("modalEV").addEventListener("hidden.bs.modal",
|
||
() => dialog.classList.remove("modal-xl"), { once: true });
|
||
|
||
const modalBodyElement = document.getElementById("modal-body");
|
||
modalBodyElement.innerHTML = loadingHTML("Wird geladen...");
|
||
modalEV.show();
|
||
|
||
fetch(contentURL, {
|
||
method: "GET",
|
||
headers: { "X-Requested-From-Modal": "a", "Requested-With-Ajax": "ajax" }
|
||
})
|
||
.then(response => response.text())
|
||
.then(html => {
|
||
modalBodyElement.innerHTML = html;
|
||
loadAutomatic();
|
||
document.getElementById("btnAddGroup").addEventListener("click", () => {
|
||
const state = ersterMesswert();
|
||
if (!state) return;
|
||
autoModel.groups.push([{ state_id: state.id, operator: state.operators[0].value, value: standardWert(state) }]);
|
||
renderConditions();
|
||
});
|
||
document.getElementById("btnAddActor").addEventListener("click", () => {
|
||
const cmd = ersterBefehl();
|
||
if (!cmd) return;
|
||
autoModel.actions.push({ command_id: cmd.id, params: standardParams(cmd) });
|
||
renderActions();
|
||
});
|
||
})
|
||
.catch(error => {
|
||
modalBodyElement.innerHTML = "Konnte den Editor nicht laden: " + error.message;
|
||
});
|
||
}
|
||
|
||
function submitAutoAction() {
|
||
leseFormular();
|
||
const nutzlast = {
|
||
id: autoModel.id,
|
||
name: autoModel.name,
|
||
floor: autoModel.floor,
|
||
enabled: autoModel.enabled,
|
||
window_from: autoModel.window_from,
|
||
window_to: autoModel.window_to,
|
||
weekdays: autoModel.weekdays,
|
||
on_vacation: autoModel.on_vacation,
|
||
on_holiday: autoModel.on_holiday,
|
||
force_once: autoModel.force_once,
|
||
lockout_secs: autoModel.lockout_secs,
|
||
conditions: [],
|
||
actions: autoModel.actions
|
||
};
|
||
autoModel.groups.forEach((gruppe, gi) => {
|
||
gruppe.forEach(bed => {
|
||
nutzlast.conditions.push({ group: gi, state_id: bed.state_id, operator: bed.operator, value: bed.value });
|
||
});
|
||
});
|
||
|
||
automationRequest("save", nutzlast).then(antwort => {
|
||
if (antwort.error) {
|
||
alert(antwort.error);
|
||
return;
|
||
}
|
||
modalEV.hide();
|
||
refreshAutomations(nutzlast.floor);
|
||
});
|
||
}
|
||
|
||
// --- Uebersicht ----------------------------------------------------------
|
||
|
||
function automationRequest(action, body) {
|
||
return fetch("./ajax/AutoAction.php?action=" + action, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json", "Requested-With-Ajax": "ajax" },
|
||
body: JSON.stringify(body)
|
||
})
|
||
.then(response => response.json())
|
||
.catch(error => ({ error: error.message }));
|
||
}
|
||
|
||
/** Die Tabelle einer Etage neu holen. Ohne Etage alle drei. */
|
||
function refreshAutomations(floor) {
|
||
const etagen = floor ? [floor] : ["UG", "EG", "OG"];
|
||
etagen.forEach(etage => {
|
||
const ziel = document.getElementById("actions-" + etage + "-list");
|
||
if (!ziel) return;
|
||
fetch("./ajax/AutoAction.php?action=list&floor=" + etage, {
|
||
method: "GET",
|
||
headers: { "Requested-With-Ajax": "ajax" }
|
||
})
|
||
.then(response => response.text())
|
||
.then(html => { ziel.innerHTML = html; })
|
||
.catch(() => { ziel.innerHTML = "<p class='text-danger'>Liste konnte nicht geladen werden.</p>"; });
|
||
});
|
||
}
|
||
|
||
function toggleAutomation(id) {
|
||
automationRequest("toggle", { id: id }).then(antwort => {
|
||
if (antwort.error) alert(antwort.error);
|
||
else refreshAutomations(null);
|
||
});
|
||
}
|
||
|
||
function deleteAutomation(id, name) {
|
||
if (!confirm("Automatik \"" + name + "\" wirklich löschen?")) return;
|
||
automationRequest("delete", { id: id }).then(antwort => {
|
||
if (antwort.error) alert(antwort.error);
|
||
else refreshAutomations(null);
|
||
});
|
||
}
|
||
|
||
document.addEventListener("DOMContentLoaded", () => refreshAutomations(null));
|