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
+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." });
}