=== 1. BEARBEITEN UND LOESCHEN, WAS SIE ANGELEGT HAT ===
Filipe: "kuemmer dich bitte auch drum dass die rechte hand, wenn sie
aufgaben an die modis oder linke hand erstellt, will ich dass sie die
moeglichkeit hat die auch zu bearbeiten und zu loeschen bitte.
perfektionier das fuer sie und fuer dogfather."
WARUM ES VORHER NICHT GING, und es sah nicht danach aus: `creator_id`
heisst nicht "wer hat sie angelegt", sondern "zu wem gehoert sie" (so
steht es am Tabellenkopf). Verteilt die rechte Hand eine Aufgabe an
einen Modi, steht dort der MODI. Sie erfuellte damit an ihrer eigenen
Aufgabe keine der drei Bedingungen von `darfAendern` und bekam 403 --
auf einen Knopf, den die Oberflaeche ihr trotzdem anbot, weil sie ihn
an `darf_verteilen` haengte: eine Auskunft ueber die PERSON, wo die
Frage der AUFGABE gilt.
Die Spalte `erstellt_von` gibt es seit jeher und wird beim Anlegen
gefuellt -- die Sichtbarkeitsregeln fragen sie an sechs Stellen ab. Sie
stand nur nie in dieser einen Zeile. Und sie fehlte in SPALTEN, kam
also in keiner Aufgabe mit: Die neue Regel waere ein Vergleich gegen
`undefined` geblieben.
DIE REGEL IST ALLGEMEIN, NICHT AUF EINE ROLLE GEMUENZT: wer etwas
angelegt hat, darf es auch aendern. Ein Rollenname waere die naechste
zweite Wahrheit -- in dieser Woche ist genau das dreimal veraltet.
LOESCHEN BEKOMMT EINE EIGENE FRAGE, weil es das Einzige ist, was sich
nicht zuruecknehmen laesst: `darfAufgabenVerteilen(person) &&
darfAendern(person, aufgabe)`. Damit darf sie ihre eigenen -- und der
Modi, bei dem die Aufgabe LIEGT, darf sie weiterhin bearbeiten, aber
nicht verschwinden lassen. Ablehnen und Abbrechen sind die Wege dafuer.
Die Loesch-Route holt die Aufgabe jetzt mit der Sichtbarkeitsregel und
antwortet mit 404 statt 403, wenn es sie fuer diese Person nicht gibt
-- sonst liesse sich durch Ausprobieren herausfinden, welche Nummern
vergeben sind. Beim Aendern stand das schon so, eine Route weiter oben.
pruef-verteilen: 19 -> 30 Punkte. Mit drei Gegenproben, ohne die "sie
darf" auch dann gruen waere, wenn jeder alles duerfte: der Modi wird
abgewiesen (403), die Aufgabe steht danach noch da, und eine FREMDE
Aufgabe loescht sie nicht.
=== 2. JEDE KARTE HAT DENSELBEN FUSS ===
Filipe: "wer hat sie soll bitte bei all diesen aufgaben stehen. bei all
diesen kategorien da. ... es soll auch immer gleich aussehen und nicht
manchmal verschoben und so."
ZWEI URSACHEN, und keine davon war Zufall:
a) "Wer hat sie?" entstand nur, solange oben "Alle" gewaehlt war
(`if (anAlle)`). Wer auf einen Namen tippte, verlor den Knopf an
ALLEN zwoelf Karten, ohne dass irgendwo stand, warum. Die Auskunft
"wer aus dem Team hat diese Vorlage" haengt aber an der VORLAGE,
nicht an der Auswahl -- sie daran zu binden war der Fehler.
b) Der Fuss war EINE Reihe mit `flex-wrap`, und wie viele Angaben
darin stehen, haengt von der Karte ab: "Frist" immer, "fuer:
Rolle" manchmal, "liegt bei 4 von 5" nur, wenn schon jemand sie
hat. Karten ohne den dritten Text hatten noch Platz fuer einen
Knopf, Karten mit ihm nicht -- also stand "An alle" mal neben der
Frist und mal darunter. Zwoelf Karten, drei verschiedene Fuesse.
Jetzt zwei Reihen mit fester Aufgabe: oben, was man LIEST; unten, was
man DRUECKT. Die Knopfreihe ist immer die letzte Zeile und sitzt am
unteren Rand, also stehen die Knoepfe bei allen Karten einer Reihe auf
derselben Hoehe -- auch wenn der Text darueber verschieden lang ist.
Die Rueckseite verteilt jetzt IMMER an alle. Vorher nahm sie
`katalogZiel()`; solange sie nur bei "Alle" existierte, war das
dasselbe. Seit sie immer da ist, waere es eine Falle: Der Knopf sagt
"Nachholen - 3 fehlen" und gaebe sie einer einzigen Person.
=== 3. DUBLETTEN AUFRAEUMEN ===
Filipe zu "Diene x6 - Ghost x6 - Marina x6 - Miss x6" bei "0 von 24":
"mach aus den 6 1 mal bitte, ich hab mich da geirrt."
`tools/aufgaben-doppelte.mjs` raeumt das auf. Es TUT VON SICH AUS
NICHTS: ohne `--wirklich` zeigt es nur, was passieren wuerde. Mit
`--wirklich` legt es ZUERST eine Kopie der Datenbank an (`VACUUM INTO`,
nicht `cp` -- eine blosse Dateikopie kann das WAL verlieren) und nennt
den Befehl, mit dem man zurueckkommt.
WELCHE BLEIBT, ist nicht beliebig: eine erledigte, wenn es sie gibt
(getane Arbeit wirft man nicht weg), sonst eine begonnene, sonst die
aelteste. An einer Wegwerf-Datenbank durchgespielt: 12 Aufgaben, zwei
Menschen, einer mit einer erledigten darunter -- es blieben genau die
richtigen zwei stehen, die Einzelaufgabe blieb unberuehrt, und das
Nachzaehlen am Ende meldete null Dubletten.
Geprueft: pruef-verteilen (30), pruef-vorlagen (24),
pruef-aufgaben-vorlagen, pruef-aufgabenbrett, pruef-modi-katalog (150),
pruef-bewerbung-aufgaben (101).
Co-Authored-By: Claude Opus 5 <[email protected]>
1498 lines
69 KiB
JavaScript
1498 lines
69 KiB
JavaScript
/* =======================================================================
|
||
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.'));
|
||
|
||
/* ==== WELCHE KARTE SICH NACH DEM VERTEILEN ZEIGEN SOLL ==========
|
||
|
||
Filipe: „wenn ich drauf drücke dreht sich die kachel und dan seh
|
||
ich wer es gemacht hat, und immer noch die option es nochmal zu
|
||
verteilen falls neue leute ins team zustoßen."
|
||
|
||
WARUM EIN MERKZETTEL UND KEIN DIREKTES UMSCHALTEN: Nach dem
|
||
Verteilen wird das ganze Brett NEU gezeichnet (die Zahlen an den
|
||
Namen stimmen sonst nicht mehr). Eine Karte, die man vorher
|
||
umdreht, ist danach weg -- samt ihrer Drehung. Der Schluessel
|
||
ueberlebt das Neuzeichnen; die Karte findet sich selbst wieder.
|
||
|
||
EINER UND NICHT EINE LISTE: Man verteilt eine Karte und sieht sie
|
||
an. Zwei gleichzeitig gedrehte Karten waeren ein Zustand, den
|
||
niemand hergestellt hat. */
|
||
let zuletztVerteilt = null;
|
||
|
||
/* 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', 'zielbare', 'zielWaehlen', '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 – tippe oben jemanden an, 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 anAlle = String(wem) === 'alle';
|
||
|
||
/* DIE LEUTE UND DIE ZEITRECHNUNG STEHEN HIER OBEN, weil sie ab
|
||
heute an ZWEI Stellen gebraucht werden: in der Reihe "An wen"
|
||
weiter unten und an jeder Karte. Zweimal dieselbe Zeile waere
|
||
der Anfang zweier Fassungen, die irgendwann verschieden sind --
|
||
und genau daran ist "alle" heute frueh gescheitert. */
|
||
const zielbar = U.zielbare() || [];
|
||
const heute = typeof window.heuteLokal === 'function'
|
||
? window.heuteLokal() : new Date().toISOString().slice(0, 10);
|
||
const nochOffen = (a) => a.status !== 'erledigt' && a.status !== 'abgebrochen';
|
||
const istSpaet = (a) => nochOffen(a) && a.frist && a.frist < heute;
|
||
|
||
/* ==== "ALLE" WAR HIER NICHT VORGESEHEN (behoben 24.09.2026) =====
|
||
|
||
Filipe hat es aufgenommen: Er waehlt "Alle", drueckt bei einer
|
||
Karte auf "An alle" -- und auf dem Bildschirm aendert sich
|
||
NICHTS. Die Karte behaelt ihren Knopf, und darueber steht
|
||
weiter "12 von 12 noch nicht uebernommen". Die Aufgaben waren
|
||
angelegt (die Zahlen an den Namen sind gestiegen), nur sah man
|
||
es an der Stelle nicht, an der man gerade arbeitet.
|
||
|
||
DIE URSACHE STAND IN DER ZEILE DARUNTER: verglichen wurde
|
||
`String(a.verantwortlich_id) !== String(wem)`. Bei "alle" ist
|
||
`wem` das WORT "alle" -- und keine Aufgabe hat je einen
|
||
Verantwortlichen namens "alle". Die Bedingung war also IMMER
|
||
wahr, jede Aufgabe wurde uebersprungen, und die Liste blieb
|
||
leer. Kein Absturz, keine Meldung: Es sah aus, als waere noch
|
||
nichts uebernommen.
|
||
|
||
Gebaut wurde "alle" am selben Vormittag; `zielWaehlen`,
|
||
`zielName` und `nachAenderung` wurden angepasst -- diese eine
|
||
Stelle nicht. Genau die Art Fehler, die eine Liste macht, die
|
||
man von Hand nachzieht.
|
||
|
||
WARUM NICHT NUR EINE ZAHL, SONDERN WER. Filipe: "wenn ich eine
|
||
aufgabe an alle verteile will ich dass dogfather und die rechte
|
||
hand individuel von jedem sehen wer es gemacht hat oder nicht."
|
||
Eine Zahl ("4 von 5") beantwortet "wie weit", aber nicht "wer
|
||
noch" -- und danach handelt man. Deshalb steht unten eine
|
||
Marke je Mensch und nicht nur ein Zaehler. */
|
||
const schonDa = new Map();
|
||
const schonWer = new Map(); // Vorlage -> Map(Personennummer -> Aufgabe)
|
||
/* ==== WER SIE HAT, WIRD IMMER AUSGERECHNET (25.09.2026) =========
|
||
|
||
Filipe: „wer hat sie soll bitte bei all diesen aufgaben stehen.
|
||
bei all diesen kategorien da. … es soll auch immer gleich
|
||
aussehen und nicht manchmal verschoben und so."
|
||
|
||
Hier stand `if (anAlle)`. Die Rueckseite der Karte -- und damit
|
||
der Knopf „Wer hat sie?" -- entstand deshalb nur, solange oben
|
||
„Alle" gewaehlt war. Wer auf einen Namen tippte, verlor den
|
||
Knopf an ALLEN zwoelf Karten, ohne dass irgendwo stand, warum.
|
||
|
||
DIE AUSKUNFT IST DIESELBE, egal wen man oben gewaehlt hat: „wer
|
||
aus dem Team hat diese Vorlage schon". Sie haengt an der
|
||
Vorlage, nicht an der Auswahl. Sie deshalb an die Auswahl zu
|
||
binden war der Fehler.
|
||
|
||
`schonDa` BLEIBT, WIE ES WAR: Das ist die andere Frage („liegt
|
||
sie bei DER Person, die ich gerade meine") und die gehoert
|
||
weiterhin zur Auswahl. Zwei Fragen, zwei Karten. */
|
||
{
|
||
const zielIds = new Set(zielbar.map((p) => String(p.id)));
|
||
for (const a of U.aufgaben()) {
|
||
if (!a.vorlage) continue;
|
||
const werHats = String(a.verantwortlich_id);
|
||
if (!zielIds.has(werHats)) continue;
|
||
if (!schonWer.has(a.vorlage)) schonWer.set(a.vorlage, new Map());
|
||
/* HAT JEMAND DIESELBE VORLAGE MEHRFACH, ZAEHLT DIE OFFENE.
|
||
Manches holt man jeden Monat neu; dann liegt daneben eine
|
||
erledigte von letztem Mal. Stuende die hier, sagte die
|
||
Karte "Erledigt", waehrend die Person die Aufgabe noch vor
|
||
sich hat -- und man haekte sie gedanklich ab. */
|
||
const bisher = schonWer.get(a.vorlage).get(werHats);
|
||
if (!bisher || (nochOffen(a) && !nochOffen(bisher))) {
|
||
schonWer.get(a.vorlage).set(werHats, a);
|
||
}
|
||
if (anAlle) schonDa.set(a.vorlage, a);
|
||
}
|
||
}
|
||
if (!anAlle && (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 GEHT DAS HIER? ===================================
|
||
|
||
DIESE REIHE WAR EINMAL WEG, UND DAS WAR EIN FEHLER (23.09.2026).
|
||
|
||
Beim Umzug des Katalogs habe ich sie geloescht, weil "Eure
|
||
Aufgaben" oben schon eine Personenwahl hat. Verlangt war das
|
||
nicht -- verlangt war, den Katalog zu VERSCHIEBEN.
|
||
|
||
Filipe: "da ist scheisse wenn du einfach sachen machst die ich
|
||
nicht verlange."
|
||
|
||
Sie steht wieder da, vollstaendig, mit den Zahlen daneben. Was
|
||
sich geaendert hat, ist nur, WAS SIE SETZT: Frueher hatte sie
|
||
eine eigene Auswahl, die nichts von der Seite wusste -- man
|
||
konnte oben den einen und hier den anderen waehlen, und dann
|
||
standen zwei Antworten auf einem Bildschirm. Jetzt waehlt ein
|
||
Tipp hier dieselbe Person wie ein Tipp auf eine Kachel oben.
|
||
Zwei Stellen zum Bedienen, EINE Antwort.
|
||
|
||
WARUM DIE ZAHLEN WICHTIG SIND (16.09.2026, unveraendert
|
||
gueltig): Zu wenig Zeit ist in den Untersuchungen zu Moderatoren
|
||
der meistgenannte Grund fuers Aufhoeren -- und wer Aufgaben
|
||
verteilt, ist der einzige, der das verhindern kann. Dafuer muss
|
||
die Zahl dort stehen, wo entschieden wird.
|
||
|
||
KEINE SCHWELLE, KEINE WARNFARBE. Was "zu viel" ist, haengt vom
|
||
Menschen ab; eine feste Grenze waere geraten. Was NICHT geraten
|
||
ist: dass etwas ueberfaellig liegt. Nur das wird markiert. */
|
||
const zielName = U.zielName();
|
||
/* `zielbar`, `heute`, `nochOffen` und `istSpaet` stehen weiter
|
||
oben -- es sind dieselben, die auch die Karten benutzen. */
|
||
if (darfVerteilenV && zielbar.length) {
|
||
koerper.append(el('p', 'feldschild', 'An wen'));
|
||
const wer = el('div', 'stufenleiter stufenleiter--wer');
|
||
wer.setAttribute('role', 'group');
|
||
wer.setAttribute('aria-label', 'Person wählen');
|
||
/* ==== „ALLE" NEBEN DEN NAMEN (24.09.2026) ===================
|
||
|
||
Filipe: „bei an ween fehlt noch die option alle neben den
|
||
namen allen."
|
||
|
||
ER STEHT VORN, nicht hinten: Wer etwas an das ganze Team
|
||
geben will, soll nicht erst an sechs Namen vorbeilesen. Und
|
||
er traegt die Summe aller offenen Aufgaben -- dieselbe Zahl,
|
||
die neben jedem Namen steht, nur zusammengezaehlt.
|
||
|
||
NUR AB ZWEI PERSONEN. Bei einer einzigen waere „alle"
|
||
dasselbe wie ihr Name, also ein zweiter Knopf fuer denselben
|
||
Handgriff. */
|
||
if (zielbar.length > 1) {
|
||
const alleOffen = (U.aufgaben() || []).filter(nochOffen).length;
|
||
const alleSpaet = (U.aufgaben() || []).filter(istSpaet).length;
|
||
const ka = el('button', 'stufenleiter__knopf');
|
||
ka.type = 'button';
|
||
ka.dataset.alle = 'ja';
|
||
if (alleSpaet) ka.dataset.spaet = 'ja';
|
||
ka.setAttribute('aria-pressed', String(String(wem) === 'alle'));
|
||
ka.title = `An alle ${zielbar.length} -- zusammen ${alleOffen} offen`
|
||
+ (alleSpaet ? `, davon ${alleSpaet} überfällig` : '');
|
||
ka.append(el('span', 'stufenleiter__name', 'Alle'));
|
||
ka.append(el('span', 'stufenleiter__zahl', String(zielbar.length)));
|
||
if (alleSpaet) ka.append(el('span', 'stufenleiter__spaet', `${alleSpaet} spät`));
|
||
ka.addEventListener('click', () => U.zielWaehlen('alle'));
|
||
wer.append(ka);
|
||
}
|
||
for (const pp of zielbar) {
|
||
const seine = (U.aufgaben() || [])
|
||
.filter((a) => String(a.verantwortlich_id) === String(pp.id));
|
||
const offenN = seine.filter(nochOffen).length;
|
||
const spaet = seine.filter(istSpaet).length;
|
||
const k = el('button', 'stufenleiter__knopf');
|
||
k.type = 'button';
|
||
if (spaet) k.dataset.spaet = 'ja';
|
||
k.setAttribute('aria-pressed', String(String(wem) === String(pp.id)));
|
||
k.title = spaet
|
||
? `${pp.name}: ${offenN} offen, davon ${spaet} überfällig`
|
||
: `${pp.name}: ${offenN} offen`;
|
||
k.append(el('span', 'stufenleiter__name', pp.name));
|
||
k.append(el('span', 'stufenleiter__zahl', String(offenN)));
|
||
if (spaet) k.append(el('span', 'stufenleiter__spaet', `${spaet} spät`));
|
||
k.addEventListener('click', () => U.zielWaehlen(pp.id));
|
||
wer.append(k);
|
||
}
|
||
koerper.append(wer);
|
||
}
|
||
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);
|
||
/* WER SIE HAT -- je Mensch, nicht als Zahl. Bei EINER gewaehlten
|
||
Person bleibt es beim alten Weg: Dort ist `schon` bereits die
|
||
individuelle Auskunft. */
|
||
const werHat = schonWer.get(v.schluessel) || new Map();
|
||
const alleHabenSie = zielbar.length > 0 && werHat.size >= zielbar.length;
|
||
/* DIE KARTE TRITT ERST ZURUECK, WENN SIE WIRKLICH DURCH IST.
|
||
Bei "alle" heisst `schon` ab EINEM Menschen -- eine Vorlage,
|
||
die einer von fuenf hat, halb auszublenden waere das Gegenteil
|
||
der Auskunft: Sie ist die, die noch Arbeit macht. */
|
||
/* ==== ZWEI FESTE REIHEN STATT EINER, DIE UMBRICHT =============
|
||
(25.09.2026)
|
||
|
||
Filipe: „es soll auch immer gleich aussehen und nicht manchmal
|
||
verschoben und so."
|
||
|
||
WORAN ES LAG, und es war kein Zufall: Im Fuss standen Angaben
|
||
und Knoepfe in EINER Reihe mit `flex-wrap`. Wie viele Angaben
|
||
es gibt, haengt aber von der Karte ab -- „Frist", manchmal
|
||
„fuer: Rolle", manchmal „liegt bei 4 von 5". Karten ohne den
|
||
dritten Text hatten in der ersten Reihe noch Platz fuer einen
|
||
Knopf, Karten mit ihm nicht. Also stand „An alle" mal neben
|
||
der Frist und mal darunter, und „Wer hat sie?" rutschte
|
||
hinterher. Zwoelf Karten, drei verschiedene Fuesse.
|
||
|
||
JETZT ZWEI REIHEN MIT FESTER AUFGABE: oben, was man LIEST,
|
||
unten, was man DRUECKT. Die Knopfreihe sitzt am unteren Rand
|
||
der Karte (`margin-top: auto` im Stilblatt), damit sie bei
|
||
allen Karten einer Zeile auf derselben Hoehe steht -- auch
|
||
wenn der Text darueber verschieden lang ist. */
|
||
if (anAlle ? alleHabenSie : !!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');
|
||
const fussInfo = el('div', 'vorlagenbrett__fuss-info');
|
||
const fussTun = el('div', 'vorlagenbrett__fuss-tun');
|
||
fuss.append(fussInfo, fussTun);
|
||
fussInfo.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) fussInfo.append(el('span', 'vorlagenbrett__frist', `für: ${v.rolle}`));
|
||
if (schon && !anAlle) {
|
||
fussInfo.append(el('span', 'vorlagenbrett__schon',
|
||
`schon übernommen · ${U.statusName[schon.status] || schon.status}`));
|
||
} else if (anAlle && werHat.size) {
|
||
/* BEI "ALLE" IST EIN FREMDER STATUS KEINE AUSKUNFT: Fuenf
|
||
Menschen haben fuenf Staende, und hier stuende der einer
|
||
einzigen -- welcher, haengt von der Reihenfolge der Daten ab.
|
||
Der Fuss sagt deshalb nur, WIE WEIT es ist; WER, steht eine
|
||
Zeile tiefer namentlich. */
|
||
fussInfo.append(el('span', 'vorlagenbrett__schon', alleHabenSie
|
||
? `liegt bei allen ${zielbar.length}`
|
||
: `liegt bei ${werHat.size} von ${zielbar.length}`));
|
||
}
|
||
|
||
/* ==== Ü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. */
|
||
/* "NOCHMAL" NUR, WENN ES WIRKLICH EIN NOCHMAL WAERE.
|
||
|
||
Bei "alle" heisst "schon da" ab EINEM Menschen. Haetten dann
|
||
alle Karten "Nochmal" gesagt, waere der Knopf eine Warnung
|
||
vor etwas, das gar nicht passiert: Der Server ueberspringt
|
||
die, die sie schon haben, und legt sie nur bei den uebrigen
|
||
an. Erst wenn wirklich alle sie liegen haben, ist der
|
||
naechste Druck eine Wiederholung. */
|
||
const wirklichNochmal = anAlle ? alleHabenSie : !!schon;
|
||
const k = el('button', 'knopf knopf--klein',
|
||
wirklichNochmal ? 'Nochmal' : (zielName ? 'An ' + zielName : 'Übernehmen'));
|
||
k.type = 'button';
|
||
if (wirklichNochmal) {
|
||
k.title = zielName
|
||
? 'Diese Vorlage liegt schon bei ' + zielName + ' – noch einmal übernehmen?'
|
||
: 'Diese Vorlage liegt schon auf dem Brett – noch einmal übernehmen?';
|
||
} else if (anAlle && werHat.size) {
|
||
k.title = `${werHat.size} haben sie schon – die bekommen sie nicht doppelt.`;
|
||
}
|
||
/* `nochmal` sagt dem Server, dass die Wiederholung GEMEINT
|
||
ist -- nicht, dass er die Sperre vergessen soll. Bei "alle"
|
||
fuellt er sonst nur die Luecken; steht hier "Nochmal", haben
|
||
wirklich alle sie, und dann ist der Druck eine Entscheidung. */
|
||
k.addEventListener('click', () => {
|
||
/* VOR dem Verteilen merken, nicht danach: `uebernehmen`
|
||
zeichnet am Ende neu, und dann gibt es diese Karte als
|
||
Objekt nicht mehr. */
|
||
if (anAlle) zuletztVerteilt = v.schluessel;
|
||
uebernehmen(k, {
|
||
art: 'katalog', nr: v.nr, gruppe: kGruppe,
|
||
person_id: katalogZiel(), nochmal: wirklichNochmal,
|
||
});
|
||
});
|
||
fussTun.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. */
|
||
fussInfo.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));
|
||
fussTun.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));
|
||
fussTun.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);
|
||
|
||
/* ==== WER HAT SIE, WER NICHT -- NAMENTLICH (24.09.2026) =======
|
||
|
||
Filipe: „wenn ich eine aufgabe an alle verteile will ich dass
|
||
dogfather und die rechte hand individuel von jedem sehen wer
|
||
es gemacht hat oder nicht. aufgabe an alle verteilen aber
|
||
individuel sehen wir das."
|
||
|
||
NUR BEI „ALLE". Ist eine einzelne Person gewaehlt, steht ihr
|
||
Stand schon im Fuss -- eine Reihe mit einem einzigen Namen
|
||
waere dieselbe Auskunft ein zweites Mal.
|
||
|
||
DAS WORT STEHT DA, NICHT NUR DIE FARBE. „Erledigt" liest auch,
|
||
wer Farben nicht unterscheidet oder die Seite schwarzweiss
|
||
ausdruckt; die Farbe ist die Abkuerzung, nicht die Auskunft.
|
||
Deshalb traegt jede Marke beides.
|
||
|
||
UND „HAT SIE NICHT" IST AUCH EINE ANTWORT -- die wichtigste
|
||
sogar. Wer nur die auffuehrt, die sie haben, beantwortet
|
||
„wie weit sind wir", aber nicht „wen muss ich noch fragen".
|
||
Die Fehlenden stehen deshalb mit da, gedaempft, aber lesbar. */
|
||
if (darfVerteilenV && zielbar.length) {
|
||
const reihe = el('div', 'vorlagenbrett__wer');
|
||
reihe.setAttribute('role', 'list');
|
||
reihe.setAttribute('aria-label', `Wer „${v.titel}“ schon hat`);
|
||
for (const p2 of zielbar) {
|
||
const a2 = werHat.get(String(p2.id));
|
||
const spaet = !!a2 && istSpaet(a2);
|
||
const marke = el('span', 'vorlagenbrett__wer-p');
|
||
marke.setAttribute('role', 'listitem');
|
||
/* EIN ZUSTAND JE MARKE, und zwar der, nach dem man handelt:
|
||
ueberfaellig sticht „offen", sonst zaehlt der Status. */
|
||
marke.dataset.stand = !a2 ? 'fehlt' : (spaet ? 'spaet' : (a2.status || 'offen'));
|
||
marke.append(el('span', 'vorlagenbrett__wer-name', p2.name));
|
||
marke.append(el('span', 'vorlagenbrett__wer-stand', !a2
|
||
? 'hat sie nicht'
|
||
: ((U.statusName[a2.status] || a2.status) + (spaet ? ' · überfällig' : ''))));
|
||
reihe.append(marke);
|
||
}
|
||
|
||
/* ==== DIE KARTE BEKOMMT ZWEI SEITEN (25.09.2026) =============
|
||
|
||
Bis heute stand die Namensreihe UNTER der Karte -- immer
|
||
sichtbar, bei zwoelf Karten also zwoelfmal. Das war Filipes
|
||
erste Entscheidung und richtig, solange es nur um "wie
|
||
weit" ging. Jetzt will er das Einzelne auf Druck sehen:
|
||
„wenn ich drauf drücke dreht sich die kachel".
|
||
|
||
VORNE BLEIBT DIE KURZFASSUNG. „liegt bei 3 von 4" steht
|
||
weiterhin im Fuss -- man soll den Stand ueberfliegen
|
||
koennen, ohne zwoelf Karten umzudrehen. Die Namen sind die
|
||
Vertiefung, nicht die Grundauskunft.
|
||
|
||
UMGEBAUT STATT NEU GEBAUT: Alles, was schon an der Karte
|
||
haengt, wandert geschlossen auf die Vorderseite. So kann
|
||
die Rueckseite nichts vergessen, was vorne steht -- und ein
|
||
kuenftiger Zusatz landet von selbst richtig. */
|
||
const vorn = el('div', 'vorlagenbrett__seite vorlagenbrett__seite--vorn');
|
||
while (karte.firstChild) vorn.append(karte.firstChild);
|
||
|
||
const hinten = el('div', 'vorlagenbrett__seite vorlagenbrett__seite--hinten');
|
||
hinten.append(el('p', 'vorlagenbrett__wer-schild', alleHabenSie
|
||
? `Alle ${zielbar.length} haben sie`
|
||
: `${werHat.size} von ${zielbar.length} haben sie`));
|
||
hinten.append(reihe);
|
||
|
||
const fussH = el('div', 'vorlagenbrett__fuss');
|
||
/* ==== „NOCHMAL VERTEILEN" GEHOERT AUF DIE RUECKSEITE =========
|
||
|
||
Filipe ausdruecklich: „und immer noch die option es nochmal
|
||
zu verteilen falls neue leute ins team zustoßen."
|
||
|
||
DER KNOPF SAGT, WAS ER TUT, und das ist bei den zwei Faellen
|
||
NICHT dasselbe: Fehlt noch jemand, holt er die Fehlenden
|
||
nach und laesst die anderen in Ruhe. Haben alle sie, ist der
|
||
naechste Druck eine bewusste Wiederholung -- dann heisst er
|
||
so und sagt es dem Server auch (`nochmal`). */
|
||
/* DIE ZAHL IN KLAMMERN UND NICHT IM SATZ (25.09.2026).
|
||
Der erste Entwurf setzte sie hinein: „Die 1 Fehlenden
|
||
nachholen" -- bei genau einer Person falsches Deutsch, und
|
||
„Die 1 Fehlende" waere je nach Mensch das falsche
|
||
Geschlecht. So stimmt der Satz bei jeder Zahl, ohne
|
||
Sonderfall und ohne zu raten. */
|
||
const fehlt = zielbar.length - werHat.size;
|
||
const nachholen = el('button', 'knopf knopf--klein', alleHabenSie
|
||
? 'Nochmal an alle'
|
||
: `Nachholen – ${fehlt === 1 ? '1 fehlt' : fehlt + ' fehlen'}`);
|
||
nachholen.type = 'button';
|
||
nachholen.title = alleHabenSie
|
||
? 'Alle bekommen sie noch einmal – auch die, die sie schon haben.'
|
||
: 'Nur die, die sie noch nicht haben. Niemand bekommt sie doppelt.';
|
||
nachholen.addEventListener('click', () => {
|
||
zuletztVerteilt = v.schluessel;
|
||
/* DIE RUECKSEITE GEHT IMMER AN ALLE (25.09.2026).
|
||
|
||
Hier stand `katalogZiel()` -- also die Person, die oben
|
||
gewaehlt ist. Solange die Rueckseite nur bei „Alle"
|
||
existierte, war das dasselbe. Seit sie immer da ist,
|
||
waere es eine Falle: Der Knopf sagt „Nachholen – 3
|
||
fehlen" und gaebe sie einer einzigen Person. Die
|
||
Rueckseite beantwortet eine Frage ueber das TEAM, also
|
||
handelt ihr Knopf auch am Team. */
|
||
uebernehmen(nachholen, {
|
||
art: 'katalog', nr: v.nr, gruppe: kGruppe,
|
||
person_id: 'alle', nochmal: alleHabenSie,
|
||
});
|
||
});
|
||
fussH.append(nachholen);
|
||
|
||
const zurueck = el('button', 'knopf knopf--klein vorlagenbrett__zurueck', 'Zurück');
|
||
zurueck.type = 'button';
|
||
zurueck.addEventListener('click', () => { karte.dataset.gedreht = 'nein'; });
|
||
fussH.append(zurueck);
|
||
hinten.append(fussH);
|
||
|
||
karte.append(vorn);
|
||
karte.append(hinten);
|
||
karte.dataset.dreh = 'ja';
|
||
/* WER SIE SCHON HAT, KANN MAN AUCH OHNE VERTEILEN NACHSEHEN --
|
||
sonst waere die Rueckseite nur als Nebenwirkung einer
|
||
Handlung erreichbar, und wer nur nachsehen will, muesste
|
||
erst etwas verteilen. */
|
||
const umdrehen = el('button', 'knopf knopf--klein vorlagenbrett__drehknopf',
|
||
'Wer hat sie?');
|
||
umdrehen.type = 'button';
|
||
umdrehen.addEventListener('click', () => { karte.dataset.gedreht = 'ja'; });
|
||
/* IMMER AN DERSELBEN STELLE: in der Knopfreihe, hinter dem
|
||
Verteilen-Knopf. Vorher ging er an den Fuss als Ganzes und
|
||
landete damit hinter dem letzten TEXT -- auf Karten ohne
|
||
„liegt bei" war das eine Zeile hoeher als auf Karten mit. */
|
||
vorn.querySelector('.vorlagenbrett__fuss-tun')?.append(umdrehen);
|
||
|
||
/* Und wenn gerade DIESE Karte verteilt wurde: gedreht zeigen. */
|
||
if (zuletztVerteilt === v.schluessel) {
|
||
karte.dataset.gedreht = 'ja';
|
||
zuletztVerteilt = null;
|
||
} else {
|
||
karte.dataset.gedreht = 'nein';
|
||
}
|
||
}
|
||
|
||
/* ==== 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;
|
||
}
|
||
/* WAS WIRKLICH PASSIERT IST, STEHT DA (23.09.2026).
|
||
|
||
Der Server legt seit heute nichts doppelt an, was noch offen
|
||
liegt. Ohne diese Zeilen hiesse der Knopf "Alle 12
|
||
uebernehmen", es entstuenden drei, und niemand wuesste warum.
|
||
Ein Knopf, der weniger tut als er verspricht UND schweigt, ist
|
||
schlimmer als einer, der zu viel tut: Man drueckt ihn noch
|
||
einmal -- und genau das war am 22.09. der Schaden.
|
||
|
||
GRUEN, NICHT ROT: Das ist keine Stoerung, sondern die
|
||
Auskunft, dass die Sperre gegriffen hat. */
|
||
const d = await a.json().catch(() => ({}));
|
||
if (d.uebersprungen) {
|
||
U.melde(d.angelegt
|
||
? d.angelegt + " übernommen. " + d.uebersprungen
|
||
+ " lagen schon offen da – die kommen nicht doppelt."
|
||
: "Alles davon liegt schon offen da (" + d.uebersprungen
|
||
+ ") – nichts doppelt angelegt.", true);
|
||
} else {
|
||
U.melde("");
|
||
}
|
||
/* Erst die Aufgaben der Person neu, dann der 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();
|
||
},
|
||
};
|
||
})();
|