Kalender: mehrere Teilnehmer je Termin und Serie

Wunsch Filipe: "wenn ich im kalender was eintrage will ich dass ich
auch 2 leute markieren kann mit denen der call ist." -- und auf
Rueckfrage: beliebig viele, und auch fuer wiederkehrende Termine.

Bis hierher hatte ein Termin GENAU EIN Gegenueber. Fuer ein Gespraech
zu dritt musste man zwei Termine anlegen und hatte zwei Wahrheiten
ueber dieselbe halbe Stunde.

DAS IST KEINE ANZEIGE, SONDERN EINE RECHTEREGEL. An der
Teilnehmerliste haengt die Sichtbarkeit: Wer eingetragen ist, sieht den
Termin. Deshalb zwei Grenzen, beide serverseitig:
  * Eintragen darf man nur, wen man ohnehin sehen darf (Leitung jeden,
    ein Scout seine betreuten Creator, ein Creator sich selbst).
  * Zuordnungen verschiebt nur die Leitung -- sonst koennte sich jemand
    selbst in fremde Termine eintragen und sie sich damit sichtbar
    machen.

Gebaut nach dem Muster von datei_personen: eigene Tabellen
termin_teilnehmer und serie_teilnehmer statt weiterer Spalten.
teilnehmer_id bleibt das Haupt-Gegenueber und wird beim Speichern immer
in die Liste mit aufgenommen.

Vorhandene Termine werden beim Start uebernommen. Zusaetzlich liest die
Abfrage das Haupt-Gegenueber IMMER mit dazu -- die Liste stimmt damit
auch dann, wenn die Uebernahme nicht gelaufen ist (Sicherung
zurueckgespielt, Neustart ausgeblieben). Genau das ist beim Bauen
aufgefallen: Ein alter Termin zeigte "niemand dabei", obwohl ein
Gegenueber eingetragen war.

Serien vererben ihre Teilnehmer an jede erzeugte Auspraegung -- sonst
saehe der zweite Mensch den woechentlichen Call einmal und danach nie
wieder.

