Filipe, zum wiederholten Mal und mit einem Bildschirmfoto genau dieser
Seite: "zum hunderstenmal, also bitte mach dass es jetzt endlich
klappt, die rechte hand sieht das immer noch nicht obwohl ich will dass
die rechte hand das auch sieht."
ZUERST NACHGEMESSEN, NICHT GERATEN. Am 24.09. habe ich auf ein
Bildschirmfoto hin an der falschen Seite gebaut und es im Commit selbst
notiert. Diesmal zuerst mess-hand-personen.mjs: dieselbe Seite, zwei
Anmeldungen, und der Unterschied wird aufgezaehlt. Ergebnis in einer
Zeile -- sie bekam vom Server alle acht Personen (HTTP 200) und sah auf
dem Bildschirm NICHTS davon. Nur das Anlege-Formular, darueber der Satz
"Codes, Sperren und das Protokoll bleiben bei DogFather".
ZWEI URSACHEN, UND NUR EINE WAR EINE SCHRANKE:
1. Die OBERFLAECHE hat die Liste versteckt, die sie laengst geladen
hatte. `personen.js` entschied die Ausbaustufe mit
`ich.rolle !== 'admin'`, setzte damit `data-nur-anlegen`, und
`personen.css` blendet darauf hin die Liste, das Protokoll und
"Alle aufklappen" aus. Diese CSS-Regel stammt vom 07.09. und war
fuer Manager und Spicy Media gedacht; die rechte Hand ist erst
danach dazugekommen und fiel stillschweigend mit hinein.
Das ist in dieser einen Datei die DRITTE Stelle, an der ein
Rollenvergleich im Browser veraltet ist -- nach dem 22.09.
("keine Knoepfe") und dem 24.09. ("keine Rollenwahl"). Jedes Mal
hatte sie das Recht und sah es nicht.
2. Das Protokoll war am Server zu (HTTP 404). Damit ist der Satz von
oben ueberholt: Filipes Ansage vom 24.09. -- "die selben rechte da
haben wie dogfather, das einzige was sie nicht kann ist die
dogfather rolle oder leute anfassen" -- laesst dafuer keinen Rest.
EINE AUSKUNFT FUER DREI STELLEN. `fuehrtDieZugaenge(person)` steht
jetzt in workspace.js und beantwortet dieselbe Frage fuer die Tuer am
Server, fuer `/api/ich` (`darf_zugaenge_fuehren`) und fuer die
Ausbaustufe der Seite. Drei Abschriften waeren drei Gelegenheiten, dass
die naechste Aenderung nur zwei davon trifft -- genau so ist dieser
Fehler entstanden.
`istHand` WAERE FALSCH GEWESEN. Es fasst beide Haende zusammen, und
fuer die linke gilt ausdruecklich das Gegenteil ("sieht weder
Bewerbungen noch den vertraulichen Meldeweg"). Wer hier den
Sammelbegriff nimmt, dreht eine ausgesprochene Entscheidung
stillschweigend um. Die Prueflung fragt sie deshalb einzeln.
DIE PRUEFUNG ZIEHT NACH (40 -> 49). Abschnitt 6 prueft beides: dass
die rechte Hand dasselbe Protokoll bekommt wie DogFather, und dass die
Auskunft, aus der die Oberflaeche ihre Ausbaustufe baut, mit der Tuer
am Server uebereinstimmt. Genau dieser Abgleich hat gefehlt: Eine
Rechtepruefung, die nur Serverantworten ansieht, hat den Fehler zwei
Tage lang nicht bemerkt. Dazu drei Gegenproben (linke Hand 404, Modi
404, linke Hand `darf_zugaenge_fuehren === false`).
Beim ersten Lauf waren diese Gegenproben rot -- mit 401 statt 404. Die
Abschnitte davor sperren und loeschen absichtlich Leute, und eine tote
Sitzung antwortet mit 401: Das sieht aus wie "darf nicht" und heisst
"gibt es nicht mehr". Ein 401 als Gegenprobe fuer ein 404 ist ein Haken
ohne Gegenstand. Abschnitt 6 legt sich deshalb frische Zugaenge an.
Geprueft: pruef-hand-personen (49, 0 Fehler), pruef-personen-liste,
pruef-personen-kachel (45), pruef-personen-loeschen,
pruef-modi-verborgen (85). Unveraendert rot und an HEAD nachgemessen,
also nicht von diesem Umbau: pruef-personen-formular (2),
pruef-community-sicht (1), pruef-spicy (3).
Co-Authored-By: Claude Opus 5 <[email protected]>
1608 lines
72 KiB
JavaScript
1608 lines
72 KiB
JavaScript
/* ===================================================================
|
||
Personen und Zugangscodes verwalten. Nur für DogFather -- der Server
|
||
liefert allen anderen ohnehin 404 auf diese Schnittstellen.
|
||
=================================================================== */
|
||
(() => {
|
||
'use strict';
|
||
|
||
/* DogFather und Manager haben dieselben Rechte -- die Unterscheidung
|
||
macht nur die Personenverwaltung, und die prueft der Server. */
|
||
/* 'spicy' gehoert dazu (07.09.2026). Ohne diesen Eintrag schickte die
|
||
Seite Spicy Media sofort auf die Startseite zurueck -- der Server
|
||
liess sie herein, das Skript warf sie wieder hinaus. Genau der
|
||
Fehler, den Filipe gemeldet hat: "bei spicy funktioniert dieser
|
||
button nicht, wenn sie drauf druecken geht es nicht auf."
|
||
|
||
Zwei Listen fuer dieselbe Frage, eine im Server und eine hier --
|
||
und beim Hinzufuegen einer Rolle wurde nur die erste gepflegt. */
|
||
const LEITUNG = window.Bereiche.LEITUNG;
|
||
|
||
const $ = (id) => document.getElementById(id);
|
||
/* 'spicy' fehlte hier bis zum 07.09.2026. Vier Stellen lesen daraus
|
||
(Codeanzeige, Personenliste, Scout-Zeile, Manager-Zeile), und alle
|
||
vier haben den Fehlgriff mit `|| p.rolle` aufgefangen -- in der
|
||
Liste stand dann schlicht „spicy" statt „Spicy Media". Ein
|
||
Auffangwert, der halbwegs plausibel aussieht, ist der beste Weg,
|
||
eine Luecke jahrelang zu behalten. */
|
||
const ROLLENNAME = window.Bereiche.ROLLEN_NAME;
|
||
|
||
/* Reihenfolge und Erklaerung an EINER Stelle. Sie gilt fuer die
|
||
Rollenwahl im Formular und fuer alles, was sonst Rollen auflistet. */
|
||
const ROLLEN = [
|
||
{ wert: 'spicy', name: 'Spicy Media', symbol: 'chili',
|
||
text: 'Überblick über alle – ohne Automationen, ohne fremde Chats. Legt Leute an.' },
|
||
/* DER EINTRAG BLEIBT, DER KNOPF ERSCHEINT NICHT (11.09.2026).
|
||
|
||
Seit `ANLEGBAR` die Rolle `admin` nicht mehr enthaelt, filtert
|
||
die Zeile unten (`darf.includes(...)`) ihn von selbst heraus --
|
||
fuer jeden, auch fuer DogFather. Filipe: "dogfather soll man
|
||
nicht auswaehlen koennen. das ist die einzige die man nicht
|
||
auswaehlen kann bitte."
|
||
|
||
Warum er trotzdem hier steht: Diese Liste ist auch das
|
||
Nachschlagewerk fuer Name und Zeichen der Rolle. Ihn zu loeschen
|
||
hiesse, an anderer Stelle eine zweite Liste zu brauchen. Der
|
||
Text sagt jetzt, was gilt -- ein Text, der etwas anderes
|
||
verspricht als die Oberflaeche tut, ist schlimmer als keiner. */
|
||
{ wert: 'admin', name: 'DogFather', symbol: 'husky',
|
||
text: 'Alle Rechte, endgültig. Diese Rolle lässt sich nicht vergeben – auch nicht von DogFather.' },
|
||
/* Der Satz stimmte bis zum 07.09.2026 -- seither legen Manager
|
||
Creator an, und ein Text, der etwas anderes verspricht als die
|
||
Oberflaeche tut, ist schlimmer als gar keiner. */
|
||
{ wert: 'manager', name: 'Manager', symbol: 'stern',
|
||
text: 'Dieselben Rechte wie DogFather, ohne Personenverwaltung – legt aber Creator an.' },
|
||
{ wert: 'scout', name: 'Scout', symbol: 'schutz',
|
||
text: 'Eigene Pipeline und die Creator, die ihm zugeteilt sind.' },
|
||
{ wert: 'creator', name: 'Creator', symbol: 'person',
|
||
text: 'Nur der eigene Bereich, eigene Ziele und Aufgaben.' },
|
||
];
|
||
|
||
function baueRollenwahl() {
|
||
const ziel = $('f-rolle');
|
||
if (!ziel || ziel.dataset.fertig) return;
|
||
ziel.dataset.fertig = 'ja';
|
||
/* DogFather und Manager kann nur DogFather vergeben -- der Server
|
||
lehnt es ohnehin ab, aber ein Knopf, der immer scheitert, gehoert
|
||
gar nicht erst hin. */
|
||
/* WER WELCHE ROLLE VERGEBEN DARF (07.09.2026).
|
||
|
||
DogFather: alle. Spicy Media: Manager und Creator. Manager: nur
|
||
Creator. Der Server setzt dasselbe durch (drei getrennte Wege,
|
||
siehe workspace-personen.js) -- hier geht es nur darum, niemandem
|
||
einen Knopf hinzustellen, der eine Absage bringt.
|
||
|
||
Aus derselben Liste wie die Beschreibungen, damit die beiden nicht
|
||
auseinanderlaufen. */
|
||
/* KEINE EIGENE LISTE MEHR (10.09.2026).
|
||
|
||
Hier standen ZWEI, drei Zeilen auseinander, und sie widersprachen
|
||
einander: Die erste erlaubte Spicy Media `['manager','creator']`,
|
||
die zweite warf den Manager mit einem `continue` wieder hinaus,
|
||
sobald man nicht DogFather war. Uebrig blieb ein einziger Knopf.
|
||
|
||
Serverseitig war die Tuer fuer den Manager die ganze Zeit offen —
|
||
es gab nur nichts zum Draufdruecken. Genau so sieht dieser Fehler
|
||
aus: Nichts ist kaputt, nichts wird rot, es fehlt einfach.
|
||
|
||
Die Liste kommt jetzt vom Server (`darf_anlegen` aus /api/ich).
|
||
Damit kann sie nicht mehr abweichen — weder zu streng noch zu
|
||
grosszuegig. Faellt die Angabe aus (alte Fassung im Zwischen-
|
||
speicher), bleibt der Creator uebrig: das Wenigste, nicht das
|
||
Meiste. */
|
||
const darf = Array.isArray(ich?.darf_anlegen) && ich.darf_anlegen.length
|
||
? ich.darf_anlegen
|
||
: ['creator'];
|
||
for (const r of ROLLEN.filter((x) => darf.includes(x.wert))) {
|
||
ziel.append(rollenKnopf(ziel, r));
|
||
}
|
||
}
|
||
|
||
/* EIN KNOPF DER ROLLENWAHL.
|
||
|
||
Steht als eigene Funktion da, seit die Auswahl aus ZWEI Quellen
|
||
kommt: der Liste oben (fuer alle gleich) und dem, was der Server
|
||
zusaetzlich schickt (siehe rollenwahlErgaenzen). Zwei Abschriften
|
||
desselben Knopfes waeren zwei Gelegenheiten, dass der eine ein
|
||
aria-checked bekommt und der andere nicht. */
|
||
function rollenKnopf(ziel, r) {
|
||
const b = el('button', 'rollenwahl__knopf');
|
||
b.type = 'button';
|
||
b.dataset.rolle = r.wert;
|
||
b.setAttribute('role', 'radio');
|
||
b.setAttribute('aria-checked', r.wert === 'creator' ? 'true' : 'false');
|
||
if (r.wert === 'creator') b.dataset.an = 'ja';
|
||
|
||
const s = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||
s.setAttribute('viewBox', '0 0 24 24');
|
||
s.setAttribute('class', 'rollenwahl__symbol');
|
||
s.setAttribute('aria-hidden', 'true');
|
||
const u = document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
||
u.setAttribute('href', '#r-' + r.symbol);
|
||
s.append(u);
|
||
|
||
const txt = el('span', 'rollenwahl__text');
|
||
txt.append(el('span', 'rollenwahl__name', r.name));
|
||
txt.append(el('span', 'rollenwahl__erklaerung', r.text));
|
||
b.append(s, txt);
|
||
b.addEventListener('click', () => {
|
||
for (const x of ziel.querySelectorAll('.rollenwahl__knopf')) {
|
||
delete x.dataset.an;
|
||
x.setAttribute('aria-checked', 'false');
|
||
}
|
||
b.dataset.an = 'ja';
|
||
b.setAttribute('aria-checked', 'true');
|
||
});
|
||
return b;
|
||
}
|
||
|
||
/* ROLLEN, DIE ERST DER SERVER NENNT (09.09.2026).
|
||
|
||
Die Liste ROLLEN oben steht in dieser Datei -- und diese Datei
|
||
bekommt jeder ausgeliefert, der die Seite oeffnet. Eine Rolle, von
|
||
der ausser der DogFather-Rolle niemand wissen soll, darf deshalb
|
||
nicht darin stehen; sie kommt aus /workspace/api/verwaltung/personen
|
||
und nur an die, die sie vergeben duerfen.
|
||
|
||
WARUM NACHTRAEGLICH UND NICHT GLEICH: baueRollenwahl() laeuft, bevor
|
||
die Personen geladen sind -- das Formular soll sofort dastehen und
|
||
nicht auf das Netz warten. Die Zusatzrolle kommt also eine Runde
|
||
spaeter dazu. Wer nichts bekommt, sieht die Auswahl wie bisher. */
|
||
function rollenwahlErgaenzen(zusatz) {
|
||
const ziel = $('f-rolle');
|
||
if (!ziel || !Array.isArray(zusatz) || !zusatz.length) return;
|
||
/* NUR, WAS MAN AUCH ANLEGEN DARF (22.09.2026).
|
||
------------------------------------------------------------------
|
||
Hier wurde jede Zusatzrolle angehaengt, die mit der Personenliste
|
||
kam -- ohne zu fragen, ob diese Person sie vergeben darf. Solange
|
||
nur DogFather das Formular sah, fiel es nicht auf: Er darf sie
|
||
alle.
|
||
|
||
Seit die rechte Hand anlegt (22.09.), bot das Formular ihr
|
||
"rechte Hand" und "linke Hand" an. Der Server haette es abgelehnt
|
||
-- aber ein Knopf, der eine Absage bringt, gehoert gar nicht erst
|
||
hin, und schlimmer: Er verraet eine Rolle, die sie nicht vergeben
|
||
soll.
|
||
|
||
Gefragt wird dieselbe Liste wie in baueRollenwahl(). Zwei Quellen
|
||
fuer "was darf ich anlegen" waeren zwei Gelegenheiten, eine davon
|
||
zu vergessen -- genau das ist hier passiert. */
|
||
const darf = Array.isArray(ich?.darf_anlegen) && ich.darf_anlegen.length
|
||
? ich.darf_anlegen
|
||
: null;
|
||
for (const r of zusatz) {
|
||
if (!r || !r.wert || ziel.querySelector(`[data-rolle="${r.wert}"]`)) continue;
|
||
if (darf && !darf.includes(r.wert)) continue;
|
||
ziel.append(rollenKnopf(ziel, r));
|
||
}
|
||
}
|
||
|
||
/* Die Rollen, die der Server mitschickt -- Auswahl beim Anlegen UND
|
||
Überschrift in der Liste. Eine Quelle für beides; zwei wären zwei
|
||
Gelegenheiten, dass eine nachgezogen wird und die andere nicht. */
|
||
let zusatzAbschnitte = [];
|
||
|
||
const gewaehlteRolle = () =>
|
||
document.querySelector('.rollenwahl__knopf[data-an="ja"]')?.dataset.rolle || 'creator';
|
||
let ich = null;
|
||
|
||
const el = (tag, klasse, text) => {
|
||
const k = document.createElement(tag);
|
||
if (klasse) k.className = klasse;
|
||
if (text !== undefined) k.textContent = text;
|
||
return k;
|
||
};
|
||
|
||
const melde = (t) => { $('fehler').textContent = t || ''; };
|
||
|
||
/* Solange ein frisch erzeugter Code auf dem Bildschirm steht, darf die
|
||
Seite NICHT von selbst zur Anmeldung springen.
|
||
|
||
Genau das ist am 02.09.2026 passiert: Der Code erschien, die Sitzung
|
||
war im selben Moment beendet, der nächste Aufruf lief in ein "nicht
|
||
angemeldet" -- und die Umleitung nahm den Code mit, bevor er
|
||
abgeschrieben war. Danach kam niemand mehr hinein.
|
||
|
||
Der Serverteil ist repariert (die eigene Sitzung bleibt jetzt
|
||
bestehen). Diese Sperre hier ist der zweite Riegel: Selbst wenn eine
|
||
Sitzung aus einem anderen Grund endet -- Zeitablauf, ein zweiter
|
||
Bildschirm, ein Neustart --, bleibt der Code stehen, bis er
|
||
weggeklickt wird. Ein Code, den man nicht mehr lesen kann, ist
|
||
schlimmer als gar keiner. */
|
||
let codeOffen = false;
|
||
|
||
async function hole(pfad, optionen = {}) {
|
||
const a = await fetch(pfad, { credentials: 'same-origin', ...optionen });
|
||
if (a.status === 401) {
|
||
if (codeOffen) {
|
||
melde('Deine Sitzung ist beendet. Schreib den Code oben JETZT ab – '
|
||
+ 'die Seite bleibt stehen, bis du „Verstanden" drückst.');
|
||
throw new Error('abgemeldet, Code steht noch');
|
||
}
|
||
location.assign('/workspace/'); throw new Error('abgemeldet');
|
||
}
|
||
if (a.status === 404 && pfad.includes('/verwaltung/')) {
|
||
location.assign('/workspace/start.html'); throw new Error('nicht erlaubt');
|
||
}
|
||
return a;
|
||
}
|
||
|
||
/* ORTSZEIT, NICHT UTC (07.09.2026).
|
||
|
||
Hier stand `iso.slice(0, 16).replace('T', ' ')` -- also ein Schnitt
|
||
durch die Zeichenkette. Der Server schreibt seine Zeitstempel in
|
||
UTC; im Sommer sind das ZWEI STUNDEN weniger. Im Protokoll stand
|
||
deshalb "03:00", wo tatsaechlich 05:00 war. Filipe hat es gemeldet:
|
||
"wie 3 uhr, wir haben 5 uhr."
|
||
|
||
Das Tueckische daran: Es sieht nie kaputt aus. Eine Uhrzeit ist
|
||
immer plausibel -- man merkt es nur, wenn man zufaellig gerade auf
|
||
die Uhr sieht. Und im Winter waere es nur eine Stunde, also noch
|
||
schwerer zu bemerken.
|
||
|
||
`new Date(iso)` liest den Zeitstempel richtig und gibt ihn in der
|
||
Zeit des Geraets aus. Ein Schnitt durch die Zeichenkette ist
|
||
ausschliesslich bei einem reinen DATUM erlaubt ("2026-09-07") --
|
||
dort gibt es keine Zeitzone, um die man sich irren koennte. */
|
||
const datum = (iso) => {
|
||
if (!iso) return '—';
|
||
const d = new Date(iso);
|
||
if (Number.isNaN(d.getTime())) return '—';
|
||
const z = (n) => String(n).padStart(2, '0');
|
||
return `${d.getFullYear()}-${z(d.getMonth() + 1)}-${z(d.getDate())} `
|
||
+ `${z(d.getHours())}:${z(d.getMinutes())}`;
|
||
};
|
||
|
||
/* ---------- Code anzeigen -------------------------------------------- */
|
||
|
||
function zeigeCode(p) {
|
||
$('code-name').textContent = `${p.name} · ${ROLLENNAME[p.rolle] || p.rolle}`;
|
||
$('code-wert').textContent = p.code;
|
||
/* DIE ADRESSE GEHOERT DAZU (19.09.2026). Sie kommt vom Server, weil
|
||
dort die Weiche steht, die Rollen auf Adressen verteilt -- eine
|
||
zweite Liste im Browser waere die, die beim naechsten Umzug
|
||
stehen bleibt. */
|
||
const adr = p.adresse || '';
|
||
const a = $('code-adresse');
|
||
a.textContent = adr.replace(/^https:\/\//, '');
|
||
a.href = adr || '#';
|
||
a.parentElement.hidden = !adr;
|
||
$('code-kasten').hidden = false;
|
||
/* Ab jetzt springt die Seite nicht mehr von selbst weg -- siehe
|
||
die Begruendung bei hole(). */
|
||
codeOffen = true;
|
||
$('code-kasten').scrollIntoView({ behavior: window.sanft?.() || 'auto', block: 'center' });
|
||
/* Den Zeiger in den Kasten setzen: Wer mit der Tastatur arbeitet,
|
||
landet sonst irgendwo in der Liste und findet den Code nicht. */
|
||
$('code-kopieren').focus();
|
||
}
|
||
|
||
/* =======================================================================
|
||
ROLLE ÄNDERN (10.09.2026)
|
||
|
||
DIE AUSWAHL WIRD NICHT NOCH EINMAL GEBAUT, sondern aus dem
|
||
Anlege-Formular gelesen. Dort stehen genau die Rollen, die der
|
||
Server dieser Person zugesteht -- einschliesslich derer, die in
|
||
keiner ausgelieferten Datei stehen duerfen und erst nachträglich
|
||
dazukommen (siehe rollenwahlErgaenzen).
|
||
|
||
Eine zweite Liste hier wäre die, in der eine Rolle fehlt oder eine
|
||
zu viel steht -- und beides fiele erst auf, wenn jemand sie
|
||
braucht. */
|
||
let rolleFuer = null;
|
||
|
||
function rolleFragen(p) {
|
||
rolleFuer = p;
|
||
$('rolle-name').textContent = `${p.name} · ${ROLLENNAME[p.rolle] || p.rolle}`;
|
||
const ziel = $('rolle-wahl');
|
||
ziel.textContent = '';
|
||
|
||
/* DIE LISTE KOMMT VOM SERVER (20.09.2026).
|
||
|
||
Hier standen die Knoepfe des ANLEGE-Formulars. Fuer die rechte
|
||
Hand ist das leer -- sie legt niemanden an, darf aber Rollen
|
||
aendern. Die Wahl waere damit leer geblieben, und das Recht
|
||
unbenutzbar.
|
||
|
||
`rollen_zum_aendern` beantwortet genau diese Frage und kommt aus
|
||
derselben Funktion wie die Schranke in der Route. Die Knoepfe
|
||
des Formulars bleiben als Ersatzweg, falls eine aeltere Fassung
|
||
des Servers antwortet. */
|
||
const ausFormular = [...document.querySelectorAll('#f-rolle .rollenwahl__knopf')]
|
||
.map((b) => ({ wert: b.dataset.rolle, name: b.querySelector('.rollenwahl__name')?.textContent || b.dataset.rolle }));
|
||
const erlaubt = Array.isArray(ich?.rollen_zum_aendern) && ich.rollen_zum_aendern.length
|
||
? ich.rollen_zum_aendern.map((w) => ({
|
||
wert: w,
|
||
name: ausFormular.find((x) => x.wert === w)?.name || ROLLENNAME[w] || w,
|
||
}))
|
||
: ausFormular;
|
||
const vorhanden = erlaubt.filter((r) => r.wert && r.wert !== p.rolle);
|
||
|
||
if (!vorhanden.length) {
|
||
ziel.append(el('p', 'chat__leer', 'Es gibt keine andere Rolle, die du vergeben kannst.'));
|
||
}
|
||
for (const r of vorhanden) {
|
||
const b = el('button', 'rollenwahl__knopf');
|
||
b.type = 'button';
|
||
b.dataset.rolle = r.wert;
|
||
b.append(el('span', 'rollenwahl__name', r.name));
|
||
b.addEventListener('click', () => {
|
||
for (const x of ziel.querySelectorAll('.rollenwahl__knopf')) delete x.dataset.an;
|
||
b.dataset.an = 'ja';
|
||
});
|
||
ziel.append(b);
|
||
}
|
||
$('rolle-kasten').hidden = false;
|
||
$('rolle-kasten').scrollIntoView({ behavior: window.sanft?.() || 'auto', block: 'center' });
|
||
}
|
||
|
||
$('rolle-abbrechen').addEventListener('click', () => {
|
||
$('rolle-kasten').hidden = true;
|
||
rolleFuer = null;
|
||
});
|
||
|
||
$('rolle-los').addEventListener('click', async () => {
|
||
if (!rolleFuer) return;
|
||
const gewaehlt = $('rolle-wahl').querySelector('.rollenwahl__knopf[data-an="ja"]')?.dataset.rolle;
|
||
if (!gewaehlt) { melde('Wähle zuerst eine Rolle.'); return; }
|
||
/* Die Rückfrage nennt beide Rollen im Klartext. "Rolle ändern?"
|
||
allein sagt nicht, WAS sich ändert -- und es ist der Handgriff,
|
||
der jemandem den Zugang zu einer ganzen Seite nimmt oder gibt. */
|
||
const name = $('rolle-wahl').querySelector('.rollenwahl__knopf[data-an="ja"] .rollenwahl__name')?.textContent || gewaehlt;
|
||
if (!await frageNach({
|
||
titel: `${rolleFuer.name}: Rolle ändern?`,
|
||
was: `Von „${ROLLENNAME[rolleFuer.rolle] || rolleFuer.rolle}" zu „${name}".`
|
||
+ ' Damit ändert sich, welche Seiten die Person öffnen darf.',
|
||
bleibt: 'Der Zugangscode bleibt derselbe. Offene Sitzungen werden beendet, '
|
||
+ 'danach kann sie sich mit dem alten Code wieder anmelden.',
|
||
ja: `Zu „${name}" machen`,
|
||
})) return;
|
||
|
||
const a = await hole(`/workspace/api/verwaltung/personen/${rolleFuer.id}/rolle`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ rolle: gewaehlt }),
|
||
});
|
||
if (!a.ok) { melde(sagWas((await a.json().catch(() => ({}))).fehler, 'Ging nicht.')); return; }
|
||
$('rolle-kasten').hidden = true;
|
||
rolleFuer = null;
|
||
await laden();
|
||
});
|
||
|
||
$('code-schliessen').addEventListener('click', () => {
|
||
$('code-kasten').hidden = true;
|
||
codeOffen = false;
|
||
/* Den Code auch aus dem Dokument entfernen, damit er nicht in einer
|
||
Bildschirmaufnahme oder im Verlauf hängen bleibt. */
|
||
$('code-wert').textContent = '';
|
||
});
|
||
|
||
/* CODE UND ADRESSE ZUSAMMEN -- der Knopf, den man wirklich braucht.
|
||
Wer nur den Code kopiert, schickt eine Zeichenfolge ohne Tuer. */
|
||
$('code-beides').addEventListener('click', async () => {
|
||
const adr = $('code-adresse').href;
|
||
const text = `Dein Zugang zu Team Dogi
|
||
|
||
Adresse: ${adr}
|
||
Code: `
|
||
+ `${$('code-wert').textContent}
|
||
|
||
Der Code gilt einmal fuer die `
|
||
+ `Anmeldung. Danach legst du dir dort ein Passwort fest.`;
|
||
try {
|
||
await navigator.clipboard.writeText(text);
|
||
$('code-beides').textContent = 'Kopiert ✓';
|
||
setTimeout(() => { $('code-beides').textContent = 'Code und Adresse kopieren'; }, 1800);
|
||
} catch {
|
||
melde('Kopieren ging nicht — bitte von Hand markieren.');
|
||
}
|
||
});
|
||
|
||
/* DAS FENSTER SCHLIESSEN, WAEHREND DER CODE OFFEN STEHT.
|
||
|
||
Die Seite war gegen das Wegspringen schon geschuetzt (siehe
|
||
hole()) -- gegen den Menschen, der den Reiter zumacht, aber nicht.
|
||
Ein Code, der in dem Moment verschwindet, ist ein Mensch, der nie
|
||
hereinkommt, und niemand merkt es bis zur ersten Nachfrage.
|
||
|
||
Der Browser zeigt dazu seinen eigenen Text; man kann ihn nicht
|
||
bestimmen. Das ist hier auch nicht noetig: Es geht nur darum, dass
|
||
ueberhaupt jemand fragt. */
|
||
window.addEventListener('beforeunload', (e) => {
|
||
if (!codeOffen) return;
|
||
e.preventDefault();
|
||
e.returnValue = '';
|
||
});
|
||
|
||
$('code-kopieren').addEventListener('click', async () => {
|
||
try {
|
||
await navigator.clipboard.writeText($('code-wert').textContent);
|
||
$('code-kopieren').textContent = 'Kopiert ✓';
|
||
setTimeout(() => { $('code-kopieren').textContent = 'Kopieren'; }, 1800);
|
||
} catch {
|
||
melde('Kopieren ging nicht — bitte von Hand markieren.');
|
||
}
|
||
});
|
||
|
||
/* ---------- Liste ----------------------------------------------------- */
|
||
|
||
let betreuerListe = [];
|
||
|
||
/** Ein oder zwei Buchstaben aus einem Namen.
|
||
*
|
||
* Zwei nur bei einem echten zweiten Wort: Aus "Spicy Media" wird SM,
|
||
* aus "BananaStift" ein B -- Binnenversalien als zweiten Namen zu
|
||
* lesen ergäbe "BS" für eine Person, die einen Namen hat. */
|
||
function initialen(name) {
|
||
const teile = String(name || '').trim().split(/[\s._-]+/).filter(Boolean);
|
||
if (!teile.length) return '?';
|
||
if (teile.length === 1) return teile[0].slice(0, 1).toUpperCase();
|
||
return (teile[0][0] + teile[teile.length - 1][0]).toUpperCase();
|
||
}
|
||
|
||
/* WIE LANGE IST DAS HER? (24.09.2026)
|
||
|
||
Dastand "letzte Anmeldung: 2026-09-14 08:12". Das ist eine genaue
|
||
Angabe und eine schlechte Antwort -- die Frage, die man an eine
|
||
letzte Anmeldung stellt, lautet "ist der noch dabei?", und dafuer
|
||
muss man das Datum erst im Kopf gegen heute rechnen. "vor drei
|
||
Tagen" beantwortet sie sofort.
|
||
|
||
Das genaue Datum geht nicht verloren, es steht als Titel dran --
|
||
wer den Zeitpunkt braucht, faehrt mit der Maus darueber. */
|
||
function herZeit(iso) {
|
||
if (!iso) return 'noch nie';
|
||
const ms = Date.now() - Date.parse(iso);
|
||
if (!Number.isFinite(ms)) return '—';
|
||
const min = Math.floor(ms / 60000);
|
||
if (min < 2) return 'gerade eben';
|
||
if (min < 60) return `vor ${min} Min.`;
|
||
const std = Math.floor(min / 60);
|
||
if (std < 24) return `vor ${std} Std.`;
|
||
const tage = Math.floor(std / 24);
|
||
if (tage === 1) return 'gestern';
|
||
if (tage < 31) return `vor ${tage} Tagen`;
|
||
const monate = Math.floor(tage / 30.44);
|
||
if (monate < 12) return `vor ${monate} Mon.`;
|
||
const jahre = Math.floor(tage / 365);
|
||
return `vor ${jahre} ${jahre === 1 ? 'Jahr' : 'Jahren'}`;
|
||
}
|
||
|
||
/** Ein Feld im Zahlenband: die Zahl gross, das Wort klein darunter. */
|
||
function wertFeld(ziel, wert, wort, titel) {
|
||
const f = el('div', 'person__wert');
|
||
f.append(el('span', 'person__wert-zahl', String(wert)));
|
||
f.append(el('span', 'person__wert-wort', wort));
|
||
if (titel) f.title = titel;
|
||
ziel.append(f);
|
||
return f;
|
||
}
|
||
|
||
/** „Neuer Code" -- EINMAL geschrieben, von zwei Stellen benutzt.
|
||
*
|
||
* Herausgezogen am 24.09.2026, weil die rechte Hand denselben Knopf
|
||
* bekommt wie DogFather. Ihn ein zweites Mal zu schreiben waere die
|
||
* Fassung, die beim naechsten Umbau nur halb nachgezogen wird. */
|
||
function neuerCodeKnopf(p) {
|
||
return tuKnopf('Neuer Code', async () => {
|
||
const eigen = p.id === ich.id;
|
||
if (!await frageNach({
|
||
titel: eigen ? 'Neuer Code für dich selbst?' : `Neuer Code für ${p.name}?`,
|
||
was: 'Der alte Code gilt danach nicht mehr.',
|
||
bleibt: eigen
|
||
? 'Alle ANDEREN Geräte werden abgemeldet – hier bleibst du '
|
||
+ 'angemeldet. Der neue Code steht danach auf dem Bildschirm, '
|
||
+ 'bis du ihn selbst wegklickst.'
|
||
: `Offene Sitzungen von ${p.name} werden beendet. Alles andere `
|
||
+ 'bleibt unverändert – nur der Code ist neu.',
|
||
endgueltig: true, ja: 'Neuen Code erzeugen',
|
||
})) return;
|
||
const a = await hole(`/workspace/api/verwaltung/personen/${p.id}/code`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(eigen ? { auch_mich: true } : {}),
|
||
});
|
||
if (!a.ok) {
|
||
melde(sagWas((await a.json().catch(() => ({}))).fehler,
|
||
'Neuer Code hat nicht geklappt.'));
|
||
return;
|
||
}
|
||
zeigeCode(await a.json());
|
||
await laden();
|
||
});
|
||
}
|
||
|
||
/** „Sperren" / „Entsperren" -- ebenfalls von zwei Stellen benutzt. */
|
||
function sperrKnopf(p) {
|
||
return tuKnopf(p.aktiv ? 'Sperren' : 'Entsperren', async () => {
|
||
if (p.aktiv && !await frageNach({
|
||
titel: `${p.name} sperren?`,
|
||
was: 'Offene Sitzungen werden sofort beendet. Anmelden geht danach nicht mehr.',
|
||
bleibt: 'Alle Daten bleiben stehen – Aufgaben, Nachrichten, Steckbrief. '
|
||
+ 'Entsperren geht jederzeit mit demselben Knopf.',
|
||
ja: 'Sperren',
|
||
})) return;
|
||
const a = await hole(`/workspace/api/verwaltung/personen/${p.id}`, {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ aktiv: !p.aktiv }),
|
||
});
|
||
if (!a.ok) { melde(sagWas((await a.json().catch(() => ({}))).fehler, 'Ging nicht.')); return; }
|
||
await laden();
|
||
});
|
||
}
|
||
|
||
function zeile(p) {
|
||
const k = el('div', 'person');
|
||
k.dataset.aktiv = p.aktiv ? 'ja' : 'nein';
|
||
/* DIE ROLLENFARBE GEHOERT AN DIE KACHEL, nicht nur an die kleine
|
||
Marke. In einem Raster sucht das Auge zuerst die Farbe und liest
|
||
erst danach -- dieselben Rollenfarben, die das Haus ohnehin
|
||
fuehrt. Alles Weitere macht das CSS. */
|
||
k.dataset.rolle = p.rolle;
|
||
|
||
const links = el('div', 'person__innen');
|
||
|
||
/* ---- Kopf: Zeichen, Name, Rolle ---------------------------------
|
||
Das Namenszeichen gab es bisher nur in der Ueberschrift einer
|
||
zugeklappten Gruppe. In der Kachel ist es der Anker, an dem man
|
||
eine Person wiedererkennt, bevor man den Namen gelesen hat. */
|
||
const kopf = el('div', 'person__kopf');
|
||
const zeichen = el('div', 'person__zeichen', initialen(p.name));
|
||
zeichen.dataset.rolle = p.rolle;
|
||
zeichen.setAttribute('aria-hidden', 'true');
|
||
kopf.append(zeichen);
|
||
|
||
const wer = el('div', 'person__wer');
|
||
wer.append(el('div', 'person__name', p.name + (p.id === ich.id ? ' (du)' : '')));
|
||
const marken = el('div', 'person__marken');
|
||
const rolle = el('span', 'marke-rolle', ROLLENNAME[p.rolle] || p.rolle);
|
||
rolle.dataset.rolle = p.rolle;
|
||
marken.append(rolle);
|
||
/* GESPERRT WAR BISHER EIN WORT IN GRAUER SCHRIFT zwischen fuenf
|
||
anderen grauen Woertern -- auf einer Kachel, die ohnehin blasser
|
||
ist, hat es niemand gesehen. Jetzt ist es eine Marke in der
|
||
Warnfarbe. */
|
||
if (!p.aktiv) {
|
||
const g = el('span', 'marke-rolle', 'gesperrt');
|
||
g.dataset.rolle = 'gesperrt';
|
||
marken.append(g);
|
||
}
|
||
wer.append(marken);
|
||
kopf.append(wer);
|
||
links.append(kopf);
|
||
|
||
/* ---- Das Zahlenband ---------------------------------------------
|
||
DIESELBEN ANGABEN WIE VORHER, nichts faellt weg -- nur nicht
|
||
mehr als Reihe halber Saetze ("offene Aufgaben: 7 · 2 offene
|
||
Sitzung(en) · betreut 3 Creator"). In einem Raster stehen sie
|
||
damit in jeder Kachel an derselben Stelle: Man vergleicht zwei
|
||
Personen mit dem Auge, statt in jeder Zeile an einer anderen
|
||
Stelle nach derselben Zahl zu suchen. */
|
||
const werte = el('div', 'person__werte');
|
||
wertFeld(werte, p.offene_aufgaben,
|
||
p.offene_aufgaben === 1 ? 'Aufgabe' : 'Aufgaben', 'offene Aufgaben');
|
||
if (p.sitzungen) {
|
||
wertFeld(werte, p.sitzungen,
|
||
p.sitzungen === 1 ? 'Sitzung' : 'Sitzungen', 'offene Sitzungen');
|
||
}
|
||
if (p.betreut_anzahl) {
|
||
wertFeld(werte, p.betreut_anzahl, 'betreut',
|
||
`betreut ${p.betreut_anzahl} Creator`);
|
||
}
|
||
/* Bei einem Manager: wie viele Scouts an ihm haengen. Ohne diese Zahl
|
||
muesste man die ganze Liste durchgehen, um zu sehen, ob eine
|
||
Zuteilung ueberhaupt angekommen ist.
|
||
|
||
Steht in DERSELBEN Zeile wie "betreut N Creator" -- ein eigenes
|
||
Element haette eine eigene Klasse gebraucht, die es im Stil nicht
|
||
gibt, und waere als nackter Text erschienen. */
|
||
if (p.rolle === 'manager' && p.scouts_anzahl) {
|
||
wertFeld(werte, p.scouts_anzahl,
|
||
p.scouts_anzahl === 1 ? 'Scout' : 'Scouts', 'Scouts zugeteilt');
|
||
}
|
||
|
||
/* DIE ZEIT STEHT ALS LETZTES FELD im selben Band. Sie ist keine
|
||
Zahl und darf deshalb nicht so schreien wie eine -- aber sie
|
||
gehoert dorthin, wo alles andere ueber diese Person steht. */
|
||
const zuletzt = el('div', 'person__wert person__wert--zeit');
|
||
zuletzt.append(el('span', 'person__wert-zahl', herZeit(p.letzter_login)));
|
||
zuletzt.append(el('span', 'person__wert-wort', 'angemeldet'));
|
||
zuletzt.title = `letzte Anmeldung: ${datum(p.letzter_login)}`;
|
||
werte.append(zuletzt);
|
||
links.append(werte);
|
||
|
||
/* Beim Scout: zu wem er gehoert. Das ist keine Zahl, sondern ein
|
||
Name -- der steht als eigene Zeile unter dem Band. */
|
||
if (p.rolle === 'scout' && p.manager_name) {
|
||
links.append(el('div', 'person__bezug', `gehört zu ${p.manager_name}`));
|
||
}
|
||
|
||
/* Zuständigkeit. Steht nur bei Creator und nur dann, wenn es
|
||
überhaupt einen Scout gibt, dem man sie geben könnte. */
|
||
if (p.rolle === 'creator' && betreuerListe.length) {
|
||
const b = el('div', 'betreuung');
|
||
const s = el('label', 'betreuung__schild', 'Betreut von');
|
||
s.htmlFor = 'betreuer-' + p.id;
|
||
const w = el('select', 'betreuung__wahl');
|
||
w.id = 'betreuer-' + p.id;
|
||
/* Der Leer-Wert heisst jetzt, was er ist. Vorher stand dort
|
||
"DogFather" -- das sah aus wie eine Zuordnung, war aber keine,
|
||
und dieselben Creator tauchten gleichzeitig im Hinweis "Creator
|
||
ohne zustaendige Person" auf. Zwei Aussagen, ein Bildschirm.
|
||
|
||
DogFather steht jetzt als echte Person in der Liste, gleichrangig
|
||
neben den Scouts. */
|
||
const leer = document.createElement('option');
|
||
leer.value = ''; leer.textContent = '— niemand —';
|
||
w.append(leer);
|
||
for (const s2 of betreuerListe) {
|
||
const o = document.createElement('option');
|
||
o.value = String(s2.id);
|
||
/* Die Rolle mit dabei: Bei gleichen oder aehnlichen Namen ist
|
||
sonst nicht zu erkennen, wen man gerade einträgt.
|
||
|
||
ABER NICHT, WENN SIE DASSELBE SAGT WIE DER NAME. Dort stand
|
||
"Dogfather (DogFather)" -- zweimal dasselbe Wort, nur einmal
|
||
anders geschrieben. Eine Klammer, die nichts hinzufuegt, ist
|
||
keine Auskunft, sondern Rauschen. Verglichen wird ohne Rücksicht
|
||
auf Gross- und Kleinschreibung und ohne Leerzeichen, sonst
|
||
faellt genau dieser Fall wieder durch. */
|
||
const rolleName = ROLLENNAME[s2.rolle] || s2.rolle || '';
|
||
const gleich = (a, b) =>
|
||
a.trim().toLowerCase().replace(/\s+/g, '') === b.trim().toLowerCase().replace(/\s+/g, '');
|
||
o.textContent = rolleName && s2.rolle !== 'scout' && !gleich(s2.name, rolleName)
|
||
? `${s2.name} (${rolleName})`
|
||
: s2.name;
|
||
w.append(o);
|
||
}
|
||
w.value = p.betreuer_id ? String(p.betreuer_id) : '';
|
||
w.addEventListener('change', async () => {
|
||
w.disabled = true; melde('');
|
||
try {
|
||
const a = await hole(`/workspace/api/verwaltung/betreuung/${p.id}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ betreuer_id: w.value || null }),
|
||
});
|
||
if (!a.ok) { melde(sagWas((await a.json().catch(() => ({}))).fehler, 'Ging nicht.')); return; }
|
||
await laden();
|
||
} catch { /* umgeleitet */ } finally { w.disabled = false; }
|
||
});
|
||
b.append(s, w);
|
||
links.append(b);
|
||
}
|
||
|
||
/* ---- Welchem Manager gehoert dieser Scout? (01.09.2026) ----
|
||
|
||
"er soll nur die Scouts sehen, die ihm zugeteilt sind."
|
||
|
||
NUR fuer DogFather sichtbar -- und das ist kein Schoenheitsgrund:
|
||
Diese Zuteilung ERWEITERT die Sicht eines Managers (auf die Leads
|
||
seiner Scouts und auf deren Creator). Duerfte er sie selbst
|
||
setzen, koennte er sich seine eigene Sichtbarkeit vergeben. Der
|
||
Server weist es ohnehin ab; hier geht es nur darum, niemandem
|
||
einen Knopf hinzustellen, der nur eine Absage bringt.
|
||
|
||
Und nur, wenn es ueberhaupt einen Manager gibt -- eine leere
|
||
Auswahl sieht aus wie ein Fehler. */
|
||
/* Manager UND DogFather -- genau wie bei "Betreut von" der Creator
|
||
(Wunsch 02.09.2026). Die beiden bewirken Verschiedenes: Bei einem
|
||
Manager entscheidet der Eintrag ueber die Sichtbarkeit, bei
|
||
DogFather haelt er nur die Zustaendigkeit fest. Beides ist eine
|
||
Antwort auf "wen frage ich?" -- und genau darum geht es in dieser
|
||
Liste. */
|
||
const managerListe = betreuerListe.filter(
|
||
(x) => x.rolle === 'manager' || x.rolle === 'admin');
|
||
if (p.rolle === 'scout' && ich.rolle === 'admin' && managerListe.length) {
|
||
const b = el('div', 'betreuung');
|
||
const s2 = el('label', 'betreuung__schild', 'Gehört zu');
|
||
s2.htmlFor = 'manager-' + p.id;
|
||
const w2 = el('select', 'betreuung__wahl');
|
||
w2.id = 'manager-' + p.id;
|
||
const leer2 = document.createElement('option');
|
||
leer2.value = '';
|
||
/* Wie bei den Creatorn: "niemand" heisst wirklich niemand.
|
||
Vorher stand hier "— direkt bei DogFather —" -- das sah aus wie
|
||
eine Zuordnung und war keine, und derselbe Fehler war bei den
|
||
Creatorn schon einmal behoben worden. DogFather steht jetzt als
|
||
echte Person in der Liste, gleichrangig neben den Managern. */
|
||
leer2.textContent = '— niemand —';
|
||
w2.append(leer2);
|
||
for (const m of managerListe) {
|
||
/* Dieselbe Regel wie bei den Creatorn: Die Rolle in Klammern nur
|
||
dann, wenn sie etwas hinzufuegt -- "Dogfather (DogFather)"
|
||
waere zweimal dasselbe Wort. */
|
||
const rn = ROLLENNAME[m.rolle] || m.rolle || '';
|
||
const gl = (a, b2) =>
|
||
a.trim().toLowerCase().replace(/\s+/g, '') === b2.trim().toLowerCase().replace(/\s+/g, '');
|
||
w2.append(new Option(gl(m.name, rn) ? m.name : `${m.name} (${rn})`, String(m.id)));
|
||
}
|
||
w2.value = p.manager_id ? String(p.manager_id) : '';
|
||
w2.addEventListener('change', async () => {
|
||
w2.disabled = true; melde('');
|
||
try {
|
||
const a = await hole(`/workspace/api/verwaltung/scout-zuteilung/${p.id}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ manager_id: w2.value || null }),
|
||
});
|
||
if (!a.ok) { melde(sagWas((await a.json().catch(() => ({}))).fehler, 'Ging nicht.')); return; }
|
||
await laden();
|
||
} catch { /* umgeleitet */ } finally { w2.disabled = false; }
|
||
});
|
||
b.append(s2, w2);
|
||
links.append(b);
|
||
}
|
||
|
||
const knoepfe = el('div', 'person__knoepfe');
|
||
|
||
/* HIER STAND: if (ich.rolle === 'hand') -> keine Knoepfe
|
||
(entfernt 24.09.2026).
|
||
|
||
Die Begruendung daneben lautete: „Der Server antwortet ihr auf
|
||
jeden davon mit 404." Am 22.09. stimmte das. Seither wurde der
|
||
Server ZWEIMAL erweitert -- die rechte Hand darf Personen
|
||
anlegen, Codes neu erzeugen und Rollen aendern -- und diese
|
||
Zeile blieb stehen. Sie hatte die Rechte und sah keinen
|
||
einzigen Knopf.
|
||
|
||
Filipe hat es gemeldet: „die rechte hand soll das auch sehen.
|
||
und die selben rechte da haben wie dogfather. das einzige was
|
||
sie nicht kann ist die dogfather rolle oder leute anfassen."
|
||
|
||
Gefragt wird jetzt der SERVER, nicht der Rollenname. Ein
|
||
Rollenvergleich im Browser ist genau die zweite Wahrheit, die
|
||
still veraltet -- und sie hat es zwei Tage lang getan.
|
||
|
||
WER GAR NICHTS DARF, bekommt weiterhin keine Knoepfe. */
|
||
if (!ich.darf_zugaenge_verwalten && !ich.darf_rollen_wechseln
|
||
&& ich.rolle !== 'admin') {
|
||
k.append(links, knoepfe);
|
||
return k;
|
||
}
|
||
|
||
/* WER NICHT DOGFATHER IST, BEKOMMT NUR, WAS ER DARF (11.09.2026).
|
||
|
||
Vorher war das eine Ja/Nein-Frage: DogFather bekam alles, alle
|
||
anderen nichts (bei Spicy Media blendete zusaetzlich das CSS die
|
||
ganze Reihe aus). Seit Filipes Wunsch -- "ich will dass die rolle
|
||
spicy und dogfather, auch die rollen wechseln koennen wenn die
|
||
personen schon drin sind" -- gibt es einen Zwischenfall: eine
|
||
Rolle mit GENAU EINEM Knopf.
|
||
|
||
Die Verzweigung hier statt einer CSS-Regel: Ein Knopf, den es
|
||
gar nicht gibt, kann auch nicht durch eine spaetere Stilaenderung
|
||
wieder auftauchen. Und "Neuer Code" und "Sperren" wuerden ihr
|
||
weiterhin 404 bringen -- die bleiben deshalb aus. */
|
||
if (ich.rolle !== 'admin') {
|
||
/* NUR BEI LEUTEN, DIE ICH WIRKLICH ANFASSEN DARF (20.09.2026).
|
||
|
||
`p.rolle !== 'admin'` allein reicht nicht mehr: Die rechte
|
||
Hand darf seit heute Rollen aendern, aber keine zweite rechte
|
||
Hand ernennen. Welche Rollen sie anfassen darf, sagt der
|
||
Server -- ein Knopf, der eine Absage bringt, ist schlimmer als
|
||
keiner. */
|
||
const anfassbar = Array.isArray(ich?.rollen_anfassbar)
|
||
? ich.rollen_anfassbar.includes(p.rolle)
|
||
: p.rolle !== 'admin';
|
||
|
||
/* ==== DIESELBEN KNOEPFE WIE DOGFATHER (24.09.2026) ==========
|
||
-- bei allen, die sie anfassen darf.
|
||
|
||
WEN das ist, sagt "rollen_anfassbar" vom Server. DogFather
|
||
und die Manager sind nicht dabei; dort weist der Server ab
|
||
(„An DogFather und Managern ändert nur DogFather etwas"), und
|
||
deshalb steht hier auch kein Knopf. Ein Knopf, der eine
|
||
Absage bringt, ist schlimmer als keiner. */
|
||
if (ich.darf_zugaenge_verwalten && anfassbar) {
|
||
knoepfe.append(neuerCodeKnopf(p));
|
||
if (p.id !== ich.id) knoepfe.append(sperrKnopf(p));
|
||
}
|
||
|
||
if (ich.darf_rollen_wechseln && anfassbar && p.id !== ich.id) {
|
||
knoepfe.append(tuKnopf('Rolle ändern', () => rolleFragen(p)));
|
||
}
|
||
|
||
/* LOESCHEN GANZ RECHTS -- der harmlosere Weg zuerst. Eigene
|
||
Auskunft vom Server, weil es das Einzige ist, was sich nicht
|
||
zuruecknehmen laesst: Schraenkt Filipe es spaeter wieder ein,
|
||
aendert sich genau eine Zeile im Server. */
|
||
if (ich.darf_personen_loeschen && anfassbar && p.id !== ich.id) {
|
||
const weg = tuKnopf('Löschen', () => loeschenFragen(p));
|
||
weg.dataset.gefahr = 'ja';
|
||
knoepfe.append(weg);
|
||
}
|
||
|
||
k.append(links, knoepfe);
|
||
return k;
|
||
}
|
||
|
||
/* DIESELBEN FUNKTIONEN WIE BEI DER RECHTEN HAND (24.09.2026).
|
||
Vorher stand der ganze Ablauf hier inline; die rechte Hand
|
||
haette eine zweite Abschrift gebraucht -- und die waere die
|
||
geworden, die beim naechsten Umbau nur halb nachgezogen wird. */
|
||
knoepfe.append(neuerCodeKnopf(p));
|
||
|
||
if (p.id !== ich.id) {
|
||
knoepfe.append(sperrKnopf(p));
|
||
|
||
/* Löschen steht ganz rechts, hinter dem Sperren -- der harmlosere
|
||
Weg zuerst. Und nur für DogFather: "Nur DogFather hat alle
|
||
endgültigen Rechte" heißt genau hier etwas. Ein Manager kann
|
||
weiterhin sperren, das reicht für den Alltag und ist umkehrbar. */
|
||
/* ROLLE ÄNDERN steht VOR dem Löschen -- der umkehrbare Weg
|
||
zuerst. Bis heute war Löschen und Neuanlegen der einzige Weg,
|
||
jemandem eine andere Aufgabe zu geben; genau das soll dieser
|
||
Knopf ersparen. */
|
||
/* ZWEI KNOEPFE, ZWEI BEDINGUNGEN (11.09.2026).
|
||
|
||
Hier stand EINE Zeile (`ich.rolle === 'admin'`) fuer beide.
|
||
Filipe: "ich will dass die rolle spicy und dogfather, auch die
|
||
rollen wechseln koennen wenn die personen schon drin sind."
|
||
Loeschen bleibt bei DogFather -- das hat er nicht genannt, und
|
||
"Nur DogFather hat alle endgueltigen Rechte" gilt weiter.
|
||
|
||
Die Antwort kommt vom Server (`darf_rollen_wechseln`), nicht
|
||
aus einem Rollenvergleich hier. Am selben Tag hat mich genau
|
||
das im Chat eine Stunde gekostet: zwei Stellen, eine Aussage,
|
||
und nur eine wusste Bescheid.
|
||
|
||
An DogFather selbst aendert nur DogFather -- Spicy Media sieht
|
||
seine Zeile ohnehin nicht, aber ein Knopf, der eine Absage
|
||
bringt, gehoert auch dann nicht hin. */
|
||
/* Ab hier ist `ich` DogFather -- die Verzweigung weiter oben hat
|
||
alle anderen schon verabschiedet. Beide Knoepfe gehoeren ihm. */
|
||
knoepfe.append(tuKnopf('Rolle ändern', () => rolleFragen(p)));
|
||
|
||
const weg = tuKnopf('Löschen', () => loeschenFragen(p));
|
||
weg.dataset.gefahr = 'ja';
|
||
knoepfe.append(weg);
|
||
}
|
||
|
||
k.append(links, knoepfe);
|
||
return k;
|
||
}
|
||
|
||
function tuKnopf(text, tu) {
|
||
const b = el('button', 'schritt', text);
|
||
b.type = 'button';
|
||
b.addEventListener('click', async () => {
|
||
b.disabled = true; melde('');
|
||
try { await tu(); } catch { /* umgeleitet */ } finally { b.disabled = false; }
|
||
});
|
||
return b;
|
||
}
|
||
|
||
/* ---------- Wer steht in der Liste ---------------------------------------
|
||
|
||
Wunsch Filipe, 31.08.2026: "ich will da nur mich und vanvan sehen,
|
||
ich will dass ich einen knopf habe wenn ich die anderen sehen will
|
||
oder nicht."
|
||
|
||
Also: DogFather immer, alle anderen auf Knopfdruck. Drei Dinge sind
|
||
dabei wichtig:
|
||
|
||
1. Das ist eine ANSICHT, kein Recht. Wer eingeklappt ist, ist nicht
|
||
weg -- er wird nur nicht gezeigt. Verwechselt man das, hält man
|
||
irgendwann jemanden für gelöscht, der noch vollen Zugang hat.
|
||
Deshalb steht auf dem Knopf IMMER, wie viele gerade fehlen.
|
||
|
||
2. Der Zustand bleibt erhalten. Ein Knopf, den man nach jedem Laden
|
||
neu drücken muss, ist keine Einstellung, sondern eine Zumutung.
|
||
|
||
3. Gesperrte Personen tauchen trotzdem auf, sobald aufgeklappt ist --
|
||
eine gesperrte Person zu verstecken wäre genau die Zeile, die man
|
||
sehen müsste.
|
||
-------------------------------------------------------------------------- */
|
||
|
||
/* ---------- Löschen ------------------------------------------------------
|
||
|
||
Die einzige Handlung hier, die sich nicht rückgängig machen lässt.
|
||
Deshalb wird sie NICHT mit einem confirm() abgetan, sondern zeigt
|
||
vorher, was daran hängt: was mitgeht, was nur seine Zuordnung
|
||
verliert, und wer danach ohne Betreuung dasteht.
|
||
|
||
Der Name muss getippt werden. Nicht als Schikane -- der Löschknopf
|
||
sitzt neben dem Sperrknopf, und die beiden sind sehr verschieden.
|
||
Wer den Namen tippt, hat die Zeile gelesen, die er trifft.
|
||
|
||
Den Knopf gibt es nur für DogFather; der Server prüft dasselbe noch
|
||
einmal und antwortet allen anderen mit 404. */
|
||
|
||
async function loeschenFragen(person) {
|
||
let v;
|
||
try {
|
||
const a = await hole(`/workspace/api/verwaltung/personen/${person.id}/loeschbar`);
|
||
if (!a.ok) { melde('Das Löschen ist hier nicht möglich.'); return; }
|
||
v = await a.json();
|
||
} catch { return; }
|
||
|
||
if (v.hindernis) { melde(v.hindernis); return; }
|
||
|
||
const mit = Object.entries(v.mit).filter(([, n]) => n > 0);
|
||
const bleibt = Object.entries(v.bleibt).filter(([, n]) => n > 0);
|
||
|
||
/* Was mitgeht und was bleibt, sind ZWEI Aussagen -- deshalb stehen
|
||
sie im Dialog auch in zwei Zeilen und nicht in einer Liste, die
|
||
man von oben nach unten durchliest und in der Mitte aufgibt. */
|
||
const wasWeg = mit.length
|
||
? 'Mitgelöscht wird: ' + mit.map(([w, n]) => `${w} (${n})`).join(', ') + '.'
|
||
: 'An dieser Person hängt nichts, was mitgelöscht würde.';
|
||
const wasBleibt = [
|
||
bleibt.length
|
||
? 'Erhalten bleibt, verliert aber die Zuordnung: '
|
||
+ bleibt.map(([w, n]) => `${w} (${n})`).join(', ') + '.'
|
||
: '',
|
||
v.verliertBetreuung.length
|
||
? `${v.verliertBetreuung.join(', ')} `
|
||
+ `${v.verliertBetreuung.length === 1 ? 'steht' : 'stehen'} danach `
|
||
+ 'ohne zuständige Person da.'
|
||
: '',
|
||
'Vorher wird automatisch eine Sicherung geschrieben.',
|
||
].filter(Boolean).join(' ');
|
||
|
||
if (!await frageNach({
|
||
titel: `„${v.person.name}" löschen?`,
|
||
was: wasWeg,
|
||
bleibt: wasBleibt,
|
||
endgueltig: true,
|
||
ja: 'Endgültig löschen',
|
||
tippen: v.person.name,
|
||
})) return;
|
||
|
||
try {
|
||
const a = await hole(`/workspace/api/verwaltung/personen/${person.id}`, {
|
||
method: 'DELETE',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ name: v.person.name }),
|
||
});
|
||
const d = await a.json().catch(() => ({}));
|
||
if (!a.ok) { melde(sagWas(d.fehler, 'Löschen hat nicht geklappt.')); return; }
|
||
/* Die Sicherung nennen. Wer gerade etwas Unumkehrbares getan hat,
|
||
soll sofort wissen, woraus es sich zurückholen lässt. */
|
||
melde(`„${v.person.name}" wurde gelöscht. Sicherung davor: ${d.sicherung}`);
|
||
await laden();
|
||
} catch { /* umgeleitet */ }
|
||
}
|
||
|
||
/* ---------- Was aufgeklappt ist ------------------------------------------
|
||
|
||
Wunsch Filipe, 31.08.2026: "ich will überall die liste zu machen
|
||
können und nur aufmachen wenn ich sie sehen will."
|
||
|
||
Also klappt JEDE Rolle einzeln auf und zu. Das ersetzt den vorherigen
|
||
Sammelknopf ("5 weitere zeigen") -- zwei Mechanismen nebeneinander,
|
||
die dasselbe verstecken, wären eine Einladung zum Missverständnis.
|
||
|
||
Die Überschrift mit der ANZAHL bleibt immer stehen, auch zugeklappt.
|
||
Das ist der entscheidende Unterschied zum Ausblenden: Man sieht
|
||
jederzeit, dass es vier Manager gibt -- man sieht nur nicht, welche.
|
||
Wer eine ganze Rolle spurlos verschwinden lässt, hält sie irgendwann
|
||
für leer.
|
||
|
||
Voreinstellung: DogFather offen, alles andere zu. */
|
||
const SPEICHER = 'dfw_personen_offen';
|
||
let alle = [];
|
||
let offen = new Set(['admin']);
|
||
try {
|
||
const g = JSON.parse(localStorage.getItem(SPEICHER) || 'null');
|
||
if (Array.isArray(g)) offen = new Set(g);
|
||
} catch { /* kaputt gespeichert = Voreinstellung */ }
|
||
|
||
function offenMerken() {
|
||
try { localStorage.setItem(SPEICHER, JSON.stringify([...offen])); } catch { /* egal */ }
|
||
}
|
||
|
||
/* Jede Rolle bekommt einen eigenen Abschnitt, in der Reihenfolge der
|
||
ZUGANGSSEITE: DogFather, Manager, Scout, Creator. Die Zusatztexte
|
||
stehen dort wörtlich genauso -- wer sich eben angemeldet hat, findet
|
||
hier dieselbe Sprache wieder.
|
||
|
||
Eine Rolle ohne Personen wird weggelassen. Eine leere Überschrift
|
||
ist kein Ordnungsmerkmal, sondern eine Lücke. */
|
||
/* SPICY MEDIA FEHLTE HIER (07.09.2026). Die Rolle gibt es seit heute
|
||
Frueh, die Personenliste kannte sie nicht -- ein Spicy-Zugang stand
|
||
damit in KEINEM Abschnitt und war in der Liste unsichtbar, obwohl
|
||
der Server ihn geliefert hat. Filipe hat es gemeldet: "bei personen
|
||
& zugangscode soll auch die kategorie spicy der rolle dogfather
|
||
angezeigt werden."
|
||
|
||
Die Reihenfolge folgt bereiche.js (Spicy Media ueber DogFather). */
|
||
const ROLLEN_ABSCHNITTE = [
|
||
{ rolle: 'spicy', name: 'Spicy Media', unter: 'Überblick & Zugänge anlegen' },
|
||
{ rolle: 'admin', name: 'DogFather', unter: 'Gesamtübersicht & Freigaben' },
|
||
{ rolle: 'manager', name: 'Manager', unter: 'Betreuung & Freigaben' },
|
||
{ rolle: 'scout', name: 'Scout', unter: 'Eigene Pipeline & Kontakte' },
|
||
{ rolle: 'creator', name: 'Creator', unter: 'Eigener Bereich & Fortschritt' },
|
||
];
|
||
|
||
function listeZeigen() {
|
||
const ziel = $('liste');
|
||
ziel.textContent = '';
|
||
|
||
/* SPICY MEDIA SIEHT ALLES AUSSER DOGFATHER (07.09.2026).
|
||
|
||
Filipe: "spicy soll genau das sehen koennen, aber nur nicht die
|
||
rolle dogfather."
|
||
|
||
Das ist dieselbe Grenze, die im ganzen Haus gilt: Die Rolle hat
|
||
den Ueberblick ueber Manager, Scouts und Creator -- DogFathers
|
||
eigene Sachen gehen sie nichts an. Hier faellt deshalb genau
|
||
dieser eine Abschnitt weg, nicht mehr und nicht weniger.
|
||
|
||
DAS IST KEINE SICHERUNG, sondern Aufraeumen: Der Server liefert
|
||
ihr die Liste ohnehin nur, wenn sie sie sehen darf. Eine
|
||
ausgeblendete Zeile hat noch nie etwas geschuetzt. */
|
||
/* DER ABSCHNITT DOGFATHER WIRD NICHT MEHR AUSGEBLENDET (07.09.2026,
|
||
zweite Praezisierung). Filipe: "die rolle spicy soll auch die
|
||
rolle dogfather sehen, aber nur dogfather und nicht vanvan."
|
||
|
||
Hier stand ein Filter, der den ganzen Abschnitt wegnahm. Jetzt
|
||
entscheidet der SERVER, wer darin steht -- er liefert Spicy Media
|
||
nur den ersten Admin-Zugang. Das ist auch die richtige Stelle
|
||
dafuer: Eine ausgeblendete Zeile hat noch nie etwas geschuetzt,
|
||
und zwei Regeln fuer dieselbe Frage laufen auseinander.
|
||
|
||
`filter((a) => a.drin.length)` erledigt den Rest von selbst: Ist
|
||
niemand darin, gibt es den Abschnitt auch nicht. */
|
||
/* ABSCHNITTE, DIE DER SERVER MITBRINGT (10.09.2026).
|
||
|
||
Die Liste oben kennt fünf Rollen. Wer eine sechste hat, wurde bis
|
||
eben NICHT GEZEICHNET -- kein Fehler, keine Lücke, kein Hinweis.
|
||
VanVan verschwand genau in dem Moment aus der Personenliste, in
|
||
dem sie die rechte Hand wurde; Filipe hat es im Bildschirmfoto
|
||
geschickt ("ich muss alle kategorien da sehen").
|
||
|
||
Die Überschriften stehen NICHT hier. Diese Datei bekommt jeder,
|
||
der die Seite öffnet, und die Namen der verborgenen Rollen
|
||
gehören dort nicht hinein -- sie kommen mit der Liste vom
|
||
Server, an die, die sie sehen dürfen. Dieselbe Lösung wie bei der
|
||
Personenauswahl im Chat.
|
||
|
||
DAS AUFFANGBECKEN DARUNTER ist der eigentliche Fortschritt: Käme
|
||
morgen eine siebte Rolle und niemand dächte an diese Stelle,
|
||
stünden ihre Leute trotzdem auf der Seite -- unter ihrem
|
||
Rollennamen, sichtbar, statt lautlos zu fehlen. Ein Abschnitt mit
|
||
einer unschönen Überschrift ist tausendmal besser als ein Mensch,
|
||
den es auf dem Bildschirm nicht gibt. */
|
||
const abschnitte = [...ROLLEN_ABSCHNITTE];
|
||
for (const r of zusatzAbschnitte) {
|
||
if (!r || !r.wert || abschnitte.some((a) => a.rolle === r.wert)) continue;
|
||
abschnitte.push({ rolle: r.wert, name: r.name || r.wert, unter: r.text || '' });
|
||
}
|
||
for (const p of alle) {
|
||
if (abschnitte.some((a) => a.rolle === p.rolle)) continue;
|
||
abschnitte.push({ rolle: p.rolle, name: p.rolle, unter: '' });
|
||
}
|
||
|
||
const vorhanden = abschnitte
|
||
.map((a) => ({ ...a, drin: alle.filter((p) => p.rolle === a.rolle) }))
|
||
.filter((a) => a.drin.length);
|
||
|
||
for (const abschnitt of vorhanden) {
|
||
const auf = offen.has(abschnitt.rolle);
|
||
const block = document.createElement('section');
|
||
block.className = 'gruppe';
|
||
/* DIE ROLLE STEHT AM ABSCHNITT (09.09.2026).
|
||
Ohne sie sehen alle fünf Zeilen gleich aus, und die Farben, die
|
||
das Haus für die Rollen ohnehin führt (Anmeldeseite, Marken,
|
||
Bereiche), hören ausgerechnet in der Personenliste auf. Das
|
||
Merkmal ist die einzige Zeile Skript, die dafür nötig ist --
|
||
alles Weitere macht das CSS. */
|
||
block.dataset.rolle = abschnitt.rolle;
|
||
/* UND DER ZUSTAND, an derselben Stelle. Das CSS fragte ihn bisher
|
||
ueber `:has(> .gruppe__kopf[aria-expanded="true"])` ab -- das
|
||
geht, ist aber ein zweiter Weg zu einer Auskunft, die hier
|
||
ohnehin vorliegt. Ein Merkmal, drei Regeln, kein Suchen. */
|
||
block.dataset.auf = auf ? 'ja' : 'nein';
|
||
|
||
/* Die Überschrift IST der Knopf. Ein eigener kleiner Schalter
|
||
daneben wäre ein zweites Ziel für dieselbe Absicht -- und ein
|
||
kleineres. Als <button> statt <div> mit Klick-Zuhörer, damit
|
||
Tastatur und Vorleseprogramme es ohne Zutun richtig machen. */
|
||
const kopf = document.createElement('button');
|
||
kopf.type = 'button';
|
||
kopf.className = 'gruppe__kopf gruppe__kopf--klappbar';
|
||
kopf.setAttribute('aria-expanded', auf ? 'true' : 'false');
|
||
|
||
const h = document.createElement('h2');
|
||
h.className = 'gruppe__name';
|
||
h.textContent = abschnitt.name;
|
||
|
||
const zahl = document.createElement('span');
|
||
zahl.className = 'gruppe__zahl';
|
||
zahl.textContent = String(abschnitt.drin.length);
|
||
|
||
const unter = document.createElement('span');
|
||
unter.className = 'gruppe__unter';
|
||
/* Zugeklappt zählt der Zusatztext weniger als die Frage "ist da
|
||
etwas Gesperrtes drin, das ich sehen müsste?". */
|
||
const gesperrt = abschnitt.drin.filter((p) => !p.aktiv).length;
|
||
unter.textContent = gesperrt
|
||
? `${abschnitt.unter} · ${gesperrt} gesperrt`
|
||
: abschnitt.unter;
|
||
if (gesperrt) unter.dataset.warn = 'ja';
|
||
|
||
const pfeil = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||
pfeil.setAttribute('viewBox', '0 0 24 24');
|
||
pfeil.setAttribute('aria-hidden', 'true');
|
||
pfeil.setAttribute('class', 'klapp-pfeil');
|
||
const pfad = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||
pfad.setAttribute('d', 'm6 9.5 6 6 6-6');
|
||
pfeil.append(pfad);
|
||
|
||
/* WER DRINSTEHT — SICHTBAR, OHNE AUFZUKLAPPEN.
|
||
|
||
Zugeklappt sagte die Zeile bisher nur, WIE VIELE es sind. Wer
|
||
wissen wollte, ob Patrick dabei ist, musste aufklappen. Vier
|
||
Namenszeichen beantworten das im Vorbeigehen; ab dem fünften
|
||
steht "+n" dahinter, damit die Zeile nicht wächst.
|
||
|
||
`aria-hidden`, weil es eine Wiederholung ist: Die Anzahl steht
|
||
als Zahl daneben, die Namen stehen beim Aufklappen vollständig
|
||
da. Ein Vorleseprogramm würde hier sonst Buchstabenpaare
|
||
vorlesen, die niemandem etwas sagen. Für die Maus steht der
|
||
volle Name im title. */
|
||
const leute = document.createElement('span');
|
||
leute.className = 'gruppe__leute';
|
||
leute.setAttribute('aria-hidden', 'true');
|
||
const ZEIGEN = 4;
|
||
for (const p of abschnitt.drin.slice(0, ZEIGEN)) {
|
||
const z = document.createElement('span');
|
||
z.className = 'gruppe__kopf-zeichen';
|
||
z.textContent = initialen(p.name);
|
||
z.title = p.aktiv ? p.name : `${p.name} (gesperrt)`;
|
||
if (!p.aktiv) z.dataset.gesperrt = 'ja';
|
||
leute.append(z);
|
||
}
|
||
if (abschnitt.drin.length > ZEIGEN) {
|
||
const mehr = document.createElement('span');
|
||
mehr.className = 'gruppe__kopf-zeichen';
|
||
mehr.dataset.mehr = 'ja';
|
||
mehr.textContent = `+${abschnitt.drin.length - ZEIGEN}`;
|
||
mehr.title = abschnitt.drin.slice(ZEIGEN).map((p) => p.name).join(', ');
|
||
leute.append(mehr);
|
||
}
|
||
|
||
kopf.append(h, zahl, unter, leute, pfeil);
|
||
kopf.addEventListener('click', () => {
|
||
if (offen.has(abschnitt.rolle)) offen.delete(abschnitt.rolle);
|
||
else offen.add(abschnitt.rolle);
|
||
offenMerken();
|
||
listeZeigen();
|
||
});
|
||
block.append(kopf);
|
||
|
||
/* DIE KACHELN STEHEN IN EINEM RASTER, nicht untereinander.
|
||
|
||
"auto-fill" statt einer festen Spaltenzahl: Wie viele
|
||
nebeneinander passen, weiss der Browser besser als jede Zahl,
|
||
die ich hier hinschreiben koennte -- und dieselbe Regel gilt
|
||
auf dem Handy wie auf 1765 px. Eine feste Zahl waere die
|
||
sechste Wiederholung desselben Fehlers in diesem Haus. */
|
||
if (auf) {
|
||
const raster = el('div', 'gruppe__raster');
|
||
for (const p of abschnitt.drin) raster.append(zeile(p));
|
||
block.append(raster);
|
||
}
|
||
ziel.append(block);
|
||
}
|
||
|
||
/* DIE NAMENSSPALTE WIRD GEMESSEN (09.09.2026, screen3).
|
||
|
||
Filipe: "die sollen schön untereinander sein und nicht so
|
||
durcheinander."
|
||
|
||
Die Koepfe stehen seit heute in vier Spalten (siehe personen.css).
|
||
Damit Zahl und Zusatztext ueber alle Abschnitte hinweg fluchten,
|
||
muss die erste Spalte ueberall gleich breit sein -- und zwar so
|
||
breit wie der LAENGSTE Rollenname.
|
||
|
||
Diese Breite wird hier am gezeichneten Text abgenommen, nicht
|
||
ausgerechnet. Eine feste Angabe (`11ch` fuer "Spicy Media") waere
|
||
auf drei Arten falsch: Sie haengt an der Schriftart, `ch` misst
|
||
die Ziffer Null und nicht Grossbuchstaben mit Sperrung, und sie
|
||
gilt nur, bis jemand eine sechste Rolle mit laengerem Namen
|
||
anlegt. Gemessen stimmt sie immer.
|
||
|
||
Erst zuruecksetzen, dann messen: Steht die Vorgabe noch, fuellt
|
||
jeder Name seine Spalte aus, und die Messung liefert die eigene
|
||
Vorgabe von eben zurueck. (`justify-self: start` im CSS haelt die
|
||
Felder zusaetzlich auf Inhaltsbreite -- Guertel und Hosentraeger,
|
||
weil dieser Fehler von aussen wie ein Zufall aussaehe.) */
|
||
ziel.style.removeProperty('--namen-spalte');
|
||
const namen = [...ziel.querySelectorAll('.gruppe__name')];
|
||
if (namen.length) {
|
||
const breit = Math.max(...namen.map((n) => n.getBoundingClientRect().width));
|
||
if (breit > 0) ziel.style.setProperty('--namen-spalte', Math.ceil(breit) + 'px');
|
||
}
|
||
|
||
/* Ein Knopf für alle auf einmal -- bei vier Rollen viermal klicken zu
|
||
müssen, nur um einmal alles zu sehen, wäre unnötige Arbeit. */
|
||
const schalter = $('rest-schalter');
|
||
schalter.hidden = vorhanden.length < 2;
|
||
const alleOffen = vorhanden.every((a) => offen.has(a.rolle));
|
||
$('rest-schalter-text').textContent = alleOffen ? 'Alle zuklappen' : 'Alle aufklappen';
|
||
schalter.setAttribute('aria-expanded', alleOffen ? 'true' : 'false');
|
||
schalter.onclick = () => {
|
||
if (alleOffen) offen.clear();
|
||
else for (const a of vorhanden) offen.add(a.rolle);
|
||
offenMerken();
|
||
listeZeigen();
|
||
};
|
||
}
|
||
|
||
async function laden() {
|
||
const ziel = $('liste');
|
||
try {
|
||
const a = await hole('/workspace/api/verwaltung/personen');
|
||
if (!a.ok) { melde('Personen konnten nicht geladen werden.'); return; }
|
||
const { personen, betreuer, zusatzrollen } = await a.json();
|
||
betreuerListe = betreuer || [];
|
||
zusatzAbschnitte = Array.isArray(zusatzrollen) ? zusatzrollen : [];
|
||
rollenwahlErgaenzen(zusatzrollen);
|
||
alle = personen;
|
||
ziel.textContent = '';
|
||
ziel.setAttribute('aria-busy', 'false');
|
||
listeZeigen();
|
||
/* DAS PROTOKOLL BLEIBT BEI DOGFATHER. Fuer Spicy Media antwortet
|
||
es mit 404 -- und `hole()` versteht ein 404 unter
|
||
`/verwaltung/` als "nicht erlaubt" und schickt zur Startseite
|
||
zurueck. Genau so hat der Aufruf sie von der Personenseite
|
||
geworfen, kaum dass die Liste da war: Die Seite baute sich auf
|
||
und sprang im naechsten Atemzug weg. Gemessen -- auf
|
||
personen.html standen die Kategorien der STARTSEITE.
|
||
|
||
Eine Abfrage, von der man weiss, dass sie 404 gibt, stellt man
|
||
nicht. */
|
||
/* DASSELBE RECHT, DIESELBE FRAGE. Hier stand `ich.rolle ===
|
||
'admin'`; seit dem 25.09.2026 sieht auch die rechte Hand das
|
||
Protokoll, und der Server sagt es (`darf_zugaenge_fuehren`).
|
||
|
||
Die Ueberlegung daneben gilt unveraendert: Fuer alle anderen
|
||
antwortet die Route mit 404, `hole()` versteht das unter
|
||
`/verwaltung/` als „nicht erlaubt" und schickt zur Startseite
|
||
zurueck. Eine Abfrage, von der man weiss, dass sie 404 gibt,
|
||
stellt man nicht. */
|
||
if (ich.darf_zugaenge_fuehren) await protokollLaden();
|
||
} catch { /* umgeleitet */ }
|
||
}
|
||
|
||
/* ==== AUS EINEM SPALTENWERT WIRD EIN SATZ (23.09.2026) ==========
|
||
|
||
Im Protokoll stehen 77 verschiedene Aktionen, und sie heissen wie
|
||
in der Datenbank: "treff_freigegeben", "einstellung_geaendert",
|
||
"chat_kanal_angelegt". Auf dem Bildschirm sah man genau das.
|
||
|
||
KEINE TABELLE MIT 77 EINTRAEGEN. Die waere am Tag der naechsten
|
||
neuen Aktion unvollstaendig -- und niemand merkt es, weil dann
|
||
einfach wieder der Rohname dasteht. Dieselbe Falle wie jede
|
||
abgeschriebene Liste in diesem Haus.
|
||
|
||
STATTDESSEN EINE REGEL: Unterstriche werden Leerzeichen, der
|
||
erste Buchstabe wird gross. Das ergibt fuer JEDE Aktion einen
|
||
lesbaren Ausdruck, auch fuer die, die es noch nicht gibt.
|
||
|
||
DIE UMLAUTE SIND DER ZWEITE TEIL. In der Datenbank stehen sie als
|
||
ae/oe/ue, weil Spaltenwerte ASCII bleiben. Blind zurueckzusetzen
|
||
waere falsch ("neue" wuerde zu "neü"), deshalb nur in WORTTEILEN,
|
||
die sicher sind. Die Liste dafuer ist nicht erfunden: Es sind die
|
||
Staemme aus helfer-deutsch.mjs, die im Protokoll wirklich
|
||
vorkommen -- gemessen an den 77 echten Aktionsnamen. Kommt eine
|
||
neue Aktion mit einem neuen Stamm, steht sie in ASCII da; das ist
|
||
unschoen, aber nicht falsch, und pruef-deutsche-texte findet es. */
|
||
const UMLAUT_TEILE = [
|
||
['geaendert', 'geändert'], ['aendern', 'ändern'],
|
||
['geloescht', 'gelöscht'], ['loeschen', 'löschen'],
|
||
['zurueck', 'zurück'], ['uebernommen', 'übernommen'],
|
||
['uebertragen', 'übertragen'], ['weggeraeumt', 'weggeräumt'],
|
||
['aufgeloest', 'aufgelöst'], ['geoeffnet', 'geöffnet'],
|
||
['gefuehrt', 'geführt'], ['erklaert', 'erklärt'],
|
||
['bestaetigt', 'bestätigt'], ['gespraech', 'gespräch'],
|
||
];
|
||
function aktionLesbar(a) {
|
||
let w = String(a || '').trim();
|
||
if (!w) return '';
|
||
for (const [ascii, schoen] of UMLAUT_TEILE) w = w.split(ascii).join(schoen);
|
||
w = w.replace(/_/g, ' ');
|
||
return w.charAt(0).toUpperCase() + w.slice(1);
|
||
}
|
||
|
||
async function protokollLaden() {
|
||
try {
|
||
const a = await hole('/workspace/api/verwaltung/protokoll?anzahl=25');
|
||
if (!a.ok) return;
|
||
const { eintraege } = await a.json();
|
||
const ziel = $('protokoll');
|
||
ziel.textContent = '';
|
||
for (const e of eintraege) {
|
||
const z = el('div', 'protokoll-zeile');
|
||
z.append(el('span', 'protokoll-zeit', datum(e.zeitpunkt)));
|
||
/* Der Rohname bleibt als Titel dran: Wer im Server danach
|
||
sucht, braucht ihn genau so, wie er in der Spalte steht. */
|
||
const was = el('span', 'protokoll-was', aktionLesbar(e.aktion));
|
||
was.title = e.aktion || '';
|
||
z.append(was);
|
||
z.append(el('span', null, [e.wer, e.detail].filter(Boolean).join(' · ')));
|
||
/* DIE IP GEHOERT DAZU, ABER NICHT NACH VORN. Sie beantwortet
|
||
eine Frage, die man selten stellt -- und sie nahm ein
|
||
Viertel der Zeile. Jetzt steht sie leise am Rand; wer sie
|
||
braucht, liest sie weiterhin ab. */
|
||
z.append(el('span', 'protokoll-ip', e.ip || ''));
|
||
ziel.append(z);
|
||
}
|
||
/* Zugeklappt stehen nur die neuesten Zeilen da. Gemeinsamer
|
||
Helfer aus kopf.js -- die Automationen-Seite nutzt denselben. */
|
||
window.klappe?.(ziel, $('protokoll-schalter'), $('protokoll-schalter-text'));
|
||
} catch { /* umgeleitet */ }
|
||
}
|
||
|
||
/* ---------- Anlegen ---------------------------------------------------- */
|
||
|
||
const umschalten = (auf) => {
|
||
$('neu').hidden = !auf; $('neu-oeffnen').hidden = auf;
|
||
if (auf) $('f-name').focus();
|
||
};
|
||
$('neu-oeffnen').addEventListener('click', () => umschalten(true));
|
||
$('neu-abbrechen').addEventListener('click', () => {
|
||
$('neu').reset(); $('neu-fehler').textContent = ''; umschalten(false);
|
||
});
|
||
|
||
$('neu').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
$('neu-fehler').textContent = '';
|
||
const knopf = $('neu-speichern');
|
||
knopf.disabled = true;
|
||
try {
|
||
/* DREI TUEREN, EINE AUSWAHL (07.09.2026).
|
||
|
||
DogFather geht durch die Verwaltungstuer und kann jede Rolle
|
||
vergeben. Manager und Spicy Media haben dort nichts zu suchen --
|
||
fuer sie gibt es zwei enge Tueren, die je genau eine Rolle
|
||
anlegen und die Rolle gar nicht erst als Feld annehmen.
|
||
|
||
Die Zuordnung steht hier und nicht in einer Abfrage im Server:
|
||
Der Server prueft ohnehin, WER durch welche Tuer darf. Diese
|
||
Zeilen sorgen nur dafuer, dass niemand gegen eine verschlossene
|
||
laeuft. */
|
||
/* EINE TUER JE ROLLE, UND ZWAR AUSGESCHRIEBEN (10.09.2026).
|
||
|
||
Hier stand `rolle === 'manager' ? manager-anlegen :
|
||
creator-anlegen` — ein Zweig mit einem Auffangbecken. Solange
|
||
es nur zwei Rollen gab, stimmte das. Mit dem Scout waere es
|
||
still falsch geworden: Er waere im `else` gelandet, und
|
||
`creator-anlegen` legt IMMER einen Creator an. Der Knopf haette
|
||
funktioniert, die Meldung haette Erfolg gemeldet, und in der
|
||
Liste stuende ein Creator, wo ein Scout stehen sollte.
|
||
|
||
Eine Zuordnung statt einer Kette: Wer keine Tuer hat, bekommt
|
||
eine Fehlermeldung — nicht die naechstbeste Tuer. */
|
||
const rolle = gewaehlteRolle();
|
||
const TUEREN = {
|
||
manager: '/workspace/api/manager-anlegen',
|
||
scout: '/workspace/api/scout-anlegen',
|
||
creator: '/workspace/api/creator-anlegen',
|
||
};
|
||
/* DER WEG HAENGT AN DER ROLLE, DIE ANGELEGT WIRD -- nicht an der
|
||
des Anlegenden (berichtigt 22.09.2026).
|
||
|
||
Hier stand `ich.rolle === 'admin' ? verwaltung : TUEREN[rolle]`.
|
||
Fuer die rechte Hand, die seit heute Modis und Community
|
||
anlegen darf, gab es damit GAR KEINEN Weg: `TUEREN` kennt nur
|
||
die drei Agenturrollen, und die Seite antwortete mit "Fuer die
|
||
Rolle modi gibt es hier keinen Weg" -- ein Satz, der wie ein
|
||
Fehler im Formular klingt und in Wahrheit eine fehlende Zeile
|
||
war.
|
||
|
||
Jetzt: Gibt es fuer diese Rolle eine eigene Tuer (manager,
|
||
scout, creator), wird sie genommen -- ausser von DogFather,
|
||
der ohnehin den allgemeinen Weg hat. Sonst der allgemeine Weg,
|
||
und der prueft `darfAnlegen` am Server. */
|
||
const eigeneTuer = TUEREN[rolle];
|
||
const weg = (eigeneTuer && ich.rolle !== 'admin')
|
||
? eigeneTuer : '/workspace/api/verwaltung/personen';
|
||
const a = await hole(weg, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
/* Die eigenen Tueren kennen ihre Rolle selbst; der allgemeine
|
||
Weg braucht sie im Rumpf. */
|
||
body: JSON.stringify(weg === '/workspace/api/verwaltung/personen'
|
||
? { name: $('f-name').value, rolle }
|
||
: { name: $('f-name').value }),
|
||
});
|
||
if (!a.ok) {
|
||
$('neu-fehler').textContent = sagWas((await a.json().catch(() => ({}))).fehler, 'Ging nicht.');
|
||
return;
|
||
}
|
||
$('neu').reset(); umschalten(false);
|
||
zeigeCode(await a.json());
|
||
await laden();
|
||
} catch { /* umgeleitet */ } finally { knopf.disabled = false; }
|
||
});
|
||
|
||
/* ---------- Start ------------------------------------------------------- */
|
||
|
||
|
||
(async () => {
|
||
try {
|
||
const a = await hole('/workspace/api/ich');
|
||
if (!a.ok) { location.assign('/workspace/'); return; }
|
||
ich = await a.json();
|
||
} catch {
|
||
/* NICHT MEHR STUMM (20.09.2026). Hier stand `catch { return; }`
|
||
-- und damit blieb die Seite bei jedem Netzaussetzer fuer
|
||
immer auf "wird geladen …" stehen. Gemessen auf 13 von 21
|
||
Seiten. `ladefehler()` sagt, was los ist, und bietet einen
|
||
Knopf an, der es noch einmal versucht. */
|
||
window.ladefehler?.();
|
||
return;
|
||
}
|
||
/* DIE RECHTE HAND KOMMT HEREIN, ABER NUR ZUM LESEN (10.09.2026).
|
||
|
||
Filipe: "ich will dass die rechte hand auch alle sieht."
|
||
|
||
`LEITUNG` aus bereiche.js kennt sie nicht und darf sie nicht
|
||
kennen -- die Rolle steht in keiner ausgelieferten Datei. Sie
|
||
wird deshalb hier daneben zugelassen, nicht dort hineingesetzt.
|
||
|
||
DAS IST KEINE SICHERUNG, sondern eine Weiche: Was sie darf,
|
||
entscheidet der Server (nurAdmin lässt sie nur an die Liste, und
|
||
nur lesend). Diese Zeile erspart ihr nur den Umweg über eine
|
||
Seite, die sie sofort wieder wegschickt. */
|
||
/* WER HIER NICHTS ANLEGEN DARF, BEKOMMT NUR DIE LISTE.
|
||
------------------------------------------------------------------
|
||
Hier stand `ich.rolle === 'hand'` -- ein Rollenname, und seit dem
|
||
22.09.2026 der falsche: Die rechte Hand LEGT AN. Filipe: "damit
|
||
sie das auch machen kann wenn er live ist."
|
||
|
||
Gefragt wird jetzt, was der Server ihr zugesteht (`darf_anlegen`
|
||
aus /api/ich). Damit kann diese Zeile nicht mehr von der Regel
|
||
abweichen -- und eine Rolle, die morgen nur lesen darf, faellt
|
||
von selbst hierher, ohne dass jemand daran denkt. */
|
||
const darfHierAnlegen = Array.isArray(ich.darf_anlegen) && ich.darf_anlegen.length > 0;
|
||
const nurLesen = !LEITUNG.has(ich.rolle) && !darfHierAnlegen;
|
||
/* WER HIER UEBERHAUPT SEIN DARF (berichtigt 22.09.2026).
|
||
------------------------------------------------------------------
|
||
Hier stand `!LEITUNG.has(rolle) && !nurLesen`. In dem Moment, in
|
||
dem die rechte Hand anlegen durfte, wurde `nurLesen` bei ihr
|
||
falsch -- und diese Zeile warf sie auf die Startseite. Die Seite
|
||
ging fuer sie einfach nicht auf, ohne Meldung, ohne Grund.
|
||
|
||
Jetzt darf hier bleiben, wer die Seite verwaltet, wer sie liest
|
||
ODER wer darauf jemanden anlegt. Die echte Schranke steht
|
||
ohnehin am Server; diese Zeile erspart nur eine leere Seite. */
|
||
if (!LEITUNG.has(ich.rolle) && !nurLesen && !darfHierAnlegen) {
|
||
location.assign('/workspace/start.html'); return;
|
||
}
|
||
window.werZeigen?.(ich);
|
||
|
||
/* ===================================================================
|
||
DIE SEITE HAT ZWEI AUSBAUSTUFEN (07.09.2026)
|
||
|
||
Filipe: "die sollen in der kategorie wo die jetzt sehen werden
|
||
auch noch das mit den hinzufuegen sehen, alles andere auf der
|
||
seite sollen die weiterhin nicht sehen."
|
||
|
||
DogFather bekommt die Seite wie bisher: Liste, Codes, Sperren,
|
||
Loeschen, Protokoll. Manager und Spicy Media bekommen genau EINEN
|
||
Teil davon -- das Anlegen.
|
||
|
||
DAS VERSTECKEN IST NICHT DIE SICHERUNG. Alles, was hier
|
||
ausgeblendet wird, haengt am Server an `nurAdmin` und antwortet
|
||
mit 404. Wuerde diese Zeile fehlen, saehen sie leere Kaesten und
|
||
Fehlermeldungen -- aber keine Daten. Das Ausblenden erspart ihnen
|
||
nur den Anblick von Dingen, die es fuer sie nicht gibt.
|
||
|
||
Und deshalb wird auch `laden()` NICHT aufgerufen: Es fragt die
|
||
Verwaltungsschnittstelle ab, und die ist zu. Eine Abfrage, von der
|
||
man weiss, dass sie 404 gibt, sollte man nicht stellen -- sie
|
||
fuellt nur das Serverlog. */
|
||
/* DRITTE AUSBAUSTUFE: NUR LESEN.
|
||
|
||
Sie steht VOR `nurAnlegen`, weil sie enger ist -- wer nur liest,
|
||
legt auch nichts an. Stünde sie danach, bekäme die rechte Hand
|
||
zuerst das Anlegeformular und danach den Hinweis, dass sie nichts
|
||
darf; sichtbar wäre beides. */
|
||
if (nurLesen) {
|
||
document.body.dataset.nurLesen = 'ja';
|
||
/* SIE LEGT NIEMANDEN AN. Das Ausblenden ist keine Sicherung --
|
||
der Server antwortet ihr mit 404 --, es erspart ihr nur ein
|
||
Formular, dessen Absenden eine Absage bringt. Beide Elemente,
|
||
nicht nur eines: Der Knopf oeffnet das Formular, und ein
|
||
verstecktes Formular mit sichtbarem Knopf ist eine Tuer ohne
|
||
Raum dahinter. */
|
||
const auf = $('neu-oeffnen'); if (auf) auf.hidden = true;
|
||
const form = $('neu'); if (form) form.hidden = true;
|
||
const zeile = $('unterzeile');
|
||
if (zeile) {
|
||
zeile.textContent = 'Du siehst hier das ganze Team. Codes, Sperren, '
|
||
+ 'Rollen und das Protokoll bleiben bei DogFather.';
|
||
zeile.hidden = false;
|
||
}
|
||
await laden();
|
||
return;
|
||
}
|
||
|
||
/* HIER STAND `ich.rolle !== 'admin'` (berichtigt 25.09.2026).
|
||
|
||
Das ist die DRITTE Stelle in dieser Datei, an der ein
|
||
Rollenvergleich im Browser veraltet ist -- nach dem 22.09.
|
||
(„keine Knoepfe") und dem 24.09. („keine Rollenwahl"). Diesmal
|
||
hat er die rechte Hand ZWEI Tage lang vor einer leeren Seite
|
||
sitzen lassen: Die Liste wurde geladen, stand vollstaendig im
|
||
Dokument, und `data-nur-anlegen` hat sie per CSS wieder
|
||
ausgeblendet. Nachgemessen mit mess-hand-personen.mjs -- am
|
||
Bildschirm, nicht im Quelltext.
|
||
|
||
Gefragt wird jetzt der Server (`darf_zugaenge_fuehren` aus
|
||
/api/ich, abgeleitet aus `fuehrtDieZugaenge`). Damit kann diese
|
||
Zeile nicht mehr von der Regel abweichen, und eine Rolle, die
|
||
morgen dazukommt, faellt von selbst auf die richtige Seite. */
|
||
const nurAnlegen = !ich.darf_zugaenge_fuehren;
|
||
if (nurAnlegen) {
|
||
document.body.dataset.nurAnlegen = 'ja';
|
||
/* DER ERKLÄRSATZ. Er stand hier schon, kam aber nie an: Ein
|
||
Element mit der Kennung `unterzeile` gab es auf der Seite
|
||
nicht, und das `&&` davor hat den Fehlgriff verschluckt.
|
||
Gemessen im Browser als Manager -- die Seite bestand aus
|
||
Überschrift und Formular, ohne ein Wort dazu, warum hier so
|
||
wenig steht. Jetzt gibt es das Element (personen.html), und
|
||
der Satz wird auch sichtbar gemacht. */
|
||
const zeile = $('unterzeile');
|
||
if (zeile) {
|
||
zeile.textContent =
|
||
'Hier legst du neue Leute an. Codes, Sperren und das Protokoll bleiben bei DogFather.';
|
||
zeile.hidden = false;
|
||
}
|
||
/* SPICY MEDIA BEKOMMT DIE LISTE -- ZUM LESEN (07.09.2026).
|
||
|
||
Filipe: "spicy soll genau das sehen koennen, aber nur nicht die
|
||
rolle dogfather." Bis hierher sah die Rolle auf dieser Seite
|
||
nur das Anlegen-Formular; die Liste wurde bewusst nicht
|
||
geladen, weil die Verwaltungsschnittstelle zu war. Sie ist es
|
||
weiterhin -- aber die LISTE ist seit heute offen, ohne
|
||
DogFathers Zeile (siehe workspace-personen.js).
|
||
|
||
Der Manager bekommt sie NICHT: Fuer ihn gilt unveraendert
|
||
"anlegen, nicht verwalten". Deshalb steht hier die Rolle und
|
||
nicht die Leitung. */
|
||
const liste = $('liste');
|
||
/* WER DIE LISTE AUCH BEKOMMT.
|
||
----------------------------------------------------------------
|
||
Spicy Media seit dem 07.09., die rechte Hand seit dem
|
||
22.09.2026: Sie muss sehen, wen es schon gibt -- sonst legt sie
|
||
jemanden zweimal an. Der Manager bekommt sie weiterhin nicht;
|
||
fuer ihn gilt unveraendert "anlegen, nicht verwalten".
|
||
|
||
DIE ECHTE SCHRANKE STEHT AM SERVER (workspace-personen.js,
|
||
`nurAdmin`). Diese Zeile entscheidet nur, ob ueberhaupt gefragt
|
||
wird -- wer hier faelschlich stuende, bekaeme eine leere Liste
|
||
und keinen Zugang. */
|
||
if (ich.rolle === 'spicy' || ich.rolle === 'hand') {
|
||
if (ich.rolle === 'spicy') document.body.dataset.nurLesen = 'ja';
|
||
laden();
|
||
} else if (liste) {
|
||
/* Fuer alle anderen hoert die Liste auf zu laden. Sie steht mit
|
||
aria-busy="true" in der Seite, und `laden()` wird fuer sie
|
||
nicht aufgerufen -- ohne diese Zeilen meldete die Seite einem
|
||
Vorleseprogramm dauerhaft "wird geladen", und es kam nie
|
||
etwas. Ein Ladehinweis, der nie endet, ist schlimmer als
|
||
keiner. */
|
||
liste.setAttribute('aria-busy', 'false');
|
||
liste.textContent = '';
|
||
}
|
||
baueRollenwahl();
|
||
/* Das Formular steht offen da -- es ist der einzige Grund, warum
|
||
diese Seite fuer sie ueberhaupt aufgeht. Ein Knopf, der eine
|
||
leere Seite oeffnet, waere ein Klick ohne Zweck. */
|
||
$('neu').hidden = false;
|
||
$('neu-oeffnen').hidden = true;
|
||
$('neu-abbrechen').hidden = true;
|
||
return;
|
||
}
|
||
|
||
baueRollenwahl();
|
||
await laden();
|
||
})();
|
||
})();
|