Wunsch Filipe: "ich will auch die moeglichkeit haben die loeschen zu
koennen."
Das ist die einzige Handlung im ganzen Workspace, die sich nicht
rueckgaengig machen laesst. Vier Vorkehrungen:
1. NUR DOGFATHER. Nicht die Leitung, nicht ein Manager -- "nur DogFather
hat alle endgueltigen Rechte" heisst genau hier etwas. Ein Manager
kann weiterhin sperren; das reicht fuer den Alltag und ist umkehrbar.
Alle anderen bekommen 404, auch fuer die Vorschau: Die verraet, wie
viel an einer Person haengt.
2. VORSCHAU. Vor dem Klick steht da, was MITGEHT (Profil, Start-Check,
Content-Saeulen, Sitzungen, Zustaendigkeit) und was BLEIBT und nur
seine Zuordnung verliert (Aufgaben, Termine, Bereichseintraege,
Dateien, Leads). Eine Aufgabe verschwinden zu lassen, weil jemand
geht, waere Geschichtsfaelschung.
Dazu die gefaehrlichste Einzelwarnung: Wer einen Scout loescht, nimmt
seinen Creators die zustaendige Person weg. Die Vorschau nennt sie
beim Namen.
3. EINE SICHERUNG DIREKT DAVOR -- und wenn sie scheitert, wird NICHT
geloescht. Damit ist "geloescht" wiederherstellbar. Das ist der
eigentliche Gewinn aus der Sicherungsarbeit von heute Nachmittag.
4. Der Name muss getippt werden. Nicht als Schikane: Der Loeschknopf
sitzt neben dem Sperrknopf, und die beiden sind sehr verschieden. Wer
den Namen tippt, hat die Zeile gelesen, die er trifft.
ZWEI FEHLER, die erst die Pruefung sichtbar gemacht hat:
a) Alle Loeschungen schrieben in DIESELBE Tagessicherung. Nach der
dritten kannte sie die erste geloeschte Person nicht mehr -- die
Sicherung haette genau in dem Fall versagt, fuer den sie da ist.
Loeschungen bekommen jetzt eine eigene Datei ("vorher-…") mit
Zeitstempel, die nie ueberschrieben wird. Aufgefallen nur, weil die
Pruefung die ANZAHL der Dateien zaehlt und nicht bloss, ob eine da ist.
b) Der Zeitstempel hatte Sekundenaufloesung -- drei Loeschungen in
derselben Sekunde ergaben wieder eine einzige Datei. Jetzt mit
Millisekunden, plus Zaehler als Notloesung. Und: Eine Sicherung vor
dem Loeschen setzt NICHT den Vermerk fuer den planmaessigen Lauf,
sonst faellt die naechtliche Sicherung aus.
Dabei auch ein Fehler in meiner eigenen Pruefung gefunden: Sie griff die
alphabetisch erste Datei und nannte sie "die aelteste" -- "vorher-…-2.db"
sortiert aber VOR "vorher-….db", weil "-" kleiner ist als ".". Sie prueft
jetzt die Eigenschaft selbst: JEDE geloeschte Person muss sich aus
irgendeiner Sicherung zurueckholen lassen.
43 Pruefungen, Schwerpunkt auf dem, was NICHT gehen darf.
Co-Authored-By: Claude Opus 5 <[email protected]>
476 lines
19 KiB
JavaScript
476 lines
19 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.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();
|
||
}));
|
||
|
||
/* 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 */ }
|
||
}
|
||
|
||
const SPEICHER = 'dfw_personen_alle';
|
||
let alle = [];
|
||
let alleZeigen = false;
|
||
try { alleZeigen = localStorage.getItem(SPEICHER) === 'ja'; } 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 oben = alle.filter((p) => p.rolle === 'admin');
|
||
const rest = alle.filter((p) => p.rolle !== 'admin');
|
||
const sichtbar = alleZeigen ? alle : oben;
|
||
|
||
for (const abschnitt of ROLLEN_ABSCHNITTE) {
|
||
const drin = sichtbar.filter((p) => p.rolle === abschnitt.rolle);
|
||
if (!drin.length) continue;
|
||
|
||
const block = document.createElement('section');
|
||
block.className = 'gruppe';
|
||
const kopf = document.createElement('div');
|
||
kopf.className = 'gruppe__kopf';
|
||
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(drin.length);
|
||
const unter = document.createElement('span');
|
||
unter.className = 'gruppe__unter';
|
||
unter.textContent = abschnitt.unter;
|
||
kopf.append(h, zahl, unter);
|
||
block.append(kopf);
|
||
|
||
for (const p of drin) block.append(zeile(p));
|
||
ziel.append(block);
|
||
}
|
||
|
||
const schalter = $('rest-schalter');
|
||
schalter.hidden = rest.length === 0;
|
||
if (!rest.length) return;
|
||
|
||
/* Die Zahl gehört auf den Knopf, nicht nur der Zustand. "Alle
|
||
zeigen" sagt nicht, wie viele man gerade nicht sieht. */
|
||
const gesperrt = rest.filter((p) => !p.aktiv).length;
|
||
$('rest-schalter-text').textContent = alleZeigen
|
||
? 'Nur DogFather zeigen'
|
||
: `${rest.length} weitere zeigen`
|
||
+ (gesperrt ? ` · ${gesperrt} gesperrt` : '');
|
||
schalter.setAttribute('aria-expanded', alleZeigen ? 'true' : 'false');
|
||
}
|
||
|
||
$('rest-schalter').addEventListener('click', () => {
|
||
alleZeigen = !alleZeigen;
|
||
try { localStorage.setItem(SPEICHER, alleZeigen ? 'ja' : 'nein'); } catch { /* egal */ }
|
||
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();
|
||
})();
|
||
})();
|