Files
dogfather-universe/workspace/assets/js/bereich.js
T
DogFatherGitandClaude Opus 5 9b47bd3ca0 Entwicklung & Nachwuchs -- zwei Bretter fuer DogFather und die rechte Hand
Filipe: "ich will dass ich genau so eine kategorie habe in der dogfather
und rechten hand rollen, wo wir aufgaben oder bewertungen ueber modis
eingeben koennen. auch fuer zuschauer die vielleicht modis werden
koennten waere auch geil. mach dich schlau informier dich so krass wie
es nur geht, hol die besten der besten und krassesten sachen,
perfektionnier die dan auch alle und dan erst setzt du alles um."

=== WAS DIE RECHERCHE ERGEBEN HAT ===

DREI BEFUNDE HABEN DAS DESIGN BESTIMMT, und der erste hat es fast
umgedreht:

  WARUM MODERATOREN AUFHOEREN (Schoepke-Gonzalez u. a., New Media &
  Society 2024): zwei Hauptgruende -- zu wenig Zeit, und Konflikte im
  Team beziehungsweise schaedliches Verhalten der Leitung. Eine
  Bewertungsfunktion ist damit genau das Werkzeug, mit dem man ein Team
  verliert, wenn man sie als Ueberwachung baut. Das ist kein Bauchgefuehl
  und keine Zimperlichkeit -- es ist der haeufigste gemessene Grund.

  WAS HAELT: Anerkennung. Dank und Rueckmeldung erhoehen die
  Verweildauer messbar; Rollenklarheit senkt Burnout.

  WORAN MAN GUTE MODERATOREN ERKENNT (ModSquad, Kitfox Games,
  Discord-Leitfaeden): nicht an Zahlen. Ruhig bleiben, wenn es hitzig
  wird; von selbst helfen; die Regeln UND die Leute kennen; regelmaessig
  da sein. Ausdruecklich NICHT: "schreibt viel" -- angenehm im Chat zu
  sein ist nachweislich etwas anderes. Und der treffsicherste Weg
  ueberhaupt ist die Empfehlung aus dem Team, gefolgt von einer
  Probezeit.

=== WAS DARAUS GEBAUT WURDE ===

ENTWICKLUNG. Keine Note, sondern eine Aufzeichnung ueber die Zeit, je
Person. Fuenf Arten, und ihre REIHENFOLGE ist Absicht: "Das laeuft gut"
und "Danke dafuer" stehen vorn. Wer ein Formular oeffnet, dessen erstes
Feld "Problem" heisst, schreibt Probleme auf.

"ZU VIEL GERADE" IST DIE WICHTIGSTE ART und die, die es sonst nirgends
gibt. Der haeufigste Grund zu gehen ist Zeitmangel, und der zeigt sich
frueh -- nur schreibt ihn niemand auf, weil es kein Feld dafuer gibt.

TALENTE. Die vier Merkmale sind die aus der Literatur, nicht
ausgedacht, dazu die Empfehlung aus dem Team. Der Status IST die
Probezeit: `offen` heisst beobachtet, `angenommen` heisst angesprochen
-- und was daraus wird, entscheidet sich in "Personen & Zugaenge" mit
einem Zugang auf Stufe "Probe". Eine eigene Kandidatentabelle waere ein
zweiter Ort fuer dieselbe Frage.

DIE BRUECKE. Die Person sieht den Entwicklungs-Bereich NICHT -- eine
halb sichtbare Akte ist schlimmer als eine geschlossene, weil niemand
mehr weiss, was der andere gerade liest. Damit Anerkennung trotzdem
ankommt, laesst sich jeder Eintrag EINMAL als Nachricht in den Chat
schicken, mit Art, Titel und dem, was daraus folgen soll. Danach steht
im Eintrag, wann es geschehen ist: Die Frage "habe ich ihr das
eigentlich schon gesagt?" beantwortet man nach zwei Wochen falsch.

ZWEI KACHELN UND NICHT DREI: Aufgaben an das Team gibt es laengst, mit
Person, Frist und Status. Eine zweite Stelle dafuer waere ein zweiter
Ort, an dem man nachsehen muesste, welche Aufgabe wirklich gilt.

