Aufgaben abbrechen, Teilnehmerwahl fuer alle, Benachrichtigungen
Zwei Wuensche vom 05.09.2026, dazu drei Fehler, die dabei ans Licht kamen.
ABBRECHEN (Wunsch: "in jedem status die aufgaben auch abbrechen koennen,
nur ich die manager und scouts")
Neuer Status mit Pflicht-Grund, aus jedem der vier Status heraus.
Festgehalten wird auch, WO die Aufgabe stand -- "im Review abgebrochen"
ist eine andere Aussage als "nie angefangen", und das Wiederaufnehmen
geht dorthin zurueck statt nach "offen".
Eigener Weg statt "abgebrochen" in der Statusliste: Dort entscheidet
darfAendern(), und das laesst auch den zustaendigen Creator aendern.
Der gewoehnliche PATCH kann diesen Zustand deshalb gar nicht erreichen
-- auch nicht fuer DogFather, sonst waere die Grund-Pflicht umgehbar.
Abgebrochenes steht in einem zugeklappten Bereich unter dem Brett, nicht
als fuenfte Spalte: Am Handy waeren dann alle fuenf unlesbar schmal.
Verschwinden darf es nicht, sonst waere der Abbruch ein Loeschen mit
Zwischenschritt.
Die Tabelle musste dafuer getauscht werden (SQLite kann CHECK nicht
aendern). Vorher auf einer Kopie durchgespielt: 40 von 40 Aufgaben,
Inhalte, Verweise und Indizes geprueft, Gegenprobe zeigt, dass der CHECK
noch lebt.
TEILNEHMERWAHL (Wunsch: "das soll viel besser aussehen und fuer jeden
verfuegbar sein")
Auf dem Bildschirm klebten die Namen aneinander: "DogfatherDogFather".
Ursache war, dass kalender.html das Stylesheet mit diesen Klassen nie
eingebunden hat -- sie standen in dateien.css. Vierzig gruene Pruefungen
zur Teilnehmerwahl hatten das nicht gemerkt, weil keine je gefragt hat,
ob es AUSSIEHT wie gedacht.
Jetzt eigene Klassen im eigenen Stylesheet, nach Rollen gruppiert: Die
Rolle steht einmal als Ueberschrift statt neunmal am Namen. Damit ist
das Kleben an der Wurzel weg, nicht zugepflastert.
"Fuer jeden" war mehr als ein hidden zu entfernen: darfEintragen() haette
einem Creator nur sich selbst erlaubt. Er haette seinen Scout gesehen,
angeklickt, und der Server haette ihn still weggelassen -- ein Knopf, der
nichts tut. einladbareIds() schaut jetzt in beide Richtungen, bewusst
getrennt von /api/personen: Wer die erweitert, gibt einem Creator
nebenbei die Moeglichkeit, seinem Scout Aufgaben zuzuweisen.
BENACHRICHTIGUNGEN (Wunsch: "sowas, und dass es perfekt funktioniert
fuer jeden")
Web Push nach RFC 8291/8292, ohne fremde Abhaengigkeit. Der Knopf sagt
in jeder Lage die Wahrheit, auch die unbequemen: abgelehnt (mit dem
Hinweis, wo man es zuruecknimmt), iPhone im Reiter (mit Anleitung),
Browser ohne Push. Ein Knopf, der bei abgelehnter Berechtigung nur
nichts tut, ist der sichere Weg zu "das funktioniert nicht".
DREI FEHLER, DIE DABEI AUFFIELEN
1. Ein defekter Zugangsdatensatz sperrte ALLE einer Rolle aus. Wirft
hashe() bei einer Person, flog die ganze Anmeldung in den catch: 503
"nicht verfuegbar" fuer jeden mit dieser Rolle. Aufgefallen durch
einen eigenen Testfehler. Jetzt wird die defekte Person uebersprungen
und laut protokolliert; die Gegenprobe zeigt, dass ein falscher Code
weiterhin abgelehnt wird.
2. Die Glocke sprengte die Kopfleiste -- zweimal. Bei 320 px lag die
Lupe des Suchknopfes auf dem Sicht-Umschalter (ein Knopf, der auf 12
Seiten ins Leere tippt), bei 768 px wurde der Abmelden-Knopf bis zu
15 px aus dem Bild geschoben, weil die Textgrenze auf 760 stand und
ein Tablet 768 hat. Nachgewiesen durch Messen mit und ohne Glocke,
nicht durch Vermuten.
3. .block__frage war viermal gestaltet und stand auf einer Seite, die
keine dieser Dateien laedt -- derselbe Fehler wie bei der
Teilnehmerwahl. Gefunden von der neuen Klassenpruefung beim ersten
Lauf.
NEUE PRUEFUNGEN
pruef-css-klassen jede gestaltete Klasse muss auf ihrer Seite ankommen
(unterscheidet Struktur-Anker von echtem Verlust)
pruef-dabei-optik die Wahl im Browser, an den echten Pixeln
pruef-abbrechen Umstellung auf einer Kopie, Rechte, Rueckweg
pruef-abbrechen-optik Knopf, Dialog, Bereich, Handy
pruef-glocke Zustaende, An/Abmelden, jede Rolle
pruef-push(-weg) Rechnung gegen die RFC-Vektoren, Zustellung
pruef-struktur prueft jetzt zusaetzlich, ob sich jedes Server-Modul als
ESM laden laesst. node --check auf einer .js-Datei prueft als CommonJS
und meldete "ok", waehrend der Import scheiterte.
Gesamtlauf: 55 von 55 Dateien, 1973 von 1973 Punkten.
Was NICHT geprueft werden konnte und deshalb dasteht: Der Schritt
"Browser holt eine Adresse beim Push-Dienst" braucht eine Verbindung zu
Googles FCM, die ein Pruef-Browser nicht hat. Die Pruefung misst das
zuerst und meldet es als dritten Ausgang, statt gruen zu sein.
Verschluesselung und Zustellung sind getrennt geprueft; diese eine
Strecke beweist sich erst auf dem Server.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+288
-2
@@ -225,6 +225,27 @@ function umstellungen(d) {
|
||||
["aufgaben", "verantwortlich_extern", "TEXT"],
|
||||
["eintraege", "creator_extern", "TEXT"],
|
||||
["dateien", "creator_extern", "TEXT"],
|
||||
|
||||
/* ABBRECHEN (05.09.2026). Wunsch: *"ich will dass man in jedem
|
||||
status die aufgaben auch abbrechen kann."*
|
||||
|
||||
Drei Spalten, weil "abgebrochen" allein zu wenig sagt:
|
||||
abbruch_grund WARUM. Pflichtfeld in der Oberflaeche -- ohne
|
||||
Grund weiss in vier Wochen niemand mehr, warum
|
||||
etwas wegfiel, und es sieht aus wie Loeschen.
|
||||
abgebrochen_am WANN.
|
||||
abbruch_von WER. Nur Leitung und Scouts duerfen es, und
|
||||
wer es war, gehoert dazu.
|
||||
status_vorher IN WELCHEM ZUSTAND. Diese Spalte ist der Grund,
|
||||
warum "abgebrochen" ein eigener Status wurde
|
||||
und nicht bloss ein Haken: Ohne sie ginge beim
|
||||
Wechsel verloren, ob die Arbeit schon lief.
|
||||
"Im Review abgebrochen" ist eine ganz andere
|
||||
Aussage als "nie angefangen". */
|
||||
["aufgaben", "abbruch_grund", "TEXT"],
|
||||
["aufgaben", "abgebrochen_am", "TEXT"],
|
||||
["aufgaben", "abbruch_von", "INTEGER REFERENCES personen(id) ON DELETE SET NULL"],
|
||||
["aufgaben", "status_vorher", "TEXT"],
|
||||
]) {
|
||||
try {
|
||||
const vorhanden = d.prepare(`PRAGMA table_info(${tabelle})`).all().map((s) => s.name);
|
||||
@@ -321,6 +342,108 @@ function umstellungen(d) {
|
||||
console.error("[workspace] Content-Arten:", fehler?.message);
|
||||
}
|
||||
|
||||
/* ---- Status "abgebrochen" erlauben (05.09.2026) ----
|
||||
|
||||
Der CHECK-Constraint einer Tabelle laesst sich in SQLite nicht
|
||||
aendern -- kein ALTER TABLE der Welt hilft, die Tabelle muss neu
|
||||
gebaut werden. Deshalb derselbe Weg wie bei der Rolle "manager"
|
||||
darunter: erst sichern, dann tauschen, danach die Verweise pruefen.
|
||||
|
||||
WARUM UEBERHAUPT EIN NEUER STATUS und nicht bloss ein Haken
|
||||
"abgebrochen": Weil die vier Spalten des Bretts nach Status
|
||||
gruppieren. Eine abgebrochene Aufgabe faellt damit von selbst aus
|
||||
allen vieren heraus -- ohne dass irgendeine Abfrage angefasst
|
||||
werden muss. Ein zusaetzlicher Haken haette bedeutet, JEDE Abfrage
|
||||
um "AND nicht abgebrochen" zu ergaenzen, und die eine vergessene
|
||||
waere ein stiller Fehler gewesen: Die Aufgabe stuende weiter im
|
||||
Brett, und niemand wuesste warum. */
|
||||
const aufgabenPlan = d.prepare(
|
||||
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'aufgaben'").get()?.sql || "";
|
||||
if (aufgabenPlan && !aufgabenPlan.includes("'abgebrochen'")) {
|
||||
const sicherung = `${DB_PFAD}.vor-abbruch-${jetztStempel}`;
|
||||
try {
|
||||
d.exec(`VACUUM INTO '${sicherung.replace(/'/g, "''")}'`);
|
||||
console.log("[workspace] Sicherung vor der Umstellung:", sicherung);
|
||||
} catch (fehler) {
|
||||
console.error("[workspace] Sicherung fehlgeschlagen, Umstellung abgebrochen:", fehler?.message);
|
||||
return;
|
||||
}
|
||||
|
||||
d.exec("PRAGMA foreign_keys = OFF");
|
||||
try {
|
||||
/* Die Spalten stehen hier vollstaendig, weil die Schleife oben
|
||||
sie zu diesem Zeitpunkt schon ergaenzt hat. Wer hier eine
|
||||
vergisst, verliert ihren Inhalt still -- deshalb wird nach dem
|
||||
Tausch die Zeilenzahl verglichen. */
|
||||
const vorher = d.prepare("SELECT COUNT(*) AS n FROM aufgaben").get().n;
|
||||
d.exec("BEGIN");
|
||||
d.exec(`
|
||||
CREATE TABLE aufgaben_neu (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
titel TEXT NOT NULL,
|
||||
beschreibung TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'offen'
|
||||
CHECK (status IN ('offen','arbeit','review','erledigt','abgebrochen')),
|
||||
prioritaet TEXT NOT NULL DEFAULT 'mittel'
|
||||
CHECK (prioritaet IN ('hoch','mittel','niedrig')),
|
||||
creator_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||||
verantwortlich_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||||
frist TEXT,
|
||||
erstellt TEXT NOT NULL,
|
||||
erstellt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||||
geaendert TEXT,
|
||||
erledigt_am TEXT,
|
||||
creator_extern TEXT,
|
||||
verantwortlich_extern TEXT,
|
||||
abbruch_grund TEXT,
|
||||
abgebrochen_am TEXT,
|
||||
abbruch_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||||
status_vorher TEXT
|
||||
);
|
||||
INSERT INTO aufgaben_neu
|
||||
(id, titel, beschreibung, status, prioritaet, creator_id, verantwortlich_id,
|
||||
frist, erstellt, erstellt_von, geaendert, erledigt_am,
|
||||
creator_extern, verantwortlich_extern,
|
||||
abbruch_grund, abgebrochen_am, abbruch_von, status_vorher)
|
||||
SELECT id, titel, beschreibung, status, prioritaet, creator_id, verantwortlich_id,
|
||||
frist, erstellt, erstellt_von, geaendert, erledigt_am,
|
||||
creator_extern, verantwortlich_extern,
|
||||
abbruch_grund, abgebrochen_am, abbruch_von, status_vorher
|
||||
FROM aufgaben;
|
||||
DROP TABLE aufgaben;
|
||||
ALTER TABLE aufgaben_neu RENAME TO aufgaben;
|
||||
CREATE INDEX IF NOT EXISTS idx_aufgaben_status ON aufgaben (status);
|
||||
CREATE INDEX IF NOT EXISTS idx_aufgaben_creator ON aufgaben (creator_id);
|
||||
`);
|
||||
/* Nach dem RENAME heisst die neue Tabelle wieder "aufgaben" --
|
||||
gezaehlt wird also unter dem alten Namen, und der Vergleich
|
||||
laeuft noch INNERHALB der Transaktion. Stimmt er nicht, ist
|
||||
ein ROLLBACK noch moeglich. */
|
||||
const nachher = d.prepare("SELECT COUNT(*) AS n FROM aufgaben").get().n;
|
||||
/* Stimmt die Zahl nicht, wird NICHT bestaetigt. Lieber laeuft das
|
||||
Abbrechen noch nicht, als dass eine Aufgabe verschwindet. */
|
||||
if (nachher !== vorher) {
|
||||
d.exec("ROLLBACK");
|
||||
console.error(`[workspace] Umstellung abgebrochen: ${vorher} Aufgaben vorher, `
|
||||
+ `${nachher} nachher. Sicherung: ${sicherung}`);
|
||||
} else {
|
||||
d.exec("COMMIT");
|
||||
const kaputt = d.prepare("PRAGMA foreign_key_check").all();
|
||||
if (kaputt.length) {
|
||||
console.error("[workspace] ACHTUNG: nach der Umstellung", kaputt.length,
|
||||
"verwaiste Verweise. Sicherung liegt unter", sicherung);
|
||||
} else {
|
||||
console.log(`[workspace] Status 'abgebrochen' freigeschaltet, ${vorher} Aufgaben, Verweise geprueft.`);
|
||||
}
|
||||
}
|
||||
} catch (fehler) {
|
||||
try { d.exec("ROLLBACK"); } catch { /* schon zurueckgerollt */ }
|
||||
console.error("[workspace] Umstellung 'abgebrochen' fehlgeschlagen:", fehler?.message);
|
||||
} finally {
|
||||
d.exec("PRAGMA foreign_keys = ON");
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Rolle "manager" erlauben ---- */
|
||||
const bauplan = d.prepare(
|
||||
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'personen'").get()?.sql || "";
|
||||
@@ -435,7 +558,7 @@ export function db() {
|
||||
titel TEXT NOT NULL,
|
||||
beschreibung TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'offen'
|
||||
CHECK (status IN ('offen','arbeit','review','erledigt')),
|
||||
CHECK (status IN ('offen','arbeit','review','erledigt','abgebrochen')),
|
||||
prioritaet TEXT NOT NULL DEFAULT 'mittel'
|
||||
CHECK (prioritaet IN ('hoch','mittel','niedrig')),
|
||||
creator_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||||
@@ -461,6 +584,71 @@ export function db() {
|
||||
von INTEGER REFERENCES personen(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
/* BENACHRICHTIGUNGEN (05.09.2026) ---------------------------------
|
||||
|
||||
Wunsch Filipe, mit dem Bild eines "Benachrichtigungen aus"-
|
||||
Knopfes: *"ich will auch sowas und dass es perfekt funktioniert
|
||||
auf der seite fuer jeden."*
|
||||
|
||||
Eine Anmeldung ist die Adresse, unter der ein bestimmter
|
||||
BROWSER auf einem bestimmten GERAET erreichbar ist -- nicht die
|
||||
Person. Wer den Workspace auf dem Rechner und auf dem Handy
|
||||
benutzt, hat zwei; beide sollen klingeln, und beide muessen
|
||||
einzeln abschaltbar sein.
|
||||
|
||||
Der Endpunkt ist der Schluessel, nicht eine eigene Nummer: Meldet
|
||||
sich derselbe Browser erneut an (nach dem Leeren der
|
||||
Website-Daten etwa), soll daraus KEIN zweiter Eintrag werden,
|
||||
sonst kaeme jede Nachricht doppelt.
|
||||
|
||||
p256dh und auth sind die Schluessel des Browsers. Ohne sie
|
||||
laesst sich nichts verschluesseln -- und ohne Verschluesselung
|
||||
nimmt kein Push-Dienst etwas an. */
|
||||
CREATE TABLE IF NOT EXISTS push_anmeldungen (
|
||||
endpunkt TEXT PRIMARY KEY,
|
||||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||||
p256dh TEXT NOT NULL,
|
||||
auth TEXT NOT NULL,
|
||||
geraet TEXT,
|
||||
erstellt TEXT NOT NULL,
|
||||
zuletzt_ok TEXT,
|
||||
fehler INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_push_person ON push_anmeldungen (person_id);
|
||||
|
||||
/* Was jemand bekommen WILL -- je Art einzeln.
|
||||
|
||||
Der Plan ist an dieser Stelle deutlich: *"je Person abschaltbar
|
||||
-- pro Art, nicht alles oder nichts. Eine Benachrichtigung, die
|
||||
nervt, wird abgeschaltet und dann fehlt auch die wichtige."*
|
||||
|
||||
Fehlt eine Zeile, gilt die Voreinstellung aus workspace-push.js
|
||||
(an). Damit muss niemand erst etwas einstellen, um etwas zu
|
||||
bekommen -- und wer etwas abstellt, bekommt genau das nicht
|
||||
mehr. */
|
||||
CREATE TABLE IF NOT EXISTS push_einstellungen (
|
||||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||||
art TEXT NOT NULL,
|
||||
an INTEGER NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (person_id, art)
|
||||
);
|
||||
|
||||
/* Was schon verschickt wurde. Ohne dieses Gedaechtnis bekaeme
|
||||
jemand dieselbe Erinnerung bei jedem Lauf erneut -- viermal am
|
||||
Tag "Aufgabe ist ueberfaellig" ist der schnellste Weg, dass
|
||||
jemand Benachrichtigungen komplett abschaltet.
|
||||
|
||||
Das Merkmal ist die Sache selbst (z. B. "aufgabe-faellig:42"),
|
||||
nicht der Zeitpunkt: Dieselbe Aufgabe erinnert einmal, nicht
|
||||
einmal je Stunde. */
|
||||
CREATE TABLE IF NOT EXISTS push_verschickt (
|
||||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||||
merkmal TEXT NOT NULL,
|
||||
zeit TEXT NOT NULL,
|
||||
PRIMARY KEY (person_id, merkmal)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_push_verschickt_zeit ON push_verschickt (zeit);
|
||||
|
||||
/* Rueckmeldungen zu einer Aufgabe (Konzept: "Aufgaben & Feedback").
|
||||
Ohne sie endet jede Rueckfrage ausserhalb des Systems -- in
|
||||
WhatsApp, und damit ausserhalb dessen, was spaeter noch
|
||||
@@ -1073,9 +1261,37 @@ workspaceRouter.post("/workspace/api/anmelden", (req, res) => {
|
||||
.prepare("SELECT id, name, rolle, code_hash, code_salt, code_n FROM personen WHERE rolle = ? AND aktiv = 1")
|
||||
.all(rolle);
|
||||
|
||||
/* EIN KAPUTTER DATENSATZ DARF NICHT ALLE AUSSPERREN (05.09.2026).
|
||||
|
||||
Hier stand die Schleife ohne Absicherung. Wirft `hashe()` bei
|
||||
EINER Person -- etwa weil ihr code_n keine Zweierpotenz ist und
|
||||
scrypt "Invalid scrypt params" meldet --, dann flog die ganze
|
||||
Anmeldung in den catch am Ende: 503 "nicht_verfuegbar", für
|
||||
JEDEN mit dieser Rolle, auch für die, deren Daten in Ordnung
|
||||
sind.
|
||||
|
||||
Aufgefallen ist es beim Bau der Abbruchpruefung, wo ich zum
|
||||
Testen versehentlich eine Person mit code_n = 1 angelegt hatte.
|
||||
Ab da kam kein einziger DogFather mehr herein -- und die Meldung
|
||||
sagte "nicht verfügbar", nicht "ein Datensatz ist defekt".
|
||||
Danach hätte man lange gesucht.
|
||||
|
||||
Ein Datenfehler bei einer Person ist jetzt ein Problem DIESER
|
||||
Person: Sie wird übersprungen, der Rest der Anmeldung läuft
|
||||
normal weiter. Und sie wird laut protokolliert, denn sie kann
|
||||
sich selbst nicht mehr anmelden -- das muss auffallen. */
|
||||
let gefunden = null;
|
||||
for (const k of kandidaten) {
|
||||
if (gleich(hashe(code, k.code_salt, k.code_n), k.code_hash)) { gefunden = k; break; }
|
||||
try {
|
||||
if (gleich(hashe(code, k.code_salt, k.code_n), k.code_hash)) { gefunden = k; break; }
|
||||
} catch (f) {
|
||||
console.error(`[workspace] Zugangsdaten von Person #${k.id} (${k.rolle}) sind defekt `
|
||||
+ `-- sie kann sich nicht anmelden. Grund: ${f?.message}`);
|
||||
protokolliere("zugangsdaten_defekt", {
|
||||
personId: k.id, rolle: k.rolle, ip,
|
||||
detail: String(f?.message || "").slice(0, 80),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!gefunden) {
|
||||
@@ -1362,6 +1578,76 @@ export function sichtbarePersonenIds(person) {
|
||||
return [...new Set([person.id, ...creator, ...scouts])];
|
||||
}
|
||||
|
||||
/** WER BETREUT MICH -- die Blickrichtung nach oben.
|
||||
*
|
||||
* betreuteIds() fragt "wen führe ich?". Diese Funktion fragt das
|
||||
* Gegenteil: "wer führt mich?" -- also der eigene Scout, dessen
|
||||
* Manager, und bei einem Scout sein Manager.
|
||||
*
|
||||
* Es gab die Richtung bisher nicht, weil sie für Sichtbarkeit nie
|
||||
* gebraucht wurde: Ein Creator soll die Daten seines Scouts nicht
|
||||
* sehen. Für eine EINLADUNG ist die Frage aber eine andere -- siehe
|
||||
* einladbareIds(). */
|
||||
export function betreuerIds(person) {
|
||||
if (!person) return [];
|
||||
try {
|
||||
const d = db();
|
||||
if (person.rolle === "creator") {
|
||||
const betreuer = d.prepare("SELECT betreuer_id FROM betreuung WHERE creator_id = ?")
|
||||
.all(person.id).map((z) => z.betreuer_id);
|
||||
if (!betreuer.length) return [];
|
||||
/* Über den Scout auch dessen Manager: Wer mit seinem Scout einen
|
||||
Call hat, hat ihn oft mit dessen Manager zusammen. */
|
||||
const manager = d.prepare(
|
||||
`SELECT manager_id FROM scout_zuteilung
|
||||
WHERE scout_id IN (${betreuer.map(() => "?").join(",")})`)
|
||||
.all(...betreuer).map((z) => z.manager_id);
|
||||
return [...new Set([...betreuer, ...manager])];
|
||||
}
|
||||
if (person.rolle === "scout") {
|
||||
return d.prepare("SELECT manager_id FROM scout_zuteilung WHERE scout_id = ?")
|
||||
.all(person.id).map((z) => z.manager_id);
|
||||
}
|
||||
return [];
|
||||
} catch {
|
||||
return []; // im Zweifel niemand -- nie mehr, als sicher ist
|
||||
}
|
||||
}
|
||||
|
||||
/** WEN DARF ICH ZU EINEM TERMIN DAZUSTELLEN?
|
||||
*
|
||||
* Eingeführt am 05.09.2026 auf den Wunsch, die Teilnehmerwahl solle
|
||||
* *"für jeden verfügbar sein"*.
|
||||
*
|
||||
* WARUM NICHT EINFACH sichtbarePersonenIds: Die Frage ist eine
|
||||
* andere. Sichtbarkeit heißt "wessen Daten darf ich sehen" und zeigt
|
||||
* bewusst nach unten -- ein Creator sieht dort nur sich selbst.
|
||||
* Einladen heißt "mit wem arbeite ich zusammen" und geht in BEIDE
|
||||
* Richtungen: Ein Creator lädt seinen Scout ein, ein Scout seinen
|
||||
* Manager.
|
||||
*
|
||||
* Und warum nicht die eine Funktion erweitern: sichtbarePersonenIds
|
||||
* hängt auch an der AUFGABENZUWEISUNG. Wer dort einen Betreuer
|
||||
* hinzufügt, gibt einem Creator nebenbei die Möglichkeit, seinem
|
||||
* Scout Aufgaben zu erteilen. Zwei Fragen, zwei Funktionen.
|
||||
*
|
||||
* Was ein Einladen bewirkt, ist eng: Die Person sieht diesen einen
|
||||
* Termin in ihrem Kalender. Keine Daten, keine Rechte, kein Zugriff.
|
||||
* Deshalb ist die weitere Menge hier vertretbar.
|
||||
*
|
||||
* null = alle (nur DogFather), wie überall in dieser Datei. */
|
||||
export function einladbareIds(person) {
|
||||
if (!person) return [];
|
||||
if (istDogFather(person)) return null;
|
||||
const sichtbar = sichtbarePersonenIds(person) || [];
|
||||
/* DogFather gehört immer dazu: Er ist der Einzige, mit dem jede
|
||||
Rolle zu tun hat, und ihn nicht einladen zu können wäre der erste
|
||||
Fall, der auffällt. */
|
||||
const chefs = db().prepare("SELECT id FROM personen WHERE rolle = 'admin' AND aktiv = 1")
|
||||
.all().map((z) => z.id);
|
||||
return [...new Set([...sichtbar, ...betreuerIds(person), ...chefs])];
|
||||
}
|
||||
|
||||
/** SQL-Baustein daraus: "diese Spalte ist eine Person, die ich sehen
|
||||
* darf". Gibt null zurück, wenn nicht eingeschränkt werden muss. */
|
||||
export function personenWo(person, spalte) {
|
||||
|
||||
Reference in New Issue
Block a user