DIE SYMBOLE IN DER TAGESKACHEL WAREN DIESELBEN -- OHNE IHRE GESTALTUNG
Sie kommen vom selben Bauer wie alle anderen, bekamen aber nie dessen
Aussehen: Alle Lagenregeln waren auf `.kachel__svg` eingegrenzt, und
dieses Zeichen heisst `.dran__svg`. Uebrig blieb eine flache Kontur --
daneben, auf derselben Seite, dieselben Zeichen mit drei Lichtern,
Tiefe und Randlicht. Die Regeln gelten jetzt fuer beide Traeger, und
das Feld darum ist dasselbe gefasste Schild wie auf den Kacheln.
"UNDEFINED" IM CHAT -- DERSELBE FEHLER WIE HEUTE FRUEH, NUR ALS OBJEKT
Ueber Cigdems Namen stand woertlich "UNDEFINED". Die Rollennamen
standen als Objekt in FUENF Skripten (chat.js zweimal, dateien.js,
kalender.js zweimal), in keinem davon 'spicy'.
Heute Frueh war es dieselbe Sache als Menge (`new Set([...])`), und
seitdem sucht `pruef-css-klassen.mjs` danach. Ein Muster, das nur eine
Schreibweise kennt, findet auch nur eine -- die Pruefung sucht jetzt
auch nach Rollen-OBJEKTEN, mit Gegenprobe. Die Namen stehen an EINER
Stelle in bereiche.js.
DOGFATHER UND SPICY MEDIA SIND IM CHAT FUER JEDEN ERREICHBAR
DogFather kam bisher als Nebeneffekt ueber die Betreuungskette mit
hinein, Spicy Media gar nicht -- die Rolle steht in keiner Kette, sie
steht daneben. Beide werden jetzt ausdruecklich hinzugefuegt: Eine
Zustaendigkeit, die nur zufaellig aus einer anderen Regel herausfaellt,
faellt beim naechsten Umbau genauso zufaellig wieder heraus.
Gemessen aus der Sicht eines Creators -- wer bei ihm ankommt, kommt
ueberall an.
DIE PERSONENLISTE: SPICY MEDIA SIEHT ALLES AUSSER DOGFATHER
Der Abschnitt "Spicy Media" fehlte in der Liste komplett -- die Rolle
gibt es seit heute Frueh, die Personenseite kannte sie nicht. Und Spicy
Media selbst sah dort bisher nur das Anlegen-Formular; sie bekommt
jetzt die Liste, ohne DogFathers Zeile, und weiterhin ohne Codes,
Sperren, Loeschen und Protokoll. Ueberblick ist nicht Verwaltung.
ZWEI FOLGEFEHLER, BEIDE VON DEN PRUEFUNGEN GEFUNDEN
* `next("route")` TUT DAS GEGENTEIL VON DEM, WONACH ES KLINGT. Mein
erster Versuch war eine Ausnahme-Route VOR der Schranke, die mit
`next("route")` weiterreicht -- das ueberspringt aber die restlichen
Handler DIESER Route und geht zur naechsten Schicht, also genau zur
Schranke. Spicy bekam weiter 404, die Oberflaeche verstand das als
"nicht erlaubt" und sprang zur Startseite. Gemessen: Auf
personen.html standen die Kategorien der STARTSEITE.
* DAS PROTOKOLL WARF SIE VON DER SEITE. Es bleibt bei DogFather und
antwortet ihr mit 404 -- und `hole()` versteht ein 404 unter
`/verwaltung/` als "nicht erlaubt". Die Seite baute sich auf und
sprang im naechsten Atemzug weg. Eine Abfrage, von der man weiss,
dass sie 404 gibt, stellt man nicht.
UND EINE MEINER EIGENEN NEUEN PRUEFUNGEN WAR WERTLOS
"bei ihr fehlt der Abschnitt DogFather" -- gruen, mit dem Zusatz
"(keine Abschnitte)". Sie war gruen, weil die Liste bei ihr GAR NICHT
DA war. Genau der Haken, der nichts beweist. Er steht jetzt neben einem
Ergebnis, das etwas enthaelt: "Spicy Media | Manager | ...".
18 Pruefungen gelaufen, alle gruen. pruef-spicy von 49 auf 57.
Co-Authored-By: Claude Opus 5 <[email protected]>
435 lines
17 KiB
JavaScript
435 lines
17 KiB
JavaScript
/* ===================================================================
|
||
Dateiablage.
|
||
|
||
Hochgeladen wird der rohe Dateiinhalt, der Name steht im Kopf --
|
||
deshalb braucht der Server kein multipart/form-data zu zerlegen.
|
||
Wer was sehen und freigeben darf, entscheidet ausschliesslich er.
|
||
=================================================================== */
|
||
(() => {
|
||
'use strict';
|
||
|
||
/* DogFather und Manager haben dieselben Rechte -- die Unterscheidung
|
||
macht nur die Personenverwaltung, und die prueft der Server. */
|
||
/* Die Leitungsliste steht in bereiche.js -- eine Antwort statt neun.
|
||
Sie stand hier frueher selbst, ohne 'spicy', und genau das war der
|
||
Fehler, den Filipe am 07.09.2026 gefunden hat. */
|
||
const LEITUNG = window.Bereiche.LEITUNG;
|
||
|
||
const $ = (id) => document.getElementById(id);
|
||
const STATUSNAME = { entwurf: 'Entwurf', review: 'Review', freigegeben: 'Freigegeben' };
|
||
const WEITER = { entwurf: 'review', review: 'freigegeben' };
|
||
const ZURUECK = { review: 'entwurf', freigegeben: 'review' };
|
||
|
||
let ich = null;
|
||
let filter = '';
|
||
let maxBytes = 25 * 1024 * 1024;
|
||
|
||
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'); }
|
||
return a;
|
||
}
|
||
|
||
function groesse(bytes) {
|
||
if (bytes < 1024) return bytes + ' B';
|
||
if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + ' KB';
|
||
return (bytes / 1024 / 1024).toFixed(1).replace('.', ',') + ' MB';
|
||
}
|
||
const datum = (iso) => (iso ? iso.slice(0, 10).split('-').reverse().join('.') : '—');
|
||
|
||
/* ---------- Wer darf die Datei sehen ------------------------------------
|
||
Kleine Schalter statt einer Mehrfachliste: In einer <select multiple>
|
||
verliert man mit einem Fehlklick die ganze Auswahl, und auf dem Handy
|
||
ist sie kaum bedienbar. Hier ist jeder Name ein eigener Schalter. */
|
||
|
||
let empfaenger = [];
|
||
let dartFreigeben = false;
|
||
|
||
function baueWahl() {
|
||
const ziel = $('wahl');
|
||
if (!ziel) return;
|
||
const vorher = new Set([...ziel.querySelectorAll('.wahl__knopf[data-an="ja"]')]
|
||
.map((b) => b.dataset.id));
|
||
ziel.textContent = '';
|
||
if (!empfaenger.length) {
|
||
ziel.append(el('p', 'leise', dartFreigeben
|
||
? 'Es gibt noch niemanden, dem du eine Datei geben könntest.'
|
||
: ''));
|
||
return;
|
||
}
|
||
for (const p of empfaenger) {
|
||
const b = el('button', 'wahl__knopf');
|
||
b.type = 'button';
|
||
b.dataset.id = String(p.id);
|
||
b.dataset.rolle = p.rolle;
|
||
if (vorher.has(String(p.id))) b.dataset.an = 'ja';
|
||
b.append(el('span', 'wahl__name', p.name));
|
||
b.append(el('span', 'wahl__rolle', ROLLENKURZ[p.rolle] || p.rolle));
|
||
b.addEventListener('click', () => {
|
||
if (b.dataset.an === 'ja') delete b.dataset.an; else b.dataset.an = 'ja';
|
||
wahlHinweis();
|
||
});
|
||
ziel.append(b);
|
||
}
|
||
wahlHinweis();
|
||
}
|
||
|
||
function wahlHinweis() {
|
||
const n = document.querySelectorAll('.wahl__knopf[data-an="ja"]').length;
|
||
const h = $('wahl-hinweis');
|
||
if (!h) return;
|
||
h.textContent = n
|
||
? `${n} ${n === 1 ? 'Person' : 'Personen'} ausgewählt – nur sie sehen die Datei.`
|
||
: 'Niemand ausgewählt – dann sieht sie nur DogFather.';
|
||
}
|
||
|
||
const ROLLENKURZ = window.Bereiche.ROLLEN_NAME;
|
||
|
||
/* ---------- Hochladen -------------------------------------------------- */
|
||
|
||
async function schicke(datei) {
|
||
melde('');
|
||
if (datei.size > maxBytes) {
|
||
melde(`„${datei.name}" ist ${groesse(datei.size)} groß – erlaubt sind ${groesse(maxBytes)}.`);
|
||
return;
|
||
}
|
||
$('fortschritt').hidden = false;
|
||
$('fortschritt-text').textContent = `„${datei.name}" wird hochgeladen …`;
|
||
try {
|
||
const kopf = {
|
||
'Content-Type': datei.type || 'application/octet-stream',
|
||
/* Kodiert, damit Umlaute und Leerzeichen im Kopf heil ankommen. */
|
||
'X-Dateiname': encodeURIComponent(datei.name),
|
||
};
|
||
if (LEITUNG.has(ich.rolle) && $('f-creator').value) kopf['X-Creator'] = $('f-creator').value;
|
||
/* Der freie Name geht URL-kodiert mit -- Kopfzeilen sind streng
|
||
genommen nur ASCII, "Müller" käme sonst verstümmelt an. */
|
||
const externName = $('f-creator')?.dataset.extern || '';
|
||
if (externName) kopf['X-Creator-Extern'] = encodeURIComponent(externName);
|
||
/* Wer die Datei sehen darf -- als Liste von Ids im Kopf. */
|
||
const gewaehlt = [...document.querySelectorAll('.wahl__knopf[data-an="ja"]')]
|
||
.map((b) => b.dataset.id);
|
||
if (gewaehlt.length) kopf['X-Personen'] = gewaehlt.join(',');
|
||
|
||
const a = await hole('/workspace/api/dateien', { method: 'POST', headers: kopf, body: datei });
|
||
if (!a.ok) {
|
||
const d = await a.json().catch(() => ({}));
|
||
melde(d.fehler || 'Hochladen hat nicht geklappt.');
|
||
return;
|
||
}
|
||
/* Auswahl nach dem Hochladen loeschen -- sonst bekaeme die
|
||
naechste Datei stillschweigend dasselbe Publikum. */
|
||
for (const b of document.querySelectorAll('.wahl__knopf[data-an="ja"]')) delete b.dataset.an;
|
||
await laden();
|
||
wahlHinweis();
|
||
} catch { /* umgeleitet */ } finally {
|
||
$('fortschritt').hidden = true;
|
||
$('feld-datei').value = '';
|
||
}
|
||
}
|
||
|
||
const ablage = $('ablage');
|
||
ablage.addEventListener('click', () => $('feld-datei').click());
|
||
ablage.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); $('feld-datei').click(); }
|
||
});
|
||
$('feld-datei').addEventListener('change', () => {
|
||
for (const d of $('feld-datei').files) schicke(d);
|
||
});
|
||
|
||
for (const art of ['dragenter', 'dragover']) {
|
||
ablage.addEventListener(art, (e) => { e.preventDefault(); ablage.dataset.bereit = 'ja'; });
|
||
}
|
||
for (const art of ['dragleave', 'drop']) {
|
||
ablage.addEventListener(art, () => { delete ablage.dataset.bereit; });
|
||
}
|
||
ablage.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
for (const d of e.dataTransfer.files) schicke(d);
|
||
});
|
||
/* Ohne diese beiden öffnet der Browser eine daneben fallengelassene
|
||
Datei einfach als Seite -- die Arbeit wäre dann weg. */
|
||
addEventListener('dragover', (e) => e.preventDefault());
|
||
addEventListener('drop', (e) => e.preventDefault());
|
||
|
||
/* ---------- Liste ------------------------------------------------------ */
|
||
|
||
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 statusSetzen(d, neu) {
|
||
const a = await hole(`/workspace/api/dateien/${d.id}`, {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ status: neu }),
|
||
});
|
||
if (!a.ok) {
|
||
melde((await a.json().catch(() => ({}))).fehler || 'Ändern hat nicht geklappt.');
|
||
return;
|
||
}
|
||
await laden();
|
||
}
|
||
|
||
function zeile(d) {
|
||
const k = el('div', 'datei');
|
||
k.dataset.status = d.status;
|
||
|
||
const zeichen = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||
zeichen.setAttribute('viewBox', '0 0 24 24');
|
||
zeichen.setAttribute('class', 'datei__zeichen');
|
||
zeichen.setAttribute('aria-hidden', 'true');
|
||
const pfad = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||
pfad.setAttribute('d', 'M13.5 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8.5L13.5 3Zm0 0V8a.5.5 0 0 0 .5.5h5');
|
||
zeichen.append(pfad);
|
||
k.append(zeichen);
|
||
|
||
k.append(el('div', 'datei__name', d.name_original));
|
||
|
||
const knoepfe = el('div', 'datei__knoepfe');
|
||
const marke = el('span', 'marke-status', STATUSNAME[d.status] || d.status);
|
||
marke.dataset.status = d.status;
|
||
knoepfe.append(marke);
|
||
|
||
const holen = el('a', 'schritt', 'öffnen');
|
||
holen.href = `/workspace/api/dateien/${d.id}/inhalt`;
|
||
knoepfe.append(holen);
|
||
|
||
if (WEITER[d.status]) {
|
||
const darf = d.status !== 'review' || LEITUNG.has(ich.rolle);
|
||
if (darf) knoepfe.append(tuKnopf(
|
||
d.status === 'entwurf' ? 'zum Review ▶' : 'freigeben ▶',
|
||
() => statusSetzen(d, WEITER[d.status])));
|
||
}
|
||
if (ZURUECK[d.status] && (d.status !== 'freigegeben' || LEITUNG.has(ich.rolle))) {
|
||
knoepfe.append(tuKnopf('◀ zurück', () => statusSetzen(d, ZURUECK[d.status])));
|
||
}
|
||
if (LEITUNG.has(ich.rolle) || (d.hochgeladen_von === ich.id && d.status !== 'freigegeben')) {
|
||
knoepfe.append(tuKnopf('löschen', async () => {
|
||
if (!confirm(`„${d.name_original}" wirklich löschen?`)) return;
|
||
const a = await hole(`/workspace/api/dateien/${d.id}`, { method: 'DELETE' });
|
||
if (!a.ok) { melde('Löschen hat nicht geklappt.'); return; }
|
||
await laden();
|
||
}));
|
||
}
|
||
k.append(knoepfe);
|
||
|
||
const zeile2 = el('div', 'datei__zeile');
|
||
zeile2.append(el('span', null, groesse(d.groesse)));
|
||
zeile2.append(el('span', null, '· ' + datum(d.erstellt)));
|
||
if (d.hochgeladen_name) zeile2.append(el('span', null, '· von ' + d.hochgeladen_name));
|
||
if (LEITUNG.has(ich.rolle) && d.creator_name) {
|
||
zeile2.append(el('span', null, '· Bereich ' + d.creator_name));
|
||
}
|
||
if (d.notiz) zeile2.append(el('span', null, '· ' + d.notiz));
|
||
k.append(zeile2);
|
||
|
||
/* Wer sie sehen darf -- sichtbar fuer alle, damit niemand raten
|
||
muss, wer eine Datei schon hat. */
|
||
const frei = el('div', 'datei__freigaben');
|
||
if (d.freigaben?.length) {
|
||
frei.append(el('span', 'datei__schild', 'Sichtbar für'));
|
||
for (const p of d.freigaben) frei.append(el('span', 'marke-person', p.name));
|
||
} else {
|
||
frei.append(el('span', 'datei__schild', 'Sichtbar für'));
|
||
frei.append(el('span', 'marke-person marke-person--leer', 'niemanden zugeteilt'));
|
||
}
|
||
|
||
if (dartFreigeben && empfaenger.length) {
|
||
const knopf = el('button', 'knopf-still', 'Ändern');
|
||
knopf.type = 'button';
|
||
knopf.addEventListener('click', () => freigabenBearbeiten(k, d));
|
||
frei.append(knopf);
|
||
}
|
||
k.append(frei);
|
||
return k;
|
||
}
|
||
|
||
/* Auswahl direkt an der Datei -- ohne Seitenwechsel und ohne Fenster,
|
||
das den Rest verdeckt. */
|
||
function freigabenBearbeiten(karte, d) {
|
||
if (karte.querySelector('.datei__wahl')) return;
|
||
const box = el('div', 'datei__wahl');
|
||
const jetzt = new Set((d.freigaben || []).map((p) => p.id));
|
||
for (const p of empfaenger) {
|
||
const b = el('button', 'wahl__knopf');
|
||
b.type = 'button';
|
||
b.dataset.id = String(p.id);
|
||
b.dataset.rolle = p.rolle;
|
||
if (jetzt.has(p.id)) b.dataset.an = 'ja';
|
||
b.append(el('span', 'wahl__name', p.name));
|
||
b.append(el('span', 'wahl__rolle', ROLLENKURZ[p.rolle] || p.rolle));
|
||
b.addEventListener('click', () => {
|
||
if (b.dataset.an === 'ja') delete b.dataset.an; else b.dataset.an = 'ja';
|
||
});
|
||
box.append(b);
|
||
}
|
||
const knoepfe = el('div', 'datei__knoepfe');
|
||
const speichern = el('button', 'knopf knopf--klein', 'Übernehmen');
|
||
speichern.type = 'button';
|
||
speichern.addEventListener('click', async () => {
|
||
speichern.disabled = true;
|
||
const ids = [...box.querySelectorAll('.wahl__knopf[data-an="ja"]')].map((b) => Number(b.dataset.id));
|
||
const a = await hole(`/workspace/api/dateien/${d.id}`, {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ freigaben: ids }),
|
||
});
|
||
speichern.disabled = false;
|
||
if (!a.ok) { melde((await a.json().catch(() => ({}))).fehler || 'Ging nicht.'); return; }
|
||
await laden();
|
||
});
|
||
const abbrechen = el('button', 'knopf-still', 'Abbrechen');
|
||
abbrechen.type = 'button';
|
||
abbrechen.addEventListener('click', () => { box.remove(); knoepfe.remove(); });
|
||
knoepfe.append(speichern, abbrechen);
|
||
karte.append(box, knoepfe);
|
||
}
|
||
|
||
async function laden() {
|
||
try {
|
||
const a = await hole('/workspace/api/dateien');
|
||
if (!a.ok) { melde('Dateien konnten nicht geladen werden.'); return; }
|
||
const daten = await a.json();
|
||
empfaenger = daten.empfaenger || [];
|
||
dartFreigeben = !!daten.darf_freigeben;
|
||
baueWahl();
|
||
maxBytes = daten.max_bytes || maxBytes;
|
||
$('ablage-grenze').textContent = 'bis ' + groesse(maxBytes);
|
||
|
||
const ziel = $('liste');
|
||
ziel.textContent = '';
|
||
ziel.setAttribute('aria-busy', 'false');
|
||
|
||
const sichtbar = daten.dateien.filter((d) => !filter || d.status === filter);
|
||
if (!sichtbar.length) {
|
||
ziel.append(el('p', 'leer-hinweis', daten.dateien.length
|
||
? 'In diesem Zustand liegt gerade nichts.'
|
||
: 'Noch keine Datei abgelegt.'));
|
||
/* AUCH HIER springen, und das ist der wichtigere Fall: Wer auf
|
||
"2 Dateien im Review" klickt und auf einer leeren Seite
|
||
landet, glaubt, er sei falsch abgebogen. Die Leiste sagt ihm,
|
||
dass er richtig ist und die Sache erledigt.
|
||
(Dieselbe Luecke gab es in bereich.js -- ein frueher Ausstieg
|
||
ueberspringt eben alles, was danach kommt.) */
|
||
if (!gesprungen) { gesprungen = true; sprungZeigen(); }
|
||
return;
|
||
}
|
||
for (const d of sichtbar) ziel.append(zeile(d));
|
||
|
||
/* Sprung vom Bericht -- erst NACH dem Zeichnen, vorher gibt es die
|
||
Zeilen nicht. Nur beim ersten Mal, sonst reisst es die Seite bei
|
||
jeder Statusaenderung wieder weg. */
|
||
if (!gesprungen) { gesprungen = true; sprungZeigen(); }
|
||
} catch { /* umgeleitet */ }
|
||
}
|
||
|
||
/* ---------- Sprung vom Bericht -------------------------------------------
|
||
|
||
Anders als beim Aufgabenbrett wird hier WIRKLICH gefiltert: Diese
|
||
Seite hat ohnehin eine Filterleiste, und die Zustandsknoepfe darin
|
||
zeigen dann mit an, wo man gerade steht. Ein zweites, unsichtbares
|
||
Auswahlverfahren daneben waere verwirrend. */
|
||
const ZEIG_STATUS = { review: 'review', frei: 'freigegeben', entwurf: 'entwurf' };
|
||
const ZEIG_TEXT = {
|
||
review: 'Dateien, die auf Freigabe warten',
|
||
frei: 'Dateien, die freigegeben sind',
|
||
entwurf: 'Dateien im Entwurf',
|
||
};
|
||
|
||
function sprungZeigen() {
|
||
const was = window.zielSchluessel?.();
|
||
if (!was || !ZEIG_STATUS[was]) return;
|
||
window.zielMarkieren?.(
|
||
document.querySelectorAll(`#liste .datei[data-status="${ZEIG_STATUS[was]}"]`),
|
||
ZEIG_TEXT[was],
|
||
/* Beim "Alles zeigen" faellt auch der Filter -- sonst bliebe die
|
||
Seite ausgeschnitten, und man haelt drei Dateien fuer alles,
|
||
was da ist. */
|
||
async () => {
|
||
filter = '';
|
||
for (const x of $('filter').querySelectorAll('.schritt')) {
|
||
x.setAttribute('aria-pressed', String(!x.dataset.status));
|
||
}
|
||
await laden();
|
||
});
|
||
}
|
||
let gesprungen = false;
|
||
|
||
for (const b of $('filter').querySelectorAll('.schritt')) {
|
||
b.addEventListener('click', async () => {
|
||
filter = b.dataset.status;
|
||
for (const x of $('filter').querySelectorAll('.schritt')) {
|
||
x.setAttribute('aria-pressed', String(x === b));
|
||
}
|
||
await laden();
|
||
});
|
||
}
|
||
|
||
/* ---------- Start ------------------------------------------------------- */
|
||
|
||
(async () => {
|
||
try {
|
||
const a = await hole('/workspace/api/ich');
|
||
if (!a.ok) { location.assign('/workspace/'); return; }
|
||
ich = await a.json();
|
||
} catch { return; }
|
||
window.werZeigen?.(ich);
|
||
|
||
/* Creator laden nichts hoch (Wunsch 28.08.2026). Das Ablegefeld
|
||
verschwindet, stattdessen steht dort, warum -- ein einfach
|
||
fehlendes Feld wirkt wie ein Fehler. Der Server weist einen
|
||
Versuch ohnehin ab; das hier ist nur die Anzeige. */
|
||
if (ich.rolle === 'creator') {
|
||
$('ablage').hidden = true;
|
||
$('nur-lesen').hidden = false;
|
||
}
|
||
|
||
if (LEITUNG.has(ich.rolle)) {
|
||
$('zuordnung').hidden = false;
|
||
try {
|
||
const { personen } = await (await hole('/workspace/api/personen')).json();
|
||
for (const p of personen.filter((p) => p.rolle === 'creator')) {
|
||
const o = document.createElement('option');
|
||
o.value = String(p.id);
|
||
o.textContent = p.name;
|
||
o.dataset.person = 'ja';
|
||
if (p.bild) o.dataset.bild = p.bild;
|
||
$('f-creator').append(o);
|
||
}
|
||
} catch { /* Zuordnung bleibt leer */ }
|
||
window.personenwahl?.('f-creator', { hinweis: "Kein Konto im Workspace – der Eintrag gehört dann zu keiner Person und taucht in personenbezogenen Auswertungen nicht auf. Sichtbar bleibt er für dich." });
|
||
} else if (ich.rolle === 'scout') {
|
||
$('zuordnung').hidden = false;
|
||
$('f-creator').closest('.feld').hidden = true;
|
||
}
|
||
|
||
/* Kommt jemand vom Bericht, wird der Filter VOR dem ersten Laden
|
||
gesetzt -- sonst baut die Seite erst alles auf, um es gleich
|
||
wieder wegzuwerfen, und das sieht man flackern. Die Filterleiste
|
||
zeigt danach von selbst, wo man steht. */
|
||
const was = window.zielSchluessel?.();
|
||
if (was && ZEIG_STATUS[was]) {
|
||
filter = ZEIG_STATUS[was];
|
||
for (const x of $('filter').querySelectorAll('.schritt')) {
|
||
x.setAttribute('aria-pressed', String(x.dataset.status === filter));
|
||
}
|
||
}
|
||
await laden();
|
||
})();
|
||
})();
|