DIE GRUPPE HEISST NICHT "TEAM FUEHREN". Das waere der bequeme Name und
der falsche: Im Haus gilt, dass DogFather nicht ueber seinem Team steht.
"Entwicklung & Nachwuchs" sagt, was drinsteht.

=== KEIN NEUER BAUKASTEN ===

Beides sind BEREICHE wie das Ideen-Board: dieselbe Tabelle, dieselbe
Seite, dieselben Regeln fuers Anlegen, Aendern und Loeschen. Neu sind
eine Spalte (`gesendet_am`) und eine Route. Die Sendefunktion selbst
steht im Chat-Modul und nicht daneben: Ein Zweier-Gespraech darf es nur
einmal geben, die Leseraender muessen mitwandern, Live-Strom und
Benachrichtigung haengen daran -- wer das nachbaut, hat zwei Fassungen,
und die zweite ist die, in der jemand eine Nachricht nicht bekommt.

=== WAS DIE PRUEFUNG GEFUNDEN HAT ===

"Zugeordneter Creator existiert nicht" -- beim Anlegen eines Eintrags
ueber ein Teammitglied. Die Regel verlangte einen Creator; bei
Entwicklung geht es um Menschen aus dem Team. Die Meldung war dabei
selbst irrefuehrend: Die Person existiert sehr wohl, sie ist nur kein
Creator. Beides behoben.

Und meine EIGENE Pruefung von vor einer Stunde wurde rot: Sie verlangte,
dass jede Zusatzkachel in der Gruppe "Team Dogi" steht. Die naheliegende
Reparatur waere gewesen, die Gruppe gar nicht mehr zu pruefen -- das
haette die Aussage weggeworfen. Jetzt steht die erlaubte Liste
ausgeschrieben da: Eine Kachel fuer zwei Menschen darf nicht in einer
Gruppe landen, die alle sehen. Kommt eine dritte Gruppe, wird die Zeile
rot, und das ist Absicht.

Die zwei neuen Farbtoene trennen ueber Saettigung und Helligkeit statt
ueber den Farbwinkel -- bei 25 Toenen ist der Kreis voll, und die
groessten Luecken waeren ein drittes Gruen zwischen zwei vorhandenen.
Bei einer achtundzwanzigsten Kachel brauchen die Kacheln Gruppenfarben
statt Einzelfarben; das steht als Notiz im Quelltext.

pruef-entwicklung 33 (neu) · pruef-start-ansicht 27 Kacheln, sechs
Gruppen · pruef-rollen 278 -> 282 · pruef-modi-checkliste 70 ·
pruef-haus-trennung 62 · pruef-rueckmeldung 30 · pruef-modi-ideen 30 ·
pruef-modi-verborgen 80 · pruef-css-klassen gruen · pruef-modi-wortleck 5.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-11 01:11:30 +02:00

