Filipe hat drei Sachen gemeldet. Zwei davon waren nicht das, wonach sie
aussahen.
1. "BEI DER SPICY ROLLE IST DA EIN PROBLEM" (Creator-Profil laedt nicht)
Nicht diese Seite war kaputt. In NEUN Skripten stand dieselbe Zeile
const LEITUNG = new Set(['admin', 'manager']);
und in keinem davon 'spicy'. profil.js nahm deshalb den Creator-Zweig
und lud das EIGENE Profil -- ein Spicy-Zugang ist kein Creator, also
404. Acht der neun sind nicht kaputtgegangen; sie haben sich still
falsch verhalten.
Die Liste steht jetzt EINMAL in bereiche.js. Beim Aufraeumen kamen mit
derselben Suche sechs weitere Luecken heraus, alle vom selben Typ:
* Aufgaben abbrechen -- Server UND Knopf ohne 'spicy'
* Teilen-Knopf in der Kopfleiste -- ohne 'spicy'
* Chat: die Gespraechsliste laeuft ueber eine feste Rollenfolge.
Wer nicht darin steht, taucht gar nicht auf -- eine Person, die es
fuer die anderen nicht gibt.
* Kalender: dieselbe Rollenfolge, dort landete Spicy Media durch
indexOf() === -1 ganz oben statt an ihrem Platz.
* Dashboard: "Creator anlegen" nur fuer admin/manager -- der Server
haette sie gelassen, den Knopf hat sie nie gesehen.
* Personenliste: ROLLENNAME ohne 'spicy'. Der Auffangwert
`|| p.rolle` schrieb "spicy" statt "Spicy Media" -- plausibel
genug, um jahrelang zu bleiben.
`pruef-css-klassen.mjs` verlangt jetzt, dass kein Workspace-Skript sich
wieder eine eigene Rollenliste baut. Mit Gegenprobe, dass der Sucher so
eine Liste auch wirklich findet.
2. "DIE ZAHLEN DIESE KATEGORIE NICHT SEHEN"
Kachel und Seite waren beim Anlegen der Rolle auf ["spicy","admin"]
gesetzt worden -- "dieselben Rechte ausser Automationen". Beides jetzt
auf DogFather allein.
3. "IN CIGDEMS KALENDER STEHT IMMER NOCH MEIN MANAGER MEETING"
Das war MEIN Denkfehler von heute Frueh, nicht ein vergessener Fall.
Ich hatte "geht alle an" als "kein Teilnehmer eingetragen" definiert --
also entschied der Server, was alle angeht, und nicht der, der den
Termin anlegt. Wer niemanden eintraegt, meint aber meistens nicht
"alle", sondern "mich".
Der Fall ist entfallen. Sichtbar ist ein Termin jetzt nur noch fuer
den, der ihn angelegt hat, dem er zugeordnet ist, der das Gegenueber
ist -- oder der in der Teilnehmerliste steht. Damit ist das Markieren
im Termin die einzige Antwort auf "wen geht das an": eine sichtbare
Entscheidung im Formular statt einer unsichtbaren Regel im Server.
Calls & Protokolle rufen dieselbe Funktion auf und koennen deshalb gar
nicht auseinanderlaufen -- das war Filipes vierter Wunsch, und er ist
mit derselben Zeile erledigt.
Am laufenden System als Cigdem nachgemessen: Profil laedt ("Profil:
Luna"), Zahlen-Kachel weg und Seite gesperrt, im Kalender NUR der
Termin, bei dem sie markiert ist, Calls zeigt genau denselben.
Fuenf Pruefungen auf die endgueltige Regel umgeschrieben statt
geloescht (sicht, spicy, teilnehmer, scout-zuteilung, css-klassen).
Die dritte Drehung bei scout-zuteilung steht mit allen drei Staenden
in der Akte -- eine geloeschte Pruefung hinterlaesst keine Spur davon,
dass hier einmal etwas anderes galt.
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 = { admin: 'DogFather', manager: 'Manager', scout: 'Scout', creator: 'Creator' };
|
||
|
||
/* ---------- 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();
|
||
})();
|
||
})();
|