Filipe: "Es darf vor allem keine Stellen geben, an denen ein Benutzer
etwas falsch machen kann, nur weil die Seite es nicht verstaendlich
genug erklaert."
Gemessen: 30 Stellen in 16 Dateien benutzten confirm() oder prompt().
Das Haus hatte die richtige Bauweise laengst -- einen <dialog>, in
aufgaben.html sogar ausfuehrlich begruendet -- aber sie stand IN EINER
SEITE. Wer anderswo etwas loeschen liess, hatte sie nicht.
confirm('Wirklich loeschen?') stellt die falsche Frage: Es fragt, ob
man sicher ist, und nennt nicht, WAS passiert, was BLEIBT und ob es
ZURUECK geht. Jetzt beantwortet jeder der 41 Dialoge alle drei.
Neu: workspace/assets/js/nachfrage.js -- window.frageNach() mit
Pflichtgrund, Zahlenfeld, einzeiliger Eingabe und Abtippsicherung.
Drei Ausgaenge: <dialog> / confirm()-Notnagel fuer Safari vor 15.4 /
Abbruch (Esc, Klick daneben, "Doch nicht" -- immer false).
DREIMAL DERSELBE FALLSTRICK, dreimal nachgemessen statt vermutet:
.dialog stand in aufgaben.css und leistung.css -> auf dateien.html
waere der Dialog ein weisser Systemkasten gewesen. 14 Regeln
klammergenau nach module.css verschoben (Klammern gezaehlt, nicht
per Muster geschnitten -- heute frueh hat ein nicht-gieriges
Muster schon einmal CSS zerrissen).
Das Formular trug .neu neu--blank -- und .neu gibt seine Abstaende
nur in aufgaben.css. Gemessen: padding 0px, und die Felder
verloren ihre height:44px. Jetzt steht alles unter
.nachfrage__form in module.css; der Dialog borgt nichts mehr.
Die erste Fassung der Pruefung zaehlte nachfrage.js SELBST als
Nutzer -- damit war jede Seite trivialerweise "Nutzer" und die
Pruefung gruen ohne Inhalt. Jetzt ausdruecklich ausgenommen.
ZWEI FUNDE NEBENBEI:
hilfeAufraeumen() wird im Betrieb NIE aufgerufen. Der Kommentar
behauptete "wird beim Start aufgerufen (siehe index.js)" -- das
war nie wahr; einziger Aufrufer ist die eigene Pruefung. Folge:
geschlossene vertrauliche Faelle bleiben unbegrenzt stehen. NICHT
eingeschaltet (das loescht echte Daten und ist Filipes
Entscheidung), sondern der Kommentar richtiggestellt.
Einen Hilfe-Fall zu schliessen ist endgueltig -- es gibt keine
Route, die ihn wieder oeffnet. Vorher stand darueber nur die
Frage nach einem Schlusswort. Jetzt sagt der Dialog es.
pruef-struktur hat meine eigene Pruefung von heute Nachmittag
erwischt: Sie bildete ihr Datum aus UTC. Beim Beheben erst
heuteLokal(datum) genommen -- die Funktion nimmt gar kein Argument
und haette still "heute" statt "+3 Tage" geliefert. Jetzt tagLokal(3),
nachgerechnet: Abstand 3 Tage.
Am Bildschirm angesehen (Rechner 1280, Handy 390): passt rein, Esc
ergibt false, Fokus liegt auf dem harmlosen Knopf, Knoepfe 44px auf
Touch. Der Platzhalter im Abtippfeld zeigte den erwarteten Namen --
das sah aus wie ein schon ausgefuelltes Feld, entfernt.
Neu: server/pruef-nachfrage.mjs -- 17/0, mit sechs Gegenproben und
beiden Richtungen (wer fragt, laedt die Datei; wer nie fragt, laedt
sie nicht -- sonst truege die Anmeldewand 4,8 KB fuer nichts).
pruef-meldungen 8/0, pruef-css-klassen gruen, pruef-struktur gruen,
pruef-leistung gruen.
1266 lines
56 KiB
JavaScript
1266 lines
56 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;
|
||
for (const r of zusatz) {
|
||
if (!r || !r.wert || ziel.querySelector(`[data-rolle="${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: 'smooth', 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 = '';
|
||
|
||
const vorhanden = [...document.querySelectorAll('#f-rolle .rollenwahl__knopf')]
|
||
.map((b) => ({ wert: b.dataset.rolle, name: b.querySelector('.rollenwahl__name')?.textContent || b.dataset.rolle }))
|
||
.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: 'smooth', 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();
|
||
}
|
||
|
||
function zeile(p) {
|
||
const k = el('div', 'person');
|
||
k.dataset.aktiv = p.aktiv ? 'ja' : 'nein';
|
||
|
||
const links = el('div');
|
||
links.append(el('div', 'person__name', p.name + (p.id === ich.id ? ' (du)' : '')));
|
||
|
||
const z = el('div', 'person__zeile');
|
||
const rolle = el('span', 'marke-rolle', ROLLENNAME[p.rolle] || p.rolle);
|
||
rolle.dataset.rolle = p.rolle;
|
||
z.append(rolle);
|
||
if (!p.aktiv) z.append(el('span', null, 'gesperrt'));
|
||
z.append(el('span', null, `letzte Anmeldung: ${datum(p.letzter_login)}`));
|
||
z.append(el('span', null, `offene Aufgaben: ${p.offene_aufgaben}`));
|
||
if (p.sitzungen) z.append(el('span', null, `${p.sitzungen} offene Sitzung(en)`));
|
||
if (p.betreut_anzahl) {
|
||
z.append(el('span', null, `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) {
|
||
z.append(el('span', null,
|
||
`${p.scouts_anzahl} ${p.scouts_anzahl === 1 ? 'Scout' : 'Scouts'} zugeteilt`));
|
||
}
|
||
/* Beim Scout umgekehrt: zu wem er gehoert. */
|
||
if (p.rolle === 'scout' && p.manager_name) {
|
||
z.append(el('span', null, `gehört zu ${p.manager_name}`));
|
||
}
|
||
links.append(z);
|
||
|
||
/* 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');
|
||
/* WER NUR LIEST, BEKOMMT KEINE KNOEPFE. Der Server antwortet ihr
|
||
auf jeden davon mit 404 -- ein Knopf, der eine Absage bringt, ist
|
||
schlimmer als kein Knopf. */
|
||
if (ich.rolle === 'hand') { 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') {
|
||
if (ich.darf_rollen_wechseln && p.rolle !== 'admin') {
|
||
knoepfe.append(tuKnopf('Rolle ändern', () => rolleFragen(p)));
|
||
}
|
||
k.append(links, knoepfe);
|
||
return k;
|
||
}
|
||
|
||
knoepfe.append(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('Neuer Code hat nicht geklappt.'); return; }
|
||
zeigeCode(await a.json());
|
||
await laden();
|
||
}));
|
||
|
||
if (p.id !== ich.id) {
|
||
knoepfe.append(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();
|
||
}));
|
||
|
||
/* 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);
|
||
|
||
if (auf) for (const p of abschnitt.drin) block.append(zeile(p));
|
||
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. */
|
||
if (ich.rolle === 'admin') await protokollLaden();
|
||
} catch { /* umgeleitet */ }
|
||
}
|
||
|
||
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)));
|
||
z.append(el('span', null, e.aktion));
|
||
z.append(el('span', null, [e.wer, e.detail].filter(Boolean).join(' · ')));
|
||
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',
|
||
};
|
||
const weg = ich.rolle === 'admin' ? '/workspace/api/verwaltung/personen' : TUEREN[rolle];
|
||
if (!weg) {
|
||
melde(`Für die Rolle „${rolle}“ gibt es hier keinen Weg.`);
|
||
return;
|
||
}
|
||
const a = await hole(weg, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(ich.rolle === 'admin'
|
||
? { 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 { 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. */
|
||
const nurLesen = ich.rolle === 'hand';
|
||
if (!LEITUNG.has(ich.rolle) && !nurLesen) {
|
||
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;
|
||
}
|
||
|
||
const nurAnlegen = ich.rolle !== 'admin';
|
||
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');
|
||
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();
|
||
})();
|
||
})();
|