644 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ===================================================================
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. */
/* 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;
/* 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('.') : '—');
/* ---------- Agentur-Events ---------------------------------------------
Ein Event ist kein Eintrag mit Ueberschrift und Text, sondern eine
Aktion mit Zeitraum, Aufgaben und Regeln. Welche Art das ist, sagt
der Server (die Arten kommen aus workspace-bereiche.js) -- die
Kennung steht hier trotzdem woertlich, weil das Formular je Art
anders aussieht und es dafuer keine allgemeine Regel gibt. Sie
steht an EINER Stelle, damit sie sich nicht verteilt. */
const EVENT_BEREICH = 'agentur';
const EVENT_ART = 'kampagne';
const istEventArt = (art) => bereich === EVENT_BEREICH && art === EVENT_ART;
/* Laeuft das Event noch, kommt es erst, ist es vorbei?
Der Vergleich laeuft auf den ISO-Zeichenketten, nicht auf Date --
"2026-09-12" < "2026-09-30" ist als Text schon richtig, und ein
Date() haette hier eine Zeitzone erfunden, die niemand gemeint hat.
Ohne Ende gilt der Beginntag als der ganze Zeitraum. */
function eventStand(e) {
const heute = window.heuteLokal ? window.heuteLokal() : new Date().toISOString().slice(0, 10);
const von = (e.datum || '').slice(0, 10);
const bis = (e.event_ende || von).slice(0, 10);
if (!von) return null;
if (heute < von) return { schluessel: 'kommt', text: 'startet ' + datum(von) };
if (heute > bis) return { schluessel: 'vorbei', text: 'vorbei seit ' + datum(bis) };
return { schluessel: 'laeuft', text: von === bis ? 'heute' : 'läuft bis ' + datum(bis) };
}
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));
/* DIE VERTRAULICHE ZEILE SAGT ES SELBST (10.09.2026).
Sie kommt ohnehin nur bei DogFather und beim Schreiber an -- der
Server laesst sie sonst gar nicht heraus. Der Hinweis ist also
keine Sperre, sondern eine Auskunft: Ohne ihn saehe die eigene
Zeile aus wie jede andere, und beim naechsten Mal schriebe man
offen, was man vertraulich gemeint hat. */
/* AN DIE PERSON SCHICKEN (11.09.2026).
Nur im Entwicklungs-Bereich, und nur dort, wo ein Eintrag
überhaupt zu jemandem gehört. Er ist die einzige Brücke aus einer
Aufzeichnung, die die Person selbst nicht sieht -- und deshalb
ein ausdrücklicher Schritt und keine Automatik.
WURDE SCHON GESCHICKT? Dann steht dort ein Satz statt eines
Knopfs. Ein Knopf, der beim zweiten Drücken eine Absage bringt,
ist eine Falle; und zweimal dasselbe Lob ist kein doppeltes Lob,
sondern ein Versehen, das jeder bemerkt. */
if (bereich === 'entwicklung' && e.creator_id) {
if (e.gesendet_am) {
const hin = el('span', 'marke-art marke-art--weg',
`geschickt ${new Date(e.gesendet_am).toLocaleDateString('de-DE',
{ day: 'numeric', month: 'short' })}`);
hin.title = 'Diese Person hat den Eintrag als Nachricht bekommen.';
kopf.append(hin);
} else {
const b = el('button', 'eintrag-schicken', 'An die Person schicken');
b.type = 'button';
b.title = 'Schickt Art, Titel und Text als Nachricht in den Chat.';
b.addEventListener('click', async () => {
if (!confirm(`„${e.titel}“ als Nachricht schicken?\n\n`
+ 'Die Person bekommt Art, Titel und Text im Chat – einmalig.')) return;
b.disabled = true;
const a = await hole(`/workspace/api/bereich/entwicklung/${e.id}/senden`,
{ method: 'POST', headers: { 'Content-Type': 'application/json' } });
if (!a.ok) {
melde((await a.json().catch(() => ({}))).fehler || 'Ging nicht.');
b.disabled = false;
return;
}
await laden();
});
kopf.append(b);
}
}
if (e.nur_leitung) {
const v = el('span', 'marke-art marke-art--nur', 'nur DogFather');
v.title = 'Das liest ausser dir nur DogFather.';
kopf.append(v);
}
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);
/* ---- Agentur-Event: Zeitraum, Aufgaben, Regeln -------------------
Die Reihenfolge ist nicht beliebig. Wer auf ein Event schaut,
fragt in genau dieser Folge: Laeuft das ueberhaupt noch? Was
muss ich tun? Was gilt dabei? Steht der Zeitraum unten im Fuss
-- da, wo bei allen anderen Karten das Datum steht --, liest man
erst die Aufgaben und danach, dass es seit einer Woche vorbei
ist. */
const stand = istEventArt(e.art) ? eventStand(e) : null;
if (stand) {
k.dataset.event = stand.schluessel;
const zeile = el('p', 'event-zeitraum');
zeile.dataset.stand = stand.schluessel;
zeile.append(el('span', 'event-zeitraum__marke', stand.text));
zeile.append(el('span', 'event-zeitraum__spanne',
e.event_ende && e.event_ende !== e.datum
? `${datum(e.datum)} – ${datum(e.event_ende)}`
: datum(e.datum)));
k.append(zeile);
}
if (e.text) k.append(el('p', 'eintrag-text', e.text));
if (stand) {
for (const [feld, name] of [['event_aufgaben', 'Aufgaben'], ['event_regeln', 'Regeln']]) {
if (!e[feld]) continue;
const block = el('div', 'event-block');
block.append(el('h4', 'event-block__schild', name));
/* Zeilenumbrueche bleiben stehen (white-space: pre-line im CSS).
Eine Aufzaehlung, die zu einem Absatz zusammenlaeuft, ist
genau das, was diese beiden Felder verhindern sollen. */
block.append(el('p', 'event-block__text', e[feld]));
k.append(block);
}
}
const fuss = el('div', 'eintrag-fuss');
/* Beim Event steht der Zeitraum schon oben -- das Datum hier waere
dieselbe Zahl ein zweites Mal. */
if (!stand) 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 ---------------------------------------------- */
/* Das Formular sieht bei einem Agentur-Event anders aus als bei allem
anderen -- nicht "zusaetzlich", sondern anders: Aus "Überschrift"
wird der Titel des Events, aus "Text" die Beschreibung, aus dem
Datum ein Zeitraum, und Dringlichkeit hat hier nichts zu suchen
(ein Event ist nicht dringend, es hat einen Termin).
Umgeschaltet wird bei JEDEM Artwechsel und beim Oeffnen. Nur beim
Oeffnen zu schalten waere der bequeme Fehler: Man oeffnet mit
"Schulung", waehlt "Agentur-Events" und fuellt ein Formular aus,
das noch nach der alten Art fragt. */
function formularAnpassen() {
const event = istEventArt($('f-art').value);
$('feld-ende').hidden = !event;
$('feld-aufgaben').hidden = !event;
$('feld-regeln').hidden = !event;
if (einstellung?.dringlichkeit) $('feld-dringlich').hidden = event;
if (einstellung?.vertraulich) $('feld-vertraulich').hidden = event;
$('schild-datum').textContent = event ? 'Von' : 'Datum';
$('schild-titel').textContent = event ? 'Titel des Events' : 'Überschrift';
/* textContent wuerde das <span class="leise"> mit erschlagen --
deshalb nur der erste Textknoten, das kleine "(optional)" bleibt
stehen. */
const textSchild = $('schild-text').firstChild;
if (textSchild) textSchild.nodeValue = event ? 'Beschreibung ' : 'Text ';
/* Leeren, was gerade unsichtbar wird. Sonst reicht ein Wechsel von
"Agentur-Events" auf "Schulung", und das Ende des Zeitraums
faehrt unsichtbar mit -- der Server nimmt es an, weil es zum
Bereich gehoert, und an der Schulung stuende ein Enddatum, das
niemand eingetragen hat und niemand sieht. */
if (!event) {
$('f-ende').value = '';
$('f-aufgaben').value = '';
$('f-regeln').value = '';
}
}
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();
formularAnpassen();
umschalten(true);
});
$('neu-abbrechen').addEventListener('click', () => {
$('neu').reset(); $('neu-fehler').textContent = ''; formularAnpassen(); umschalten(false);
});
/* "change" und nicht "input": Die Auswahl wird ueber wahl.js zu einer
Knopfliste umgebaut, und die meldet ihre Wahl als change-Ereignis
am urspruenglichen <select> -- ohne dieses Ereignis merkte das
Formular vom Artwechsel nichts. */
$('f-art').addEventListener('change', formularAnpassen);
$('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,
};
const event = istEventArt(daten.art);
if (einstellung.dringlichkeit && !event) daten.dringlichkeit = $('f-dringlich').value;
if (einstellung.vertraulich) daten.nur_leitung = $('f-vertraulich').checked ? 1 : 0;
/* Verborgen heisst auch: nichts mitschicken. Ein Wert, der in einem
unsichtbaren Feld stehenbleibt, wandert sonst beim naechsten
Speichern mit -- und niemand sieht, woher er kommt. */
if (einstellung.einsatzFeld && $('f-einsatz')) daten.einsatz = $('f-einsatz').value.trim();
if (einstellung.bewertung) daten.bewertung = $('f-bewertung').value || null;
if (event) {
daten.event_ende = $('f-ende').value || null;
daten.event_aufgaben = $('f-aufgaben').value || null;
daten.event_regeln = $('f-regeln').value || null;
}
/* Im Agentur-Bereich gehoert ein Eintrag allen -- es gibt nichts
zuzuordnen, also wird auch nichts mitgeschickt. Der Server setzt
beides ohnehin auf leer (ohneCreatorBezug); hier zu schweigen
spart die stille Annahme, das Formular haette etwas zu sagen. */
if (LEITUNG.has(ich.rolle) && !einstellung.ohneCreatorBezug) {
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;
/* Das Wort ueber dem Titel. Es steht fest in bereich.html
("Betreuung") und stimmte fuer jede Akte -- nicht aber fuer eine
Sammelstelle, die niemandem gehoert. Bringt ein Bereich ein
eigenes mit, gilt seines; sonst bleibt es wie bisher. */
if (einstellung.ober && $('ober')) $('ober').textContent = einstellung.ober;
$('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;
/* Zwei verschiedene Saetze, weil es zwei verschiedene Wahrheiten
sind. In den Betreuungsbereichen sieht ein Creator, was ZU IHM
gehoert -- in der Agentur sieht er ALLES. Stuende hier weiter
"was zu dir gehört", laese sich eine vollstaendige Liste wie
ein Ausschnitt, und die naheliegende Frage waere: Was sehe ich
hier eigentlich nicht? */
$('unterzeile').textContent = (UNTERZEILE[bereich] || '')
+ (einstellung.fuerAlle
? ' Eingetragen wird von der Agentur – du siehst alles, was hier steht.'
: ' 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-vertraulich').hidden = !einstellung.vertraulich;
/* =================================================================
ZWEI ZAHLEN, ANDERE NAMEN (10.09.2026)
Bei den Angeboten heisst `bewertung` NUTZEN und `dringlichkeit`
AUFWAND. Die Felder gibt es laengst -- sie mit neuen Namen zu
belegen ist besser, als zwei weitere Spalten anzulegen, die
dasselbe koennen. Aber ein Feld, ueber dem "Dringlichkeit" steht
und das den Aufwand meint, ist falsch beschriftet, und das faellt
beim Ausfuellen niemandem auf -- erst beim Lesen.
Die Namen kommen aus den Bereichsangaben; steht dort keiner,
bleibt es wie bisher. */
if (einstellung.dringlichkeitName && $('dringlich-schild')) {
$('dringlich-schild').textContent = einstellung.dringlichkeitName;
}
if (einstellung.bewertungName && $('bewertung-schild')) {
$('bewertung-schild').textContent = `${einstellung.bewertungName} (1–5)`;
}
/* Das Einsatz-Feld nur, wo der Bereich es kennt. */
if ($('feld-einsatz')) $('feld-einsatz').hidden = !einstellung.einsatzFeld;
if (einstellung.einsatzName && $('einsatz-schild')) {
$('einsatz-schild').textContent = einstellung.einsatzName;
}
$('feld-bewertung').hidden = !einstellung.bewertung;
if (LEITUNG.has(ich.rolle)) {
$('feld-creator').hidden = false;
/* AGENTUR: EINE EINZIGE WAHL, UND DIE HEISST "AGENTUR" (07.09.2026).
Hier stand dieselbe Creator-Liste wie ueberall -- und sie war
eine Falle. Wer ein Event fuer alle anlegt und dabei aus
Gewohnheit einen Namen anklickt, hat es damit UNSICHTBAR
gemacht: fuer alle anderen Creator, ohne Warnung, ohne dass es
der Liste danach anzusehen waere.
Kein Feld auszublenden waere die halbe Loesung gewesen: Dann
fragt man sich, wem der Eintrag gehoert. Eine Liste mit genau
einer Wahl beantwortet die Frage, statt sie wegzunehmen.
Die Personenwahl (freier Name, Suchfeld) wird hier bewusst
NICHT aufgesetzt -- sie erlaubt getippte Namen, und genau das
soll es hier nicht geben. */
if (einstellung.ohneCreatorBezug) {
$('schild-creator').textContent = 'Gilt für';
$('f-creator').textContent = '';
const o = document.createElement('option');
o.value = ''; o.textContent = 'Agentur';
$('f-creator').append(o);
const hinweis = $('hinweis-creator');
hinweis.textContent = 'Agentur-Einträge gehören allen – jeder im Workspace sieht sie.';
hinweis.hidden = false;
} else {
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;
/* Bild ans <option> -- wahl.js zeichnet daraus das Gesicht. */
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." });
}
}
formularAnpassen();
/* 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();
})();
})();