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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 774 KiB

After

Width:  |  Height:  |  Size: 774 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 875 KiB

After

Width:  |  Height:  |  Size: 873 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 774 KiB

After

Width:  |  Height:  |  Size: 774 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 895 KiB

After

Width:  |  Height:  |  Size: 894 KiB

+295
View File
@@ -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);
+150 -9
View File
@@ -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 -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);
+102 -2
View File
@@ -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] };
+84 -1
View File
@@ -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." });
}
+7 -7
View File
@@ -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 -8
View File
@@ -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>
+12 -12
View File
@@ -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>
+9 -9
View File
@@ -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>
+10 -10
View File
@@ -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 -8
View File
@@ -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>
+2 -2
View File
@@ -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>
+25 -8
View File
@@ -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 -8
View File
@@ -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>
+9 -9
View File
@@ -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>
+9 -9
View File
@@ -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 -8
View File
@@ -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>
+6 -6
View File
@@ -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>
+9 -9
View File
@@ -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 -8
View File
@@ -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>
+6 -6
View File
@@ -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 -8
View File
@@ -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>