diff --git a/pruef-ampel.png b/pruef-ampel.png index f40f5e1a..5e6e95d3 100644 Binary files a/pruef-ampel.png and b/pruef-ampel.png differ diff --git a/pruef-aufgabenbrett.png b/pruef-aufgabenbrett.png new file mode 100644 index 00000000..eb104ced Binary files /dev/null and b/pruef-aufgabenbrett.png differ diff --git a/pruef-merkzeichen.png b/pruef-merkzeichen.png index 8b531e8b..e7db538e 100644 Binary files a/pruef-merkzeichen.png and b/pruef-merkzeichen.png differ diff --git a/pruef-personen.png b/pruef-personen.png index 87a428b5..e15587fa 100644 Binary files a/pruef-personen.png and b/pruef-personen.png differ diff --git a/pruef-protokoll-auf.png b/pruef-protokoll-auf.png index afffbce8..97e6bc71 100644 Binary files a/pruef-protokoll-auf.png and b/pruef-protokoll-auf.png differ diff --git a/pruef-protokoll-zu.png b/pruef-protokoll-zu.png index 75a39296..17a53dc6 100644 Binary files a/pruef-protokoll-zu.png and b/pruef-protokoll-zu.png differ diff --git a/pruef-schulung.png b/pruef-schulung.png index 41d9edb3..c7e90129 100644 Binary files a/pruef-schulung.png and b/pruef-schulung.png differ diff --git a/pruef-uebersicht-computer.png b/pruef-uebersicht-computer.png index fbf2bda3..4d55a715 100644 Binary files a/pruef-uebersicht-computer.png and b/pruef-uebersicht-computer.png differ diff --git a/pruef-uebersicht-creator.png b/pruef-uebersicht-creator.png index a9ed3321..2b0ae6a3 100644 Binary files a/pruef-uebersicht-creator.png and b/pruef-uebersicht-creator.png differ diff --git a/pruef-uebersicht-handy.png b/pruef-uebersicht-handy.png index 89c93678..d98a5d0f 100644 Binary files a/pruef-uebersicht-handy.png and b/pruef-uebersicht-handy.png differ diff --git a/pruef-zustand.png b/pruef-zustand.png index 41e29612..fe45acf3 100644 Binary files a/pruef-zustand.png and b/pruef-zustand.png differ diff --git a/server/pruef-aufgabenbrett.mjs b/server/pruef-aufgabenbrett.mjs new file mode 100644 index 00000000..3efe593c --- /dev/null +++ b/server/pruef-aufgabenbrett.mjs @@ -0,0 +1,368 @@ +/* Prueft das Aufgabenbrett fuer alle vier Rollen. + + Wunsch vom 01.09.2026: "verbessere die Aufgabenseite auch bei Manager, + Scout und Creator. Die Scouts und Manager sollen vielleicht ein + bisschen mehr Optionen haben, wie ihre zugeteilten Creator. Aber auch + die Creator sollen es besser und detaillierter sehen. + ABER NUR DIE ROLLE DOGFATHER SOLL WIRKLICH WEITERHIN ALLEINE ALLES + SEHEN KOENNEN. DIE ANDEREN SOLLEN NUR IHRE ZUGETEILTEN AUFGABEN VON + IHREN CREATOR SEHEN." + + DER WICHTIGSTE TEIL DIESER PRUEFUNG IST DER ZWEITE SATZ, nicht der + erste. Eine huebschere Filterleiste ist eine Annehmlichkeit; eine + Rechteregel, die zu viel zeigt, ist ein Schaden. Deshalb steht die + Abgrenzung vorn und wird an KONKRETEN Aufgaben geprueft, deren Titel + verraten, wem sie gehoeren. + + Zwei Aenderungen an den Rechten werden hier festgehalten: + + 1. Ein MANAGER sah auf diesem Brett bisher GAR NICHTS -- er fiel in + den Default-Zweig ("unbekannte Rolle sieht nichts"). Ein leeres + Brett sieht aus wie "nichts zu tun", nicht wie ein Fehler; deshalb + ist das lange niemandem aufgefallen. + + 2. In Kalender, Dateien und Bereichen sah ein Manager dagegen ALLES + (istLeitung). Beides ist jetzt dasselbe: nur die zugeteilten + Creator. Nur DogFather sieht alles. + + Was ein Manager DARF (freigeben, aendern), ist davon unberuehrt -- + das wird hier ausdruecklich mitgeprueft, damit die Sichtbarkeits- + aenderung ihm nicht stillschweigend die halben Rechte nimmt. */ + +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const ordner = mkdtempSync(join(tmpdir(), "ws-brett-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = "4216"; +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1"; +process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; + +const express = (await import("express")).default; +const ec = express.response.cookie; +express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; +await import("./index.js"); +await new Promise((r) => setTimeout(r, 700)); +const BASIS = "http://127.0.0.1:4216"; +setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 300_000).unref?.(); + +let fehler = 0; +const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +await fetch(BASIS + "/workspace/api/anmelden", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {}); + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes } = await import("node:crypto"); +for (let i = 0; i < 60; i++) { + try { + const p = new DatabaseSync(process.env.WORKSPACE_DB); + try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); } + } catch { await new Promise((r) => setTimeout(r, 250)); } +} +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); +const heute = jetzt.slice(0, 10); +const gestern = new Date(Date.now() - 86400_000).toISOString().slice(0, 10); +const morgen = new Date(Date.now() + 86400_000).toISOString().slice(0, 10); + +function anlegen(name, rolle, code) { + const salt = randomBytes(16).toString("hex"); + const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); + d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)") + .run(name, rolle, hash, salt, 32768, jetzt); + return d.prepare("SELECT last_insert_rowid() AS id").get().id; +} +const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001"); +const idMara = anlegen("Mara", "manager", "CODE-MANA-0001"); +const idPat = anlegen("Patrick", "scout", "CODE-SCOU-0001"); +const idTili = anlegen("Tili", "creator", "CODE-ANNA-0001"); +const idLuna = anlegen("Luna", "creator", "CODE-LUNA-0001"); +const idRoy = anlegen("Roy", "creator", "CODE-ROYY-0001"); + +/* Mara (Manager) betreut Tili UND Luna -- damit hat sie mehr als einen + Creator und die Creator-Reihe erscheint wirklich. + Patrick (Scout) betreut Roy. */ +d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idTili, idMara, jetzt); +d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idLuna, idMara, jetzt); +d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idRoy, idPat, jetzt); + +const neu = (titel, creator, verant, frist = null, status = "offen") => + d.prepare(`INSERT INTO aufgaben (titel, beschreibung, status, prioritaet, frist, + creator_id, verantwortlich_id, erstellt, erstellt_von) VALUES (?,?,?,?,?,?,?,?,?)`) + .run(titel, "", status, "mittel", frist, creator, verant, jetzt, idDogi); + +neu("TILI-OFFEN", idTili, idMara); +neu("TILI-UEBERFAELLIG", idTili, idMara, gestern); +neu("LUNA-OFFEN", idLuna, idMara); +neu("LUNA-HEUTE", idLuna, idMara, heute); +neu("ROY-OFFEN", idRoy, idPat); +neu("ROY-UEBERFAELLIG", idRoy, idPat, gestern); +neu("NUR-DOGI", null, idDogi, morgen); +/* Eine Aufgabe, die Tili SELBST verantwortet -- ein Creator muss auch + die sehen, nicht nur die "ueber" ihn. */ +neu("TILI-SELBST", idTili, idTili); +d.close(); + +async function anmelden(rolle, code) { + const a = await fetch(BASIS + "/workspace/api/anmelden", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rolle, code }), + }); + return (a.headers.getSetCookie?.() || []).map((x) => x.split(";")[0]).join("; "); +} +const brett = async (keks) => { + const a = await fetch(BASIS + "/workspace/api/aufgaben", { headers: { cookie: keks } }); + if (!a.ok) return { status: a.status, titel: [] }; + return { status: a.status, titel: ((await a.json()).aufgaben || []).map((x) => x.titel).sort() }; +}; + +const kDogi = await anmelden("admin", "CODE-DOGI-0001"); +const kMara = await anmelden("manager", "CODE-MANA-0001"); +const kPat = await anmelden("scout", "CODE-SCOU-0001"); +const kTili = await anmelden("creator", "CODE-ANNA-0001"); + +/* ================================================================ + 1. WER SIEHT WAS -- der wichtigste Abschnitt + ================================================================ */ +console.log("\n=== Nur DogFather sieht alles"); + +const dogi = await brett(kDogi); +ok(dogi.titel.length === 8, `DogFather sieht alle acht (${dogi.titel.length})`); + +const mara = await brett(kMara); +ok(mara.titel.length === 5, + `der Manager sieht seine fuenf: Tili + Luna (${mara.titel.join(", ")})`); +ok(!mara.titel.includes("NUR-DOGI"), + " und NICHT die Aufgabe, die nur DogFather gehoert"); +ok(!mara.titel.some((t) => t.startsWith("ROY")), + " und NICHT die Creator eines anderen Betreuers"); + +const pat = await brett(kPat); +ok(pat.titel.length === 2 && pat.titel.every((t) => t.startsWith("ROY")), + `der Scout sieht nur seinen Creator (${pat.titel.join(", ")})`); +ok(!pat.titel.some((t) => t.startsWith("TILI") || t.startsWith("LUNA")), + " und nichts von den Creatorn des Managers"); + +const tili = await brett(kTili); +ok(tili.titel.length === 3 && tili.titel.every((t) => t.startsWith("TILI")), + `der Creator sieht nur seine eigenen (${tili.titel.join(", ")})`); +ok(tili.titel.includes("TILI-SELBST"), + " auch die, die er selbst verantwortet"); +ok(!tili.titel.some((t) => t.startsWith("LUNA")), + " und nichts von einem anderen Creator desselben Betreuers"); + +/* DIE GEGENPROBE ZUR AENDERUNG: Vorher sah ein Manager auf diesem Brett + GAR NICHTS. Waere die Regel weiterhin so, waere die Zahl oben 0 -- + deshalb wird sie hier ausdruecklich als "mehr als null" festgehalten. */ +ok(mara.titel.length > 0, + "Gegenprobe zur alten Regel: der Manager sieht ueberhaupt etwas (vorher: nichts)"); + +/* ================================================================ + 2. AUCH IN DEN ANDEREN BEREICHEN nur die zugeteilten + ================================================================ */ +console.log("\n=== Und in den anderen Bereichen ebenso"); +{ + const d2 = new DatabaseSync(process.env.WORKSPACE_DB); + for (const [titel, creator] of [["TILI-EINTRAG", idTili], ["ROY-EINTRAG", idRoy]]) { + d2.prepare(`INSERT INTO eintraege (bereich, art, titel, text, status, dringlichkeit, + datum, creator_id, erstellt, erstellt_von) VALUES ('live','vorbereitung',?,?,?,?,?,?,?,?)`) + .run(titel, "", "offen", "niedrig", heute, creator, jetzt, idDogi); + } + d2.close(); + const eintraege = async (keks) => { + const a = await fetch(BASIS + "/workspace/api/bereich/live", { headers: { cookie: keks } }); + if (!a.ok) return []; + return ((await a.json()).eintraege || []).map((x) => x.titel).sort(); + }; + const eD = await eintraege(kDogi); + const eM = await eintraege(kMara); + ok(eD.length === 2, `DogFather sieht beide Bereichseintraege (${eD.join(", ")})`); + ok(eM.length === 1 && eM[0] === "TILI-EINTRAG", + `der Manager nur den seines Creators (${eM.join(", ")})`); +} + +/* ================================================================ + 3. WAS EIN MANAGER WEITERHIN DARF + Die Sichtbarkeit wurde eingeschraenkt -- die RECHTE nicht. Ohne diese + Pruefung haette der Wunsch "er soll weniger sehen" stillschweigend + auch "er darf weniger" bedeutet, und das stand nirgends. + ================================================================ */ +console.log("\n=== Der Manager darf weiterhin, was er durfte"); +{ + const a = await fetch(BASIS + "/workspace/api/aufgaben", { + method: "POST", headers: { cookie: kMara, "Content-Type": "application/json" }, + body: JSON.stringify({ titel: "MARA-NEU", prioritaet: "mittel", creator_id: idTili }), + }); + ok(a.ok, `er kann eine Aufgabe anlegen (${a.status})`); + + const p = await fetch(BASIS + "/workspace/api/personen", { headers: { cookie: kMara } }); + ok(p.ok, `er sieht weiterhin die Personenliste (${p.status})`); +} + +/* ================================================================ + 4. DIE SEITE SELBST + ================================================================ */ +console.log("\n=== Das Brett im Browser"); +const { chromium } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); +const browser = await chromium.launch(); + +async function seiteAls(rolle, code, breite = 1280) { + const kontext = await browser.newContext({ viewport: { width: breite, height: 1100 } }); + const konsole = []; + const seite = await kontext.newPage(); + seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); }); + seite.on("pageerror", (e) => konsole.push("pageerror: " + e.message)); + await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" }); + await seite.click(`.rolle[data-rolle="${rolle}"]`); + await seite.fill("#code", code); + await seite.click("button[type=submit]"); + await seite.waitForURL("**/start.html", { timeout: 15000 }); + await seite.goto(BASIS + "/workspace/aufgaben.html", { waitUntil: "networkidle" }); + await seite.waitForTimeout(1200); + return { kontext, seite, konsole }; +} + +const lesen = (seite) => seite.evaluate(() => ({ + lage: document.getElementById("lage")?.hidden === false + ? document.querySelector(".lage__satz")?.textContent : null, + stufe: document.getElementById("lage")?.dataset.stufe, + meins: document.querySelector(".lage__meins")?.textContent || null, + schnitte: [...document.querySelectorAll(".schnitt")].map((b) => ({ + name: b.querySelector("span")?.textContent, + zahl: b.querySelector(".schnitt__zahl")?.textContent, + aus: b.disabled, + })), + creator: [...document.querySelectorAll(".creator-knopf")].map((b) => + b.querySelector(".creator-knopf__name")?.textContent), + creatorReiheDa: document.getElementById("creator-reihe")?.hidden === false, + karten: document.querySelectorAll(".karte").length, + ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth, + /* Ist der Stil angekommen? Die Klassen stehen in aufgaben.css -- diese + Seite laedt sie, aber genau das ist hier schon zweimal schiefgegangen. */ + gestaltet: (() => { + const b = document.querySelector(".schnitt"); + if (!b) return null; + const st = getComputedStyle(b); + return parseFloat(st.borderTopWidth) >= 1 && parseFloat(st.borderTopLeftRadius) >= 8; + })(), +})); + +/* ---- Der Manager: mehr Optionen, aber nur seine Creator ---- */ +const bMara = await seiteAls("manager", "CODE-MANA-0001"); +const rM = await lesen(bMara.seite); +ok(rM.lage !== null, `der Manager bekommt einen Lagesatz ("${rM.lage}")`); +ok(rM.stufe === "warnung", `und er warnt, weil etwas ueberfaellig ist (${rM.stufe})`); +ok(rM.gestaltet === true, "die Stile sind angekommen (Rand und Ecken gemessen)"); +ok(rM.schnitte.length === 4, + `vier Ausschnitte: ${rM.schnitte.map((s) => `${s.name}=${s.zahl}`).join(" · ")}`); +ok(rM.schnitte.some((s) => s.name === "Nur meine"), + " darunter 'Nur meine' – die hat ein Betreuer"); +const ueberf = rM.schnitte.find((s) => s.name === "Überfällig"); +ok(ueberf?.zahl === "1", ` 'Überfällig' zeigt die richtige Zahl (${ueberf?.zahl})`); +ok(rM.creatorReiheDa && rM.creator.join(", ") === "Alle, Tili, Luna", + `die Creator-Reihe zeigt genau seine beiden (${rM.creator.join(", ")})`); +ok(!rM.creator.includes("Roy"), + " und NICHT den Creator eines anderen Betreuers"); +ok(rM.ueber <= 0, `nichts steht ueber (${rM.ueber}px)`); + +/* Einschraenken auf einen Creator -- und wirklich weniger sehen. */ +const vorher = rM.karten; +await bMara.seite.click('.creator-knopf:has(.creator-knopf__name:text-is("Luna"))') + .catch(async () => { + await bMara.seite.evaluate(() => { + [...document.querySelectorAll(".creator-knopf")] + .find((b) => b.textContent.includes("Luna"))?.click(); + }); + }); +await bMara.seite.waitForTimeout(400); +const nachher = await bMara.seite.evaluate(() => ({ + karten: document.querySelectorAll(".karte").length, + titel: [...document.querySelectorAll(".karte__titel")].map((e) => e.textContent).sort(), +})); +ok(nachher.karten < vorher && nachher.titel.every((t) => t.startsWith("LUNA")), + `ein Klick auf einen Creator zeigt nur dessen Aufgaben ` + + `(${vorher} -> ${nachher.karten}: ${nachher.titel.join(", ")})`); +ok(bMara.konsole.length === 0, + `keine Fehler in der Konsole${bMara.konsole.length ? " -> " + bMara.konsole.join(" | ") : ""}`); +await bMara.seite.screenshot({ path: "pruef-aufgabenbrett.png", fullPage: false }); +await bMara.kontext.close(); + +/* ---- Der Creator: mehr Detail, aber keine fremden Optionen ---- */ +const bTili = await seiteAls("creator", "CODE-ANNA-0001"); +const rT = await lesen(bTili.seite); +ok(rT.lage !== null, `der Creator bekommt denselben Lagesatz ("${rT.lage}")`); +ok(rT.schnitte.length === 3, + `drei Ausschnitte – ohne 'Nur meine' (${rT.schnitte.map((s) => s.name).join(" · ")})`); +ok(!rT.schnitte.some((s) => s.name === "Nur meine"), + " denn bei ihm ist ohnehin alles seins"); +ok(!rT.creatorReiheDa, + "keine Creator-Reihe – ein Filter mit einem Eintrag ist ein Knopf, der nichts tut"); +ok(rT.meins === null, "und keine 'davon bei dir'-Marke"); +/* KEINE FESTE ZAHL. Der erste Anlauf erwartete hier drei -- und lag + falsch, weil der Abschnitt davor ("darf der Manager noch anlegen?") + eine vierte Aufgabe fuer denselben Creator erzeugt. Die Pruefung war + damit von ihrer eigenen Reihenfolge abhaengig, und das ist eine + Zeitbombe: Wer spaeter einen Abschnitt einfuegt, bekommt einen + Fehlschlag, der nichts mit seiner Aenderung zu tun hat. + + Verglichen wird jetzt gegen die Schnittstelle -- das Brett muss genau + so viele Karten zeigen, wie der Server ihm gibt. Das ist ohnehin die + bessere Frage: nicht "sind es drei", sondern "verschluckt die Seite + etwas". */ +const tiliJetzt = await brett(kTili); +ok(rT.karten === tiliJetzt.titel.length, + `das Brett zeigt genau, was der Server ihm gibt ` + + `(${rT.karten} Karten, ${tiliJetzt.titel.length} vom Server)`); +ok(tiliJetzt.titel.every((t) => t.startsWith("TILI") || t.startsWith("MARA")), + `und alles davon gehoert zu ihm (${tiliJetzt.titel.join(", ")})`); +ok(bTili.konsole.length === 0, + `keine Fehler in der Konsole${bTili.konsole.length ? " -> " + bTili.konsole.join(" | ") : ""}`); +await bTili.kontext.close(); + +/* ---- DogFather: dieselbe Leiste, aber ueber ALLE Creator ---- + Ausdruecklich nachgereicht ("die Seite Aufgabe auch bei DogFather + machen bitte"). Er ist keine Creator-Rolle, bekommt die Leiste also + ohnehin -- geprueft wird, dass sie bei ihm auch WIRKLICH alles + umfasst und nicht nur die, die ihm zugeteilt waeren. */ +const bDogi = await seiteAls("admin", "CODE-DOGI-0001"); +const rD = await lesen(bDogi.seite); +ok(rD.lage !== null, `DogFather bekommt den Lagesatz ("${rD.lage}")`); +ok(rD.schnitte.length === 4, + `vier Ausschnitte: ${rD.schnitte.map((x) => `${x.name}=${x.zahl}`).join(" · ")}`); +ok(rD.creatorReiheDa, "und die Creator-Reihe"); +ok(["Tili", "Luna", "Roy"].every((n) => rD.creator.includes(n)), + `sie umfasst ALLE Creator, nicht nur zugeteilte (${rD.creator.join(", ")})`); +ok(rD.karten >= 8, `er sieht weiterhin alles (${rD.karten} Karten)`); +/* Die Beschriftung: "Deine Creator" waere bei ihm eine falsche Auskunft. */ +const schild = await bDogi.seite.evaluate(() => + document.querySelector(".creator-reihe__schild")?.textContent); +ok(schild === "Alle Creator", + `die Reihe heisst bei ihm "Alle Creator" statt "Deine" ("${schild}")`); +ok(bDogi.konsole.length === 0, + `keine Fehler in der Konsole${bDogi.konsole.length ? " -> " + bDogi.konsole.join(" | ") : ""}`); +await bDogi.kontext.close(); + +/* ---- Der Scout: eine Creator-Reihe waere hier sinnlos ---- */ +const bPat = await seiteAls("scout", "CODE-SCOU-0001"); +const rP = await lesen(bPat.seite); +ok(!rP.creatorReiheDa, + "der Scout mit nur einem Creator bekommt die Reihe ebenfalls nicht"); +ok(rP.karten === 2, `sieht seine zwei (${rP.karten})`); +await bPat.kontext.close(); + +/* ---- Handy ---- */ +const bHandy = await seiteAls("manager", "CODE-MANA-0001", 390); +const rH = await lesen(bHandy.seite); +ok(rH.ueber <= 0, `auf dem Handy steht nichts ueber (${rH.ueber}px)`); +ok(rH.schnitte.length === 4, `die Ausschnitte sind auch dort da (${rH.schnitte.length})`); +await bHandy.kontext.close(); + +await browser.close(); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } +console.log(fehler ? `\n${fehler} Prüfung(en) fehlgeschlagen.` : "\nAlles in Ordnung."); +process.exit(fehler ? 1 : 0); diff --git a/server/workspace-aufgaben.js b/server/workspace-aufgaben.js index a447cf00..99cf10e6 100644 --- a/server/workspace-aufgaben.js +++ b/server/workspace-aufgaben.js @@ -53,13 +53,22 @@ aufgabenRouter.use("/workspace/api", angemeldet); Stelle definiert, damit keine Abfrage sie versehentlich vergisst. */ export function sichtbar(person) { switch (person.rolle) { + /* NUR DogFather sieht alles. Ausdruecklich so gewuenscht + (01.09.2026) -- und ausdruecklich NUR er. */ case "admin": return { wo: "1=1", werte: [] }; case "creator": return { wo: "(a.creator_id = ? OR a.verantwortlich_id = ?)", werte: [person.id, person.id] }; - case "scout": { - /* Eigene Aufgaben -- und die der Creator, die dieser Scout - betreut. Wer jemanden betreut, muss dessen Brett sehen. */ + /* MANAGER STAND HIER BIS ZUM 01.09.2026 NICHT -- er fiel in den + Default und sah damit auf dem Aufgabenbrett GAR NICHTS. Ein + leeres Brett sieht aus wie "nichts zu tun", nicht wie ein Fehler; + deshalb ist das vermutlich lange niemandem aufgefallen. + + Jetzt gilt fuer ihn dieselbe Regel wie fuer einen Scout: eigene + Aufgaben plus die der Creator, die er betreut. Das ist genau das + Gewuenschte -- "nur ihre zugeteilten Aufgaben von ihren Creator". */ + case "scout": + case "manager": { const b = betreutWo(person, "a.creator_id"); return b ? { wo: `(a.verantwortlich_id = ? OR ${b.wo})`, werte: [person.id, ...b.werte] } diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js index e200664d..d1d77707 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -16,7 +16,7 @@ import express from "express"; import { - db, protokolliere, echteIp, sitzungLesen, betreutWo, darfCreator, betreuteIds, istLeitung, + db, protokolliere, echteIp, sitzungLesen, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, } from "./workspace.js"; export const bereicheRouter = express.Router(); @@ -188,7 +188,17 @@ for (const weg of ["post", "patch", "delete", "put"]) { /* Scouts haben mit der Creator-Betreuung nichts zu tun -- sie sehen hier nichts. Creator sehen ihren eigenen Bereich, das Management alles. */ export function sichtbar(person) { - if (istLeitung(person)) return { wo: "1=1", werte: [] }; + /* NUR DogFather sieht alles (01.09.2026). Vorher stand hier + istLeitung() -- damit sah auch jeder Manager jeden Creator. Ein + Manager faellt jetzt in dieselbe Regel wie ein Scout: nur die + Creator, die ihm zugeteilt sind. Ausdruecklicher Wunsch: + "NUR DIE ROLLE DOGFATHER SOLL WIRKLICH ALLEINE ALLES SEHEN." + + Geaendert wird ausschliesslich, wer was SIEHT. Was ein Manager + darf (freigeben, aendern, Personen verwalten), haengt weiterhin an + istLeitung und bleibt unveraendert -- sonst haette dieser eine + Wunsch stillschweigend seine halben Rechte mitgenommen. */ + if (istDogFather(person)) return { wo: "1=1", werte: [] }; if (person.rolle === "creator") return { wo: "e.creator_id = ?", werte: [person.id] }; /* Ein Scout sieht die Bereiche der Creator, die er betreut -- und ohne Zuteilung gar nichts (null heisst: Bereich existiert fuer dich nicht). */ diff --git a/server/workspace-checkliste.js b/server/workspace-checkliste.js index 84595f58..01a1ff55 100644 --- a/server/workspace-checkliste.js +++ b/server/workspace-checkliste.js @@ -34,7 +34,7 @@ import express from "express"; import { - db, sitzungLesen, istLeitung, darfCreator, betreuteIds, + db, sitzungLesen, istLeitung, istDogFather, darfCreator, betreuteIds, protokolliere, echteIp, } from "./workspace.js"; import { @@ -205,7 +205,9 @@ checklisteRouter.get("/workspace/api/checkliste/:bereich", (req, res) => { /* Wen darf die Person auswaehlen? */ let creatoren = []; if (req.person.rolle !== "creator") { - const ids = istLeitung(req.person) + /* Nur DogFather waehlt aus allen Creatorn; ein Manager aus + denen, die ihm zugeteilt sind (01.09.2026). */ + const ids = istDogFather(req.person) ? db().prepare("SELECT id FROM personen WHERE rolle = 'creator' AND aktiv = 1") .all().map((z) => z.id) : betreuteIds(req.person); diff --git a/server/workspace-content.js b/server/workspace-content.js index 07be7bb6..3db1cdf4 100644 --- a/server/workspace-content.js +++ b/server/workspace-content.js @@ -26,7 +26,7 @@ import express from "express"; import { db, protokolliere, echteIp, sitzungLesen, betreutWo, darfCreator, betreuteIds, - istLeitung, + istLeitung, istDogFather, } from "./workspace.js"; export const contentRouter = express.Router(); @@ -75,7 +75,17 @@ for (const weg of ["post", "patch", "delete", "put"]) { /* Welche Creator darf diese Person sehen? Gibt eine Liste von Nummern zurueck -- oder null fuer "alle" (Leitung). */ function sichtbareCreator(person) { - if (istLeitung(person)) return null; + /* NUR DogFather sieht alles (01.09.2026). Vorher stand hier + istLeitung() -- damit sah auch jeder Manager jeden Creator. Ein + Manager faellt jetzt in dieselbe Regel wie ein Scout: nur die + Creator, die ihm zugeteilt sind. Ausdruecklicher Wunsch: + "NUR DIE ROLLE DOGFATHER SOLL WIRKLICH ALLEINE ALLES SEHEN." + + Geaendert wird ausschliesslich, wer was SIEHT. Was ein Manager + darf (freigeben, aendern, Personen verwalten), haengt weiterhin an + istLeitung und bleibt unveraendert -- sonst haette dieser eine + Wunsch stillschweigend seine halben Rechte mitgenommen. */ + if (istDogFather(person)) return null; if (person.rolle === "creator") return [person.id]; return betreuteIds(person); } diff --git a/server/workspace-dateien.js b/server/workspace-dateien.js index 6677efc7..0de9f334 100644 --- a/server/workspace-dateien.js +++ b/server/workspace-dateien.js @@ -29,7 +29,7 @@ import { randomBytes } from "node:crypto"; import { join, extname, basename } from "node:path"; import { mkdirSync, createReadStream, writeFileSync, unlinkSync, statSync } from "node:fs"; import { - db, protokolliere, echteIp, sitzungLesen, DATEN_ORDNER, betreutWo, darfCreator, betreuteIds, istLeitung, ROLLEN_SORTIERUNG, + db, protokolliere, echteIp, sitzungLesen, DATEN_ORDNER, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, ROLLEN_SORTIERUNG, } from "./workspace.js"; export const dateienRouter = express.Router(); @@ -68,7 +68,17 @@ dateienRouter.use("/workspace/api/dateien", angemeldet); /* Sichtbarkeit wie überall: an einer Stelle, damit keine Abfrage sie vergisst. */ export function sichtbar(person) { - if (istLeitung(person)) return { wo: "1=1", werte: [] }; + /* NUR DogFather sieht alles (01.09.2026). Vorher stand hier + istLeitung() -- damit sah auch jeder Manager jeden Creator. Ein + Manager faellt jetzt in dieselbe Regel wie ein Scout: nur die + Creator, die ihm zugeteilt sind. Ausdruecklicher Wunsch: + "NUR DIE ROLLE DOGFATHER SOLL WIRKLICH ALLEINE ALLES SEHEN." + + Geaendert wird ausschliesslich, wer was SIEHT. Was ein Manager + darf (freigeben, aendern, Personen verwalten), haengt weiterhin an + istLeitung und bleibt unveraendert -- sonst haette dieser eine + Wunsch stillschweigend seine halben Rechte mitgenommen. */ + if (istDogFather(person)) return { wo: "1=1", werte: [] }; /* Ausdrueckliche Freigabe -- gilt fuer jede Rolle gleich. Wer in der Liste steht, sieht die Datei, egal ob Creator oder Scout. */ diff --git a/server/workspace-kalender.js b/server/workspace-kalender.js index 8a171b3c..e2ca7625 100644 --- a/server/workspace-kalender.js +++ b/server/workspace-kalender.js @@ -13,7 +13,7 @@ import express from "express"; import { - db, protokolliere, echteIp, sitzungLesen, betreutWo, istLeitung, + db, protokolliere, echteIp, sitzungLesen, betreutWo, istLeitung, istDogFather, } from "./workspace.js"; export const kalenderRouter = express.Router(); @@ -46,7 +46,17 @@ kalenderRouter.use("/workspace/api/termine", angemeldet); /* Wer sieht welchen Termin? Gleiche Logik wie bei den Aufgaben: Management alles, sonst nur, woran man selbst beteiligt ist. */ export function sichtbar(person) { - if (istLeitung(person)) return { wo: "1=1", werte: [] }; + /* NUR DogFather sieht alles (01.09.2026). Vorher stand hier + istLeitung() -- damit sah auch jeder Manager jeden Creator. Ein + Manager faellt jetzt in dieselbe Regel wie ein Scout: nur die + Creator, die ihm zugeteilt sind. Ausdruecklicher Wunsch: + "NUR DIE ROLLE DOGFATHER SOLL WIRKLICH ALLEINE ALLES SEHEN." + + Geaendert wird ausschliesslich, wer was SIEHT. Was ein Manager + darf (freigeben, aendern, Personen verwalten), haengt weiterhin an + istLeitung und bleibt unveraendert -- sonst haette dieser eine + Wunsch stillschweigend seine halben Rechte mitgenommen. */ + if (istDogFather(person)) return { wo: "1=1", werte: [] }; const eigen = "(t.creator_id = ? OR t.teilnehmer_id = ? OR t.erstellt_von = ?)"; const werte = [person.id, person.id, person.id]; const b = betreutWo(person, "t.creator_id"); diff --git a/server/workspace.js b/server/workspace.js index 56151ff7..1105ce11 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -889,10 +889,25 @@ export function codeErzeugen(gruppen = 4, laenge = 4) { Rechteregel, die irgendwann an fuenf Stellen stimmt. ===================================================================== */ -/* Ids der Creator, die diese Person betreut. Fuer alle anderen Rollen - leer: Das Management sieht ohnehin alles, ein Creator sich selbst. */ +/* Ids der Creator, die diese Person betreut. + + GILT SEIT DEM 01.09.2026 AUCH FUER MANAGER, nicht mehr nur fuer + Scouts. Vorher stand hier "das Management sieht ohnehin alles" -- und + genau das soll nicht mehr sein: + + "NUR DIE ROLLE DOGFATHER SOLL WIRKLICH WEITERHIN ALLEINE ALLES + SEHEN KOENNEN. DIE ANDEREN SOLLEN NUR IHRE ZUGETEILTEN AUFGABEN + VON IHREN CREATOR SEHEN." + + Die Datenbank konnte das laengst: In `betreuung` steht eine beliebige + Person als Betreuer, auch ein Manager oder DogFather. Nur diese + Funktion hat alle ausser Scouts abgewiesen -- ein Manager bekam + deshalb eine leere Liste und faellt in den Regeln unten auf "sieht + alles" oder (beim Aufgabenbrett) auf "sieht nichts" zurueck. + + Ein Creator bleibt aussen vor: Er betreut niemanden, er wird betreut. */ export function betreuteIds(person) { - if (!person || person.rolle !== "scout") return []; + if (!person || (person.rolle !== "scout" && person.rolle !== "manager")) return []; try { return db().prepare("SELECT creator_id FROM betreuung WHERE betreuer_id = ?") .all(person.id).map((z) => z.creator_id); diff --git a/workspace/assets/css/aufgaben.css b/workspace/assets/css/aufgaben.css index 1ba30d41..d2ef4647 100644 --- a/workspace/assets/css/aufgaben.css +++ b/workspace/assets/css/aufgaben.css @@ -345,3 +345,123 @@ overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } + +/* ---------- Die Leiste über dem Brett (01.09.2026) ------------------------ + + "verbessere die Aufgabenseite auch bei Manager, Scout und Creator … + aber auch die Creator sollen es besser und detaillierter sehen." + + Drei Zeilen, die zusammen die Frage beantworten, mit der man auf diese + Seite kommt: Muss ich mir Sorgen machen, und wo fange ich an? Ein + Brett aus vier Spalten kann das nicht sagen – man müsste alle vier + durchsehen, um zu wissen, dass nichts brennt. */ + +.lage { + --l-ton: #6f89a8; + display: flex; align-items: center; gap: 12px; flex-wrap: wrap; + margin: 0 0 14px; padding: 11px 15px; + border: 1px solid color-mix(in srgb, var(--l-ton) 28%, var(--rand)); + border-left-width: 3px; + border-radius: var(--radius-klein); + background: + linear-gradient(90deg, color-mix(in srgb, var(--l-ton) 10%, transparent), transparent 55%), + var(--flaeche); +} +.lage[data-stufe="warnung"] { --l-ton: #e0a33c; } +.lage[data-stufe="offen"] { --l-ton: #6f89a8; } +.lage[data-stufe="ruhig"] { --l-ton: #4dab77; } +.lage__satz { font-size: .95rem; color: var(--text); } +.lage[data-stufe="warnung"] .lage__satz { color: #f0c67c; } +/* Steht rechts und leiser: "wie viel davon ist meins" ist die zweite + Frage, nicht die erste. */ +.lage__meins { + margin-left: auto; padding: 3px 10px; + border: 1px solid var(--rand); border-radius: 999px; + font-size: .78rem; color: var(--text-leise); +} + +.ausschnitte, .creator-reihe { + display: flex; align-items: center; gap: 8px; flex-wrap: wrap; + margin-bottom: 12px; +} +.creator-reihe { margin-bottom: 18px; } +.creator-reihe__schild { + font-size: .72rem; font-weight: 600; + letter-spacing: .1em; text-transform: uppercase; + color: var(--text-still); margin-right: 2px; +} + +/* Ein Ausschnitt-Knopf trägt IMMER seine Zahl. Ein Filter, der sich erst + nach dem Klick als leer herausstellt, kostet zweimal Aufmerksamkeit -- + und man klickt ihn beim nächsten Mal trotzdem wieder an. */ +.schnitt, .creator-knopf { + display: inline-flex; align-items: center; gap: 8px; + min-height: 38px; padding: 7px 13px; + border: 1px solid var(--rand); border-radius: 999px; + background: rgba(255, 255, 255, .03); + color: var(--text-leise); font: inherit; font-size: .85rem; + cursor: pointer; + transition: border-color var(--tempo), color var(--tempo), background var(--tempo); +} +.schnitt:hover:not([disabled]), .creator-knopf:hover { + color: var(--text); border-color: rgba(255, 255, 255, .22); +} +.schnitt[aria-pressed="true"], .creator-knopf[aria-pressed="true"] { + color: var(--text); + border-color: color-mix(in srgb, var(--akzent) 55%, transparent); + background: color-mix(in srgb, var(--akzent) 13%, transparent); +} +.schnitt:focus-visible, .creator-knopf:focus-visible { + outline: 2px solid var(--akzent); outline-offset: 2px; +} +/* Ein Ausschnitt ohne Inhalt wird abgeschaltet statt versteckt: Wer + gestern "3 überfällig" gesehen hat, soll heute sehen, dass es 0 sind + -- und nicht rätseln, wo der Knopf hin ist. */ +.schnitt[disabled] { opacity: .42; cursor: default; } + +.schnitt__zahl, .creator-knopf__zahl { + min-width: 20px; padding: 1px 7px; + border-radius: 999px; + background: rgba(255, 255, 255, .07); + font-size: .76rem; font-weight: 700; + font-variant-numeric: tabular-nums; + text-align: center; +} +/* "Überfällig" trägt seine Farbe nur, wenn wirklich etwas überfällig ist. + Ein dauerhaft warnender Knopf warnt nach zwei Tagen niemanden mehr. */ +.schnitt[data-warn="ja"]:not([disabled]) { + color: #f0c67c; border-color: rgba(224, 163, 60, .42); +} +.schnitt[data-warn="ja"]:not([disabled]) .schnitt__zahl { + background: rgba(224, 163, 60, .18); +} +.schnitt[data-warn="ja"][aria-pressed="true"] { + background: rgba(224, 163, 60, .16); + border-color: rgba(224, 163, 60, .6); +} + +.creator-knopf__name { font-weight: 600; color: inherit; } +/* Die kleine Warnzahl am Creator: wie viele seiner Aufgaben überfällig + sind. Sie steht NEBEN der Gesamtzahl, nicht statt ihr -- sonst weiß + man nicht, ob "2" viel oder wenig ist. */ +.creator-knopf__warn { + min-width: 20px; padding: 1px 7px; + border-radius: 999px; + background: rgba(224, 163, 60, .18); + border: 1px solid rgba(224, 163, 60, .38); + color: #f0c67c; + font-size: .74rem; font-weight: 700; + font-variant-numeric: tabular-nums; text-align: center; +} + +@media (max-width: 560px) { + .lage { flex-direction: column; align-items: flex-start; gap: 6px; } + .lage__meins { margin-left: 0; } + /* Waagerecht scrollen statt umbrechen: Bei fünf Creatorn würde die + Reihe sonst drei Zeilen hoch und das Brett wäre weg. */ + .ausschnitte, .creator-reihe { + flex-wrap: nowrap; overflow-x: auto; padding-bottom: 4px; + scrollbar-width: thin; + } + .schnitt, .creator-knopf { flex: none; } +} diff --git a/workspace/assets/js/aufgaben.js b/workspace/assets/js/aufgaben.js index 795b1990..64ad95a4 100644 --- a/workspace/assets/js/aufgaben.js +++ b/workspace/assets/js/aufgaben.js @@ -230,6 +230,171 @@ return b; } + /* ---------- Die Leiste ueber dem Brett ----------------------------------- + + Wunsch vom 01.09.2026: "verbessere die Aufgabenseite auch bei + Manager, Scout und Creator. Die Scouts und Manager sollen vielleicht + ein bisschen mehr Optionen haben, wie ihre zugeteilten Creator. + Aber auch die Creator sollen es besser und detaillierter sehen." + + Drei Dinge, und die Reihenfolge ist die Aussage: + + 1. WAS BRENNT. Ein Satz beim Reinkommen, ob etwas ueberfaellig ist. + Ein Brett mit vier Spalten beantwortet das nicht -- man muss vier + Spalten durchsehen, um zu wissen, ob man ruhig anfangen kann. + + 2. AUSSCHNITTE. "Nur meine", "Heute faellig", "Ueberfaellig" -- die + Fragen, die man wirklich hat. Sie filtern das Brett, statt eine + zweite Liste aufzumachen: Vier Spalten nebeneinander sind der + Wert dieser Seite, den nimmt man nicht weg. + + 3. DIE CREATOR, nur fuer Betreuer. Wer drei betreut, will sehen, + was bei einem davon liegt -- ohne die anderen zwei dazwischen. + Ein Creator bekommt diese Reihe nicht: Er hat nur sich, und ein + Filter mit einem einzigen Eintrag ist ein Knopf, der nichts tut. + + Alles hier ist ANSICHT. Wer was sehen darf, hat der Server + entschieden, bevor eine einzige Aufgabe hier ankommt. */ + + let ausschnitt = ''; /* '', 'meine', 'faellig', 'heute' */ + let nurCreator = 0; /* 0 = alle */ + let alleAufgaben = []; + + const istOffen = (a) => a.status !== 'erledigt'; + const istFaellig = (a) => istOffen(a) && a.frist && a.frist < heute; + const istHeute = (a) => istOffen(a) && a.frist === heute; + const istMeine = (a) => a.verantwortlich_id === ich.id; + + function passt(a) { + if (nurCreator && a.creator_id !== nurCreator) return false; + if (ausschnitt === 'meine') return istMeine(a); + if (ausschnitt === 'faellig') return istFaellig(a); + if (ausschnitt === 'heute') return istHeute(a); + return true; + } + + function lageZeigen(alle) { + const ziel = $('lage'); + if (!ziel) return; + const faellig = alle.filter(istFaellig).length; + const heuteN = alle.filter(istHeute).length; + const offen = alle.filter(istOffen).length; + const meine = alle.filter((a) => istOffen(a) && istMeine(a)).length; + + ziel.textContent = ''; + let satz, stufe; + if (faellig) { + satz = `${faellig} ${faellig === 1 ? 'Aufgabe ist' : 'Aufgaben sind'} überfällig – das zuerst.`; + stufe = 'warnung'; + } else if (heuteN) { + satz = `${heuteN} ${heuteN === 1 ? 'Aufgabe ist' : 'Aufgaben sind'} heute fällig.`; + stufe = 'offen'; + } else if (offen) { + satz = `${offen} ${offen === 1 ? 'Aufgabe steht' : 'Aufgaben stehen'} an, nichts davon brennt.`; + stufe = 'offen'; + } else { + satz = 'Nichts offen – alles abgearbeitet.'; + stufe = 'ruhig'; + } + ziel.dataset.stufe = stufe; + ziel.append(el('span', 'lage__satz', satz)); + /* Die eigene Zahl steht daneben, nicht im Satz: Auf einem Brett, das + auch fremde Aufgaben zeigt, ist "wie viel davon ist meins" die + zweite Frage -- aber eben die zweite. */ + if (ich.rolle !== 'creator' && meine) { + ziel.append(el('span', 'lage__meins', `${meine} davon bei dir`)); + } + ziel.hidden = false; + } + + function ausschnitteBauen(alle) { + const ziel = $('ausschnitte'); + if (!ziel) return; + ziel.textContent = ''; + + const knopf = (wert, name, anzahl) => { + const b = el('button', 'schnitt'); + b.type = 'button'; + b.dataset.wert = wert; + b.setAttribute('aria-pressed', String(ausschnitt === wert)); + b.append(el('span', null, name)); + /* Die Zahl steht am Knopf. Ein Filter, der sich erst nach dem + Klick als leer herausstellt, kostet zweimal Aufmerksamkeit. */ + b.append(el('span', 'schnitt__zahl', String(anzahl))); + if (!anzahl && wert) b.disabled = true; + b.addEventListener('click', () => { + ausschnitt = ausschnitt === wert ? '' : wert; + neuZeichnen(); + }); + return b; + }; + + ziel.append(knopf('', 'Alle', alle.length)); + if (ich.rolle !== 'creator') { + ziel.append(knopf('meine', 'Nur meine', + alle.filter((a) => istOffen(a) && istMeine(a)).length)); + } + ziel.append(knopf('heute', 'Heute fällig', alle.filter(istHeute).length)); + const f = knopf('faellig', 'Überfällig', alle.filter(istFaellig).length); + f.dataset.warn = 'ja'; + ziel.append(f); + ziel.hidden = false; + } + + function creatorReiheBauen(alle) { + const ziel = $('creator-reihe'); + if (!ziel) return; + ziel.textContent = ''; + if (ich.rolle === 'creator') { ziel.hidden = true; return; } + + /* Die Liste kommt aus den Aufgaben selbst, nicht aus einer + Personenabfrage: Dann stehen dort genau die Creator, die man + ohnehin sehen darf -- und niemals einer mehr. */ + const nach = new Map(); + for (const a of alle) { + if (!a.creator_id || !a.creator_name) continue; + const w = nach.get(a.creator_id) || { name: a.creator_name, offen: 0, faellig: 0 }; + if (istOffen(a)) w.offen++; + if (istFaellig(a)) w.faellig++; + nach.set(a.creator_id, w); + } + /* Unter zwei Creatorn ist die Reihe eine Zeile ohne Auswahl. */ + if (nach.size < 2) { ziel.hidden = true; return; } + + /* "Deine Creator" stimmt fuer Scout und Manager -- bei DogFather + waeren es ALLE, und "deine" waere dort eine falsche Auskunft. */ + ziel.append(el('span', 'creator-reihe__schild', + ich.rolle === 'admin' ? 'Alle Creator' : 'Deine Creator')); + const knopf = (id, name, offen, faellig) => { + const b = el('button', 'creator-knopf'); + b.type = 'button'; + b.setAttribute('aria-pressed', String(nurCreator === id)); + b.append(el('span', 'creator-knopf__name', name)); + b.append(el('span', 'creator-knopf__zahl', String(offen))); + if (faellig) { + const m = el('span', 'creator-knopf__warn', String(faellig)); + m.title = `${faellig} überfällig`; + b.append(m); + } + b.addEventListener('click', () => { + nurCreator = nurCreator === id ? 0 : id; + neuZeichnen(); + }); + return b; + }; + ziel.append(knopf(0, 'Alle', alle.filter(istOffen).length, alle.filter(istFaellig).length)); + for (const [id, w] of [...nach.entries()].sort((a, b) => b[1].offen - a[1].offen)) { + ziel.append(knopf(id, w.name, w.offen, w.faellig)); + } + ziel.hidden = false; + } + + function neuZeichnen() { + ausschnitteBauen(alleAufgaben); + creatorReiheBauen(alleAufgaben); + zeichne(alleAufgaben.filter(passt)); + } + function zeichne(aufgaben) { const brett = $('brett'); brett.textContent = ''; @@ -293,7 +458,9 @@ try { const antwort = await hole('/workspace/api/aufgaben'); if (!antwort.ok) { melde('Aufgaben konnten nicht geladen werden.'); return; } - zeichne((await antwort.json()).aufgaben); + alleAufgaben = (await antwort.json()).aufgaben || []; + lageZeigen(alleAufgaben); + neuZeichnen(); /* Erst NACH dem Zeichnen -- vorher gibt es die Karten nicht, und der Sprung ginge ins Leere, ohne dass jemand etwas merkt. Nur beim ersten Laden: Sonst springt die Seite nach jeder diff --git a/workspace/assets/js/personen.js b/workspace/assets/js/personen.js index d81f4ef7..83bfb433 100644 --- a/workspace/assets/js/personen.js +++ b/workspace/assets/js/personen.js @@ -161,9 +161,19 @@ const o = document.createElement('option'); o.value = String(s2.id); /* Die Rolle mit dabei: Bei gleichen oder aehnlichen Namen ist - sonst nicht zu erkennen, wen man gerade einträgt. */ - o.textContent = s2.rolle && s2.rolle !== 'scout' - ? `${s2.name} (${ROLLENNAME[s2.rolle] || s2.rolle})` + sonst nicht zu erkennen, wen man gerade einträgt. + + ABER NICHT, WENN SIE DASSELBE SAGT WIE DER NAME. Dort stand + "Dogfather (DogFather)" -- zweimal dasselbe Wort, nur einmal + anders geschrieben. Eine Klammer, die nichts hinzufuegt, ist + keine Auskunft, sondern Rauschen. Verglichen wird ohne Rücksicht + auf Gross- und Kleinschreibung und ohne Leerzeichen, sonst + faellt genau dieser Fall wieder durch. */ + const rolleName = ROLLENNAME[s2.rolle] || s2.rolle || ''; + const gleich = (a, b) => + a.trim().toLowerCase().replace(/\s+/g, '') === b.trim().toLowerCase().replace(/\s+/g, ''); + o.textContent = rolleName && s2.rolle !== 'scout' && !gleich(s2.name, rolleName) + ? `${s2.name} (${rolleName})` : s2.name; w.append(o); } diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 691d668d..202b0cfd 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -7,9 +7,9 @@ - - - + + + @@ -43,6 +43,19 @@ + + + + + - - - - + + + + diff --git a/workspace/automation.html b/workspace/automation.html index 0abdb2e3..ebe98aae 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -132,9 +132,9 @@ - - - - + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index 9192814c..75775f8a 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -115,13 +115,13 @@ - - - - - - - - + + + + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index 101ab520..2ac22974 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -54,10 +54,10 @@ - - - - - + + + + + diff --git a/workspace/content.html b/workspace/content.html index b7a87b21..b0390df6 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -164,11 +164,11 @@ - - - - - - + + + + + + diff --git a/workspace/dateien.html b/workspace/dateien.html index ccdab788..e7a3f0e1 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -93,9 +93,9 @@ - - - - + + + + diff --git a/workspace/index.html b/workspace/index.html index dd45ef78..f56f395b 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -25,7 +25,7 @@ type="image/webp" media="(max-width: 899px)" /> - + @@ -143,6 +143,6 @@

