Filipe: "dogfather soll auch zaehlen wie bananastift und patrick."
Beim Nachsehen war das kein Wunsch, sondern ein Fehlerbericht. In der
Auswahl "Betreut von" stand "DogFather" -- aber als LEER-Wert, nicht als
Person. Es sah aus wie eine Zuordnung und war keine. Genau dieselben
Creator zaehlten deshalb gleichzeitig im Hinweis "Creator ohne
zustaendige Person". Zwei Aussagen ueber denselben Sachverhalt, beide auf
demselben Bildschirm, beide fuer sich stimmig.
Jetzt kann jede betreuende Rolle eingetragen werden -- DogFather, Manager
und Scouts, in der ueblichen Reihenfolge. Der Leer-Wert heisst, was er
ist: "— niemand —". Bei DogFather und Manager steht die Rolle in
Klammern dabei; bei aehnlichen Namen ist sonst nicht zu erkennen, wen man
eintraegt. Und "betreut N Creator" steht jetzt an jeder betreuenden
Person, nicht nur an Scouts.
DER WICHTIGE TEIL: An den RECHTEN aendert das nichts.
Die Zustaendigkeit steuert die Sichtbarkeit NUR beim Scout -- die Leitung
sieht ohnehin jeden Creator. Waere das anders, haette eine
Anzeigeeinstellung still Rechte vergeben. Der Test weist beide Richtungen
nach:
* Tili auf DogFather eingetragen -> KEIN Scout sieht sie.
* Tili auf Patrick eingetragen -> nur Patrick sieht sie, BananaStift
weiterhin nicht.
* Zurueck auf DogFather -> Patrick verliert die Sicht wieder.
* DogFather sieht in allen drei Faellen unveraendert beide Creator.
Ein Creator kann nicht zustaendig sein -- das waere eine Rolle, die es
nicht gibt. Und ein Scout kann die Zustaendigkeit weiterhin nicht selbst
setzen (404), sonst haette er die Rechtevergabe in der Hand, die ihn
begrenzen soll.
20 Pruefungen, darunter die Gegenprobe zum Hinweis: Auf "niemand"
zurueckgesetzt MUSS er wiederkommen, sonst waere er wertlos.
Co-Authored-By: Claude Opus 5 <[email protected]>
535 lines
22 KiB
JavaScript
535 lines
22 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. */
|
||
const LEITUNG = new Set(['admin', 'manager']);
|
||
|
||
const $ = (id) => document.getElementById(id);
|
||
const ROLLENNAME = { admin: 'DogFather', manager: 'Manager', scout: 'Scout', creator: 'Creator' };
|
||
|
||
/* Reihenfolge und Erklaerung an EINER Stelle. Sie gilt fuer die
|
||
Rollenwahl im Formular und fuer alles, was sonst Rollen auflistet. */
|
||
const ROLLEN = [
|
||
{ wert: 'admin', name: 'DogFather', symbol: 'krone',
|
||
text: 'Alle Rechte, endgültig. Nur DogFather legt DogFather und Manager an.' },
|
||
{ wert: 'manager', name: 'Manager', symbol: 'stern',
|
||
text: 'Dieselben Rechte wie DogFather – außer der Personenverwaltung.' },
|
||
{ 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. */
|
||
for (const r of ROLLEN) {
|
||
if ((r.wert === 'admin' || r.wert === 'manager') && ich.rolle !== 'admin') continue;
|
||
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');
|
||
});
|
||
ziel.append(b);
|
||
}
|
||
}
|
||
|
||
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 || ''; };
|
||
|
||
async function hole(pfad, optionen = {}) {
|
||
const a = await fetch(pfad, { credentials: 'same-origin', ...optionen });
|
||
if (a.status === 401) { 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;
|
||
}
|
||
|
||
const datum = (iso) => (iso ? iso.slice(0, 16).replace('T', ' ') : '—');
|
||
|
||
/* ---------- Code anzeigen -------------------------------------------- */
|
||
|
||
function zeigeCode(p) {
|
||
$('code-name').textContent = `${p.name} · ${ROLLENNAME[p.rolle] || p.rolle}`;
|
||
$('code-wert').textContent = p.code;
|
||
$('code-kasten').hidden = false;
|
||
$('code-kasten').scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
}
|
||
|
||
$('code-schliessen').addEventListener('click', () => {
|
||
$('code-kasten').hidden = true;
|
||
/* Den Code auch aus dem Dokument entfernen, damit er nicht in einer
|
||
Bildschirmaufnahme oder im Verlauf hängen bleibt. */
|
||
$('code-wert').textContent = '';
|
||
});
|
||
|
||
$('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 = [];
|
||
|
||
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`));
|
||
}
|
||
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. */
|
||
o.textContent = s2.rolle && s2.rolle !== 'scout'
|
||
? `${s2.name} (${ROLLENNAME[s2.rolle] || s2.rolle})`
|
||
: 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((await a.json().catch(() => ({}))).fehler || 'Ging nicht.'); return; }
|
||
await laden();
|
||
} catch { /* umgeleitet */ } finally { w.disabled = false; }
|
||
});
|
||
b.append(s, w);
|
||
links.append(b);
|
||
}
|
||
|
||
const knoepfe = el('div', 'person__knoepfe');
|
||
knoepfe.append(tuKnopf('Neuer Code', async () => {
|
||
const eigen = p.id === ich.id;
|
||
const frage = eigen
|
||
? `Neuer Code für dich selbst?\n\nDeine aktuelle Sitzung wird sofort beendet und du musst dich mit dem neuen Code neu anmelden.`
|
||
: `Neuer Code für ${p.name}?\n\nDer alte Code gilt dann nicht mehr, offene Sitzungen werden beendet.`;
|
||
if (!confirm(frage)) 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 && !confirm(`${p.name} sperren?\n\nOffene Sitzungen werden sofort beendet.`)) 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((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. */
|
||
if (ich.rolle === 'admin') {
|
||
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 zeilen = [];
|
||
const mit = Object.entries(v.mit).filter(([, n]) => n > 0);
|
||
const bleibt = Object.entries(v.bleibt).filter(([, n]) => n > 0);
|
||
|
||
zeilen.push(`„${v.person.name}" wirklich löschen?`, '');
|
||
if (mit.length) {
|
||
zeilen.push('Wird MITGELÖSCHT:');
|
||
for (const [was, n] of mit) zeilen.push(` · ${was}: ${n}`);
|
||
zeilen.push('');
|
||
}
|
||
if (bleibt.length) {
|
||
zeilen.push('BLEIBT erhalten, verliert nur die Zuordnung:');
|
||
for (const [was, n] of bleibt) zeilen.push(` · ${was}: ${n}`);
|
||
zeilen.push('');
|
||
}
|
||
if (v.verliertBetreuung.length) {
|
||
zeilen.push(`ACHTUNG: ${v.verliertBetreuung.join(', ')} `
|
||
+ `${v.verliertBetreuung.length === 1 ? 'steht' : 'stehen'} danach ohne zuständige Person da.`, '');
|
||
}
|
||
if (!mit.length && !bleibt.length && !v.verliertBetreuung.length) {
|
||
zeilen.push('An dieser Person hängt nichts.', '');
|
||
}
|
||
zeilen.push('Vorher wird automatisch eine Sicherung geschrieben.', '',
|
||
`Zum Bestätigen den Namen genau eingeben: ${v.person.name}`);
|
||
|
||
const eingabe = prompt(zeilen.join('\n'), '');
|
||
if (eingabe === null) return;
|
||
if (eingabe.trim() !== v.person.name) {
|
||
melde('Der Name stimmt nicht – es wurde nichts gelöscht.');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const a = await hole(`/workspace/api/verwaltung/personen/${person.id}`, {
|
||
method: 'DELETE',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ name: eingabe.trim() }),
|
||
});
|
||
const d = await a.json().catch(() => ({}));
|
||
if (!a.ok) { melde(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. */
|
||
const ROLLEN_ABSCHNITTE = [
|
||
{ 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 = '';
|
||
|
||
const vorhanden = ROLLEN_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 Ü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);
|
||
|
||
kopf.append(h, zahl, unter, 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);
|
||
}
|
||
|
||
/* 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 } = await a.json();
|
||
betreuerListe = betreuer || [];
|
||
alle = personen;
|
||
ziel.textContent = '';
|
||
ziel.setAttribute('aria-busy', 'false');
|
||
listeZeigen();
|
||
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 {
|
||
const a = await hole('/workspace/api/verwaltung/personen', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ name: $('f-name').value, rolle: gewaehlteRolle() }),
|
||
});
|
||
if (!a.ok) {
|
||
$('neu-fehler').textContent = (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; }
|
||
if (!LEITUNG.has(ich.rolle)) { location.assign('/workspace/start.html'); return; }
|
||
$('wer').textContent = `${ich.name} · ${ich.rolle_name || ich.rolle}`;
|
||
baueRollenwahl();
|
||
await laden();
|
||
})();
|
||
})();
|