Files
dogfather-universe/workspace/assets/js/vorlagenbrett.js
T
DogFatherGitandClaude Opus 5 84e4048ab4 Der Aufgabenkatalog zieht dorthin, wo verteilt wird
Filipe: "das soll auch bitte nicht in der kategorie aufgaben sondern
eure aufgaben sein bitte. setzt das perfekt da rein und pass es
uebeertrieben krass rein."

Und das ist richtig: "Aufgaben" zeigt, WAS liegt. Verteilt wird seit
dem 22.09. auf "Eure Aufgaben" -- dort wird die Person gewaehlt, dort
steht das Formular, dort ihre Aufgaben. Der Katalog ist nichts anderes
als ein zweiter Weg zu derselben Handlung: 101 fertige Aufgaben statt
einer selbst getippten.

WAS DER ERSTE ANLAUF ZERSTOERT HAETTE. Der Block enthaelt ZWEI
Kataloge, und sie gehen an zwei verschiedene Personenkreise -- das
entscheidet der Server. Team Dogi bekommt die 101 Aufgaben in 14
Kategorien, die Agentur den Creator-Katalog (vier Bereiche, vier
Stufen). Ihn einfach herauszuschneiden und drueben hinzulegen haette
dem zweiten Katalog die Heimat genommen. Gemerkt hat das keine
Ueberlegung, sondern die Frage, wer die Zeile "ich.rolle === 'creator'"
eigentlich bedient -- pruef-aufgaben-vorlagen misst sie seit Tagen.

Deshalb eine gemeinsame Datei (vorlagenbrett.js) statt zweier
Abschriften: Die gemeinsamen Teile -- Abruf, Klappkopf, Uebernehmen --
gibt es weiter genau einmal, und jede Seite sagt beim Einrichten, ob
sie den Team- oder den Creator-Zweig zeigt. Fehlt ihr dabei eine
Angabe, sagt die Datei das in der Konsole, statt stumm nichts zu
zeichnen.

WAS DER UMZUG NEBENBEI LOESCHT: Drueben brauchte der Katalog eine
EIGENE Personenwahl, weil es dort keine gab -- eine dritte Knopfreihe
unter zwei anderen, und die Moeglichkeit, oben den einen und unten den
anderen zu waehlen. Hier ist die Person laengst gewaehlt, mitsamt ihren
Zahlen. Eine Auswahl statt zwei.

DREI DINGE, DIE ERST DADURCH AUFFIELEN:

"schon uebernommen" galt im Team-Katalog fuer JEDEN. Sobald irgendwer
eine Vorlage hatte, stand es an der Karte -- auch fuer alle anderen.
Auf einer Seite ohne Personenwahl fiel das kaum auf; hier waere es
offen falsch: Man waehlt Frida, und der Katalog behauptet, sie habe die
Aufgabe schon, weil Rieke sie hat. Der Creator-Zweig machte es von
Anfang an richtig. Und wer verteilt, bekommt ohne gewaehlte Person gar
keine Markierung mehr: "irgendwer hat sie" liest man als "brauche ich
nicht mehr zu vergeben" und ueberspringt, was dem Menschen vor einem
fehlt.

Der Katalog blieb fuer einen Modi GANZ weg. window.__ich kommt ueber
das Netz und ist beim ersten Zeichnen noch nicht da; ein stummes
"return" liess den Block dauerhaft verschwinden, weil niemand ein
zweites Mal zeichnet. Gefunden hat das kein Codelesen, sondern ein
Bildschirmfoto -- die Seite sah vollstaendig aus, nur ohne den Block.
Gewartet wird jetzt mit der Wartestelle des Hauses.

Und er markierte bei einem Modi nichts mehr: Die Aufgabenliste wurde
nur beim Personenwechsel geholt, und ein Modi waehlt nie jemanden.
Damit war die Sperre gegen das zweite Uebernehmen derselben Vorlage
weg. Jetzt gibt es einen Abrufweg fuer beide.

Der Knopf nennt den Namen ("An Rieke"), der Satz darueber auch. Statt
einer Wegbeschreibung zur Personenwahl steht ein Knopf, der hinfuehrt
-- "waehle oben" waere falsch, die Kacheln stehen weiter unten. Und
"auf dem Brett darunter" stimmt hier nicht mehr: Der Kopftext sagt
jetzt, WAS entsteht, nicht WO es landet.

Gemessen: pruef-modi-katalog 129 (vorher 125), pruef-modi-kategorien
29 (vorher 25 mit 3 Fehlern), pruef-aufgaben-vorlagen, pruef-struktur
und pruef-css-klassen alles in Ordnung. Dazu eine Abnahme ueber beide
Seiten und drei Rollen: 32 Messungen, 0 Befunde -- darunter die
Gegenprobe, dass Marinas uebernommene Aufgabe bei Frida NICHT als
uebernommen gilt.

