/* =================================================================== 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.rolle === 'scout' && 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; const leer = document.createElement('option'); leer.value = ''; leer.textContent = 'DogFather'; w.append(leer); for (const s2 of betreuerListe) { const o = document.createElement('option'); o.value = String(s2.id); o.textContent = 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(); })); } 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; } 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 || []; ziel.textContent = ''; ziel.setAttribute('aria-busy', 'false'); for (const p of personen) ziel.append(zeile(p)); 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); } } 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(); })(); })();