diff --git a/js/solar/autoActionFuncs.js b/js/solar/autoActionFuncs.js index 1a2403a..50d279d 100644 --- a/js/solar/autoActionFuncs.js +++ b/js/solar/autoActionFuncs.js @@ -82,74 +82,115 @@ function option(e, gewaehlt) { } /** - * Geraeteliste gruppiert - nach Raum, wo zugeordnet, sonst nach Geraeteart. + * Geraeteliste fuer die Auswahl: eine Ueberschrift je Etage, darin nach Raum + * sortiert. * - * Beim Anlegen einer Automatik denkt man in Raeumen ("im Bad soll..."), nicht - * in Geraetearten. Der Raum steht aber nur da, wo er von Hand vergeben wurde - * (siehe Zuordnung ueber das Zahnrad in der Karte "Automatismen"); solange - * das niemand getan hat, bleibt die Gruppierung nach Art - besser als eine - * einzige Gruppe "Nicht zugeordnet". + * Beim Anlegen einer Automatik denkt man in Raeumen ("im Bad soll ..."), eine + * Ueberschrift je Raum waeren aber zwanzig Stueck - in einer Liste, die + * fuenfzehn Zeilen hoch ist, saehe man dann mehr Ueberschrift als Geraet, und + * "OG · Bad", "EG · Bad", "UG · Bad" schrieben die Etage immer wieder + * dazu. Deshalb gruppiert nur die Etage; der Raum steht klein an jeder Zeile, + * und weil danach sortiert wird, stehen die Geraete eines Raums trotzdem + * beisammen. + * + * Der Raum steht nur da, wo er von Hand vergeben wurde (Zuordnung ueber das + * Zahnrad in der Karte "Automatismen"). Solange das niemand getan hat, gaebe + * die Etage nichts her - dann bleibt es bei der Ordnung nach Geraeteart. */ function geraeteGruppen(geraete) { if (!geraete.some(d => d.room)) { - return gruppiere(geraete, d => d.type, null, "Einzelne", true); + return nachGeraeteart(geraete); } - const gruppen = gruppiere(geraete, d => d.roomLabel, raumReihenfolge(), - "Nicht zugeordnet", false); - // Was noch keinen Raum hat, bleibt sonst ein einziger Eimer - solange erst - // ein Teil zugeordnet ist, waeren das die meisten Geraete. Innerhalb des - // Eimers gilt deshalb weiter die Ordnung nach Geraeteart. - const letzte = gruppen[gruppen.length - 1]; - if (!letzte || !letzte.titel.startsWith("Nicht zugeordnet")) return gruppen; - gruppen.pop(); - return gruppen.concat( - gruppiere(letzte.geraete, d => d.type, null, "Einzelne", true) - .map(g => ({ titel: "Ohne Raum · " + g.titel, geraete: g.geraete }))); -} - -/** Reihenfolge der Raum-Ueberschriften, wie in rooms.php notiert. */ -function raumReihenfolge() { - return autoRooms.map(r => r.label).concat(["Nicht zugeordnet"]); + const etagen = etagenReihenfolge(); + // "Ohne Raum" und "Unbekannter Raum" stehen in keiner Raumliste. Ihr + // indexOf() waere -1 und damit ganz vorne; sie gehoeren aber ans Ende. + const platz = etage => etagen.includes(etage) ? etagen.indexOf(etage) : etagen.length; + const nach = new Map(); + geraete.forEach(d => { + if (!nach.has(d.floorLabel)) nach.set(d.floorLabel, []); + nach.get(d.floorLabel).push(d); + }); + return Array.from(nach.keys()) + .sort((a, b) => platz(a) - platz(b) || a.localeCompare(b, "de")) + .map(etage => { + const liste = nach.get(etage); + liste.sort((a, b) => raumIndex(a.room) - raumIndex(b.room) + || a.type.localeCompare(b.type, "de") + || a.label.localeCompare(b.label, "de")); + return { titel: etage + " (" + liste.length + ")", geraete: liste }; + }); } /** - * Nach einem Merkmal gruppieren. - * - * `einzelneSammeln` entscheidet, was mit Gruppen aus einem einzigen Geraet - * passiert. Bei Gerätearten sollen sie zusammenfallen - fuenfzehn - * Ueberschriften mit je einem Eintrag waeren unuebersichtlicher als die - * flache Liste. Bei Raeumen nicht: "OG · Bad" ist auch mit einem einzigen - * Geraet die Ueberschrift, die man sucht, und in der Wohnung haengt oft nur - * ein Thermostat je Raum. + * Reihenfolge der Etagen-Ueberschriften, wie rooms.php sie notiert - erst OG, + * dann EG, dann UG, dann Aussen. */ -function gruppiere(geraete, merkmal, reihenfolge, restTitel, einzelneSammeln) { +function etagenReihenfolge() { + const etagen = []; + autoRooms.forEach(r => { if (!etagen.includes(r.floor)) etagen.push(r.floor); }); + return etagen; +} + +/** + * Platz eines Raums in rooms.php - die Sortierung innerhalb einer Etage. + * + * Alles ohne Raum bekommt denselben letzten Platz; dort entscheidet dann die + * Geraeteart, sonst stuenden Jalousie, Steckdose und Thermostat wild + * durcheinander. + */ +function raumIndex(key) { + const i = autoRooms.findIndex(r => r.key === key); + return i < 0 ? autoRooms.length : i; +} + +/** + * Was klein hinter dem Geraetenamen steht. + * + * Unter einer Etagen-Ueberschrift genuegt der Raum. In der Trefferliste einer + * Suche steht keine Ueberschrift darueber, dort muss die Etage mit. Wo kein + * Raum vergeben ist, steht die Geraeteart - die ist immer da und sagt + * wenigstens, was fuer ein Ding das ist. + */ +function zeilenZusatz(d, mitEtage) { + if (!d.roomName) return d.type; + return mitEtage ? d.roomLabel : d.roomName; +} + +/** + * Worueber die Suche laeuft: Name, Raum und Geraeteart in einem Zug. + * + * Der Raum gehoert dazu, seit die Liste nach Etagen gruppiert - "bad" soll + * auch den Handtuchtrockner finden, der im Bad haengt, ohne es im Namen zu + * tragen. Ohne Zuordnung faellt der Raum weg; "Nicht zugeordnet" als + * Suchbegriff waere kein Fund, sondern ein Versehen. + */ +function suchfeld(d) { + return (d.label + " " + (d.roomName ? d.roomLabel : "") + " " + d.type).toLowerCase(); +} + +/** + * Nach Geraeteart gruppieren - der Rueckfall, solange kein Raum vergeben ist. + * + * Gruppen aus einem einzigen Geraet fallen dabei unter "Einzelne" zusammen: + * fuenfzehn Ueberschriften mit je einem Eintrag waeren unuebersichtlicher als + * die flache Liste darunter. + */ +function nachGeraeteart(geraete) { const nach = new Map(); geraete.forEach(d => { - const schluessel = merkmal(d) || restTitel; - if (!nach.has(schluessel)) nach.set(schluessel, []); - nach.get(schluessel).push(d); - }); - const titel = Array.from(nach.keys()); - titel.sort((a, b) => { - if (reihenfolge) { - const ia = reihenfolge.indexOf(a), ib = reihenfolge.indexOf(b); - if (ia !== ib) return (ia < 0 ? 999 : ia) - (ib < 0 ? 999 : ib); - } - return a.localeCompare(b, "de"); + if (!nach.has(d.type)) nach.set(d.type, []); + nach.get(d.type).push(d); }); const gruppen = []; const rest = []; - titel.forEach(t => { - const liste = nach.get(t); - if (!einzelneSammeln || liste.length > 1 || t === restTitel) { - gruppen.push({ titel: t + " (" + liste.length + ")", geraete: liste }); - } else { - rest.push(liste[0]); - } + Array.from(nach.keys()).sort((a, b) => a.localeCompare(b, "de")).forEach(art => { + const liste = nach.get(art); + if (liste.length > 1) gruppen.push({ titel: art + " (" + liste.length + ")", geraete: liste }); + else rest.push(liste[0]); }); if (rest.length) { rest.sort((a, b) => a.label.localeCompare(b.label, "de")); - gruppen.push({ titel: restTitel + " (" + rest.length + ")", geraete: rest }); + gruppen.push({ titel: "Einzelne (" + rest.length + ")", geraete: rest }); } return gruppen; } @@ -158,9 +199,10 @@ function gruppiere(geraete, merkmal, reihenfolge, restTitel, einzelneSammeln) { * Geraeteauswahl mit Suchfeld. * * Bei ueber fuenfzig Geraeten hilft Tippen mehr als Blaettern - und es hilft - * quer zur Gruppierung: "bad" findet das Thermostat im Bad und die Jalousie - * "Bad Links", "jalousie" findet alle elf auf einmal. Gesucht wird deshalb - * ueber Beschriftung und Geraeteart zugleich. + * quer zur Gruppierung: "bad" findet das Thermostat im Bad, die Jalousie + * "Bad Links" und alles andere, was einem Bad zugeordnet ist, "jalousie" + * findet alle elf auf einmal. Gesucht wird deshalb ueber Beschriftung, Raum + * und Geraeteart zugleich. * * Statt einer Auswahlliste steht ein Knopf in der Zeile; die Liste klappt * darunter ueber die volle Breite auf. Auf dem Handy ist das mehr Platz als @@ -195,7 +237,7 @@ function oeffneWaehler(knopf, geraete, aktuelleId, beiAuswahl) { const suche = document.createElement("input"); suche.type = "search"; suche.className = "form-control form-control-sm mb-2"; - suche.placeholder = "Suchen – Name oder Geräteart"; + suche.placeholder = "Suchen – Name, Raum oder Geräteart"; const liste = document.createElement("div"); liste.className = "list-group list-group-flush"; liste.style.maxHeight = "15rem"; @@ -206,13 +248,18 @@ function oeffneWaehler(knopf, geraete, aktuelleId, beiAuswahl) { // input-group stuende die Liste sonst als weitere Spalte daneben. knopf.closest(".input-group").after(panel); - const eintrag = (d) => { + const eintrag = (d, mitEtage) => { const zeile = document.createElement("button"); zeile.type = "button"; + // Der Zusatz steht rechts am Rand statt gleich hinter dem Namen: so + // stehen die Raeume untereinander in einer Spalte und lassen sich + // ueberfliegen, statt bei jeder Zeile woanders anzufangen. zeile.className = "list-group-item list-group-item-action py-1" + + " d-flex justify-content-between align-items-center gap-2" + (d.id === aktuelleId ? " active" : ""); - zeile.innerHTML = "" + textSicher(d.label) + "" - + "" + textSicher(d.type) + ""; + zeile.innerHTML = "" + textSicher(d.label) + "" + + "" + + textSicher(zeilenZusatz(d, mitEtage)) + ""; zeile.onclick = () => { schliesseWaehler(); beiAuswahl(d); }; return zeile; }; @@ -220,17 +267,16 @@ function oeffneWaehler(knopf, geraete, aktuelleId, beiAuswahl) { const zeichne = () => { const suchtext = suche.value.trim().toLowerCase(); liste.innerHTML = ""; - const passend = geraete.filter(d => !suchtext - || d.label.toLowerCase().includes(suchtext) - || d.type.toLowerCase().includes(suchtext)); + const passend = geraete.filter(d => !suchtext || suchfeld(d).includes(suchtext)); if (!passend.length) { liste.innerHTML = "