Die 403-Zeilen in pruef-modi-kategorien waren ebenfalls eine Altlast:
Seit dem 22.09. legt im Team Dogi nur die Leitung an ("sie sich nicht
selber aufgaben geben"), die Pruefung tat es weiter als Modi. Sie misst
das jetzt -- samt der Gegenprobe, die es vorher nicht gab.

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

1110 lines
50 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.
/* =======================================================================
AUFGABEN AUS DER VORLAGE -- fuer beide Seiten, die sie brauchen
=======================================================================
WARUM DAS EINE EIGENE DATEI IST (23.09.2026).
Filipe: "das soll auch bitte nicht in der kategorie aufgaben sondern
eure aufgaben sein bitte. setzt das perfekt da rein und pass es
uebeertrieben krass rein."
Hier stecken ZWEI Kataloge, und sie gehen an zwei verschiedene
Personenkreise -- das entscheidet der Server (/workspace/api/vorlagen):
* der TEAM-Katalog: 101 fertige Aufgaben in 14 Kategorien.
Er zieht auf "Eure Aufgaben" (entwicklung.html) um, denn dort
wird verteilt: dort steht die Personenwahl, dort das Formular,
dort die Liste dessen, was bei ihr liegt.
* der CREATOR-Katalog: vier Bereiche, vier Stufen.
Er bleibt auf "Aufgaben" (aufgaben.html), wo ein Betreuer einen
Creator auswaehlt.
Der erste Anlauf schnitt schlicht den ganzen Block heraus und legte
ihn drueben wieder hin. Das haette dem zweiten Katalog die Heimat
genommen -- und gemerkt hat es keine Ueberlegung, sondern die Frage,
wer die Zeile "ich.rolle === 'creator'" eigentlich bedient.
pruef-aufgaben-vorlagen misst sie, Zeile 285.
Die Alternative waere gewesen, die gemeinsamen Teile -- Abruf,
Klappkopf, Uebernehmen -- zweimal hinzuschreiben. Zwei Abschriften
derselben Sache laufen auseinander, sobald jemand nur eine davon
anfasst; dafuer gibt es in diesem Haus genug Beispiele. Also EINE
Datei, und jede Seite sagt beim Einrichten, welchen Zweig sie zeigt.
WAS DIE SEITE MITBRINGEN MUSS, steht in "einrichten" -- und fehlt
etwas davon, sagt die Datei das in der Konsole, statt stumm nichts
zu zeichnen. Ein Block, der ohne Grund leer bleibt, sieht aus wie
ein kaputter Server.
======================================================================= */
(() => {
'use strict';
const $ = (id) => document.getElementById(id);
const el = (tag, klasse, text) => {
const k = document.createElement(tag);
if (klasse) k.className = klasse;
if (text !== undefined) k.textContent = text;
return k;
};
/* WAS DIE SEITE MITBRINGT. Bis "einrichten" gelaufen ist, gibt es
hier nichts -- und "zeigen" sagt das dann auch. */
let U = null;
/* Der Fehlertext des Hauses (meldung.js). Ausdruecklich ueber
"window" und mit Rueckfall: Wuerde die Datei ohne meldung.js
geladen, waere "sagWas(...)" ein ReferenceError -- und der traete
ausgerechnet dann auf, wenn ohnehin schon etwas schiefgegangen
ist. Eine Fehlerbehandlung, die selbst abstuerzt, verschluckt den
eigentlichen Fehler. */
const sagWas = (fehler, ersatz) => (typeof window.sagWas === 'function'
? window.sagWas(fehler, ersatz)
: (ersatz || 'Das hat nicht geklappt.'));
/* DIE PFLICHTANGABEN als Liste und nicht als Reihe von if-Abfragen:
Eine Liste laesst sich nicht halb vergessen. */
const NOETIG = ['zweig', 'hole', 'melde', 'ich', 'aufgaben', 'ziel',
'zielName', 'statusName', 'nachAenderung'];
/* ---- Der Satz "an wen geht das hier" --------------------------------
OHNE RICHTUNGSANGABE. Im Entwurf stand "Waehle oben jemanden aus"
-- auf "Eure Aufgaben" steht die Personenwahl aber WEITER UNTEN.
Eine Wegbeschreibung, die nicht stimmt, ist schlimmer als keine:
Man sucht an der falschen Stelle und haelt die Seite fuer kaputt.
Genau dieser Fehler stand bis zum 22.09. am Verteil-Knopf
("Waehl links jemanden aus", und die Auswahl lag 206 px darunter).
Statt einer Richtung ein KNOPF, der hinfuehrt. Der weiss immer, wo
die Wahl steht, auch wenn die Seite spaeter umgebaut wird. */
function zielSatz(koerper, name) {
if (name) {
koerper.append(el('p', 'stufenleiter__hilfe',
'Was du hier übernimmst, geht an ' + name + '.'));
return;
}
const zeile = el('p', 'stufenleiter__hilfe',
'Noch niemand gewählt – such dir jemanden aus, dann kannst du hier '
+ 'etwas für ihn übernehmen. ');
const wahl = $('personen');
if (wahl) {
const hin = el('button', 'knopf-still', 'Zur Personenwahl');
hin.type = 'button';
hin.addEventListener('click', () => {
wahl.scrollIntoView({ behavior: 'smooth', block: 'center' });
window.zielMarkieren?.([wahl], 'hier wählst du');
});
zeile.append(hin);
}
koerper.append(zeile);
}
/* =====================================================================
DIE KATALOGE
Vier Bereiche, vier Stufen, drei Aufgaben je Feld. Der Block ist
bewusst KEINE zweite Aufgabenverwaltung: Er legt eine ganz normale
Aufgabe an, mit Frist und Verantwortlichem, und verschwindet danach
aus dem Weg. Alles Weitere macht das Brett darunter.
WARUM DIE BEIDEN REIHEN DIESELBEN KNOEPFE SIND WIE IN DER
CHECKLISTE: Es sind dieselben vier Bereiche und dieselben vier
Stufen. Zwei verschiedene Darstellungen fuer dieselbe Einteilung
waeren zwei Sachen zum Lernen statt einer.
OHNE CREATOR KEINE AUFGABE. Ein Creator legt sie fuer sich an, ein
Betreuer fuer den, den er oben gewaehlt hat. Ist keiner gewaehlt,
steht das da -- statt eines Knopfes, der beim Druecken einen Fehler
zeigt.
===================================================================== */
const BEREICHE_VORLAGE = [
{ schluessel: 'live', name: 'LIVE-Analyse' },
{ schluessel: 'content', name: 'Content-Ideen' },
{ schluessel: 'technik', name: 'Technik' },
{ schluessel: 'community', name: 'Community' },
];
const STUFEN_VORLAGE = [
{ schluessel: 'anfaenger', name: 'Anfänger',
hilfe: 'Was von Anfang an sitzen muss. Meist an einem Abend zu schaffen.' },
{ schluessel: 'fort', name: 'Fortgeschritten',
hilfe: 'Aus Zufall wird Routine. Braucht Wiederholung, also Wochen.' },
{ schluessel: 'profi', name: 'Profi',
hilfe: 'Gemessen statt geschätzt: ein Wert, ein Versuch, ein Beleg.' },
{ schluessel: 'meister', name: 'Meister',
hilfe: 'Was auch ohne dich weiterläuft — Vorlage, Vertretung, Ablage.' },
];
let vorlagen = null;
/* Der zweite Katalog -- welcher es ist, sagt der Server. Hier steht
bewusst nur "katalog": Im Browser darf nichts stehen, woraus sich
schliessen laesst, fuer wen er gedacht ist. */
let katalog = null;
let kGruppe = null;
/* WELCHE STUFE. `null` heisst alle -- und das ist die Vorgabe:
Wer eine Kategorie aufmacht, will erst sehen, was es gibt. */
let kStufe = null;
/* Getrennt vom Inhalt, weil "nichts bekommen" ein gueltiges Ergebnis
ist. Ohne dieses Merkmal fragte die Seite bei jedem Neuzeichnen
erneut nach -- und bekaeme jedes Mal wieder nichts. */
let vorlagenGeladen = false;
/* ==== BEWERBUNGEN AUF EINE VORLAGE (23.09.2026) ====================
Filipe: „die modis sollen bei all diesen voschlägen auch nur
bewerben können ... und nur dogfather und die rechte hand sollen
annehmen oder ablehnen können, mit einem text als notiz."
WER WAS DARF, SAGT DER SERVER. Hier steht kein Rollenname -- diese
Datei bekommt jeder, der die Seite öffnet, und genau daran ist
dieses Brett am 17.09. schon einmal gescheitert. */
let bewerbungen = [];
let darfEntscheidenV = false;
let darfVerteilenV = false;
let vBereich = 'live';
let vStufe = 'anfaenger';
/* ---------- DAS VORLAGENBRETT IST ZUGEKLAPPT (09.09.2026) ------------
Filipe: "ich will nur dass du dich informierst und schaust was man
vielleicht noch besser machen koennte von der benutzung ... ich red
von den aufgaben. das system von den aufgaben, die bedienung."
Gemessen auf dem Handy: Der Vorlagenblock fuellte anderthalb
Bildschirme, BEVOR die erste eigene Aufgabe kam. Nach dem Ausbau
von Profi und Meister am selben Tag ist er noch laenger geworden.
Das ist die falsche Reihenfolge: Vorlagen holt man selten, das
Brett benutzt man jeden Tag. Der Block bleibt an seiner Stelle --
dort gehoert er hin, weil daraus Aufgaben auf das Brett darunter
wandern --, aber er ist jetzt zu. Wer ihn aufmacht, findet ihn
beim naechsten Mal wieder offen: Wer Vorlagen holt, holt meistens
mehrere. */
/* JE ZWEIG EIN EIGENES GEDAECHTNIS (23.09.2026).
Vorher EIN Schluessel fuer beide Kataloge -- das ging, solange
beide auf derselben Seite standen und einander ausschlossen. Seit
der Team-Katalog auf „Eure Aufgaben" steht, waeren es zwei Bloecke
auf zwei Seiten mit einem gemeinsamen Merker: Wer hier zuklappt,
faende drueben alles zu und wuesste nicht, warum.
Gelesen wird ERST, wenn die Seite gesagt hat, welcher Zweig sie
ist -- beim Laden der Datei steht das noch nicht fest. */
const VORLAGEN_OFFEN = () => 'dfw_vorlagenbrett_offen'
+ (U.zweig === 'team' ? '_team' : '');
let vOffen = null;
const klappStand = () => {
if (vOffen !== null) return vOffen;
try { vOffen = localStorage.getItem(VORLAGEN_OFFEN()) === 'ja'; }
catch { vOffen = false; }
return vOffen;
};
/** Den Katalog holen -- und mit ihm die Bewerbungen.
*
* ALS EIGENE FUNKTION, weil sie zweimal gebraucht wird: beim ersten
* Aufbau und nach JEDER Entscheidung. Ohne das zweite bliebe eine
* beantwortete Bewerbung stehen, bis jemand die Seite neu lädt --
* und zwei Leute würden über dieselbe Bitte zweimal entscheiden.
*
* DIE BEWERBUNGEN KOMMEN IN DERSELBEN ANTWORT wie der Katalog. Eine
* zweite Abfrage daneben wäre ein zweiter Weg, auf dem die beiden
* auseinanderlaufen können. */
async function vorlagenHolen() {
try {
const a = await U.hole('/workspace/api/vorlagen');
if (!a.ok) return false;
const d = await a.json();
vorlagen = d.aufgaben || null;
/* Ein Katalog mit null Eintraegen ist keiner. Ohne diese Pruefung
haette ein leeres `{gruppen:[],aufgaben:[]}` einen Block mit
Ueberschrift und nichts darin ergeben -- das sieht aus wie ein
Fehler, und es waere auch einer. */
katalog = d.katalog && Array.isArray(d.katalog.aufgaben) && d.katalog.aufgaben.length
? d.katalog : null;
bewerbungen = Array.isArray(d.bewerbungen) ? d.bewerbungen : [];
darfEntscheidenV = !!d.darf_entscheiden;
darfVerteilenV = !!d.darf_verteilen;
vorlagenGeladen = true;
return true;
} catch { return false; }
}
async function vorlagenbrettZeigen() {
const ziel = $('vorlagenbrett');
if (!ziel) return;
/* WER BIN ICH? DIE ANTWORT KOMMT SPAETER ALS DIESE SEITE.
Hier stand ein stummes "|| !U.ich()) return". Auf "Aufgaben"
war das folgenlos: Dort holt die Seite ihre Auskunft selbst und
zeichnet den Katalog erst danach. Auf "Eure Aufgaben" kommt sie
ueber kopf.js aus dem Netz -- beim ersten Zeichnen steht sie
noch nicht da, und weil niemand ein zweites Mal zeichnet, blieb
der Katalog fuer einen Modi DAUERHAFT weg.
Gefunden hat das kein Codelesen, sondern ein Bildschirmfoto:
Die Seite sah vollstaendig aus, nur ohne den Block. Genau das
ist die Sorte Fehler, die ein stummes "return" erzeugt -- es
gibt nichts zu sehen, was falsch waere.
Gewartet wird mit der Wartestelle des Hauses (kopf.js), nicht
mit einer geratenen Zahl Millisekunden. Sie gibt nach laengstens
drei Sekunden auf; danach wird hier NICHT weiter versucht, also
kann daraus keine Schleife werden. */
if (!U.ich()) {
if (typeof window.wennIchDaBin === 'function') {
await window.wennIchDaBin();
if (U.ich()) return vorlagenbrettZeigen();
}
return;
}
/* WER DIESEN KATALOG BEKOMMT, ENTSCHEIDET DER SERVER.
Hier stand kurzzeitig ein Vergleich auf einen Rollennamen -- in
einer Datei, die JEDER bekommt, der die Seite oeffnet. Jetzt
liefert /workspace/api/vorlagen fuer die Betroffenen schlicht
nichts, und `if (!vorlagen) return` weiter unten laesst das Brett
verborgen. Dieselbe Wirkung, ohne eine Zeile, die verraet, fuer
wen sie gilt. */
if (!vorlagenGeladen) {
if (!(await vorlagenHolen())) return;
}
/* ZWEI KATALOGE, ZWEI SEITEN (23.09.2026).
Bis heute entschied allein der Server, welcher der beiden
Kataloge kommt -- sie schlossen einander aus, und beide standen
auf „Aufgaben".
Filipe: „das soll auch bitte nicht in der kategorie aufgaben
sondern eure aufgaben sein bitte."
Seither entscheidet ZUSAETZLICH die Seite, welchen Zweig sie
zeigt. „Aufgaben" zeigt, was liegt; verteilt wird auf „Eure
Aufgaben" -- dort steht die Personenwahl, dort das Formular,
dort ihre Aufgaben. Ein Katalog ist nichts anderes als ein
zweiter Weg zu derselben Handlung, also gehoert er dorthin.
Der Server bleibt die Schranke: Wer einen Katalog nicht
bekommen darf, bekommt ihn auch dann nicht, wenn die Seite ihn
anfordert. Die Seite waehlt nur aus, was SIE zeigen will. */
if (U.zweig === 'team') {
if (!katalog) { ziel.hidden = true; return; }
katalogBrett(ziel); return;
}
if (!vorlagen) { ziel.hidden = true; return; }
ziel.hidden = false;
ziel.textContent = '';
/* DER KOPF IST DER KNOPF. Ein eigener kleiner Schalter daneben waere
ein zweites Ziel fuer dieselbe Absicht -- und ein kleineres. */
const kopf = el('button', 'vorlagenbrett__kopf');
kopf.type = 'button';
kopf.setAttribute('aria-expanded', String(klappStand()));
kopf.setAttribute('aria-controls', 'vorlagenbrett-koerper');
const kText = el('span', 'vorlagenbrett__kopftext');
kText.append(el('span', 'feldschild', 'Aufgaben aus der Vorlage'));
/* Zugeklappt zaehlt nicht die Erklaerung, sondern WIE VIEL darin
liegt -- sonst sieht der Block aus wie eine Ueberschrift ohne
Inhalt und niemand macht ihn je auf. */
const gesamt = Object.values(vorlagen)
.reduce((n, b) => n + Object.values(b).reduce((m, l) => m + l.length, 0), 0);
kText.append(el('span', 'vorlagenbrett__unter', klappStand()
? 'Vier Bereiche, vier Stufen. Übernommen wird als ganz normale Aufgabe '
+ '– mit Frist, veränderbar, auf dem Brett darunter.'
: `${gesamt} fertige Aufgaben in vier Bereichen und vier Stufen – zum Übernehmen.`));
kopf.append(kText);
const pfeil = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
pfeil.setAttribute('viewBox', '0 0 24 24');
pfeil.setAttribute('class', 'klapp-pfeil');
pfeil.setAttribute('aria-hidden', 'true');
const pfad = document.createElementNS('http://www.w3.org/2000/svg', 'path');
pfad.setAttribute('d', 'm6 9.5 6 6 6-6');
pfeil.append(pfad);
kopf.append(pfeil);
kopf.addEventListener('click', () => {
vOffen = !klappStand();
try { localStorage.setItem(VORLAGEN_OFFEN(), vOffen ? 'ja' : 'nein'); } catch { /* egal */ }
vorlagenbrettZeigen();
});
ziel.append(kopf);
ziel.dataset.auf = klappStand() ? 'ja' : 'nein';
/* Zugeklappt wird der Rest gar nicht erst gebaut. Ihn nur zu
verstecken hiesse, bei jedem Neuzeichnen achtzig Karten
anzulegen, die niemand sieht. */
if (!klappStand()) return;
const koerper = el('div', 'vorlagenbrett__koerper');
koerper.id = 'vorlagenbrett-koerper';
ziel.append(koerper);
/* Reihe 1: der Bereich. */
const r1 = el('div', 'stufenleiter');
r1.setAttribute('role', 'group');
r1.setAttribute('aria-label', 'Bereich wählen');
for (const b of BEREICHE_VORLAGE) {
const k = el('button', 'stufenleiter__knopf');
k.type = 'button';
k.dataset.stufe = b.schluessel;
k.setAttribute('aria-pressed', String(vBereich === b.schluessel));
k.append(el('span', 'stufenleiter__name', b.name));
k.addEventListener('click', () => { vBereich = b.schluessel; vorlagenbrettZeigen(); });
r1.append(k);
}
koerper.append(r1);
/* Reihe 2: die Stufe, mit der Zahl der Aufgaben darin. */
const r2 = el('div', 'stufenleiter');
r2.setAttribute('role', 'group');
r2.setAttribute('aria-label', 'Könnensstufe wählen');
for (const st of STUFEN_VORLAGE) {
const n = ((vorlagen[vBereich] || {})[st.schluessel] || []).length;
if (!n) continue;
const k = el('button', 'stufenleiter__knopf');
k.type = 'button';
k.dataset.stufe = st.schluessel;
k.setAttribute('aria-pressed', String(vStufe === st.schluessel));
k.title = st.hilfe;
k.append(el('span', 'stufenleiter__name', st.name));
k.append(el('span', 'stufenleiter__zahl', String(n)));
k.addEventListener('click', () => { vStufe = st.schluessel; vorlagenbrettZeigen(); });
r2.append(k);
}
koerper.append(r2);
const hilfe = STUFEN_VORLAGE.find((x) => x.schluessel === vStufe)?.hilfe;
if (hilfe) koerper.append(el('p', 'stufenleiter__hilfe', hilfe));
/* Für wen? */
const fuer = U.ziel();
if (!fuer) {
koerper.append(el('p', 'vorlagenbrett__hinweis',
'Wähle oben einen Creator, dann lassen sich diese Aufgaben übernehmen.'));
}
/* WAS SCHON GEHOLT WURDE, STEHT DA (09.09.2026).
Vorher konnte man dieselbe Vorlage zweimal uebernehmen und
bekam die Aufgabe zweimal auf das Brett -- ohne jeden Hinweis.
Bei achtzig Vorlagen ueber vier Stufen weiss niemand auswendig,
was er letzten Monat schon geholt hat.
Verglichen wird ueber die KENNUNG der Vorlage, nicht ueber den
Titel: Ein Titel darf geaendert werden, und dann waere der
Vergleich still falsch. Die Kennung liegt seit heute an der
Aufgabe (Spalte `vorlage`).
Es braucht dafuer keine zusaetzliche Abfrage -- die Aufgaben
liegen ohnehin schon im Browser. */
const schonDa = new Map();
for (const a of U.aufgaben()) {
if (!a.vorlage) continue;
if (fuer && a.creator_id !== fuer) continue;
schonDa.set(a.vorlage, a);
}
const liste = (vorlagen[vBereich] || {})[vStufe] || [];
const box = el('div', 'vorlagenbrett__liste');
for (const [nr, v] of liste.entries()) {
const karte = el('div', 'vorlagenbrett__karte');
const schon = schonDa.get(v.schluessel);
if (schon) karte.dataset.schon = 'ja';
karte.append(el('h3', 'vorlagenbrett__titel', v.titel));
karte.append(el('p', 'vorlagenbrett__text', v.text));
const fuss = el('div', 'vorlagenbrett__fuss');
fuss.append(el('span', 'vorlagenbrett__frist',
v.tage === 1 ? 'Frist: morgen' : `Frist: in ${v.tage} Tagen`));
if (schon) {
/* Der Zustand steht dabei, nicht nur die Tatsache. "Schon
uebernommen" allein liesse offen, ob sie noch offen ist oder
laengst abgehakt -- und danach entscheidet man, ob man sie
ein zweites Mal holt. */
fuss.append(el('span', 'vorlagenbrett__schon',
`schon übernommen · ${U.statusName[schon.status] || schon.status}`));
}
if (fuer) {
const k = el('button', 'knopf knopf--klein', schon ? 'Nochmal' : 'Übernehmen');
k.type = 'button';
if (schon) k.title = 'Diese Vorlage liegt schon auf dem Brett – noch einmal übernehmen?';
k.addEventListener('click', () => uebernehmen(k, { art: 'aufgabe', nr }, fuer));
fuss.append(k);
}
karte.append(fuss);
box.append(karte);
}
/* Und wie viele der Stufe noch offen sind -- die Zahl, nach der man
eigentlich sucht, wenn man den Block aufmacht. */
if (liste.length) {
const offen = liste.filter((v) => !schonDa.has(v.schluessel)).length;
koerper.append(el('p', 'vorlagenbrett__stand', offen
? `${offen} von ${liste.length} noch nicht übernommen.`
: `Alle ${liste.length} dieser Stufe sind übernommen.`));
}
koerper.append(box);
if (fuer && liste.length > 1) {
const alle = el('button', 'abmelden', `Alle ${liste.length} übernehmen`);
alle.title = 'Legt alle Aufgaben dieser Stufe an – auch die, die schon auf dem Brett liegen.';
alle.type = 'button';
alle.style.marginTop = '12px';
alle.addEventListener('click', () => uebernehmen(alle, { art: 'aufgaben_alle' }, fuer));
koerper.append(alle);
}
}
/* =====================================================================
DAS ZWEITE VORLAGENBRETT (10.09.2026)
Derselbe Block, derselbe Kopf, dieselben Karten -- nur eine andere
Einteilung: neun Etappen statt vier Bereichen mal vier Stufen. Die
Bausteine sind bewusst dieselben (vorlagenbrett__karte,
stufenleiter, ...): Ein zweites Aussehen fuer dieselbe Sache waere
zwei Dinge zum Lernen statt einem -- und eine zweite Stelle, an der
jemand eine neue CSS-Klasse in sieben Kopien pflegen muesste.
WAS DRINSTEHT, KOMMT VOM SERVER, samt Gruppennamen und Texten. In
dieser Datei steht kein Wort darueber, fuer wen dieser Katalog ist.
===================================================================== */
function katalogBrett(ziel) {
ziel.hidden = false;
ziel.textContent = '';
if (!kGruppe) kGruppe = (katalog.gruppen[0] || {}).schluessel || null;
const kopf = el('button', 'vorlagenbrett__kopf');
kopf.type = 'button';
kopf.setAttribute('aria-expanded', String(klappStand()));
kopf.setAttribute('aria-controls', 'vorlagenbrett-koerper');
const kText = el('span', 'vorlagenbrett__kopftext');
kText.append(el('span', 'feldschild', 'Aufgaben aus der Vorlage'));
/* DER SATZ MUSS SAGEN, WONACH SORTIERT IST. Bis zum 16.09.2026 stand
hier "Nach Etappen sortiert" -- und seit dem Umbau auf Kategorie
mal Stufe war das schlicht falsch. Ein Satz, der etwas anderes
beschreibt als das, was darunter steht, kostet mehr Zeit als er
spart. */
/* "AUF DEM BRETT DARUNTER" STIMMTE BIS ZUM 23.09.2026.
Solange der Katalog auf "Aufgaben" stand, lag darunter wirklich
das Brett. Auf "Eure Aufgaben" liegt darunter die Aufgabenliste
der gewaehlten Person -- und wer dem Satz folgt, sucht ein
Brett, das es auf dieser Seite nicht gibt. Dieselbe Sorte
Fehler wie "Waehl links jemanden aus", als die Auswahl
darunter stand.
Der Satz sagt jetzt, WAS entsteht, nicht WO es landet. Das
gilt auf beiden Seiten und ueberlebt den naechsten Umbau. */
kText.append(el('span', 'vorlagenbrett__unter', klappStand()
? 'Nach Kategorie und Stufe sortiert. Daraus wird eine ganz normale '
+ 'Aufgabe – mit Frist und Kategorie, jederzeit veränderbar.'
: `${katalog.aufgaben.length} fertige Aufgaben in `
+ `${katalog.gruppen.length} Kategorien – zum Übernehmen.`));
kopf.append(kText);
const pfeil = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
pfeil.setAttribute('viewBox', '0 0 24 24');
pfeil.setAttribute('class', 'klapp-pfeil');
pfeil.setAttribute('aria-hidden', 'true');
const pfad = document.createElementNS('http://www.w3.org/2000/svg', 'path');
pfad.setAttribute('d', 'm6 9.5 6 6 6-6');
pfeil.append(pfad);
kopf.append(pfeil);
kopf.addEventListener('click', () => {
vOffen = !klappStand();
try { localStorage.setItem(VORLAGEN_OFFEN(), vOffen ? 'ja' : 'nein'); } catch { /* egal */ }
vorlagenbrettZeigen();
});
ziel.append(kopf);
ziel.dataset.auf = klappStand() ? 'ja' : 'nein';
/* Zugeklappt wird der Rest gar nicht erst gebaut -- sonst lägen bei
jedem Neuzeichnen sechzig Karten da, die niemand sieht. */
if (!klappStand()) return;
const koerper = el('div', 'vorlagenbrett__koerper');
koerper.id = 'vorlagenbrett-koerper';
ziel.append(koerper);
/* =====================================================================
WAS WARTET -- QUER ÜBER ALLE KATEGORIEN (23.09.2026)
=====================================================================
DAS HIER SCHLIESST EIN LOCH, das ich selbst gebaut habe. Die
Bewerbungen stehen an ihrer Karte, und das ist richtig: Man
entscheidet über eine bestimmte Aufgabe, und ihr Text ist die
halbe Auskunft.
Nur steht die Karte in EINER von vierzehn Kategorien. Bewirbt
sich Frida unter „Wachstum" und DogFather öffnet wie immer
„Chat-Moderation", sieht er nichts — die Bewerbung ist da, das
Brett ist offen, und trotzdem findet er sie nie.
Die Benachrichtigung hilft, aber sie ist ein Moment: Wer sie
wegwischt, hat keinen zweiten Weg mehr. Ein Brett, auf dem
etwas wartet, muss das selbst sagen können.
ES DIENT BEIDEN SEITEN, und deshalb steht es nur einmal hier:
Wer entscheidet, liest „wartet auf deine Antwort"; wer sich
beworben hat, liest „du hast dich beworben". Der Server schickt
ohnehin jedem nur, was ihn angeht (vorlagenBewerbungenFuer).
DER SPRUNG STELLT AUCH DIE STUFE ZURÜCK. Ohne das landet man in
der richtigen Kategorie und sieht trotzdem nichts, weil der
Stufenfilter die Karte gerade ausblendet — eine Reise ins
Nichts ist schlimmer als kein Knopf. */
if (bewerbungen.length) {
const band = el('div', 'vorlagenbrett__warten');
const fuerMich = darfEntscheidenV;
band.append(el('p', 'vorlagenbrett__warten-schild',
fuerMich
? (bewerbungen.length === 1
? 'Eine Bewerbung wartet auf deine Antwort:'
: `${bewerbungen.length} Bewerbungen warten auf deine Antwort:`)
: (bewerbungen.length === 1
? 'Du hast dich beworben — hier steht sie:'
: `Du hast dich ${bewerbungen.length}-mal beworben:`)));
const reihe = el('div', 'vorlagenbrett__warten-reihe');
for (const b2 of bewerbungen) {
const v2 = katalog.aufgaben.find((x) => x.schluessel === b2.vorlage);
const k2 = el('button', 'vorlagenbrett__warten-knopf');
k2.type = 'button';
/* WER UND WAS -- in dieser Reihenfolge. Bei drei Bitten
untereinander will man zuerst wissen, von wem. Steht man
selbst davor, ist der Name überflüssig: Man weiß, dass man
es war. */
if (fuerMich) k2.append(el('span', 'vorlagenbrett__warten-wer', b2.person_name));
k2.append(el('span', 'vorlagenbrett__warten-was', v2?.titel || b2.vorlage));
k2.addEventListener('click', () => {
if (v2) { kGruppe = v2.gruppe; kStufe = null; }
vorlagenbrettZeigen().then(() => {
const ziel = document.querySelector(
`.vorlagenbrett__karte[data-vorlage="${b2.vorlage}"]`);
if (!ziel) return;
ziel.scrollIntoView({ block: 'center', behavior: window.sanft?.() || 'auto' });
/* KURZ HERVORHEBEN. Wer auf einem Brett mit zwölf Karten
landet, sucht sonst die, wegen der er hergekommen ist. */
ziel.dataset.hervor = 'ja';
setTimeout(() => { delete ziel.dataset.hervor; }, 2400);
});
});
reihe.append(k2);
}
band.append(reihe);
koerper.append(band);
}
/* Die Etappen als Reihe -- mit der Zahl darin, damit man sieht, wo
etwas liegt, bevor man klickt. */
const reihe = el('div', 'stufenleiter');
reihe.setAttribute('role', 'group');
reihe.setAttribute('aria-label', 'Kategorie wählen');
for (const g of katalog.gruppen) {
const n = katalog.aufgaben.filter((v) => v.gruppe === g.schluessel).length;
if (!n) continue;
const k = el('button', 'stufenleiter__knopf');
k.type = 'button';
k.dataset.stufe = g.schluessel;
k.setAttribute('aria-pressed', String(kGruppe === g.schluessel));
k.append(el('span', 'stufenleiter__name', g.name));
k.append(el('span', 'stufenleiter__zahl', String(n)));
k.addEventListener('click', () => { kGruppe = g.schluessel; vorlagenbrettZeigen(); });
reihe.append(k);
}
koerper.append(reihe);
/* DERSELBE BAUSTEIN WIE BEIM ANDEREN BRETT (16.09.2026).
Filipe: "ich will dass es vom aussehen auch genau gleich
aussieht und so geil wie bei den anderen kategorien."
Hier stand `vorlagenbrett__stand` -- das ist die Zeile fuer
ZAHLEN ("3 von 11 noch nicht uebernommen"), kleiner und mit
Ziffern in Tabellenbreite. Die Erklaerung unter der Auswahl
heisst drueben `stufenleiter__hilfe` und sieht anders aus.
Zwei Bretter, die dasselbe tun und verschieden aussehen, wirken
wie zwei verschiedene Sachen. */
const gruppe = katalog.gruppen.find((g) => g.schluessel === kGruppe);
if (gruppe?.text) koerper.append(el('p', 'stufenleiter__hilfe', gruppe.text));
/* ---- Die Stufe (16.09.2026) ---------------------------------------
Filipe: "wie bei den aufgaben wo sie für mich haben auch mit
kategorien und vielen aufgaben."
Dieselbe Kategorie bedeutet für jemanden in der ersten Woche
etwas anderes als für jemanden, der einarbeitet. Eine Aufgabe
der Stufe "erfahren" an einen Neuen zu geben ist kein
Kompliment, sondern ein Überfallen.
DIE STUFEN KOMMEN VOM SERVER, nicht aus einer Liste hier: Eine
zweite Liste im Browser wäre die, die beim nächsten Zusatz
veraltet -- und ausgeliefert wird sie an jeden. */
const inGruppe = katalog.aufgaben.filter((v) => v.gruppe === kGruppe);
if (Array.isArray(katalog.stufen) && katalog.stufen.length) {
/* VOLLE GROESSE, nicht `--fein`: Drueben ist die zweite Reihe
(die Stufen) genauso gross wie die erste. */
const sr = el('div', 'stufenleiter');
sr.setAttribute('role', 'group');
sr.setAttribute('aria-label', 'Stufe wählen');
const alle = el('button', 'stufenleiter__knopf');
alle.type = 'button';
alle.setAttribute('aria-pressed', String(kStufe === null));
alle.append(el('span', 'stufenleiter__name', 'Alle'));
alle.append(el('span', 'stufenleiter__zahl', String(inGruppe.length)));
alle.addEventListener('click', () => { kStufe = null; vorlagenbrettZeigen(); });
sr.append(alle);
for (const st of katalog.stufen) {
const n = inGruppe.filter((v) => v.stufe === st.schluessel).length;
if (!n) continue;
const k = el('button', 'stufenleiter__knopf');
k.type = 'button';
k.dataset.stufe = st.schluessel;
k.setAttribute('aria-pressed', String(kStufe === st.schluessel));
k.title = st.text || '';
k.append(el('span', 'stufenleiter__name', st.name));
k.append(el('span', 'stufenleiter__zahl', String(n)));
k.addEventListener('click', () => { kStufe = st.schluessel; vorlagenbrettZeigen(); });
sr.append(k);
}
koerper.append(sr);
const gewaehlt = katalog.stufen.find((x) => x.schluessel === kStufe);
if (gewaehlt?.text) koerper.append(el('p', 'stufenleiter__hilfe', gewaehlt.text));
}
/* Was schon auf dem Brett liegt -- aus den Aufgaben, die ohnehin
geladen sind. Verglichen wird die Kennung der Vorlage und nicht
der Titel: Der darf geändert werden.
BEI WEM SIE LIEGT, ZAEHLT MIT (23.09.2026).
Hier stand keine Bedingung: Sobald IRGENDWER eine Vorlage hatte,
stand an der Karte „schon übernommen" -- auch fuer alle anderen.
Solange der Block auf einer Seite ohne Personenwahl stand, fiel
das kaum auf. Auf „Eure Aufgaben" waere es offen falsch: Man
waehlt Frida, und der Katalog behauptet, sie habe die Aufgabe
schon -- weil Rieke sie hat.
Der Creator-Zweig machte es von Anfang an richtig (er
vergleicht `creator_id`); nur dieser hier nicht. Ohne gewaehlte
Person bleibt es wie bisher: dann ist „irgendwer" die einzige
Auskunft, die es gibt. */
/* WESSEN AUFGABEN SEHE ICH UEBERHAUPT? Daran haengt, was
"schon uebernommen" bedeuten kann.
Wer NICHT verteilt, bekommt vom Server nur die eigenen -- bei
ihm heisst die Markierung zurecht "die habe ich schon", ohne
dass er jemanden waehlen muesste (er sieht die Personenwahl gar
nicht).
Wer verteilt, bekommt die Aufgaben ALLER. Ohne gewaehlte Person
hiesse die Markierung dann "irgendwer hat sie" -- und das ist
keine Antwort auf die Frage, die man an dieser Stelle stellt.
Man liest sie als "brauche ich nicht mehr zu vergeben" und
ueberspringt eine Aufgabe, die dem Menschen vor einem fehlt.
LIEBER KEINE AUSKUNFT ALS EINE, DIE MAN FALSCH LIEST. Sobald
jemand gewaehlt ist, steht sie wieder da -- und meint dann
genau ihn. */
const wem = U.ziel();
const schonDa = new Map();
if (wem || !darfVerteilenV) {
for (const a of U.aufgaben()) {
if (!a.vorlage) continue;
if (wem && String(a.verantwortlich_id) !== String(wem)) continue;
schonDa.set(a.vorlage, a);
}
}
/* ---- AN WEN? Mit der Last daneben (16.09.2026) -------------------
Bis heute kam die Person aus dem Formular GANZ OBEN auf der
Seite: Man wählt sie dort, scrollt herunter und drückt hier
„Übernehmen". Wer das nicht weiß, bekommt „Bitte zuerst eine
Person wählen" und sucht.
UND DAS WICHTIGERE: Man sah beim Vergeben nicht, wer schon wie
viel hat. Zu wenig Zeit ist in den Untersuchungen zu
Moderatoren der meistgenannte Grund fürs Aufhören — und die
Person, die Aufgaben verteilt, ist die einzige, die das
verhindern kann. Dafür muss die Zahl dort stehen, wo entschieden
wird, nicht auf einer Auswertung, die man hinterher aufruft.
KEINE SCHWELLE, KEINE WARNFARBE. Was „zu viel" ist, hängt vom
Menschen ab; eine feste Grenze wäre geraten. Was NICHT geraten
ist: dass etwas überfällig liegt. Nur das wird markiert. */
/* AN WEN? DAS STEHT JETZT OBEN (23.09.2026).
Hier stand eine eigene Knopfreihe mit allen Leuten -- die dritte
unter zwei anderen. Auf „Aufgaben" war sie noetig, weil es dort
keine Personenwahl gab. Auf „Eure Aufgaben" ist die Person oben
gewaehlt, und dieselbe Wahl ein zweites Mal anzubieten waere
nicht nur ueberfluessig: Man koennte oben den einen und unten
den anderen waehlen und haette zwei Wahrheiten auf einem
Bildschirm.
Was die Reihe zusaetzlich konnte -- zeigen, wer schon wie viel
offen hat --, kann die Auswahl oben ebenfalls: Sie traegt die
Zahlen an jeder Karte. Es geht also nichts verloren.
Statt der Reihe steht ein Satz, und der Knopf an jeder Karte
nennt den Namen gleich mit. */
const zielName = U.zielName();
if (darfVerteilenV) {
zielSatz(koerper, zielName);
}
const liste = katalog.aufgaben
.map((v, nr) => ({ ...v, nr }))
.filter((v) => v.gruppe === kGruppe)
.filter((v) => !kStufe || v.stufe === kStufe);
const box = el('div', 'vorlagenbrett__liste');
for (const v of liste) {
const karte = el('div', 'vorlagenbrett__karte');
/* DIE KENNUNG AN DER KARTE, damit das Band oben sie
wiederfinden kann. Über den Titel zu suchen wäre die
naheliegende Abkürzung — und sie fällt um, sobald zwei
Vorlagen ähnlich heißen. */
karte.dataset.vorlage = v.schluessel;
const schon = schonDa.get(v.schluessel);
if (schon) karte.dataset.schon = 'ja';
karte.append(el('h3', 'vorlagenbrett__titel', v.titel));
karte.append(el('p', 'vorlagenbrett__text', v.text));
const fuss = el('div', 'vorlagenbrett__fuss');
fuss.append(el('span', 'vorlagenbrett__frist',
v.tage === 1 ? 'Frist: morgen' : `Frist: in ${v.tage} Tagen`));
/* Die empfohlene Rolle steht im Dokument neben jeder Aufgabe und
ist die halbe Auskunft: "Technik-Check" heisst etwas anderes,
je nachdem, wer ihn macht. */
if (v.rolle) fuss.append(el('span', 'vorlagenbrett__frist', `für: ${v.rolle}`));
if (schon) {
fuss.append(el('span', 'vorlagenbrett__schon',
`schon übernommen · ${U.statusName[schon.status] || schon.status}`));
}
/* ==== ÜBERNEHMEN ODER BEWERBEN? (23.09.2026) ==================
Filipe: „die modis sollen bei all diesen voschlägen auch nur
bewerben können."
DER SERVER SAGT, WAS ICH DARF (`darf_verteilen`). Hier steht
kein Rollenname — diese Datei bekommt jeder, der die Seite
öffnet. Und die Schranke selbst liegt ohnehin in der Route:
Ein ausgeblendeter Knopf ist eine Bitte, abgelehnt wird am
Server. */
const meine = bewerbungen.find((b2) => b2.selbst && b2.vorlage === v.schluessel);
if (darfVerteilenV) {
/* DER KNOPF SAGT, AN WEN (23.09.2026). „Übernehmen" allein
laesst offen, wer sie bekommt -- und die Antwort steht weit
oben auf der Seite. „An Rieke" beantwortet die Frage dort,
wo man drueckt. Ohne gewaehlte Person bleibt es beim kurzen
Wort; dann waere ein Name geraten. */
const k = el('button', 'knopf knopf--klein',
schon ? 'Nochmal' : (zielName ? 'An ' + zielName : 'Übernehmen'));
k.type = 'button';
if (schon) {
k.title = zielName
? 'Diese Vorlage liegt schon bei ' + zielName + ' – noch einmal übernehmen?'
: 'Diese Vorlage liegt schon auf dem Brett – noch einmal übernehmen?';
}
k.addEventListener('click', () => uebernehmen(k,
{ art: 'katalog', nr: v.nr, gruppe: kGruppe, person_id: katalogZiel() }));
fuss.append(k);
} else if (meine) {
/* SCHON BEWORBEN. Der Satz sagt, WER antwortet -- sonst wartet
man auf niemanden und fragt am nächsten Tag im Chat. */
fuss.append(el('span', 'vorlagenbrett__warte',
'beworben – DogFather oder die rechte Hand antworten'));
const zurueck = el('button', 'knopf knopf--klein', 'Zurückziehen');
zurueck.type = 'button';
zurueck.addEventListener('click', () => bewerbungZurueck(zurueck, meine));
fuss.append(zurueck);
} else if (!schon) {
const k = el('button', 'knopf knopf--klein', 'Bewerben');
k.type = 'button';
k.title = 'DogFather oder die rechte Hand entscheiden, ob du sie bekommst.';
k.addEventListener('click', () => bewerben(k, v));
fuss.append(k);
}
/* WER SIE SCHON HAT, BEKOMMT KEINEN BEWERBEN-KNOPF. Der Server
lehnt das ohnehin ab (liegt_schon_bei_dir) -- ein Knopf, der
eine Absage holt, ist schlimmer als keiner. Der Vermerk „schon
übernommen" steht weiter daneben; er ist die Antwort auf die
Frage, warum hier nichts zu tun ist. */
karte.append(fuss);
/* ==== DIE BEWERBUNGEN ANDERER -- nur für die, die entscheiden ==
Sie stehen AN DER KARTE und nicht in einer eigenen Liste
irgendwo: Entschieden wird über eine bestimmte Aufgabe, und
die Frage „will ich, dass Frida das macht?" beantwortet man
besser mit dem Text der Aufgabe davor.
JE MENSCH EINE ZEILE, nicht ein Knopf für alle. Man entscheidet
über einen Menschen, nicht über einen Listeneintrag — dieselbe
Überlegung wie am Aufgabenbrett. */
if (darfEntscheidenV) {
const offeneB = bewerbungen.filter((b2) => b2.vorlage === v.schluessel);
if (offeneB.length) {
karte.dataset.beworben = 'ja';
const block = el('div', 'vorlagenbrett__bewerbungen');
block.append(el('p', 'vorlagenbrett__bschild', offeneB.length === 1
? 'Eine Bewerbung wartet auf deine Antwort:'
: `${offeneB.length} Bewerbungen warten auf deine Antwort:`));
offeneB.forEach((b2, i) => {
const reihe = el('div', 'vorlagenbrett__bewerbung');
reihe.append(el('span', 'vorlagenbrett__bname', b2.person_name));
/* DER ERSTE WIRD BENANNT, nicht nur oben hingestellt. Eine
Reihenfolge allein hält man für Zufall oder Alphabet.
Nur bei mehr als einer -- sonst ist es Füllwerk. */
if (i === 0 && offeneB.length > 1) {
reihe.append(el('span', 'vorlagenbrett__bzuerst', 'zuerst da'));
}
if (b2.text) reihe.append(el('span', 'vorlagenbrett__bwort', `„${b2.text}"`));
for (const [weg, schild, klasse] of [
['annehmen', 'Annehmen', 'knopf--klar'],
['ablehnen', 'Ablehnen', ''],
]) {
const b3 = el('button', `knopf knopf--klein ${klasse}`, schild);
b3.type = 'button';
b3.addEventListener('click', () => entscheiden(b3, weg, schild, b2, v));
reihe.append(b3);
}
block.append(reihe);
});
karte.append(block);
}
}
box.append(karte);
}
if (liste.length) {
const offen = liste.filter((v) => !schonDa.has(v.schluessel)).length;
koerper.append(el('p', 'vorlagenbrett__stand', offen
? `${offen} von ${liste.length} noch nicht übernommen.`
: `Alle ${liste.length} davon sind übernommen.`));
}
koerper.append(box);
/* „ALLE ÜBERNEHMEN" GIBT ES NUR FÜR DIE, DIE VERTEILEN (23.09.2026).
Ein „Alle bewerben" waere der schnellste Weg, zwölf Bitten auf
einmal loszuschicken -- und damit zwölf Entscheidungen fuer
jemand anderen. Wer sich bewirbt, waehlt aus; das ist der
Unterschied zwischen einer Bewerbung und einem Griff ins Regal. */
if (liste.length > 1 && darfVerteilenV) {
const alle = el('button', 'abmelden', `Alle ${liste.length} übernehmen`);
/* "auf dem Brett" hiess bis zum 23.09.2026 das Aufgabenbrett
einer anderen Seite. Hier zaehlt, ob sie bei DIESER Person
schon liegt -- und genau das markiert der Katalog seit heute
auch an den Karten. */
alle.title = 'Legt alle Aufgaben an, die gerade in der Liste stehen – '
+ 'auch die, die schon übernommen sind.';
alle.type = 'button';
alle.style.marginTop = '12px';
/* DIE STUFE WANDERT MIT (16.09.2026). Ohne sie legte dieser Knopf
alle Aufgaben der Kategorie an -- auch die, die gerade
ausgeblendet sind. Der Knopf sagt "Alle 4 übernehmen" und
legte elf an; das merkt man erst auf dem Brett. */
alle.addEventListener('click', () => uebernehmen(alle,
{ art: 'katalog_alle', gruppe: kGruppe, stufe: kStufe || '',
person_id: katalogZiel() }));
koerper.append(alle);
}
}
/* FUER WEN wird uebernommen? Wer nur eigene Aufgaben hat, meint sich
selbst -- der Server setzt das dann ein und braucht hier nichts.
Wer fuer andere eintragen darf, muss die Person oben ausgewaehlt
haben; sonst sagt der Server es.
SEIT DEM 23.09.2026 FRAGT DAS DIE SEITE. Vorher hielt dieser Block
eine eigene Auswahl (`kZiel`) und fiel notfalls auf zwei
Formularfelder zurueck -- drei Quellen fuer eine Frage. Jetzt gibt
es genau eine, und sie steht dort, wo man sie auch SIEHT: in der
Personenwahl der Seite. */
function katalogZiel() { return U.ziel(); }
/* Ein Weg fuer beide Knoepfe -- einzeln und alle. Zwei fast gleiche
Funktionen waeren der sichere Weg dazu, dass die eine irgendwann
anders reagiert als die andere. */
/* =====================================================================
BEWERBEN, ZURÜCKZIEHEN, ENTSCHEIDEN (23.09.2026)
=====================================================================
DREI HANDGRIFFE, EIN MUSTER: fragen, schicken, frisch holen,
neu zeichnen. Das „frisch holen" ist der Teil, den man vergisst --
ohne ihn bliebe eine beantwortete Bewerbung stehen, bis jemand die
Seite neu lädt, und zwei Leute entschieden über dieselbe Bitte.
GEFRAGT WIRD MIT `frageNach`, dem Kasten des Hauses. Er bringt das
Textfeld für die Notiz gleich mit -- ein eigener wäre ein zweiter
Dialog mit eigener Tastaturbedienung. */
/** Nach einer Entscheidung: Katalog UND Brett neu -- in dieser
* Reihenfolge. Die Aufgabe, die gerade entstanden ist, soll man
* dort sehen, wo sie ab jetzt lebt. */
async function nachBewerbung() {
await vorlagenHolen();
await U.nachAenderung();
await vorlagenbrettZeigen();
}
async function bewerben(knopf, v) {
const antwort = await window.frageNach({
titel: 'Für diese Aufgabe bewerben?',
was: `„${v.titel}" – DogFather oder die rechte Hand entscheiden, ob du sie bekommst.`,
bleibt: 'Du kannst deine Bewerbung jederzeit wieder zurückziehen.',
grund: 'Willst du etwas dazuschreiben? (freiwillig)',
grundPflicht: false,
platzhalter: 'z. B. ich habe Freitag Zeit dafür',
ja: 'Bewerben', nein: 'Doch nicht',
});
if (!antwort) return;
knopf.disabled = true;
try {
const a = await U.hole('/workspace/api/vorlagen/bewerben', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ vorlage: v.schluessel, text: (antwort && antwort.grund) || '' }),
});
if (!a.ok) {
const f = await a.json().catch(() => ({}));
U.melde(sagWas(f.fehler, 'Das hat nicht geklappt.'));
knopf.disabled = false;
return;
}
U.melde('');
await nachBewerbung();
} catch {
U.melde('Keine Verbindung – die Bewerbung ist nicht angekommen.');
knopf.disabled = false;
}
}
async function bewerbungZurueck(knopf, b) {
knopf.disabled = true;
try {
const a = await U.hole('/workspace/api/vorlagen/bewerbung-zurueck', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: b.id }),
});
if (!a.ok) {
const f = await a.json().catch(() => ({}));
U.melde(sagWas(f.fehler, 'Das hat nicht geklappt.'));
knopf.disabled = false;
return;
}
U.melde('');
await nachBewerbung();
} catch {
U.melde('Keine Verbindung.');
knopf.disabled = false;
}
}
async function entscheiden(knopf, weg, schild, b, v) {
const wem = b.person_name;
const antwort = await window.frageNach({
titel: `${schild} — ${wem}`,
was: weg === 'annehmen'
? `${wem} bekommt „${v.titel}" als Aufgabe auf das Brett.`
: `${wem} bekommt sie nicht. Die Vorlage bleibt für alle offen.`,
/* DER TEXT IST DER GRUND, WARUM FILIPE DAS SO WOLLTE: „mit einem
text als notiz". Eine Absage ohne Satz ist die, nach der
niemand weiß, woran es lag. */
bleibt: `Dein Text bleibt an der Entscheidung stehen.`,
grund: 'Willst du etwas dazuschreiben? (freiwillig)',
grundPflicht: false,
platzhalter: weg === 'annehmen'
? 'z. B. danke dir, mach das gern'
: 'z. B. das macht diesmal jemand anders',
ja: schild, nein: 'Abbrechen',
});
if (!antwort) return;
knopf.disabled = true;
try {
const a = await U.hole(`/workspace/api/vorlagen/bewerbung/${weg}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: b.id, kommentar: (antwort && antwort.grund) || '' }),
});
if (!a.ok) {
const f = await a.json().catch(() => ({}));
U.melde(sagWas(f.fehler, 'Das hat nicht geklappt.'));
knopf.disabled = false;
return;
}
U.melde('');
await nachBewerbung();
} catch {
U.melde('Keine Verbindung – die Entscheidung ist nicht angekommen.');
knopf.disabled = false;
}
}
async function uebernehmen(knopf, was, fuer) {
knopf.disabled = true;
const vorher = knopf.textContent;
knopf.textContent = 'Wird übernommen …';
try {
const a = await U.hole('/workspace/api/vorlagen/uebernehmen', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...was, bereich: vBereich, stufe: vStufe, creator_id: fuer }),
});
if (!a.ok) {
const f = await a.json().catch(() => ({}));
U.melde(sagWas(f.fehler, 'Das hat nicht geklappt.'));
knopf.disabled = false; knopf.textContent = vorher;
return;
}
U.melde('');
/* Erst das Brett neu laden, dann den Block -- so sieht man die
neue Aufgabe sofort dort, wo sie ab jetzt lebt. */
await U.nachAenderung();
await vorlagenbrettZeigen();
} catch {
U.melde('Das hat nicht geklappt.');
knopf.disabled = false; knopf.textContent = vorher;
}
}
/* =====================================================================
WAS DIE SEITE BENUTZT
===================================================================== */
window.Vorlagenbrett = {
/** Die Seite sagt, welchen Zweig sie zeigt und woher die Daten
* kommen. Alles als FUNKTION, nicht als Wert: Die gewaehlte Person
* und die Aufgabenliste aendern sich, waehrend die Seite offen ist
* -- ein Wert waere der Stand vom Seitenaufbau. */
einrichten(o) {
const fehlt = NOETIG.filter((k) => o?.[k] === undefined);
if (fehlt.length) {
/* DER DRITTE AUSGANG: nicht in Ordnung, aber auch nicht stumm.
pruef-sicht liest die Browserkonsole mit -- damit faellt ein
vergessenes Feld beim naechsten Lauf auf und nicht erst,
wenn jemand sich wundert, warum der Block leer bleibt. */
console.error('Vorlagenbrett: die Seite hat nichts gesagt zu: '
+ fehlt.join(', '));
return false;
}
if (o.zweig !== 'team' && o.zweig !== 'creator') {
console.error('Vorlagenbrett: unbekannter Zweig ' + o.zweig);
return false;
}
U = o;
return true;
},
/** Neu zeichnen -- beim Start und nach jedem Wechsel der Person. */
async zeigen() {
if (!U) { console.error('Vorlagenbrett: zeigen() vor einrichten()'); return; }
return vorlagenbrettZeigen();
},
/** Nach einer Aenderung ANDERSWO (jemand hat von Hand eine Aufgabe
* angelegt): Der Katalog markiert "schon uebernommen" aus der
* Aufgabenliste, also muss er mitkommen. */
async auffrischen() {
if (!U) return;
vorlagenGeladen = false;
return vorlagenbrettZeigen();
},
};
})();