Das Akkordeon schloss beim Oeffnen die anderen Faecher - Ausloeser und
Aktion waren also nie gleichzeitig zu sehen, ausgerechnet bei einer
Regel, die genau daraus besteht. Und das mittlere Fach hiess
"Bedingungen", enthielt aber die Rahmenbedingungen; die echten standen
im ersten. Jetzt drei sichtbare Baender: Wenn, Wann, Dann, gebaut aus
.card mit farbigem Rand und .card-header.
Die Bedingungszeile magert von fuenf gleich schweren Kaesten auf zwei
ab. Das Geraet steht klein und matt ueber dem Messwert - es beantwortet
"woher", nicht "was" -, der Messwert ist Text mit gepunkteter Linie,
und nur Vergleich und Wert sehen noch wie Eingabefelder aus. Dort
steckt die Aussage der Bedingung, dort tippt man auch. Die Einheit
sitzt im Wertfeld statt in einem sechsten Kasten, der Papierkorb
erscheint erst beim Ueberfahren.
Neben jeder Bedingung laeuft ihr aktueller Wert mit. ?action=werte
liefert alle acht Sekunden {state_id: {value, unit}} aus actor_states -
bewusst von dort und nicht ueber MQTT, weil dort nur ein Teil der
Geraete auftaucht: die Jalousien haengen an der Tahoma-Box, die
gerechneten Werte an gar nichts. Das ersetzt den Satz, der frueher am
Ende stand und die Regel Wort fuer Wort nacherzaehlte: statt zu
wiederholen, was darueber steht, beantwortet die Zeile die Frage, die
man wirklich hat - warum laeuft die Automatik gerade nicht?
Bei time, date, datetime, deltatime und elapsed faellt der Browser
bewusst kein Urteil. Ob "Uhrzeit um 07:30" zutrifft, haengt am
Nachholfenster, am Tagesrand und bei elapsed am echten Abstand seit der
letzten Ausloesung - das steht im Runner. Es hier nachzubauen hiesse,
eine zweite Wahrheit zu pflegen, die auseinanderlaeuft. Diese Zeilen
bekommen einen gestrichelten Punkt statt einer Aussage.
Der Takt malt nur nach, statt neu zu bauen: renderConditions() ersetzt
den ganzen Block, damit waeren alle acht Sekunden eine offene
Geraeteliste weg und der Cursor aus einem Wertfeld, in das man gerade
tippt.
Der Rahmen steht zusammengefaltet da, solange nichts von der Vorgabe
abweicht - und das ist der Normalfall; vorher nahm er die meiste
Flaeche fuer den seltensten Inhalt. Aufgeklappt tauschen die drei
Vorlagen (taeglich, werktags, Wochenende) mit der Kurzfassung den
Platz. Sie stehen ausserhalb des Elements mit data-bs-toggle, und das
ist kein Zufall: Bootstrap haelt seinen Umschalter am Dokument in der
Capture-Phase, ein stopPropagation() im Knopf kaeme grundsaetzlich zu
spaet.
Getragen wird das von vier Variablen in solar.css, damit der Rest der
Seite mitzieht statt hinterherzuhinken: --ton-radius auf .875rem,
--ton-radius-klein neu auf .5rem (beide an Bootstraps
--bs-border-radius und -sm), --font-anzeige auf Poppins fuer alle
Ueberschriften. Poppins lag seit jeher unter assets/fonts und trug
bisher die vier Etagenknoepfe im Aussenplan - eine ganze Schrift fuer
vier Buchstaben. Fliesstext und Zahlenkolonnen bleiben beim Theme, weil
Poppins keine Tabellenziffern mitbringt.
Drei neue Klassen stehen absichtlich in solar.css und nicht im Editor.
.feld-als-text ist ein Auswahlfeld, das wie Text aussieht, bis man es
anfaehrt - dieselbe Not haben das Raum-Modal und die
Kachel-Einstellungen. .punkt ist an oder aus. .wert-jetzt ist die eine
echte Ausnahme und als einzelne Klasse auch als solche erkennbar. Der
Satz im Wann-Band braucht gar keine: .callout gibt es in AdminLTE, und
seine Toene kommen aus Bootstraps *-bg-subtle-Variablen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
467 lines
21 KiB
PHP
467 lines
21 KiB
PHP
<?php
|
||
/**
|
||
* Endpunkt fuer die Automatiken: Editor, Uebersicht, Speichern, Loeschen.
|
||
*
|
||
* Alles laeuft ueber ?action=... , damit Formular und Uebersicht dieselbe
|
||
* Modell-Schicht (restricted/automations.php) benutzen und nicht zwei
|
||
* Wahrheiten ueber denselben Datensatz entstehen.
|
||
*
|
||
* GET ?action=editor&id=N Editor fuer eine vorhandene Automatik
|
||
* GET ?action=editor&floor=OG Editor fuer eine neue Automatik
|
||
* GET ?action=list&floor=OG Tabelle fuer die Karte "Automatismen"
|
||
* POST ?action=save JSON-Rumpf, legt an oder ueberschreibt
|
||
* GET ?action=followers&id=N JSON: wer haengt an dieser Automatik?
|
||
* GET ?action=werte&ids=1,2,3 JSON: was diese Messwerte gerade sagen
|
||
* POST ?action=delete {"id": N, "mode": "abhaengen"|"mitloeschen"}
|
||
* POST ?action=toggle {"id": N} - pausieren / fortsetzen
|
||
*/
|
||
|
||
require_once("../helper.php");
|
||
require_once("../restricted/automations.php");
|
||
|
||
if (!checkLogin()) {
|
||
http_response_code(403);
|
||
exit;
|
||
}
|
||
|
||
function jsonAntwort($data, $code = 200)
|
||
{
|
||
http_response_code($code);
|
||
header("Content-Type: application/json; charset=utf-8");
|
||
echo json_encode($data, JSON_UNESCAPED_UNICODE);
|
||
exit;
|
||
}
|
||
|
||
$action = $_GET["action"] ?? "editor";
|
||
$floor = in_array($_GET["floor"] ?? "", $floors, true) ? $_GET["floor"] : "";
|
||
|
||
// --- Schreibende Aufrufe -------------------------------------------------
|
||
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
||
$body = json_decode(file_get_contents("php://input"), true);
|
||
if (!is_array($body)) {
|
||
jsonAntwort(["error" => "Konnte die Anfrage nicht lesen."], 400);
|
||
}
|
||
try {
|
||
switch ($action) {
|
||
case "save":
|
||
$id = saveAutomation($body);
|
||
jsonAntwort(["id" => $id]);
|
||
case "delete":
|
||
// Der Modus kommt aus der Rueckfrage im Browser. Unbekanntes
|
||
// faellt auf das schonendere "abhaengen" zurueck - lieber eine
|
||
// pausierte Automatik zu viel als eine geloeschte zu viel.
|
||
$modus = ($body["mode"] ?? "") === "mitloeschen"
|
||
? "mitloeschen" : "abhaengen";
|
||
deleteAutomation(intval($body["id"] ?? 0), $modus);
|
||
jsonAntwort(["ok" => true]);
|
||
case "toggle":
|
||
jsonAntwort(["enabled" => toggleAutomation(intval($body["id"] ?? 0))]);
|
||
default:
|
||
jsonAntwort(["error" => "Unbekannte Aktion."], 400);
|
||
}
|
||
} catch (InvalidArgumentException $e) {
|
||
jsonAntwort(["error" => $e->getMessage()], 400);
|
||
} catch (Throwable $e) {
|
||
jsonAntwort(["error" => "Speichern fehlgeschlagen: " . $e->getMessage()], 500);
|
||
}
|
||
}
|
||
|
||
// --- Wer haengt an dieser Automatik? -------------------------------------
|
||
// Wird vor dem Loeschen gefragt, damit die Rueckfrage im Browser die Namen
|
||
// nennen kann und nicht nur eine Zahl.
|
||
if ($action === "followers") {
|
||
jsonAntwort(["followers" => automationFollowers(intval($_GET["id"] ?? 0))]);
|
||
}
|
||
|
||
// --- Was sagen diese Messwerte gerade? -----------------------------------
|
||
// Der offene Editor fragt das im Takt nach, damit neben jeder Bedingung der
|
||
// laufende Wert steht. Absichtlich ein eigener, winziger Endpunkt: den
|
||
// ganzen Geraetekatalog neu zu holen waeren je Runde ueber hundert
|
||
// Kilobyte fuer ein paar Zahlen.
|
||
if ($action === "werte") {
|
||
$ids = array_slice(explode(",", $_GET["ids"] ?? ""), 0, 200);
|
||
jsonAntwort(["werte" => stateValues($ids)]);
|
||
}
|
||
|
||
// --- Uebersichtstabelle --------------------------------------------------
|
||
if ($action === "list") {
|
||
$tage = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
||
$list = listAutomations($floor);
|
||
if (!$list) {
|
||
echo "<p class='text-body-secondary mb-2'>Für diese Etage ist noch nichts eingerichtet.</p>";
|
||
} else {
|
||
echo "<div class='table-responsive'><table class='table table-hover align-middle'>";
|
||
// Je schmaler der Schirm, desto weniger Spalten: auf dem Handy bleiben
|
||
// Name, Ausloeser und die Knoepfe. Sonst muss man seitwaerts scrollen,
|
||
// um eine Automatik ueberhaupt pausieren zu koennen.
|
||
echo "<thead><tr><th>Name</th><th>Auslöser</th>"
|
||
. "<th class='d-none d-sm-table-cell'>Aktion</th>"
|
||
. "<th class='d-none d-md-table-cell'>Wann</th>"
|
||
. "<th class='d-none d-md-table-cell'>Zuletzt</th><th></th></tr></thead><tbody>";
|
||
|
||
/*
|
||
* Eine Automatik, die von einer anderen ausgeloest wird, steht unter
|
||
* ihr statt alphabetisch irgendwo dazwischen - sonst liest man
|
||
* "Rollladen Magdalena morgens" und sieht nicht, woran es haengt.
|
||
*
|
||
* Eingeklappt, weil die Uebersicht sonst mit jeder Verkettung laenger
|
||
* wird, ohne mehr zu sagen: der Ausloeser ist das, was man sucht. Der
|
||
* Zaehler am Pfeil verraet, dass da noch etwas ist.
|
||
*
|
||
* Gruppiert wird nur die Darstellung. Jeder Nachfolger bleibt eine
|
||
* vollwertige Automatik mit eigener Pause, eigenen Rahmenbedingungen
|
||
* und eigener Zeile im Protokoll - sonst waere zu klaeren, wem die
|
||
* Pause gehoert und was das Protokoll zeigt.
|
||
*/
|
||
$nachFolgern = [];
|
||
foreach ($list as $a) {
|
||
foreach ($a["triggers"] as $davor) {
|
||
$nachFolgern[intval($davor)][] = intval($a["id"]);
|
||
}
|
||
}
|
||
$vorhanden = [];
|
||
foreach ($list as $a) {
|
||
$vorhanden[intval($a["id"])] = $a;
|
||
}
|
||
// Wessen Ausloeser nicht in dieser Etage steht, faengt selbst oben an.
|
||
$wurzeln = [];
|
||
foreach ($list as $a) {
|
||
$hatVater = false;
|
||
foreach ($a["triggers"] as $davor) {
|
||
if (isset($vorhanden[intval($davor)])) {
|
||
$hatVater = true;
|
||
}
|
||
}
|
||
if (!$hatVater) {
|
||
$wurzeln[] = intval($a["id"]);
|
||
}
|
||
}
|
||
$reihe = [];
|
||
$einordnen = function ($id, $tiefe, $vater) use (&$einordnen, &$reihe, $nachFolgern, $vorhanden) {
|
||
if (!isset($vorhanden[$id]) || isset($reihe[$id])) {
|
||
return;
|
||
}
|
||
$reihe[$id] = ["auto" => $vorhanden[$id], "tiefe" => $tiefe,
|
||
"vater" => $vater, "kinder" => 0];
|
||
foreach ($nachFolgern[$id] ?? [] as $kind) {
|
||
$einordnen($kind, $tiefe + 1, $id);
|
||
}
|
||
};
|
||
foreach ($wurzeln as $id) {
|
||
$einordnen($id, 0, 0);
|
||
}
|
||
// Was durch einen Kreis nicht erreicht wurde, kommt trotzdem mit.
|
||
foreach ($list as $a) {
|
||
$einordnen(intval($a["id"]), 0, 0);
|
||
}
|
||
// Gezaehlt wird erst jetzt, und nur was wirklich eingeordnet wurde:
|
||
// wer zwei Ausloeser hat, steht nur unter dem ersten. Aus der rohen
|
||
// Nachfolgerliste gezaehlt verspraeche der zweite Pfeil eine Zeile,
|
||
// die er nicht aufklappen kann.
|
||
foreach ($reihe as $eintrag) {
|
||
if ($eintrag["vater"]) {
|
||
$reihe[$eintrag["vater"]]["kinder"]++;
|
||
}
|
||
}
|
||
|
||
foreach ($reihe as $eintrag) {
|
||
$a = $eintrag["auto"];
|
||
$tiefe = $eintrag["tiefe"];
|
||
$kinder = $eintrag["kinder"];
|
||
$vater = $eintrag["vater"];
|
||
// Nur die aktiven Tage nennen. Sieben Haekchen untereinander
|
||
// haben die Zeile frueher unnoetig hoch gemacht.
|
||
$aktiveTage = [];
|
||
for ($i = 0; $i < 7; $i++) {
|
||
if ($a["weekdays"] & (1 << $i)) {
|
||
$aktiveTage[] = $tage[$i];
|
||
}
|
||
}
|
||
// Kalendertage auf "immer" gehoeren zur Tagesangabe, nicht in
|
||
// die Nachbemerkung: sie fuegen Tage hinzu. Ohne angehakten
|
||
// Wochentag tragen sie die Automatik sogar allein - "nur
|
||
// Feiertage" ist damit eine gueltige Einstellung.
|
||
$dazu = [];
|
||
if ($a["on_vacation"] == 2) {
|
||
$dazu[] = "Ferientage";
|
||
}
|
||
if ($a["on_holiday"] == 2) {
|
||
$dazu[] = "Feiertage";
|
||
}
|
||
if (count($aktiveTage) === 7) {
|
||
$wann = "täglich";
|
||
} elseif ($aktiveTage) {
|
||
$wann = implode(" ", $aktiveTage);
|
||
if ($dazu) {
|
||
$wann .= " + " . implode(" + ", $dazu);
|
||
}
|
||
} elseif ($dazu) {
|
||
$wann = "nur " . implode(" + ", $dazu);
|
||
} else {
|
||
$wann = "nie – kein Tag ausgewählt";
|
||
}
|
||
if ($a["window_from"] !== "00:00" || $a["window_to"] !== "23:59") {
|
||
$wann .= ", " . $a["window_from"] . "–" . $a["window_to"];
|
||
}
|
||
if ($a["on_holiday"] == 0) {
|
||
$wann .= ", nicht an Feiertagen";
|
||
}
|
||
if ($a["on_vacation"] == 0) {
|
||
$wann .= ", nicht in den Ferien";
|
||
}
|
||
if ($a["once_per_day"]) {
|
||
$wann .= ", nur einmal am Tag";
|
||
}
|
||
if ($a["lockout_secs"] > 0) {
|
||
$wann .= ", frühestens alle "
|
||
. ($a["lockout_secs"] >= 60
|
||
? intdiv($a["lockout_secs"], 60) . " Min."
|
||
: $a["lockout_secs"] . " Sek.");
|
||
}
|
||
|
||
$zuletzt = $a["last_run"] ? date("d.m. H:i", strtotime($a["last_run"])) : "–";
|
||
$pause = $a["enabled"] ? "bi-pause-fill" : "bi-play-fill";
|
||
$titel = $a["enabled"] ? "Pausieren" : "Fortsetzen";
|
||
$id = intval($a["id"]);
|
||
|
||
$klassen = [];
|
||
if (!$a["enabled"]) {
|
||
$klassen[] = "opacity-50";
|
||
}
|
||
if ($tiefe > 0) {
|
||
// Eingeklappt starten: die Zeile gehoert dem Ausloeser, und
|
||
// der steht direkt darueber.
|
||
$klassen[] = "auto-folge";
|
||
$klassen[] = "auto-von-" . $vater;
|
||
$klassen[] = "d-none";
|
||
}
|
||
$klasse = $klassen ? " class='" . implode(" ", $klassen) . "'" : "";
|
||
|
||
// Der Pfeil klappt die Nachfolger auf. Er steht nur da, wo es
|
||
// welche gibt - ein Pfeil ohne Inhalt waere eine Einladung ins
|
||
// Leere.
|
||
$griff = "";
|
||
if ($kinder > 0) {
|
||
$griff = "<button type='button' class='btn btn-link btn-sm p-0 me-1 text-decoration-none'"
|
||
. " title='Abhängige Automatiken zeigen'"
|
||
. " data-gruppe='" . $id . "'"
|
||
. " onclick='toggleAutomationGroup(" . $id . ", this)'>"
|
||
. "<i class='bi bi-chevron-right'></i> " . $kinder . "</button>";
|
||
}
|
||
$einzug = $tiefe > 0
|
||
? "<span class='text-body-secondary me-1'>"
|
||
. str_repeat(" ", $tiefe)
|
||
. "<i class='bi bi-arrow-return-right'></i></span> "
|
||
: "";
|
||
|
||
echo "<tr data-auto='" . $id . "'" . $klasse . ">";
|
||
echo "<td class='text-nowrap'>" . $einzug . $griff
|
||
. htmlspecialchars($a["name"]) . "</td>";
|
||
echo "<td>" . htmlspecialchars($a["conditionText"]) . "</td>";
|
||
echo "<td class='d-none d-sm-table-cell'>" . htmlspecialchars($a["actionText"]) . "</td>";
|
||
echo "<td class='text-nowrap d-none d-md-table-cell'>" . htmlspecialchars($wann) . "</td>";
|
||
echo "<td class='text-nowrap d-none d-md-table-cell'>" . $zuletzt . "</td>";
|
||
echo "<td class='text-nowrap'>"
|
||
. "<button type='button' class='btn btn-info btn-sm' title='" . $titel . "'"
|
||
. " onclick='toggleAutomation(" . $id . ")'><i class='bi " . $pause . "'></i></button> "
|
||
. "<button type='button' class='btn btn-warning btn-sm' title='Bearbeiten'"
|
||
. " onclick=\"openAutoActionModal('?action=editor&id=" . $id . "')\"><i class='bi bi-pencil-square'></i></button> "
|
||
. "<button type='button' class='btn btn-danger btn-sm' title='Löschen'"
|
||
. " onclick='deleteAutomation(" . $id . ", \"" . htmlspecialchars($a["name"], ENT_QUOTES) . "\")'><i class='bi bi-trash3'></i></button>"
|
||
. "</td></tr>";
|
||
}
|
||
echo "</tbody></table></div>";
|
||
}
|
||
exit;
|
||
}
|
||
|
||
// --- Editor --------------------------------------------------------------
|
||
$id = intval($_GET["id"] ?? 0);
|
||
$auto = $id > 0 ? loadAutomation($id) : null;
|
||
if (!$auto) {
|
||
$auto = emptyAutomation($floor);
|
||
}
|
||
|
||
// Geraetekatalog und Datensatz reisen im selben Dokument mit. Frueher holte
|
||
// der Editor die Geraeteliste und je Zeile noch einmal deren Details nach -
|
||
// zwei zusaetzliche Anfragen pro Bedingung, und die Dropdowns fuellten sich
|
||
// asynchron, weshalb ein gespeicherter Wert nicht zuverlaessig ankam.
|
||
$nutzdaten = json_encode([
|
||
"automation" => $auto,
|
||
"devices" => deviceCatalog(),
|
||
"lockouts" => lockoutChoices(),
|
||
"ferien" => kalendertagChoices("Ferien", "in den Ferien"),
|
||
"feiertage" => kalendertagChoices("Feiertage", "an Feiertagen"),
|
||
"tagvorlagen" => tagVorlagen(),
|
||
// Nur fuer die Reihenfolge der Raum-Gruppen: rooms.php gibt sie vor, und
|
||
// Etage fuer Etage liest sich besser als alphabetisch.
|
||
"rooms" => raumListe(),
|
||
], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
|
||
|
||
?>
|
||
<script type="application/json" id="autoActionData"><?= $nutzdaten ?></script>
|
||
<form id="autoActionForm" class="p-2">
|
||
<div class="row g-2 mb-2">
|
||
<div class="col-8">
|
||
<div class="form-floating">
|
||
<input type="text" class="form-control" id="autoName" placeholder="Name" maxlength="100">
|
||
<label for="autoName">Name der Automatik</label>
|
||
</div>
|
||
</div>
|
||
<div class="col-4">
|
||
<div class="form-floating">
|
||
<!-- Ohne Etage waere die Automatik in keinem der drei Reiter zu
|
||
sehen und nur noch in der Datenbank auffindbar. Reihenfolge wie
|
||
die Reiter, damit der Rueckfall auf den ersten Eintrag dieselbe
|
||
Etage trifft wie die Startseite. -->
|
||
<select class="form-select" id="autoFloor">
|
||
<?php foreach ($floors as $etage): ?>
|
||
<option value="<?= $etage ?>" title="<?= htmlspecialchars(floorLabel($etage)) ?>"><?= $etage ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<label for="autoFloor">Etage</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!--
|
||
Drei Baender statt eines Akkordeons: Wenn - Wann - Dann.
|
||
|
||
Das Akkordeon schloss beim Oeffnen die anderen Faecher, Ausloeser und
|
||
Aktion waren also nie gleichzeitig zu sehen - ausgerechnet bei einer
|
||
Regel, die genau daraus besteht. Und das mittlere Fach hiess
|
||
"Bedingungen", enthielt aber die *Rahmen*bedingungen; die echten
|
||
Bedingungen standen im ersten.
|
||
|
||
Gebaut aus .card mit farbigem Rand und .card-header - kein eigenes
|
||
Bauteil. Die Ueberschriften tragen .card-title und bekommen darueber die
|
||
Anzeigeschrift aus solar.css.
|
||
-->
|
||
|
||
<div class="card border-primary mb-2">
|
||
<div class="card-header d-flex flex-wrap align-items-center gap-2 py-2">
|
||
<i class="bi bi-lightning-charge-fill text-primary"></i>
|
||
<span class="card-title mb-0 fs-5 text-primary">Wenn</span>
|
||
<span class="small text-body-secondary">… das alles zusammenkommt</span>
|
||
<span class="flex-grow-1"></span>
|
||
<!-- Wie viele Bedingungen gerade zutreffen. Fuellt renderConditions(). -->
|
||
<span id="condStand" class="d-flex align-items-center gap-2"></span>
|
||
</div>
|
||
<div class="card-body p-2">
|
||
<!-- Ein Block ist eine UND-Gruppe, zwischen den Bloecken steht ODER.
|
||
Die Klammerung ist damit gezeichnet und nicht bloss vereinbart. -->
|
||
<div id="condBlocks"></div>
|
||
<button class="btn btn-outline-secondary btn-sm mt-2" type="button" id="btnAddGroup">
|
||
<i class="bi bi-plus-lg"></i> oder stattdessen …
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card mb-2">
|
||
<!--
|
||
Der Rahmen steht zusammengefaltet da, solange er nur Vorgaben enthaelt -
|
||
und das ist der Normalfall. Frueher nahm er die meiste Flaeche fuer den
|
||
seltensten Inhalt.
|
||
-->
|
||
<!--
|
||
Zugeklappt liest man hier, was gilt; aufgeklappt greift man zu. Die drei
|
||
Vorlagen tauschen deshalb mit der Kurzfassung den Platz, statt eine
|
||
eigene Zeile im Rumpf zu bekommen - das war eine Zeile mehr und eine
|
||
Ueberschrift ("An welchen Tagen?"), die neben dem Wort "Wann" ohnehin
|
||
nichts Neues sagte.
|
||
|
||
ACHTUNG beim Umbauen: die Vorlagen duerfen NICHT innerhalb des Elements
|
||
stehen, das data-bs-toggle traegt. Bootstrap haengt seinen Umschalter
|
||
nicht an dieses Element, sondern ans Dokument - und dort in der
|
||
Capture-Phase, die vor jedem Klick auf einen Knopf darin laeuft. Ein
|
||
stopPropagation() im Knopf kommt deshalb grundsaetzlich zu spaet, und
|
||
jede Vorlage waere zugleich ein Zuklappen. Deshalb traegt nur der linke
|
||
Teil den Umschalter, die Vorlagen stehen daneben.
|
||
-->
|
||
<div class="card-header d-flex flex-wrap align-items-center gap-2 py-2">
|
||
<div class="d-flex align-items-center gap-2 flex-grow-1" id="rahmenKopf"
|
||
role="button" data-bs-toggle="collapse" data-bs-target="#rahmenBody"
|
||
aria-expanded="false">
|
||
<i class="bi bi-calendar-week text-warning"></i>
|
||
<span class="card-title mb-0 fs-5 text-warning">Wann</span>
|
||
<span id="rahmenKurz" class="text-body-secondary"></span>
|
||
<span class="flex-grow-1"></span>
|
||
<span class="small text-body-secondary text-nowrap" id="rahmenAendern">ändern <i class="bi bi-chevron-down"></i></span>
|
||
</div>
|
||
<div class="btn-group btn-group-sm" role="group" id="tagVorlagen" hidden></div>
|
||
<i class="bi bi-chevron-up text-body-secondary" id="rahmenZu" role="button"
|
||
data-bs-toggle="collapse" data-bs-target="#rahmenBody" hidden></i>
|
||
</div>
|
||
<div id="rahmenBody" class="collapse">
|
||
<div class="card-body">
|
||
<div class="btn-group btn-group-sm flex-wrap mb-2" role="group" id="weekdayGroup"></div>
|
||
<div class="row g-2 mb-2">
|
||
<div class="col-sm-6 d-flex align-items-center gap-2">
|
||
<span class="text-body-secondary small" style="min-width:5rem">Ferien</span>
|
||
<div class="btn-group btn-group-sm" role="group" id="ferienGroup"></div>
|
||
</div>
|
||
<div class="col-sm-6 d-flex align-items-center gap-2">
|
||
<span class="text-body-secondary small" style="min-width:5rem">Feiertage</span>
|
||
<div class="btn-group btn-group-sm" role="group" id="feiertagGroup"></div>
|
||
</div>
|
||
</div>
|
||
<!--
|
||
.callout ist ein AdminLTE-Bauteil: farbiger Balken links, getoenter
|
||
Grund, und die Toene kommen aus Bootstraps *-bg-subtle-Variablen -
|
||
es zieht also mit dem Theme mit, ohne dass hier eine Regel steht.
|
||
Als .alert stand der Satz vorher in einem eingesunkenen Kasten mit
|
||
Rahmen ringsum und las sich wie eine Warnung; er ist aber das
|
||
Ergebnis der Schalter darueber. Die Polsterung stellen Hilfsklassen
|
||
kleiner - .callout ist auf ganze Absaetze ausgelegt, hier steht ein
|
||
Satz.
|
||
-->
|
||
<div class="callout callout-primary py-2 px-3 mt-0 mb-3" id="rahmenSummary"></div>
|
||
|
||
<div class="row g-2 mb-3">
|
||
<div class="col-sm-6">
|
||
<div class="input-group input-group-sm">
|
||
<span class="input-group-text">von</span>
|
||
<input type="time" class="form-control" id="tSpanFrom" value="00:00">
|
||
<span class="input-group-text">bis</span>
|
||
<input type="time" class="form-control" id="tSpanTo" value="23:59">
|
||
</div>
|
||
</div>
|
||
<div class="col-sm-6">
|
||
<div class="input-group input-group-sm">
|
||
<span class="input-group-text">Sperre</span>
|
||
<select class="form-select" id="lockoutSecs"></select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="checkbox" id="oncePerDay">
|
||
<label class="form-check-label" for="oncePerDay">
|
||
Nur einmal am Tag – danach bis Mitternacht gesperrt
|
||
</label>
|
||
</div>
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="checkbox" id="runOnce">
|
||
<label class="form-check-label" for="runOnce">
|
||
Am Ende des Zeitraums auf jeden Fall ausführen, falls der Auslöser nie zutraf
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card border-success mb-0">
|
||
<div class="card-header d-flex flex-wrap align-items-center gap-2 py-2">
|
||
<i class="bi bi-play-fill text-success"></i>
|
||
<span class="card-title mb-0 fs-5 text-success">Dann</span>
|
||
<span class="small text-body-secondary">… passiert das hier</span>
|
||
</div>
|
||
<div class="card-body p-2">
|
||
<div id="actionList"></div>
|
||
<button class="btn btn-outline-success btn-sm mt-2" type="button" id="btnAddActor">
|
||
<i class="bi bi-plus-lg"></i> und noch etwas tun
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</form>
|