- + diff --git a/workspace/kalender.html b/workspace/kalender.html index d32fa121..7b0d5e6c 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -147,9 +147,9 @@ - - - - + + + + diff --git a/workspace/personen.html b/workspace/personen.html index a04ac17a..3e379058 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -121,9 +121,9 @@ - - - - + + + + diff --git a/workspace/profil.html b/workspace/profil.html index 4beb4053..1e5e0b53 100644 --- a/workspace/profil.html +++ b/workspace/profil.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -193,10 +193,10 @@ - - - - - + + + + + diff --git a/workspace/report.html b/workspace/report.html index bde160fe..c583a4cb 100644 --- a/workspace/report.html +++ b/workspace/report.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -91,10 +91,10 @@ - - - - - + + + + + diff --git a/workspace/scouting.html b/workspace/scouting.html index 38e56b1b..f74c5db3 100644 --- a/workspace/scouting.html +++ b/workspace/scouting.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -124,9 +124,9 @@ - - - - + + + + diff --git a/workspace/start.html b/workspace/start.html index 9a13872d..407497de 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -7,8 +7,8 @@ - - + + @@ -99,9 +99,9 @@

- - - - + + + + diff --git a/workspace/startcheck.html b/workspace/startcheck.html index 43cbfecc..51013a90 100644 --- a/workspace/startcheck.html +++ b/workspace/startcheck.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -70,10 +70,10 @@ - - - - - + + + + + diff --git a/workspace/steckbrief.html b/workspace/steckbrief.html index b78bcd9f..f39d22ed 100644 --- a/workspace/steckbrief.html +++ b/workspace/steckbrief.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -111,9 +111,9 @@ - - - - + + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index 83fbcae9..12d38027 100644 --- a/workspace/uebersicht.html +++ b/workspace/uebersicht.html @@ -7,9 +7,9 @@ - - - + + + @@ -57,9 +57,9 @@ - + - - + + diff --git a/workspace/wissen.html b/workspace/wissen.html index e7ed63ef..6de33050 100644 --- a/workspace/wissen.html +++ b/workspace/wissen.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -175,9 +175,9 @@ - - - - + + + +