Neue Pruefung pruef-teilnehmer.mjs mit 40 Punkten: beide sehen ihn,
Fremde nicht, kein Selbsteintragen, Hinzufuegen und Entfernen, Serien,
Unsinn in der Liste. Mit Gegenprobe, die nachweist, dass die Messung
"nicht sichtbar" ueberhaupt erkennt. Im echten Browser gegengeprueft:
Schalter da, zwei angehakt, gespeichert, beide zurueck, keine
Konsolenfehler.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-05 14:21:09 +02:00
co-authored by Claude Opus 5
parent 8759a46e5b
commit 529c814389
28 changed files with 842 additions and 151 deletions
+59 -4
View File
@@ -59,7 +59,7 @@
import express from "express";
import {
db, protokolliere, echteIp, sitzungLesen, istLeitung, termineSichtbar,
heuteLokal, tagLokal, externPruefen, externSql,
heuteLokal, tagLokal, externPruefen, externSql, betreuteIds,
} from "./workspace.js";
export const serienRouter = express.Router();
@@ -269,13 +269,35 @@ export function serieNachfuellen(serieId) {
serie_beruehrt)
VALUES (?,?,?,?,?,?,?,?,?,0,?,?,?,?,0)`);
/* DIE TEILNEHMER DER SERIE ERBEN (05.09.2026).
Die Teilnehmer gehoeren zur REGEL, nicht zur einzelnen Auspraegung
-- sonst muesste man sie jede Woche neu eintragen. Beim Anlegen
eines Termins werden sie deshalb mit hinuebergeschrieben.
Das ist nicht nur Bequemlichkeit: An der Teilnehmerliste haengt die
SICHTBARKEIT. Ohne diese Zeilen saehe der zweite Teilnehmer den
einzelnen Termin nicht, obwohl er in der Serie steht -- der Call
stuende in seinem Kalender einmal drin und beim naechsten Mal
nicht. */
const serienLeute = d.prepare(
"SELECT person_id FROM serie_teilnehmer WHERE serie_id = ?").all(s.id).map((r) => r.person_id);
const dazu = d.prepare(
"INSERT OR IGNORE INTO termin_teilnehmer (termin_id, person_id) VALUES (?,?)");
let neu = 0;
const stempel = jetztStempel();
for (const tag of tage) {
if (da.has(tag) || aus.has(tag)) continue;
einfuegen.run(s.titel, s.beschreibung, s.art, `${tag}T${s.uhrzeit}`, s.dauer_min,
const { lastInsertRowid } = einfuegen.run(
s.titel, s.beschreibung, s.art, `${tag}T${s.uhrzeit}`, s.dauer_min,
s.ort, s.creator_id, s.teilnehmer_id, s.teilnehmer_extern ?? null,
stempel, s.erstellt_von, s.id, tag);
/* Haupt-Gegenueber und Serienteilnehmer -- dieselbe Regel wie beim
einzelnen Termin: Die Liste ist die vollstaendige Antwort. */
for (const pid of new Set([...(s.teilnehmer_id ? [s.teilnehmer_id] : []), ...serienLeute])) {
dazu.run(Number(lastInsertRowid), pid);
}
neu++;
}
return neu;
@@ -405,7 +427,7 @@ export function serienPruefen(körper, { neu }) {
/** Legt eine Serie an und füllt sie sofort auf. Wird auch vom
* Kalender-Router benutzt, wenn im Terminformular ein Rhythmus gewählt
* wurde -- ein Weg, eine Prüfung, ein Protokolleintrag. */
export function serieAnlegen(aus, person) {
export function serieAnlegen(aus, person, teilnehmer = null) {
const d = db();
const { lastInsertRowid } = d.prepare(`
INSERT INTO termin_serien
@@ -418,6 +440,13 @@ export function serieAnlegen(aus, person) {
jetztStempel(), person.id);
const id = Number(lastInsertRowid);
/* Die Teilnehmer VOR dem Nachfuellen eintragen -- der Nachfueller
kopiert sie an jeden erzeugten Termin. Stuenden sie erst danach da,
waeren die Termine des ersten Horizonts ohne Teilnehmer, und der
zweite Mensch saehe genau die nicht. */
serieTeilnehmerSetzen(id, teilnehmer, person, { hauptId: aus.teilnehmer_id ?? null });
const angelegt = serieNachfuellen(id);
protokolliere("serie_angelegt", {
personId: person.id, rolle: person.rolle, ip: null,
@@ -426,6 +455,32 @@ export function serieAnlegen(aus, person) {
return { id, angelegt };
}
/** Teilnehmer einer SERIE setzen. Wortgleich zur Fassung im Kalender --
* dieselbe Grenze, wer eingetragen werden darf, nur auf der anderen
* Tabelle. Ein Aufruf ueber die Modulgrenze waere hier ein Ringschluss
* (kalender importiert serien und umgekehrt), deshalb die zweite
* Fassung; die gemeinsame Regel dahinter (`betreuteIds`) ist aber
* dieselbe. */
export function serieTeilnehmerSetzen(serieId, ids, person, { hauptId = null } = {}) {
const d = db();
const erlaubt = istLeitung(person) ? null : new Set([person.id, ...betreuteIds(person)]);
const sauber = new Set();
if (hauptId) sauber.add(Number(hauptId));
for (const roh of Array.isArray(ids) ? ids : []) {
const z = Number(roh);
if (!Number.isInteger(z) || z < 1) continue;
if (erlaubt && !erlaubt.has(z)) continue;
sauber.add(z);
}
const gueltig = [...sauber].filter((id) =>
d.prepare("SELECT 1 FROM personen WHERE id = ? AND aktiv = 1").get(id));
d.prepare("DELETE FROM serie_teilnehmer WHERE serie_id = ?").run(serieId);
const einf = d.prepare("INSERT OR IGNORE INTO serie_teilnehmer (serie_id, person_id) VALUES (?,?)");
for (const id of gueltig) einf.run(serieId, id);
return gueltig;
}
/** Zuordnung erzwingen für alle, die nicht Leitung sind: Sie legen nur
* für sich selbst an. Dieselbe Regel wie beim einzelnen Termin -- sonst
* wäre die Wiederholung ein Weg, sie zu umgehen. */
@@ -521,7 +576,7 @@ serienRouter.post("/workspace/api/serien", gleicheHerkunft, (req, res) => {
return res.status(400).json({ fehler: "Zugeordnete Person gibt es nicht." });
}
}
const { id, angelegt } = serieAnlegen(aus, req.person);
const { id, angelegt } = serieAnlegen(aus, req.person, req.body?.teilnehmer);
res.status(201).json({ id, angelegt });
} catch (fehler) {
console.error("[workspace] Serie anlegen:", fehler?.message);