/* =================================================================== Eine Ansicht für fünf Bereiche (LIVE, Content, Technik, Community, Schutz). Welcher gemeint ist, steht in der Adresse: ?b=live Welche Arten es gibt und ob Bewertung oder Dringlichkeit dazugehören, sagt der Server in der Antwort -- diese Datei kennt die Bereiche nicht auswendig. Ein neuer Bereich braucht hier also keine Zeile. =================================================================== */ (() => { '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']); /* Wer hier eintraegt: DogFather, Manager und Scout. Der Creator liest nur mit -- diese Bereiche sind die Betreuungsakte, nicht sein Notizbuch. Der Server setzt das ebenfalls durch (siehe workspace-bereiche.js); hier geht es nur darum, ihm keine Knoepfe hinzustellen, die ohnehin nur eine Absage bringen. */ const darfEintragen = () => ich && ich.rolle !== 'creator'; const $ = (id) => document.getElementById(id); const UNTERZEILE = { live: 'Vorbereitung, Mitschrift und Auswertung – LIVE systematisch verbessern.', content: 'Ideen, Produktion und Veröffentlichtes an einer Stelle statt in Chats.', technik: 'Setups, Fehlerbilder und Lösungen pro Creator sauber dokumentiert.', community: 'Moderation, Aktionen und Konflikte – ruhig statt Drama.', schutz: 'Richtlinien, Vorfälle und was daraus gelernt wurde.', /* Der Satz nennt die Trennung beim Namen, die das Konzept fordert: Betreuung und Umsetzung sind unsere Seite, offizielle Wege sind Agenturseite. Solange das nur im Kopf steht, wird es bei jedem Streitfall neu verhandelt. */ agentur: 'Kampagnen, Schulungen und der offizielle Weg zur Agentur – mit Stand und Antwort.', }; let ich = null; let bereich = null; let einstellung = null; let filter = ''; /* Nur beim ersten Laden springen -- sonst reisst es die Seite nach jeder Bewertung erneut an dieselbe Stelle. */ let gesprungen = false; 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 || ''; }; const datum = (iso) => (iso ? iso.slice(0, 10).split('-').reverse().join('.') : '—'); 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) { location.assign('/workspace/start.html'); throw new Error('unbekannt'); } return a; } 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; } /* ---------- Zeichnen --------------------------------------------------- */ function karte(e) { const k = el('article', 'eintrag-karte'); k.dataset.dringlich = e.dringlichkeit; k.dataset.status = e.status; const kopf = el('div', 'eintrag-kopf'); kopf.append(el('span', 'marke-art', einstellung.arten[e.art] || e.art)); kopf.append(el('h3', 'eintrag-titel', e.titel)); if (einstellung.bewertung && e.bewertung) { const b = el('span', 'bewertung'); b.setAttribute('aria-label', `Bewertung ${e.bewertung} von 5`); for (let i = 1; i <= 5; i++) { const punkt = el('span', 'bewertung__punkt'); if (i <= e.bewertung) punkt.dataset.voll = 'ja'; b.append(punkt); } kopf.append(b); } k.append(kopf); if (e.text) k.append(el('p', 'eintrag-text', e.text)); const fuss = el('div', 'eintrag-fuss'); fuss.append(el('span', null, datum(e.datum))); if (LEITUNG.has(ich.rolle) && e.creator_name) fuss.append(el('span', null, '· ' + e.creator_name)); if (e.erstellt_name) fuss.append(el('span', null, '· von ' + e.erstellt_name)); if (e.status === 'erledigt') fuss.append(el('span', null, '· erledigt')); if (!darfEintragen()) { k.append(fuss); return k; } const knoepfe = el('div', 'eintrag-knoepfe'); knoepfe.append(tuKnopf(e.status === 'offen' ? 'erledigt ▶' : '◀ wieder offen', async () => { const a = await hole(`/workspace/api/bereich/${bereich}/${e.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: e.status === 'offen' ? 'erledigt' : 'offen' }), }); if (!a.ok) { melde('Ändern hat nicht geklappt.'); return; } await laden(); })); if (LEITUNG.has(ich.rolle) || e.erstellt_von === ich.id) { knoepfe.append(tuKnopf('löschen', async () => { if (!confirm(`„${e.titel}" wirklich löschen?`)) return; const a = await hole(`/workspace/api/bereich/${bereich}/${e.id}`, { method: 'DELETE' }); if (!a.ok) { melde('Löschen hat nicht geklappt.'); return; } await laden(); })); } fuss.append(knoepfe); k.append(fuss); return k; } /* Sprung vom Bericht (01.09.2026). "dringend" ist kein Filter der Seite -- die Dringlichkeit steht als data-dringlich an der Karte, und genau die wird hervorgehoben. "offen" dagegen IST ein Filter, der wurde schon vor dem ersten Laden gesetzt. */ function sprungZeigen(ziel, karten) { if (gesprungen) return; gesprungen = true; const was = window.zielSchluessel?.(); if (was === 'dringend') { /* OFFEN und hoch -- genau so zaehlt der Bericht (workspace-reports.js: "status = 'offen' AND dringlichkeit = 'hoch'"). Ohne das status-Merkmal hebt die Seite auch erledigte dringende Eintraege hervor: Man klickt auf "2" und bekommt drei markiert. Gefunden von pruef-sprung.mjs -- mit blossem Auge waere das nie aufgefallen, beide Zahlen sehen plausibel aus. */ window.zielMarkieren?.( ziel.querySelectorAll('.eintrag-karte[data-dringlich="hoch"][data-status="offen"]'), 'Einträge, die offen und als dringend eingestuft sind'); } else if (was === 'offen') { window.zielMarkieren?.([...karten.values()], 'Einträge, die noch offen sind', async () => { filter = ''; await laden(); }); } } function filterBauen() { const ziel = $('filter'); ziel.textContent = ''; const knopf = (wert, text) => { const b = el('button', 'schritt', text); b.type = 'button'; b.setAttribute('aria-pressed', String(filter === wert)); b.addEventListener('click', async () => { filter = wert; await laden(); }); return b; }; ziel.append(knopf('', 'Alle')); ziel.append(knopf('offen', 'Nur offene')); for (const [wert, name] of Object.entries(einstellung.arten)) ziel.append(knopf(wert, name)); } async function laden() { try { const a = await hole(`/workspace/api/bereich/${bereich}`); if (!a.ok) { melde('Einträge konnten nicht geladen werden.'); return; } const daten = await a.json(); einstellung = daten.einstellung; filterBauen(); const ziel = $('liste'); ziel.textContent = ''; ziel.setAttribute('aria-busy', 'false'); const sichtbar = daten.eintraege.filter((e) => { if (!filter) return true; if (filter === 'offen') return e.status === 'offen'; return e.art === filter; }); if (!sichtbar.length) { ziel.append(el('p', 'leer-hinweis', daten.eintraege.length ? 'Zu dieser Auswahl gibt es nichts.' : 'Noch kein Eintrag in diesem Bereich.')); /* AUCH HIER springen -- und das ist kein Schoenheitsfehler, sondern der wichtigere Fall: Wer auf "3 dringende Punkte" klickt und auf einer leeren Seite landet, glaubt, er sei falsch abgebogen. Die Leiste sagt ihm, dass er richtig ist und die Sache erledigt. */ sprungZeigen(ziel, new Map()); return; } const karten = new Map(); for (const e of sichtbar) { const k = karte(e); karten.set(e.id, k); ziel.append(k); } sprungZeigen(ziel, karten); /* Bewertung und Rueckmeldungen. In EINER Abfrage fuer die ganze Liste -- zwanzig Eintraege einzeln zu fragen waeren zwanzig Anfragen, und die Seite ruckelte sichtbar beim Aufbau. Nur in den Bereichen, in denen eine Beurteilung ueberhaupt Sinn ergibt: LIVE, Community und Technik. Bei Content sind es eigene Ideen des Creators -- die bewertet niemand mit einer Ampel. */ /* "agentur" ist seit dem 06.09.2026 dabei, und aus demselben Grund wie die anderen vier: Ein Anliegen an die Agentur hat einen STAND -- eingereicht, in Klärung, beantwortet. Genau dafür ist die Ampel da, samt der Nachrichten daran. Ohne sie wäre der "offizielle Weg" wieder nur eine Liste, in der etwas steht und niemand weiß, wie es ausgegangen ist. */ if (["live", "community", "technik", "schutz", "agentur"].includes(bereich) && window.ampelAnhaengen && karten.size) { const stand = await window.ampelnHolen([...karten.keys()]); for (const [id, k] of karten) { window.ampelAnhaengen(k, id, { ampel: stand.ampeln?.[id] || null, anzahl: stand.anzahlNachrichten?.[id] || 0, }); } } } catch { /* umgeleitet */ } finally { /* IM finally, nicht dahinter. laden() steigt oben frueh aus, wenn die Liste leer ist -- und dann wurden diese beiden Bloecke nie gebaut. Also ausgerechnet auf der leeren Seite, fuer die sie gedacht sind. Die Browserpruefung hat es sofort gemeldet ("eine Unterweisung: 0"). */ await vorlagenZeigen(); await schulungZeigen(); } } /* Unterweisungen -- nur im Bereich Schutz & Regeln. Dort gehoeren Richtlinien hin, und dort sucht sie auch jemand. */ async function schulungZeigen() { const behaelter = $('schulung'); if (!behaelter || bereich !== "schutz" || !window.schulungBlock) return; await window.schulungBlock(behaelter); } /* Fertige Punkte zum Uebernehmen -- derzeit nur fuer die LIVE-Analyse. Sie stehen ueber der Liste: Wer noch nichts hat, soll nicht erst an einem leeren Kasten vorbeiscrollen. Welcher Abschnitt gezeigt wird, richtet sich nach dem gewaehlten Filter. Ohne Filter die Vorbereitung -- das ist der Teil, ohne den die anderen beiden gar nicht erst sinnvoll sind. */ /* Die FESTE Liste -- nicht mehr Vorschlaege zum Uebernehmen. Der Unterschied ist grundlegend: Eine Liste, die man sich erst holen muss, ist keine Checkliste. Und zwei Creator haetten unterschiedliche Listen gehabt, je nachdem wer sich was geholt hat -- was einen Vergleich unmoeglich macht, und um Vergleich geht es bei einer Betreuung. Jetzt stehen dieselben Punkte fuer alle fest da, gruppiert. Was sich unterscheidet, ist der Stand je Creator. */ async function vorlagenZeigen() { const behaelter = $('vorlagen'); if (!behaelter || !window.checkliste) return; if (!['live', 'community', 'technik'].includes(bereich)) return; await window.checkliste(behaelter, bereich); } /* Fuer wen wird uebernommen? Ein Creator immer fuer sich (der Server setzt das ohnehin), ein Betreuer fuer den gewaehlten Creator. */ function creatorWahl() { const w = document.getElementById('f-creator'); return w && w.value ? Number(w.value) : null; } /* ---------- Neuer Eintrag ---------------------------------------------- */ const umschalten = (auf) => { /* Auch hier die Regel, nicht nur einmal beim Aufbau: Diese Funktion ist die einzige Stelle, die das Formular je wieder sichtbar macht. Sperrt man nur beim Laden, holt der naechste Aufruf den Knopf zurueck -- und niemand faellt es auf, weil es der Alltagsweg nicht ist. */ if (!darfEintragen()) { $('neu').hidden = true; $('neu-oeffnen').hidden = true; return; } $('neu').hidden = !auf; $('neu-oeffnen').hidden = auf; if (auf) $('f-titel').focus(); }; $('neu-oeffnen').addEventListener('click', () => { $('f-datum').value = window.heuteLokal(); 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'); const daten = { art: $('f-art').value, titel: $('f-titel').value, text: $('f-text').value, datum: $('f-datum').value, }; if (einstellung.dringlichkeit) daten.dringlichkeit = $('f-dringlich').value; if (einstellung.bewertung) daten.bewertung = $('f-bewertung').value || null; if (LEITUNG.has(ich.rolle)) { daten.creator_id = $('f-creator').value || null; daten.creator_extern = $('f-creator').dataset.extern || null; } knopf.disabled = true; try { const a = await hole(`/workspace/api/bereich/${bereich}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(daten), }); if (!a.ok) { $('neu-fehler').textContent = (await a.json().catch(() => ({}))).fehler || 'Ging nicht.'; return; } $('neu').reset(); umschalten(false); await laden(); } catch { /* umgeleitet */ } finally { knopf.disabled = false; } }); /* ---------- Start -------------------------------------------------------- */ (async () => { bereich = new URLSearchParams(location.search).get('b') || ''; /* Wird eine Bewertung gesetzt, baut sich die Liste neu auf. Der Zuhoerer haengt am Dokument und wird genau einmal gesetzt -- laden() laeuft mehrfach, und zwei Zuhoerer wuerden die Liste zweimal neu bauen. */ document.addEventListener('ampel-geaendert', () => laden()); if (!/^[a-z]+$/.test(bereich)) { location.assign('/workspace/start.html'); return; } try { const a = await hole('/workspace/api/ich'); if (!a.ok) { location.assign('/workspace/'); return; } ich = await a.json(); } catch { return; } window.werZeigen?.(ich); let daten; try { const a = await hole(`/workspace/api/bereich/${bereich}`); if (!a.ok) { location.assign('/workspace/start.html'); return; } daten = await a.json(); } catch { return; } einstellung = daten.einstellung; document.title = einstellung.name + ' · Creator Workspace'; $('marke-bereich').textContent = einstellung.name; $('titel').textContent = einstellung.name; $('unterzeile').textContent = UNTERZEILE[bereich] || ''; /* Fuer den Creator verschwindet alles Schreibende -- und die Unterzeile sagt, warum. Ein leerer Bereich ohne Erklaerung wirkt kaputt; einer mit Erklaerung wirkt gedacht. */ if (!darfEintragen()) { $('neu-oeffnen').hidden = true; $('neu').hidden = true; $('unterzeile').textContent = (UNTERZEILE[bereich] || '') + ' Hier trägt deine Betreuung ein – du siehst alles, was zu dir gehört.'; } for (const [wert, name] of Object.entries(einstellung.arten)) { const o = document.createElement('option'); o.value = wert; o.textContent = name; $('f-art').append(o); } $('feld-dringlich').hidden = !einstellung.dringlichkeit; $('feld-bewertung').hidden = !einstellung.bewertung; if (LEITUNG.has(ich.rolle)) { $('feld-creator').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; $('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." }); } /* Kommt jemand vom Bericht mit "nur die offenen", wird der Filter VOR dem ersten Laden gesetzt -- sonst baut die Seite alles auf, um es gleich wieder wegzuwerfen, und das flackert sichtbar. */ if (window.zielSchluessel?.() === 'offen') filter = 'offen'; await laden(); })(); })();