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:
@@ -14,7 +14,7 @@
|
||||
import express from "express";
|
||||
import {
|
||||
db, protokolliere, echteIp, sitzungLesen, istLeitung, istDogFather, betreutWo, termineSichtbar, heuteLokal,
|
||||
externPruefen, externSql,
|
||||
externPruefen, externSql, betreuteIds, ROLLEN_SORTIERUNG,
|
||||
} from "./workspace.js";
|
||||
import {
|
||||
nachfuellenAlle, serienPruefen, serieAnlegen, zuordnungErzwingen,
|
||||
@@ -76,6 +76,111 @@ const VERBUND = `
|
||||
LEFT JOIN personen pc ON pc.id = t.creator_id
|
||||
LEFT JOIN personen pt ON pt.id = t.teilnehmer_id`;
|
||||
|
||||
/* =====================================================================
|
||||
MEHRERE TEILNEHMER (05.09.2026)
|
||||
|
||||
Wunsch Filipe: *"wenn ich im kalender was eintrage will ich dass ich
|
||||
auch 2 leute markieren kann mit denen der call ist."*
|
||||
|
||||
`teilnehmer_id` bleibt das Haupt-Gegenueber; die Tabelle
|
||||
`termin_teilnehmer` sagt, wer sonst noch dabei ist. Beim Speichern
|
||||
wird das Haupt-Gegenueber IMMER mit in die Liste geschrieben -- so
|
||||
muss keine Abfrage zwei Stellen zusammensuchen, und die Liste ist die
|
||||
vollstaendige Antwort auf "wer ist dabei".
|
||||
|
||||
WER DARF EINGETRAGEN WERDEN. Dieselbe Grenze wie bei den Dateien:
|
||||
Die Leitung jeden, ein Scout seine betreuten Creator und sich selbst,
|
||||
ein Creator nur sich selbst. Sonst koennte man sich in fremde Termine
|
||||
eintragen -- und weil an der Liste die SICHTBARKEIT haengt, waere das
|
||||
ein Weg, fremde Termine lesbar zu machen. Deshalb wird hier gefiltert
|
||||
und nicht in der Oberflaeche.
|
||||
===================================================================== */
|
||||
|
||||
/** Die Nummern, die diese Person an einem Termin eintragen darf. */
|
||||
function darfEintragen(person) {
|
||||
if (istLeitung(person)) return null; // null = alle
|
||||
const erlaubt = new Set([person.id]);
|
||||
for (const id of betreuteIds(person)) erlaubt.add(id);
|
||||
return erlaubt;
|
||||
}
|
||||
|
||||
/** Teilnehmerliste eines Termins setzen. Ersetzt die bisherige. */
|
||||
export function teilnehmerSetzen(terminId, ids, person, { hauptId = null } = {}) {
|
||||
const d = db();
|
||||
const erlaubt = darfEintragen(person);
|
||||
const sauber = new Set();
|
||||
|
||||
/* Das Haupt-Gegenueber gehoert immer dazu -- auch wenn die Oberflaeche
|
||||
es nicht noch einmal mitschickt. */
|
||||
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; // still weglassen, nicht ablehnen
|
||||
sauber.add(z);
|
||||
}
|
||||
|
||||
/* Nur aktive Personen. Eine gesperrte Person in der Liste zu lassen
|
||||
hiesse, ihr einen Termin sichtbar zu halten, den sie nicht mehr
|
||||
sehen soll. */
|
||||
const gueltig = [...sauber].filter((id) =>
|
||||
d.prepare("SELECT 1 FROM personen WHERE id = ? AND aktiv = 1").get(id));
|
||||
|
||||
d.prepare("DELETE FROM termin_teilnehmer WHERE termin_id = ?").run(terminId);
|
||||
const einf = d.prepare("INSERT OR IGNORE INTO termin_teilnehmer (termin_id, person_id) VALUES (?,?)");
|
||||
for (const id of gueltig) einf.run(terminId, id);
|
||||
return gueltig;
|
||||
}
|
||||
|
||||
/** Die Teilnehmer mehrerer Termine auf einmal -- eine Abfrage statt
|
||||
* einer je Zeile. Bei dreissig Terminen im Blick waeren das sonst
|
||||
* dreissig Abfragen fuer eine Liste.
|
||||
*
|
||||
* DAS HAUPT-GEGENUEBER IST IMMER DABEI, auch wenn es in
|
||||
* termin_teilnehmer fehlt.
|
||||
*
|
||||
* Beim Umstellen werden die vorhandenen `teilnehmer_id` einmalig in die
|
||||
* neue Tabelle uebernommen (siehe workspace.js). Diese Abfrage
|
||||
* verlaesst sich aber NICHT darauf: Sie liest das Haupt-Gegenueber
|
||||
* ohnehin mit und legt es dazu.
|
||||
*
|
||||
* Der Unterschied faellt genau dann auf, wenn die Uebernahme nicht
|
||||
* gelaufen ist -- etwa bei einem Termin, der aus einer Sicherung
|
||||
* zurueckkam, oder waehrend eines Deploys, bei dem der Neustart
|
||||
* ausblieb. Dann stuende an einem Termin mit Gegenueber "niemand
|
||||
* dabei". Eine Liste, die je nach Zeitpunkt etwas anderes behauptet,
|
||||
* ist schlimmer als eine, die einmal zu viel nachsieht. */
|
||||
export function teilnehmerZu(terminIds) {
|
||||
const karte = new Map();
|
||||
if (!terminIds.length) return karte;
|
||||
const platz = terminIds.map(() => "?").join(",");
|
||||
|
||||
const zeilen = db().prepare(`
|
||||
SELECT tt.termin_id, p.id, p.name, p.rolle
|
||||
FROM termin_teilnehmer tt JOIN personen p ON p.id = tt.person_id
|
||||
WHERE tt.termin_id IN (${platz})
|
||||
ORDER BY ${ROLLEN_SORTIERUNG.replace(/rolle/g, "p.rolle")}, p.name`).all(...terminIds);
|
||||
for (const z of zeilen) {
|
||||
if (!karte.has(z.termin_id)) karte.set(z.termin_id, []);
|
||||
karte.get(z.termin_id).push({ id: z.id, name: z.name, rolle: z.rolle });
|
||||
}
|
||||
|
||||
/* Das Haupt-Gegenueber dazu, falls es nicht ohnehin schon dasteht. */
|
||||
const haupt = db().prepare(`
|
||||
SELECT t.id AS termin_id, p.id, p.name, p.rolle
|
||||
FROM termine t JOIN personen p ON p.id = t.teilnehmer_id
|
||||
WHERE t.id IN (${platz})`).all(...terminIds);
|
||||
for (const z of haupt) {
|
||||
if (!karte.has(z.termin_id)) karte.set(z.termin_id, []);
|
||||
const liste = karte.get(z.termin_id);
|
||||
if (!liste.some((p) => p.id === z.id)) {
|
||||
liste.unshift({ id: z.id, name: z.name, rolle: z.rolle });
|
||||
}
|
||||
}
|
||||
return karte;
|
||||
}
|
||||
|
||||
/* ---------- Lesen ------------------------------------------------------- */
|
||||
|
||||
kalenderRouter.get("/workspace/api/termine", (req, res) => {
|
||||
@@ -105,6 +210,12 @@ kalenderRouter.get("/workspace/api/termine", (req, res) => {
|
||||
WHERE ${wo} AND t.beginn >= ? AND t.beginn < ?
|
||||
ORDER BY t.beginn`).all(...werte, von, bis + "T23:59:59Z");
|
||||
|
||||
/* Die Teilnehmer in EINER Abfrage fuer alle Termine nachladen --
|
||||
nicht je Zeile eine. Bei dreissig Terminen im Blick waeren das
|
||||
sonst dreissig zusaetzliche Abfragen fuer eine einzige Ansicht. */
|
||||
const wer = teilnehmerZu(termine.map((t) => t.id));
|
||||
for (const t of termine) t.teilnehmer = wer.get(t.id) || [];
|
||||
|
||||
/* Fristen aus den Aufgaben -- nur zum Anzeigen, nicht bearbeitbar.
|
||||
Die Sichtbarkeitsregel der Aufgaben gilt dabei unveraendert. */
|
||||
/* HIER STAND istLeitung -- und damit sah JEDE Managerin die Fristen
|
||||
@@ -245,7 +356,7 @@ kalenderRouter.post("/workspace/api/termine", gleicheHerkunft, (req, res) => {
|
||||
const gepruft = serienPruefen(serie, { neu: true });
|
||||
if (gepruft.fehler.length) return res.status(400).json({ fehler: gepruft.fehler.join(" ") });
|
||||
zuordnungErzwingen(gepruft.aus, req.person);
|
||||
const { id, angelegt } = serieAnlegen(gepruft.aus, req.person);
|
||||
const { id, angelegt } = serieAnlegen(gepruft.aus, req.person, req.body?.teilnehmer);
|
||||
return res.status(201).json({ serie_id: id, angelegt });
|
||||
}
|
||||
|
||||
@@ -258,11 +369,18 @@ kalenderRouter.post("/workspace/api/termine", gleicheHerkunft, (req, res) => {
|
||||
aus.dauer_min ?? 30, aus.ort ?? null, aus.creator_id ?? null,
|
||||
aus.teilnehmer_id ?? null, aus.teilnehmer_extern ?? null, jetzt(), req.person.id);
|
||||
|
||||
/* Die weiteren Teilnehmer. Das Haupt-Gegenueber kommt als `hauptId`
|
||||
mit hinein, damit die Liste vollstaendig ist -- an ihr haengt die
|
||||
Sichtbarkeit. */
|
||||
const dabei = teilnehmerSetzen(Number(lastInsertRowid), req.body?.teilnehmer, req.person,
|
||||
{ hauptId: aus.teilnehmer_id ?? null });
|
||||
|
||||
protokolliere("termin_angelegt", {
|
||||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||||
detail: `#${lastInsertRowid} ${aus.titel}`.slice(0, 120),
|
||||
detail: `#${lastInsertRowid} ${aus.titel}`.slice(0, 120)
|
||||
+ (dabei.length > 1 ? ` (${dabei.length} Teilnehmer)` : ""),
|
||||
});
|
||||
res.status(201).json({ id: Number(lastInsertRowid) });
|
||||
res.status(201).json({ id: Number(lastInsertRowid), teilnehmer: dabei });
|
||||
} catch (fehler) {
|
||||
console.error("[workspace] Termin anlegen:", fehler?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
@@ -286,8 +404,18 @@ kalenderRouter.patch("/workspace/api/termine/:id", gleicheHerkunft, (req, res) =
|
||||
delete aus.creator_id; delete aus.teilnehmer_id; delete aus.teilnehmer_extern;
|
||||
}
|
||||
|
||||
/* Die Teilnehmerliste aendern darf, wer den Termin aendern darf --
|
||||
aber nur die Leitung darf ueberhaupt Zuordnungen verschieben
|
||||
(siehe oben). Fuer alle anderen wird die Liste gar nicht erst
|
||||
angefasst; sonst koennte ein Creator sich selbst aus einem
|
||||
Termin entfernen, den das Management angesetzt hat, oder sich in
|
||||
einen fremden eintragen. */
|
||||
const teilnehmerNeu = istLeitung(req.person) && req.body?.teilnehmer !== undefined;
|
||||
|
||||
const felder = Object.keys(aus);
|
||||
if (!felder.length) return res.status(400).json({ fehler: "nichts_zu_aendern" });
|
||||
if (!felder.length && !teilnehmerNeu) {
|
||||
return res.status(400).json({ fehler: "nichts_zu_aendern" });
|
||||
}
|
||||
|
||||
/* Stammt der Termin aus einer Wiederholung, gilt er ab jetzt als
|
||||
"angefasst". Das entscheidet später zweierlei: Beim Abstellen der
|
||||
@@ -295,14 +423,27 @@ kalenderRouter.patch("/workspace/api/termine/:id", gleicheHerkunft, (req, res) =
|
||||
neu gebaut. Was jemand verschoben, umbenannt oder abgehakt hat,
|
||||
räumt die Automatik ihm nicht weg. */
|
||||
const beruehrt = termin.serie_id ? ", serie_beruehrt = 1" : "";
|
||||
db().prepare(`UPDATE termine SET ${felder.map((f) => `${f} = ?`).join(", ")}${beruehrt}
|
||||
WHERE id = ?`).run(...felder.map((f) => aus[f]), id);
|
||||
if (felder.length) {
|
||||
db().prepare(`UPDATE termine SET ${felder.map((f) => `${f} = ?`).join(", ")}${beruehrt}
|
||||
WHERE id = ?`).run(...felder.map((f) => aus[f]), id);
|
||||
} else if (beruehrt) {
|
||||
db().prepare("UPDATE termine SET serie_beruehrt = 1 WHERE id = ?").run(id);
|
||||
}
|
||||
|
||||
/* Das Haupt-Gegenueber ist nach dem Aendern moeglicherweise ein
|
||||
anderes -- deshalb der Wert aus `aus`, nicht der alte aus
|
||||
`termin`. Wurde es nicht angefasst, gilt der alte weiter. */
|
||||
let dabei = null;
|
||||
if (teilnehmerNeu) {
|
||||
const haupt = aus.teilnehmer_id !== undefined ? aus.teilnehmer_id : termin.teilnehmer_id;
|
||||
dabei = teilnehmerSetzen(id, req.body.teilnehmer, req.person, { hauptId: haupt });
|
||||
}
|
||||
|
||||
protokolliere("termin_geaendert", {
|
||||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||||
detail: `#${id} ${felder.join(",")}`.slice(0, 120),
|
||||
detail: `#${id} ${felder.join(",")}${dabei ? ` teilnehmer:${dabei.length}` : ""}`.slice(0, 120),
|
||||
});
|
||||
res.json({ ok: true });
|
||||
res.json({ ok: true, ...(dabei ? { teilnehmer: dabei } : {}) });
|
||||
} catch (fehler) {
|
||||
console.error("[workspace] Termin ändern:", fehler?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
|
||||
Reference in New Issue
Block a user