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]>
|
Before Width: | Height: | Size: 774 KiB After Width: | Height: | Size: 774 KiB |
|
Before Width: | Height: | Size: 875 KiB After Width: | Height: | Size: 873 KiB |
|
Before Width: | Height: | Size: 228 KiB After Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 774 KiB After Width: | Height: | Size: 774 KiB |
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 895 KiB After Width: | Height: | Size: 894 KiB |
@@ -0,0 +1,295 @@
|
||||
/* MEHRERE TEILNEHMER AN EINEM TERMIN
|
||||
|
||||
Wunsch Filipe (05.09.2026): *"wenn ich im kalender was eintrage will
|
||||
ich dass ich auch 2 leute markieren kann mit denen der call ist."*
|
||||
|
||||
WARUM DAS EINE EIGENE PRUEFUNG BRAUCHT. An der Teilnehmerliste haengt
|
||||
die SICHTBARKEIT: Wer eingetragen ist, sieht den Termin. Damit ist
|
||||
jede Zeile hier gleichzeitig eine Rechteregel. Zwei Fehler waeren
|
||||
moeglich, und beide fallen im Alltag nicht auf:
|
||||
|
||||
zu wenig Ein Eingetragener sieht seinen eigenen Termin nicht.
|
||||
Er merkt es nur, wenn er ihn sucht -- und haelt es dann
|
||||
fuer sein eigenes Versehen.
|
||||
|
||||
zu viel Jemand traegt sich in einen fremden Termin ein und macht
|
||||
ihn sich damit sichtbar. Das ist ein Datenleck mit
|
||||
Selbstbedienung.
|
||||
|
||||
Beides wird hier gemessen, nicht angenommen.
|
||||
|
||||
DIE ANZAHL GEHOERT IN DIE BEDINGUNG -- ein Lauf, der keinen Termin
|
||||
angelegt hat, ist kein bestandener Lauf.
|
||||
|
||||
GEGENPROBE: Am Ende muss nachgewiesen sein, dass diese Pruefung
|
||||
ueberhaupt "nicht sichtbar" erkennen kann. */
|
||||
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const ordner = mkdtempSync(join(tmpdir(), "ws-teiln-"));
|
||||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||||
/* Der Port muss frei sein -- sonst wird gegen einen fremden Server
|
||||
gemessen. Begruendung in helfer-port.mjs. */
|
||||
const { portMussFreiSein } = await import("./helfer-port.mjs");
|
||||
await portMussFreiSein(4321, "die Teilnehmerpruefung");
|
||||
|
||||
process.env.PORT = "4321";
|
||||
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
||||
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, 900));
|
||||
const BASIS = "http://127.0.0.1:4321";
|
||||
|
||||
let fehler = 0;
|
||||
const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||||
|
||||
/* ---- Bestand ------------------------------------------------------------ */
|
||||
const { DatabaseSync } = await import("node:sqlite");
|
||||
const { scryptSync, randomBytes } = await import("node:crypto");
|
||||
await fetch(BASIS + "/workspace/api/anmelden", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
|
||||
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);
|
||||
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("Chef", "admin", "CODE-DOGI-0001");
|
||||
const idPat = anlegen("Pat", "scout", "CODE-SCOU-0001");
|
||||
const idBea = anlegen("Bea", "scout", "CODE-SCOU-0002");
|
||||
const idLuna = anlegen("Luna", "creator", "CODE-CREA-0001");
|
||||
const idNora = anlegen("Nora", "creator", "CODE-CREA-0002");
|
||||
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idLuna, idPat, jetzt);
|
||||
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idNora, idBea, jetzt);
|
||||
|
||||
/* EIN ALTER TERMIN, angelegt wie vor der Umstellung: nur
|
||||
teilnehmer_id, kein Eintrag in termin_teilnehmer. Er prueft, dass die
|
||||
Uebernahme beim Start wirklich gelaufen ist. */
|
||||
d.prepare(`INSERT INTO termine (titel, art, beginn, dauer_min, teilnehmer_id, erstellt, erstellt_von)
|
||||
VALUES ('Alter Termin von frueher','call',?,30,?,?,?)`)
|
||||
.run(heute + "T09:00", idLuna, jetzt, idDogi);
|
||||
const idAlt = d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||||
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 }) });
|
||||
const keks = (a.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; ");
|
||||
if (!keks) throw new Error(`Anmeldung ${rolle} fehlgeschlagen (${a.status})`);
|
||||
return keks;
|
||||
}
|
||||
const K = {
|
||||
dogi: await anmelden("admin", "CODE-DOGI-0001"),
|
||||
pat: await anmelden("scout", "CODE-SCOU-0001"),
|
||||
bea: await anmelden("scout", "CODE-SCOU-0002"),
|
||||
luna: await anmelden("creator", "CODE-CREA-0001"),
|
||||
nora: await anmelden("creator", "CODE-CREA-0002"),
|
||||
};
|
||||
|
||||
const ruf = async (art, pfad, rumpf, keks) => {
|
||||
const a = await fetch(BASIS + pfad, {
|
||||
method: art,
|
||||
headers: { "Content-Type": "application/json", cookie: keks, origin: BASIS },
|
||||
body: rumpf === undefined ? undefined : JSON.stringify(rumpf),
|
||||
});
|
||||
let text = ""; try { text = await a.text(); } catch { /* egal */ }
|
||||
let daten = null; try { daten = JSON.parse(text); } catch { /* kein JSON */ }
|
||||
return { status: a.status, text, daten };
|
||||
};
|
||||
|
||||
/** Sieht diese Person den Termin mit dieser Nummer? */
|
||||
async function siehtTermin(keks, id) {
|
||||
const a = await ruf("GET", "/workspace/api/termine?tage=30", undefined, keks);
|
||||
if (a.status !== 200) return false;
|
||||
return (a.daten?.termine || []).some((t) => t.id === id);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
1. Die Uebernahme alter Termine
|
||||
======================================================================= */
|
||||
console.log("\n=== Alte Termine behalten ihren Teilnehmer ===");
|
||||
{
|
||||
const a = await ruf("GET", "/workspace/api/termine?tage=30", undefined, K.dogi);
|
||||
const alt = (a.daten?.termine || []).find((t) => t.id === idAlt);
|
||||
ok(!!alt, "der alte Termin ist da");
|
||||
ok(Array.isArray(alt?.teilnehmer) && alt.teilnehmer.some((p) => p.id === idLuna),
|
||||
`sein Teilnehmer wurde uebernommen (${(alt?.teilnehmer || []).map((p) => p.name).join(", ") || "LEER"})`);
|
||||
ok(await siehtTermin(K.luna, idAlt), "und Luna sieht ihn weiterhin");
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
2. Zwei Teilnehmer anlegen
|
||||
======================================================================= */
|
||||
console.log("\n=== Ein Call mit zwei Leuten ===");
|
||||
let idCall = 0;
|
||||
{
|
||||
const a = await ruf("POST", "/workspace/api/termine", {
|
||||
titel: "Call zu dritt", art: "call", beginn: heute + "T14:00",
|
||||
teilnehmer_id: idLuna, teilnehmer: [idLuna, idPat],
|
||||
}, K.dogi);
|
||||
ok(a.status === 201, `angelegt (${a.status})`);
|
||||
idCall = a.daten?.id || 0;
|
||||
ok(idCall > 0, `hat eine Nummer bekommen (#${idCall})`);
|
||||
ok((a.daten?.teilnehmer || []).length === 2,
|
||||
`zwei Teilnehmer gespeichert (${(a.daten?.teilnehmer || []).length})`);
|
||||
|
||||
const b = await ruf("GET", "/workspace/api/termine?tage=30", undefined, K.dogi);
|
||||
const t = (b.daten?.termine || []).find((x) => x.id === idCall);
|
||||
ok((t?.teilnehmer || []).length === 2,
|
||||
`und sie kommen zurueck: ${(t?.teilnehmer || []).map((p) => p.name).join(", ") || "LEER"}`);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
3. BEIDE sehen den Termin — das ist der Kern
|
||||
======================================================================= */
|
||||
console.log("\n=== Beide Eingetragenen sehen ihn ===");
|
||||
ok(await siehtTermin(K.luna, idCall), "Luna sieht den Call (Haupt-Gegenueber)");
|
||||
ok(await siehtTermin(K.pat, idCall), "Pat sieht den Call (zweiter Teilnehmer)");
|
||||
ok(await siehtTermin(K.dogi, idCall), "DogFather sieht ihn ohnehin");
|
||||
|
||||
/* =======================================================================
|
||||
4. Und die anderen NICHT
|
||||
======================================================================= */
|
||||
console.log("\n=== Wer nicht dabei ist, sieht ihn nicht ===");
|
||||
ok(!(await siehtTermin(K.nora, idCall)), "Nora (fremde Creatorin) sieht ihn nicht");
|
||||
ok(!(await siehtTermin(K.bea, idCall)), "Bea (fremde Scoutin) sieht ihn nicht");
|
||||
|
||||
/* =======================================================================
|
||||
5. Niemand traegt sich selbst in fremde Termine ein
|
||||
======================================================================= */
|
||||
console.log("\n=== Kein Eintragen in fremde Termine ===");
|
||||
{
|
||||
/* Nora versucht, sich in den Call einzutragen. Sie sieht ihn nicht --
|
||||
also muss schon das Aendern scheitern (404, nicht 403: Was man
|
||||
nicht sehen darf, existiert fuer einen nicht). */
|
||||
const a = await ruf("PATCH", `/workspace/api/termine/${idCall}`,
|
||||
{ teilnehmer: [idNora] }, K.nora);
|
||||
ok(a.status === 404, `Nora aendert den fremden Call -> ${a.status} (erwartet 404)`);
|
||||
ok(!(await siehtTermin(K.nora, idCall)), "und sie sieht ihn danach immer noch nicht");
|
||||
|
||||
/* Pat SIEHT den Call (er ist Teilnehmer). Aendert er die Liste,
|
||||
darf das nichts bewirken -- Zuordnungen verschiebt nur die
|
||||
Leitung. */
|
||||
const b = await ruf("PATCH", `/workspace/api/termine/${idCall}`,
|
||||
{ teilnehmer: [idPat, idNora] }, K.pat);
|
||||
ok(b.status < 500, `Pat aendert die Liste -> ${b.status} (kein Serverfehler)`);
|
||||
ok(!(await siehtTermin(K.nora, idCall)),
|
||||
"Nora ist dadurch NICHT hineingekommen — ein Scout verschiebt keine Zuordnungen");
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
6. Aendern: jemanden hinzufuegen und wieder entfernen
|
||||
======================================================================= */
|
||||
console.log("\n=== Hinzufuegen und Entfernen ===");
|
||||
{
|
||||
const a = await ruf("PATCH", `/workspace/api/termine/${idCall}`,
|
||||
{ teilnehmer: [idLuna, idPat, idBea] }, K.dogi);
|
||||
ok(a.status === 200, `dritte Person hinzugefuegt (${a.status})`);
|
||||
ok(await siehtTermin(K.bea, idCall), "Bea sieht den Call jetzt");
|
||||
|
||||
const b = await ruf("PATCH", `/workspace/api/termine/${idCall}`,
|
||||
{ teilnehmer: [idLuna] }, K.dogi);
|
||||
ok(b.status === 200, `wieder entfernt (${b.status})`);
|
||||
ok(!(await siehtTermin(K.bea, idCall)), "Bea sieht ihn nicht mehr");
|
||||
ok(await siehtTermin(K.luna, idCall), "Luna weiterhin schon");
|
||||
|
||||
/* Das Haupt-Gegenueber bleibt drin, auch wenn die Liste es nicht
|
||||
nennt -- sonst verlöre der Termin seinen eigentlichen Partner. */
|
||||
const c = await ruf("PATCH", `/workspace/api/termine/${idCall}`,
|
||||
{ teilnehmer: [] }, K.dogi);
|
||||
ok(c.status === 200, `leere Liste geschickt (${c.status})`);
|
||||
ok(await siehtTermin(K.luna, idCall),
|
||||
"das Haupt-Gegenueber bleibt trotzdem eingetragen");
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
7. Serien vererben ihre Teilnehmer
|
||||
======================================================================= */
|
||||
console.log("\n=== Wiederkehrende Termine ===");
|
||||
{
|
||||
const a = await ruf("POST", "/workspace/api/serien", {
|
||||
titel: "Woechentlicher Team-Call", art: "call", takt: "woechentlich",
|
||||
start_tag: heute, uhrzeit: "11:00", dauer_min: 30,
|
||||
teilnehmer_id: idLuna, teilnehmer: [idLuna, idPat],
|
||||
}, K.dogi);
|
||||
ok(a.status === 201, `Serie angelegt (${a.status})`);
|
||||
ok((a.daten?.angelegt || 0) > 1, `${a.daten?.angelegt} Termine erzeugt`);
|
||||
|
||||
const b = await ruf("GET", "/workspace/api/termine?tage=60", undefined, K.dogi);
|
||||
const ausSerie = (b.daten?.termine || []).filter((t) => t.titel === "Woechentlicher Team-Call");
|
||||
ok(ausSerie.length > 1, `${ausSerie.length} davon stehen im Kalender`);
|
||||
const alleMitBeiden = ausSerie.every((t) =>
|
||||
(t.teilnehmer || []).some((p) => p.id === idLuna)
|
||||
&& (t.teilnehmer || []).some((p) => p.id === idPat));
|
||||
ok(alleMitBeiden, "JEDE Auspraegung hat beide Teilnehmer geerbt");
|
||||
|
||||
const c = await ruf("GET", "/workspace/api/termine?tage=60", undefined, K.pat);
|
||||
const patSieht = (c.daten?.termine || []).filter((t) => t.titel === "Woechentlicher Team-Call");
|
||||
ok(patSieht.length === ausSerie.length,
|
||||
`Pat sieht alle ${patSieht.length} von ${ausSerie.length} — nicht nur den ersten`);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
8. Unsinn in der Liste
|
||||
======================================================================= */
|
||||
console.log("\n=== Unsinn in der Teilnehmerliste ===");
|
||||
{
|
||||
for (const [wie, wert] of [
|
||||
["Text statt Nummern", ["abc", "xyz"]],
|
||||
["negative Nummer", [-1]],
|
||||
["null", [null]],
|
||||
["gar keine Liste", "keine Liste"],
|
||||
["riesige Liste", Array.from({ length: 500 }, (_, i) => i + 1)],
|
||||
["doppelte Eintraege", [idLuna, idLuna, idLuna]],
|
||||
["Person, die es nicht gibt", [999999]],
|
||||
]) {
|
||||
const a = await ruf("PATCH", `/workspace/api/termine/${idCall}`,
|
||||
{ teilnehmer: wert }, K.dogi);
|
||||
ok(a.status < 500, `${wie} -> ${a.status} (kein Serverfehler)`);
|
||||
}
|
||||
ok(await siehtTermin(K.luna, idCall),
|
||||
"nach all dem ist das Haupt-Gegenueber immer noch eingetragen");
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
GEGENPROBE — kann diese Pruefung "nicht sichtbar" erkennen?
|
||||
======================================================================= */
|
||||
console.log("\n=== Gegenprobe ===");
|
||||
{
|
||||
const a = await ruf("POST", "/workspace/api/termine", {
|
||||
titel: "Nur fuer DogFather", art: "termin", beginn: heute + "T20:00",
|
||||
}, K.dogi);
|
||||
const geheim = a.daten?.id;
|
||||
ok(geheim > 0, `ein Termin ohne Teilnehmer angelegt (#${geheim})`);
|
||||
ok(await siehtTermin(K.dogi, geheim), "DogFather sieht ihn");
|
||||
ok(!(await siehtTermin(K.luna, geheim)),
|
||||
"Luna sieht ihn NICHT — die Messung erkennt also 'nicht sichtbar'");
|
||||
|
||||
const b = await ruf("PATCH", `/workspace/api/termine/${geheim}`,
|
||||
{ teilnehmer: [idLuna] }, K.dogi);
|
||||
ok(b.status === 200, `Luna eingetragen (${b.status})`);
|
||||
ok(await siehtTermin(K.luna, geheim),
|
||||
"jetzt sieht sie ihn — die Messung erkennt auch 'sichtbar'");
|
||||
}
|
||||
|
||||
console.log(`\n${fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`}`);
|
||||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
||||
process.exit(fehler ? 1 : 0);
|
||||
@@ -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" });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -248,6 +248,38 @@ function umstellungen(d) {
|
||||
console.error("[workspace] Index idx_termine_serie:", fehler?.message);
|
||||
}
|
||||
|
||||
/* ---- Die vorhandenen Teilnehmer in die neue Tabelle uebernehmen ----
|
||||
|
||||
Ohne diesen Schritt haette jeder Termin, der vor dem 05.09.2026
|
||||
angelegt wurde, eine LEERE Teilnehmerliste -- und weil an dieser
|
||||
Liste die Sichtbarkeit haengt, faende die eingetragene Person ihren
|
||||
eigenen Termin nicht mehr. Ein stiller Datenverlust, der erst
|
||||
auffaellt, wenn jemand einen Call sucht, den es noch gibt.
|
||||
|
||||
`INSERT OR IGNORE` macht den Lauf wiederholbar: Beim zweiten Start
|
||||
ist alles schon da und nichts passiert. Deshalb steht das hier bei
|
||||
den Umstellungen und nicht in einem einmaligen Skript, das jemand
|
||||
vergessen koennte. */
|
||||
for (const [tabelle, quelle, ziel, schluessel] of [
|
||||
["termin_teilnehmer", "termine", "termin_id", "id"],
|
||||
["serie_teilnehmer", "termin_serien", "serie_id", "id"],
|
||||
]) {
|
||||
try {
|
||||
const spalten = d.prepare(`PRAGMA table_info(${quelle})`).all().map((s) => s.name);
|
||||
if (!spalten.includes("teilnehmer_id")) continue;
|
||||
const vorher = d.prepare(`SELECT COUNT(*) AS n FROM ${tabelle}`).get().n;
|
||||
d.exec(`INSERT OR IGNORE INTO ${tabelle} (${ziel}, person_id)
|
||||
SELECT ${schluessel}, teilnehmer_id FROM ${quelle}
|
||||
WHERE teilnehmer_id IS NOT NULL`);
|
||||
const nachher = d.prepare(`SELECT COUNT(*) AS n FROM ${tabelle}`).get().n;
|
||||
if (nachher > vorher) {
|
||||
console.log(`[workspace] ${nachher - vorher} vorhandene Teilnehmer nach ${tabelle} uebernommen.`);
|
||||
}
|
||||
} catch (fehler) {
|
||||
console.error(`[workspace] Uebernahme nach ${tabelle}:`, fehler?.message);
|
||||
}
|
||||
}
|
||||
|
||||
/* Content-Saeulen: die drei bis fuenf Themen, aus denen der Kanal
|
||||
besteht. Aus der Recherche: 3-5 Saeulen nach der 70/20/10-Regel
|
||||
(70 % Wert, 20 % Community, 10 % Eigenwerbung); eine Saeule wird
|
||||
@@ -492,6 +524,52 @@ export function db() {
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_termine_beginn ON termine (beginn);
|
||||
|
||||
/* MEHRERE TEILNEHMER AN EINEM TERMIN (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."*
|
||||
|
||||
Bis hierher hatte ein Termin GENAU EIN Gegenueber
|
||||
(termine.teilnehmer_id). Fuer ein Gespraech zu dritt musste man
|
||||
zwei Termine anlegen -- und hatte damit zwei Wahrheiten ueber
|
||||
dieselbe halbe Stunde.
|
||||
|
||||
Gebaut nach demselben Muster wie datei_personen weiter unten:
|
||||
eine eigene kleine Tabelle statt weiterer Spalten. Zwei Spalten
|
||||
"teilnehmer2_id", "teilnehmer3_id" waeren beim vierten Menschen
|
||||
wieder am Ende, und jede Abfrage muesste alle einzeln
|
||||
aufzaehlen.
|
||||
|
||||
WICHTIG -- teilnehmer_id BLEIBT und behaelt seine Bedeutung:
|
||||
Es ist das HAUPT-Gegenueber, mit dem der Termin vereinbart
|
||||
wurde. Diese Tabelle sagt, WER SONST NOCH dabei ist. Beim
|
||||
Umstellen wird der vorhandene Wert mit uebernommen, damit die
|
||||
Teilnehmerliste von Anfang an vollstaendig ist und keine
|
||||
Abfrage zwei Stellen zusammensuchen muss.
|
||||
|
||||
An dieser Tabelle haengt die SICHTBARKEIT (siehe
|
||||
termineSichtbar): Wer hier steht, sieht den Termin. Ein
|
||||
vergessener Eintrag ist deshalb kein Schoenheitsfehler,
|
||||
sondern ein Termin, den jemand nicht findet. */
|
||||
CREATE TABLE IF NOT EXISTS termin_teilnehmer (
|
||||
termin_id INTEGER NOT NULL REFERENCES termine(id) ON DELETE CASCADE,
|
||||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (termin_id, person_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_termin_teilnehmer ON termin_teilnehmer (person_id);
|
||||
|
||||
/* Dasselbe fuer die Wiederholungen. Eine Serie ist eine Regel --
|
||||
die Teilnehmer gehoeren zur Regel, nicht zur einzelnen
|
||||
Auspraegung, sonst muesste man sie jede Woche neu eintragen.
|
||||
Der Nachfueller kopiert sie beim Anlegen jedes Termins hierher
|
||||
hinueber (siehe workspace-serien.js). */
|
||||
CREATE TABLE IF NOT EXISTS serie_teilnehmer (
|
||||
serie_id INTEGER NOT NULL REFERENCES termin_serien(id) ON DELETE CASCADE,
|
||||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (serie_id, person_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_serie_teilnehmer ON serie_teilnehmer (person_id);
|
||||
|
||||
/* Wiederkehrende Termine (02.09.2026) --------------------------------
|
||||
|
||||
Eine Serie ist eine REGEL, kein Termin: "jeden Dienstag um 18:00
|
||||
@@ -1373,8 +1451,30 @@ export const externSql = (personSpalte, externSpalte) =>
|
||||
export function termineSichtbar(person, praefix = "t") {
|
||||
if (istDogFather(person)) return { wo: "1=1", werte: [] };
|
||||
const p = praefix;
|
||||
const eigen = `(${p}.creator_id = ? OR ${p}.teilnehmer_id = ? OR ${p}.erstellt_von = ?)`;
|
||||
const werte = [person.id, person.id, person.id];
|
||||
|
||||
/* MITGEZAEHLT WIRD AUCH DIE TEILNEHMERLISTE (05.09.2026).
|
||||
|
||||
Seit ein Termin mehrere Teilnehmer haben kann, reicht
|
||||
`teilnehmer_id` nicht mehr: Das ist nur das Haupt-Gegenueber. Wer
|
||||
als zweiter oder dritter dabei ist, steht in termin_teilnehmer --
|
||||
und ohne diese Zeile saehe er den Termin nicht, an dem er
|
||||
teilnimmt.
|
||||
|
||||
Die beiden Aufrufer arbeiten auf verschiedenen Tabellen: der
|
||||
Kalender auf `termine` (praefix t), die Wiederholungen auf
|
||||
`termin_serien` (praefix s). Deshalb wird hier die passende
|
||||
Nebentabelle gewaehlt statt einer festen -- eine falsche Zuordnung
|
||||
waere kein Fehler, den man sieht, sondern eine Liste, in der
|
||||
jemandem etwas fehlt. */
|
||||
const nebenTabelle = p === "s"
|
||||
? { tabelle: "serie_teilnehmer", spalte: "serie_id" }
|
||||
: { tabelle: "termin_teilnehmer", spalte: "termin_id" };
|
||||
const dabei = `EXISTS (SELECT 1 FROM ${nebenTabelle.tabelle} tn`
|
||||
+ ` WHERE tn.${nebenTabelle.spalte} = ${p}.id AND tn.person_id = ?)`;
|
||||
|
||||
const eigen = `(${p}.creator_id = ? OR ${p}.teilnehmer_id = ? OR ${p}.erstellt_von = ?`
|
||||
+ ` OR ${dabei})`;
|
||||
const werte = [person.id, person.id, person.id, person.id];
|
||||
const b = betreutWo(person, `${p}.creator_id`);
|
||||
if (!b) return { wo: eigen, werte };
|
||||
return { wo: `(${eigen} OR ${b.wo})`, werte: [...werte, ...b.werte] };
|
||||
|
||||
@@ -75,6 +75,59 @@
|
||||
let ich = null;
|
||||
let daten = null;
|
||||
let serien = [];
|
||||
|
||||
/* WER SONST NOCH DABEI IST (05.09.2026).
|
||||
Einzelne Schalter statt eines <select multiple> -- dasselbe Muster
|
||||
wie bei den Dateien: Ein Mehrfach-Auswahlfeld verliert bei einem
|
||||
Fehlklick die ganze Auswahl und ist am Handy kaum zu bedienen. */
|
||||
let alleLeute = [];
|
||||
|
||||
const ROLLENKURZ = { admin: 'DogFather', manager: 'Manager', scout: 'Scout', creator: 'Creator' };
|
||||
|
||||
/** Baut die Schalterreihe. `gewaehlt` sind die Nummern, die an
|
||||
* sein sollen -- beim Bearbeiten die des Termins, sonst nichts. */
|
||||
function baueDabei(gewaehlt = []) {
|
||||
const ziel = $('dabei-wahl');
|
||||
if (!ziel) return;
|
||||
const an = new Set(gewaehlt.map(Number));
|
||||
ziel.textContent = '';
|
||||
if (!alleLeute.length) {
|
||||
$('dabei-hinweis').textContent = '';
|
||||
return;
|
||||
}
|
||||
for (const p of alleLeute) {
|
||||
const b = document.createElement('button');
|
||||
b.type = 'button';
|
||||
b.className = 'wahl__knopf';
|
||||
b.dataset.id = String(p.id);
|
||||
b.dataset.rolle = p.rolle;
|
||||
if (an.has(p.id)) b.dataset.an = 'ja';
|
||||
const n = document.createElement('span'); n.className = 'wahl__name'; n.textContent = p.name;
|
||||
const r = document.createElement('span'); r.className = 'wahl__rolle';
|
||||
r.textContent = ROLLENKURZ[p.rolle] || p.rolle;
|
||||
b.append(n, r);
|
||||
b.addEventListener('click', () => {
|
||||
if (b.dataset.an === 'ja') delete b.dataset.an; else b.dataset.an = 'ja';
|
||||
dabeiHinweis();
|
||||
});
|
||||
ziel.append(b);
|
||||
}
|
||||
dabeiHinweis();
|
||||
}
|
||||
|
||||
/** Die Nummern, die gerade angehakt sind. */
|
||||
const dabeiGewaehlt = () =>
|
||||
[...$('dabei-wahl').querySelectorAll('.wahl__knopf[data-an="ja"]')].map((b) => Number(b.dataset.id));
|
||||
|
||||
function dabeiHinweis() {
|
||||
const n = dabeiGewaehlt().length;
|
||||
/* Sagt, was passiert -- nicht nur, wie viele es sind. Wer hier
|
||||
steht, sieht den Termin in seinem Kalender; das ist der
|
||||
eigentliche Punkt und niemandem von aussen anzusehen. */
|
||||
$('dabei-hinweis').textContent = n
|
||||
? `${n} ${n === 1 ? 'Person sieht' : 'Personen sehen'} diesen Termin im eigenen Kalender.`
|
||||
: 'Niemand zusätzlich ausgewählt.';
|
||||
}
|
||||
let ansicht = 'monat';
|
||||
let anker = heuteText(); // Datum, um das herum angezeigt wird
|
||||
const zeigen = { call: true, termin: true, review: true, frist: true };
|
||||
@@ -263,6 +316,20 @@
|
||||
jede Ansicht arbeitet danach nur noch mit dieser einen Liste und
|
||||
kann gar nicht mehr etwas anderes zeigen als die andere. */
|
||||
|
||||
/** "mit Luna", "mit Luna und Pat", "mit Luna, Pat +2".
|
||||
*
|
||||
* Fällt auf den frei eingetragenen Namen zurück, wenn keine Person
|
||||
* verknüpft ist ("Agentur Müller") -- der steht nicht in der
|
||||
* Teilnehmerliste, weil er kein Konto hat, gehört aber genauso
|
||||
* dazu. */
|
||||
function mitWem(t) {
|
||||
const namen = (t.teilnehmer || []).map((p) => p.name);
|
||||
if (!namen.length) return t.teilnehmer_name ? 'mit ' + t.teilnehmer_name : null;
|
||||
if (namen.length === 1) return 'mit ' + namen[0];
|
||||
if (namen.length === 2) return `mit ${namen[0]} und ${namen[1]}`;
|
||||
return `mit ${namen[0]}, ${namen[1]} +${namen.length - 2}`;
|
||||
}
|
||||
|
||||
function vereinheitlichen(roh) {
|
||||
const raus = [];
|
||||
for (const t of roh.termine || []) {
|
||||
@@ -270,10 +337,16 @@
|
||||
art: t.art, tag: t.beginn.slice(0, 10), uhr: t.beginn.slice(11, 16),
|
||||
titel: t.titel, erledigt: !!t.erledigt, id: t.id,
|
||||
farbe: ARTFARBE[t.art] || ARTFARBE.termin,
|
||||
/* WER DABEI IST -- alle, nicht nur das Haupt-Gegenueber
|
||||
(05.09.2026). Bei zwei Leuten stehen beide Namen da, ab drei
|
||||
wird gekürzt: "mit Luna, Pat +2". Die ganze Liste in einer
|
||||
Tageszeile würde alles andere aus dem Bild schieben; die
|
||||
vollständige Aufzählung steht im Detail darunter. */
|
||||
zusatz: [t.dauer_min ? t.dauer_min + ' Min' : null,
|
||||
t.teilnehmer_name ? 'mit ' + t.teilnehmer_name : null,
|
||||
mitWem(t),
|
||||
t.ort || null].filter(Boolean).join(' · '),
|
||||
ort: t.ort, beschreibung: t.beschreibung,
|
||||
teilnehmer: t.teilnehmer || [],
|
||||
creator_name: t.creator_name, teilnehmer_name: t.teilnehmer_name,
|
||||
erstellt_von: t.erstellt_von, dauer_min: t.dauer_min,
|
||||
serie_id: t.serie_id || null,
|
||||
@@ -1057,6 +1130,10 @@
|
||||
/* Die Auswahl der Rhythmen hängt am Datum ("jeden Dienstag") und
|
||||
muss deshalb bei jedem Öffnen neu beschriftet werden. */
|
||||
taktAuswahlFuellen();
|
||||
/* Die Teilnehmerschalter zurücksetzen -- ein frisches Formular darf
|
||||
nicht die Auswahl des letzten Termins mitbringen. `reset()` fasst
|
||||
sie nicht an, weil es keine Formularfelder sind. */
|
||||
baueDabei([]);
|
||||
umschalten(true);
|
||||
}
|
||||
|
||||
@@ -1101,6 +1178,9 @@
|
||||
/* Ein frei eingetragener Name statt einer Person. Der Server
|
||||
raeumt dann creator_id ab -- siehe workspace-kalender.js. */
|
||||
koerper.teilnehmer_extern = $('f-teilnehmer').dataset.extern || null;
|
||||
/* Wer sonst noch dabei ist. Das Haupt-Gegenueber muss hier NICHT
|
||||
mitgeschickt werden -- der Server legt es ohnehin dazu. */
|
||||
koerper.teilnehmer = dabeiGewaehlt();
|
||||
}
|
||||
/* Mit Rhythmus entsteht keine einzelne Zeile, sondern eine Regel --
|
||||
der Server legt daraus die Termine an, den ersten eingeschlossen.
|
||||
@@ -1148,6 +1228,7 @@
|
||||
|
||||
if (LEITUNG.has(ich.rolle)) {
|
||||
$('feld-teilnehmer').hidden = false;
|
||||
$('feld-dabei').hidden = false;
|
||||
try {
|
||||
const { personen } = await (await hole('/workspace/api/personen')).json();
|
||||
for (const p of personen) {
|
||||
@@ -1156,6 +1237,8 @@
|
||||
o.textContent = p.name + ' (' + p.rolle + ')';
|
||||
$('f-teilnehmer').append(o);
|
||||
}
|
||||
alleLeute = personen;
|
||||
baueDabei();
|
||||
} catch { /* Zuordnung bleibt leer */ }
|
||||
window.personenwahl?.('f-teilnehmer', { hinweis: "Kein Konto im Workspace – der Name steht nur an diesem Eintrag." });
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609051419" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -153,9 +153,9 @@
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/aufgaben.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/aufgaben.js?v=202609051419" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/automation.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/automation.css?v=202609051419" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -133,9 +133,9 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/automation.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/automation.js?v=202609051419" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/bereich.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/bereich.css?v=202609051419" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -116,13 +116,13 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/checkliste.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/ampel.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/schulung.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/vorlagen.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/bereich.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/checkliste.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/ampel.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/schulung.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/vorlagen.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/bereich.js?v=202609051419" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/calls.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/calls.css?v=202609051419" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -75,10 +75,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/ki.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/calls.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/ki.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/calls.js?v=202609051419" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/content.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/content.css?v=202609051419" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -165,11 +165,11 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/checkliste.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/vorlagen.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/content.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/checkliste.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/vorlagen.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/content.js?v=202609051419" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/dateien.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/dateien.css?v=202609051419" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -94,9 +94,9 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/dateien.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/dateien.js?v=202609051419" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609051419" />
|
||||
</head>
|
||||
|
||||
<body class="gate">
|
||||
@@ -144,6 +144,6 @@
|
||||
</p>
|
||||
</main>
|
||||
|
||||
<script src="assets/js/gate.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/gate.js?v=202609051419" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/kalender.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/kalender.css?v=202609051419" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -106,6 +106,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WER SONST NOCH DABEI IST (05.09.2026).
|
||||
Wunsch: "ich will auch 2 leute markieren können mit denen der
|
||||
call ist."
|
||||
|
||||
Einzelne Schalter statt eines <select multiple>: Das verliert
|
||||
bei einem Fehlklick die ganze Auswahl und ist am Handy kaum zu
|
||||
bedienen. Genau so ist es bei den Dateien schon gebaut, die
|
||||
Bedienung ist also bekannt.
|
||||
|
||||
Das Feld erscheint nur für die Leitung -- wer keine Zuordnungen
|
||||
verschieben darf, bekommt es gar nicht erst zu sehen. -->
|
||||
<div id="feld-dabei" hidden>
|
||||
<p class="feld-schild">Wer ist noch dabei?</p>
|
||||
<div class="wahl" id="dabei-wahl"></div>
|
||||
<p class="leise" id="dabei-hinweis"></p>
|
||||
</div>
|
||||
|
||||
<!-- Erscheint nur, wenn ein Rhythmus gewählt ist. Der Satz darunter
|
||||
sagt in Worten, was gleich passiert – eine Regel, die man nicht
|
||||
vorlesen kann, stellt man später nicht mehr richtig ab. -->
|
||||
@@ -179,9 +196,9 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/kalender.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/kalender.js?v=202609051419" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/personen.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/personen.css?v=202609051419" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -122,9 +122,9 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/personen.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/personen.js?v=202609051419" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/profil.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/profil.css?v=202609051419" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -200,10 +200,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/steckbrief.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/profil.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/steckbrief.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/profil.js?v=202609051419" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/report.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/report.css?v=202609051419" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -92,10 +92,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/ki.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/report.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/ki.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/report.js?v=202609051419" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/scouting.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/scouting.css?v=202609051419" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -125,9 +125,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/scouting.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/scouting.js?v=202609051419" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609051419" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -100,9 +100,9 @@
|
||||
</p>
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/start.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/start.js?v=202609051419" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609051419" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -71,10 +71,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/ki.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/startcheck.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/ki.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/startcheck.js?v=202609051419" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/profil.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/profil.css?v=202609051419" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -112,9 +112,9 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/steckbrief.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/steckbrief.js?v=202609051419" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609051419" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -58,9 +58,9 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/bereiche.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609051419" defer></script>
|
||||
|
||||
<script src="assets/js/kopf.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/uebersicht.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/uebersicht.js?v=202609051419" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
<meta name="theme-color" content="#05070d" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/wissen.css?v=202609050102" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609051419" />
|
||||
<link rel="stylesheet" href="assets/css/wissen.css?v=202609051419" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -176,9 +176,9 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/wissen.js?v=202609050102" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609051419" defer></script>
|
||||
<script src="assets/js/wissen.js?v=202609051419" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||