Deutsch oeffnet mit „ und schliesst mit “. An 199 Stellen, die ein
Mensch liest, stand als Schlusszeichen ein GERADES " -- „Nächstes"
statt „Nächstes“. Auf sechzehn oeffentlichen Seiten, in vierzig
Dateien des Workspace und in zwoelf Servermodulen, die Texte
verschicken.
Auf dem Bildschirm sieht man den Unterschied sofort. Beim Schreiben
nicht: Das gerade " liegt auf der Tastatur, die anderen nicht.
WARUM EIN ERSTER ANLAUF ZURUECKGENOMMEN WURDE
Ein gerades " ist an vielen Stellen SYNTAX und kein Schriftzeichen --
Grenze einer Zeichenkette, Grenze eines HTML-Attributs, Zeichen in
einem regulaeren Ausdruck. Wer stumpf ersetzt, macht aus
„<a href="https://… ein kaputtes Attribut
/^["'„»\s]+|["'“«.\s]+$/ einen kaputten Ausdruck
"… nichts „mal " + "eben …" eine kaputte Zeichenkette
DIE UNTERSCHEIDUNG LAEUFT AN MERKMALEN, NICHT AN EINER LISTE
< > = dazwischen -> HTML-Marke oder Attribut
endet auf Leerzeichen -> die Zeichenkette hoert hier auf, der
Satz geht in der naechsten Zeile weiter.
Ein deutsches Schlusszeichen steht NIE
hinter einem Leerzeichen.
Rueckstrich mittendrin -> regulaerer Ausdruck
${ ohne } -> mitten in einem Ausdruck
Eine Liste erlaubter Ausnahmen waere die naechste, die niemand
pflegt. Zwoelf Stellen bleiben dadurch stehen, alle zwoelf einzeln
angesehen und alle zwoelf zu Recht -- dort steht das richtige
Schlusszeichen ohnehin weiter unten im Satz.
Fuenf davon waren allerdings ECHTE Fehler HINTER dem Link
(`…>HasiDog</a>".`) -- die erste Regel hatte nur das Attribut
gesehen, nicht den Satz danach. Gezielt nachgezogen.
Ein maskiertes `\"` in workspace-vorlagen.js (28 Hooks) wird zu “ --
ohne Rueckstrich, denn “ begrenzt nichts.
KOMMENTARE BLEIBEN, WIE SIE SIND. Dort liest es niemand ausser mir;
eine Wache, die auch Kosmetik anmahnt, wird weggeklickt. Beim ersten
Messen fielen ausserdem acht Stellen aus buehne.html faelschlich an,
weil `/* */` in HTML (in <style> und <script>) nicht ausgeblendet
war -- jetzt schon.
DIE WACHE DAZU
pruef-struktur prueft es ab sofort mit derselben Regel: 322 Dateien
mit sichtbarem Text, 0 Funde, und die zwoelf bewussten Ausnahmen
werden GEZAEHLT und genannt (erlaubt: 12). Eine Ausnahme, die niemand
sieht, waechst -- und irgendwann steht der echte Fall darin.
GEPRUEFT
pruef-struktur 59 -> 68 Pruefungen, 0 Fehler
node --check auf allen geaenderten JS-Dateien
nachgemessen: 199 geaendert, 12 mit Grund stehen geblieben
gruen geblieben: bewerbung-aufgaben 163, nachwuchs 262,
reaktion 421, support 63, content 45, terminregel 35, treff 85
Und nachgesehen, ob eine Pruefung noch die alte Schreibweise
ERWARTET: 13 Fundstellen, alle dreizehn nur Text in ihrer eigenen
Ausgabe, keine einzige ein Vergleich mit dem Seitentext.
GEGENPROBE: In reaktion.html ein Schlusszeichen zurueckgedreht ->
„workspace/reaktion.html:546 „Nächstes"", mit Datei und Zeile. Und
die Erkennung einzeln gegen HTML-Attribut, fortgesetzte
Zeichenkette, regulaeren Ausdruck und eingesetzten Wert geprueft.
Stempel gesetzt: workspace 670 Verweise in 45 Dateien, oeffentlich
554 in 48 Seiten.
Co-Authored-By: Claude Opus 5 <[email protected]>
2499 lines
111 KiB
JavaScript
2499 lines
111 KiB
JavaScript
/* =====================================================================
|
||
Die Entwicklungskarte (11.09.2026)
|
||
|
||
Vier Knöpfe je Beobachtung, ein Feld für den Anlass, und oben die
|
||
Abweichungen zwischen zwei Augenpaaren. Mehr ist es nicht — und das
|
||
ist Absicht: „so einfach wie möglich, aber so professionell wie
|
||
möglich" (Filipe).
|
||
|
||
WAS HIER NICHT PASSIERT: rechnen. Es gibt keine Punktzahl, keinen
|
||
Durchschnitt, keine Ampel je Person. Eine Zahl über einem Menschen
|
||
wird gelesen, verglichen und weitererzählt — gebraucht wird ein
|
||
Gesprächsanlass.
|
||
|
||
UND KEINE RECHTEPRÜFUNG. Ob jemand Block 5 sehen darf, entscheidet
|
||
der Server: Er liefert ihn gar nicht erst aus. Was hier steht, ist
|
||
höchstens ein Abschnitt, der leer bleibt.
|
||
===================================================================== */
|
||
(() => {
|
||
const $ = (id) => document.getElementById(id);
|
||
const el = (t, k, text) => {
|
||
const n = document.createElement(t);
|
||
if (k) n.className = k;
|
||
if (text !== undefined) n.textContent = text;
|
||
return n;
|
||
};
|
||
/* EINE GUTE NACHRICHT DARF NICHT AUSSEHEN WIE EIN FEHLER
|
||
(22.09.2026). Auf dem ersten Bildschirmfoto der neuen
|
||
Aufgabenanlage stand „Clips vom Samstag schneiden steht jetzt bei
|
||
Rieke." in Warnrot -- weil `melde` immer in denselben Absatz
|
||
schreibt und der `.fehler` heisst.
|
||
|
||
Es ist nicht schlimm und genau deshalb heimtueckisch: Wer eine
|
||
Bestaetigung in Rot liest, sucht den Fehler. Und wer sich daran
|
||
gewoehnt, ueberliest den echten.
|
||
|
||
Die Vorgabe bleibt „Fehler" -- alle neun bisherigen Aufrufe meinen
|
||
das, und ein Wechsel per Vorgabe haette sie alle stillschweigend
|
||
umgefaerbt. */
|
||
const melde = (t, gut) => {
|
||
const f = $('fehler');
|
||
if (!f) return;
|
||
f.textContent = t;
|
||
f.hidden = !t;
|
||
f.dataset.gut = gut ? 'ja' : 'nein';
|
||
};
|
||
|
||
const datum = (iso) => {
|
||
if (!iso) return '';
|
||
const d = new Date(iso);
|
||
return Number.isNaN(d.getTime()) ? '' : d.toLocaleDateString('de-DE',
|
||
{ day: 'numeric', month: 'short', year: 'numeric' });
|
||
};
|
||
|
||
/* Der Abruf steht seit dem 22.09.2026 in assets/js/holen.js.
|
||
Er stand ACHTMAL da, Zeichen fuer Zeichen gleich -- und als
|
||
die Regel darin sich als falsch herausstellte (jeder 404
|
||
warf auf die Startseite, auch bei einer blossen Handlung),
|
||
haette man sie achtmal aendern muessen. */
|
||
const hole = (pfad, optionen) => window.holen(pfad, optionen);
|
||
|
||
let stufen = [];
|
||
/* Welche Erwartungsstufe gerade gefiltert ist ('' = alle).
|
||
|
||
ER LIEGT AUSSERHALB VON karte(), wie die Klappzustände: Beim
|
||
Neuzeichnen soll er nicht zurückspringen. Wer „Fortgeschritten"
|
||
gewählt hat und einen Punkt setzt, will danach immer noch
|
||
„Fortgeschritten" sehen.
|
||
|
||
DIE VORGABE IST SEIT DEM 01.10.2026 „ZUGETRAGEN" -- wenn es
|
||
etwas zu zeigen gibt.
|
||
|
||
Hier stand „Alle", und die Begründung war: „Ein Filter, der beim
|
||
Öffnen schon etwas versteckt, lässt einen Punkte suchen, die
|
||
gestern noch da waren." Der Einwand ist richtig geblieben — er
|
||
trifft nur genau EINEN Fall: den, in dem gar nichts zugetragen
|
||
ist. Dann zeigte „Zugetragen" eine leere Karte, und eine leere
|
||
Karte sieht aus wie ein Fehler.
|
||
|
||
Deshalb steht er jetzt in der BEDINGUNG statt im Weg (siehe
|
||
unten, wo `allePunkte` bekannt ist): Hat dieser Mensch
|
||
zugetragene Punkte, steht der Filter darauf. Hat er keine, steht
|
||
er auf „Alle".
|
||
|
||
`null` HEISST „FÜR DIESEN MENSCHEN NOCH NICHT ENTSCHIEDEN" und
|
||
ist etwas anderes als `''` („Alle"). */
|
||
let stufenFilter = null;
|
||
|
||
/* WELCHE KATEGORIEN OFFEN STEHEN (15.09.2026).
|
||
|
||
Filipe: „ich will auch dass man die kategorien von den fragen, also
|
||
die liste auf und zu drücken kann mit einem button."
|
||
|
||
Der Zustand liegt hier und nicht an den Elementen: Die Karte wird
|
||
nach dem ersten Klick auf einen Punkt neu gezeichnet (erst danach
|
||
darf der Stand der anderen ueberhaupt ausgeliefert werden). Waere
|
||
er an den Elementen, klappte dabei alles wieder zu -- mitten in der
|
||
Arbeit, bei jedem ersten Klick. */
|
||
let offeneBloecke = null;
|
||
let karteFuer = null;
|
||
|
||
/* Der Pfeil des Aufklappers. Als Zeichnung und nicht als Textzeichen:
|
||
„▸" sieht auf jedem Geraet anders aus und faellt bei manchen
|
||
Schriften ganz aus. */
|
||
const pfeil = () => {
|
||
const s = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||
s.setAttribute('class', 'e-block__pfeil');
|
||
s.setAttribute('viewBox', '0 0 24 24');
|
||
s.setAttribute('aria-hidden', 'true');
|
||
const p = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||
p.setAttribute('d', 'M9 6l6 6-6 6');
|
||
s.append(p);
|
||
return s;
|
||
};
|
||
let offen = null; // die gerade geöffnete Person
|
||
/* ALLE Aufgaben des Hauses -- fuer den Vorlagenkatalog weiter unten.
|
||
Er markiert daran, was schon uebernommen wurde.
|
||
|
||
Sie stehen hier und werden nicht ein zweites Mal geholt:
|
||
"aufgabenZeigen" ruft dieselbe Liste ohnehin ab, um die einer
|
||
Person herauszufiltern. Ein zweiter Abruf waere Arbeit fuer den
|
||
Server und ein zweiter Stand im Browser -- und zwei Staende
|
||
derselben Sache laufen auseinander. */
|
||
let alleAufgabenV = [];
|
||
|
||
/* DIE ZAHL AUF DER PERSONENKACHEL LEBT WOANDERS ALS DIE IN DER KARTE
|
||
(15.09.2026).
|
||
|
||
Filipe: „und wieso steht bei vanvan 0 von 28 obwohl ich alle durch
|
||
habe."
|
||
|
||
Weil die Kachel aus /entwicklung/lage kommt -- EINMAL, beim Laden
|
||
der Seite. Die Karte holt sich ihren Stand bei jedem Öffnen neu.
|
||
Wer 28 Punkte durchklickt, sieht in der Karte 28 von 28 und auf der
|
||
Kachel darüber weiterhin die Zahl von vorhin. Zwei Wahrheiten auf
|
||
einem Bildschirm, und die falsche steht oben.
|
||
|
||
Die Liste deshalb NICHT neu zu holen ist Absicht: Jeder Neuaufbau
|
||
bewegt die Seite, und genau darüber hat Filipe sich im selben
|
||
Atemzug beschwert. Stattdessen wandert die Zahl mit. */
|
||
const personZahlen = new Map(); // Personennummer -> Element mit „x von 28“
|
||
let letzteKarte = null; // { id, daten } der gerade offenen Karte
|
||
|
||
/** Alle sichtbaren Zähler auf den Stand bringen, den die Karte hat.
|
||
* Ohne Neuladen, ohne Sprung. */
|
||
function zaehlerNachziehen() {
|
||
if (!letzteKarte) return;
|
||
const r = letzteKarte.daten;
|
||
const alle = r.bloecke.flatMap((b) => b.punkte);
|
||
/* GEZAEHLT WIRD DAS ZUGETRAGENE (25.09.2026).
|
||
|
||
Filipe: „es sollen nur die menge angezeigt werden die wir
|
||
zutragen und erledigte dan auch nur die die erledigt wurden von
|
||
den zugetragenen."
|
||
|
||
BEIDE HAELFTEN MUESSEN MITZIEHEN. `r.gesamt` ist seit heute die
|
||
Zahl der zugetragenen Aufgaben, nicht mehr 68. Zaehlte die linke
|
||
Zahl weiter ALLE Einschaetzungen, stuende an einer Kachel
|
||
„13 von 3" -- eine Zahl, die groesser ist als ihr Ganzes, und
|
||
die niemand mehr erklaeren kann. An dieser Stelle faellt so
|
||
etwas besonders leicht durch: Der Ring rechnet mit derselben
|
||
Zahl weiter. */
|
||
const gesetzt = alle.filter((pt) => pt.zugetragen && pt.meins).length;
|
||
const satz = r.gesamt
|
||
? `${gesetzt} von ${r.gesamt} erledigt`
|
||
: 'noch nichts zugetragen';
|
||
|
||
const kopf = document.querySelector('#karte .e-karte__stand');
|
||
if (kopf) kopf.textContent = satz;
|
||
const kachel = personZahlen.get(letzteKarte.id);
|
||
if (kachel) {
|
||
/* DER RING WANDERT MIT (24.09.2026).
|
||
|
||
Hier standen bis heute die zwei Ziffern der Bilanz-Kaesten.
|
||
Die gibt es nicht mehr -- die Kachel traegt jetzt einen Ring
|
||
und darunter "46 von 68 angesehen". Beides muss sich beim
|
||
Durchklicken bewegen, sonst ist genau der Fehler wieder da,
|
||
wegen dem diese Funktion ueberhaupt entstanden ist: Die Karte
|
||
zeigt den neuen Stand, die Kachel darueber den von vorhin. */
|
||
const anteilNeu = r.gesamt > 0 ? Math.round(gesetzt / r.gesamt * 100) : 0;
|
||
kachel.bogen.setAttribute('stroke-dasharray',
|
||
Math.max(0, Math.min(100, anteilNeu)) + ' 100');
|
||
kachel.ringZahl.textContent = r.gesamt > 0 ? String(anteilNeu) : '–';
|
||
kachel.ring.setAttribute('aria-label', r.gesamt > 0
|
||
? `${gesetzt} von ${r.gesamt} erledigt (${anteilNeu} Prozent)`
|
||
: 'noch nichts zugetragen');
|
||
kachel.standZahl.textContent = String(gesetzt);
|
||
/* Fertig ist ein ruhiger voller Ring -- kein Gruen, keine Feier:
|
||
Es ist der Normalzustand, nicht ein Preis. */
|
||
if (r.gesamt > 0 && gesetzt >= r.gesamt) {
|
||
kachel.ring.dataset.voll = 'ja';
|
||
kachel.stand.dataset.voll = 'ja';
|
||
} else {
|
||
delete kachel.ring.dataset.voll;
|
||
delete kachel.stand.dataset.voll;
|
||
}
|
||
/* Und der Punkt bei null verschwindet, sobald der Erste sitzt. */
|
||
if (anteilNeu <= 0) kachel.ring.dataset.leer = 'ja';
|
||
else delete kachel.ring.dataset.leer;
|
||
}
|
||
|
||
/* Und die Köpfe der Kategorien -- sonst steht zugeklappt „0 / 7“
|
||
über sieben beantworteten Fragen. */
|
||
for (const b of r.bloecke) {
|
||
const abschnitt = document.querySelector(`.e-block[data-block="${b.schluessel}"]`);
|
||
if (!abschnitt) continue;
|
||
/* MIT DEMSELBEN FILTER WIE BEIM ZEICHNEN (17.09.2026). Ohne
|
||
diese drei Zeilen zieht der Zaehler nach jedem Klick auf die
|
||
UNGEFILTERTE Zahl zurueck -- der Kopf sagte dann "3 / 14",
|
||
waehrend vier Zeilen darunter stehen. Und man zweifelt an der
|
||
Zahl, nicht am Filter. */
|
||
const punkte = punkteGefiltert(b.punkte);
|
||
const n = punkte.filter((pt) => pt.meins).length;
|
||
const haktHier = punkte.filter((pt) => pt.meins?.stufe === 'hakt').length;
|
||
const stand = abschnitt.querySelector('.e-block__stand');
|
||
if (stand) {
|
||
stand.textContent = `${n} / ${punkte.length}`;
|
||
if (n === punkte.length) stand.dataset.fertig = 'ja';
|
||
else delete stand.dataset.fertig;
|
||
}
|
||
if (n === punkte.length) abschnitt.dataset.fertig = 'ja';
|
||
else delete abschnitt.dataset.fertig;
|
||
const hakt = abschnitt.querySelector('.e-block__hakt');
|
||
if (hakt) {
|
||
hakt.hidden = !haktHier;
|
||
const zahl = hakt.querySelector('.e-block__hakt-zahl');
|
||
if (zahl) zahl.textContent = `${haktHier} ×`;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ---- Die vier Knöpfe ------------------------------------------------
|
||
Sie stehen immer in derselben Reihenfolge, auch wenn einer gesetzt
|
||
ist. Ein Knopfsatz, der sich umsortiert, sobald man ihn benutzt,
|
||
ist der sicherste Weg, beim zweiten Mal danebenzutippen. */
|
||
function knopfsatz(aktuell, waehle) {
|
||
const reihe = el('div', 'e-wahl');
|
||
for (const s of stufen) {
|
||
const b = el('button', 'e-wahl__knopf');
|
||
b.type = 'button';
|
||
b.dataset.stufe = s.schluessel;
|
||
b.setAttribute('aria-pressed', String(aktuell === s.schluessel));
|
||
b.title = s.text;
|
||
b.append(el('span', 'e-wahl__zeichen', s.zeichen));
|
||
b.append(el('span', 'e-wahl__name', s.name));
|
||
b.addEventListener('click', () => waehle(s.schluessel, b));
|
||
reihe.append(b);
|
||
}
|
||
return reihe;
|
||
}
|
||
|
||
/* ---- Die Ampel ------------------------------------------------------ */
|
||
async function ampel() {
|
||
const ziel = $('ampel');
|
||
let a;
|
||
try { a = await hole('/workspace/api/entwicklung/ampel'); } catch { return; }
|
||
if (!a.ok) return; // nicht jede Rolle hat sie
|
||
const r = await a.json();
|
||
ziel.textContent = '';
|
||
ziel.hidden = false;
|
||
ziel.dataset.lage = r.sagbar ? r.lage : 'still';
|
||
ziel.append(el('p', 'e-ampel__satz', r.sagbar ? r.text : r.grund));
|
||
if (r.sagbar) {
|
||
ziel.append(el('p', 'e-ampel__zusatz',
|
||
`${r.geantwortet} von ${r.team} im Team haben die Fragen schon beantwortet. `
|
||
+ 'Namen stehen hier nicht – auch für dich nicht.'));
|
||
}
|
||
}
|
||
|
||
/* ---- Die Personen --------------------------------------------------- */
|
||
/* DER BILANZ-KASTEN IST HIER WEG (24.09.2026).
|
||
|
||
Er stand seit dem 17.09. da und war damals richtig: Die Zahlen
|
||
auf der Kachel sahen anders aus als drueben auf der Checkliste,
|
||
und `bilanz-zahl` aus start.css hat das eingeebnet.
|
||
|
||
Die Kachel traegt seit heute keine drei Zahlenkaesten mehr,
|
||
sondern einen Ring, ihre offenen Aufgaben und eine feine Zeile
|
||
(siehe personKarte weiter unten). Damit hatte diese Funktion
|
||
keinen einzigen Aufrufer mehr.
|
||
|
||
STEHENGELASSEN HAETTE SIE GESCHADET: Wer sie beim naechsten Mal
|
||
findet, haelt sie fuer den Weg, wie Zahlen auf dieser Seite
|
||
aussehen -- und baut damit neben dem neuen Aufbau einen zweiten
|
||
auf. `bilanz-zahl` selbst bleibt unangetastet; die Checkliste
|
||
und die Startseite benutzen sie weiter. */
|
||
|
||
/* =====================================================================
|
||
DIE AUFGABEN AUF DER KACHEL (24.09.2026)
|
||
=====================================================================
|
||
|
||
Filipe: "die aufgaben die wir verteilen sollen da auch zu sehen
|
||
sein und so."
|
||
|
||
Er verteilt sie eine Handbreit weiter unten -- und die Kacheln
|
||
darueber wussten nichts davon. Wer wissen wollte, ob bei jemandem
|
||
schon etwas liegt, musste ihn erst anklicken, und dann stand es
|
||
unter der Karte. Sechs Leute hiessen sechs Klicks.
|
||
|
||
GERECHNET WIRD AUS `alleAufgabenV` -- derselben Liste, aus der
|
||
auch die Liste unter der Karte und der Vorlagenkatalog ihre Zahlen
|
||
nehmen. Ein zweiter Abruf waere eine zweite Wahrheit ueber
|
||
dieselbe Sache, und die, die beim naechsten Verteilen nicht
|
||
mitwandert.
|
||
|
||
WER "MEINE" AUFGABE IST, entscheidet dieselbe Regel wie unter der
|
||
Karte: verantwortlich ODER zugeteilt und nicht abgelehnt. Eine
|
||
eigene Regel an dieser Stelle waere zwei Antworten auf dieselbe
|
||
Frage, eine Handbreit auseinander.
|
||
===================================================================== */
|
||
|
||
/** Was liegt bei dieser Person? -- in die Zeile an ihrer Kachel.
|
||
*
|
||
* IST DIE LISTE NOCH NICHT DA, bleibt die Zeile weg, statt "0
|
||
* offen" zu behaupten. Eine Null, die in Wahrheit "ich weiss es
|
||
* noch nicht" heisst, ist schlimmer als gar nichts -- man glaubt
|
||
* ihr ja. */
|
||
function auftragZeichnen(zeile, personId) {
|
||
if (!zeile) return;
|
||
zeile.textContent = '';
|
||
const nr = Number(personId);
|
||
const alle = Array.isArray(alleAufgabenV) ? alleAufgabenV : [];
|
||
if (!alle.length || !nr) { zeile.hidden = true; return; }
|
||
|
||
const heute = typeof window.heuteLokal === 'function'
|
||
? window.heuteLokal() : new Date().toISOString().slice(0, 10);
|
||
const meine = alle.filter((x) => x.status !== 'erledigt' && x.status !== 'abgebrochen')
|
||
.filter((x) => Number(x.verantwortlich_id) === nr
|
||
|| (x.zuteilung || []).some((z) => Number(z.person_id) === nr
|
||
&& z.zustand !== 'abgelehnt'));
|
||
|
||
zeile.hidden = false;
|
||
delete zeile.dataset.spaet;
|
||
if (!meine.length) {
|
||
/* AUCH "NICHTS" IST EINE ANTWORT -- und zwar die, nach der man
|
||
beim Verteilen sucht. Die Zeile bleibt deshalb stehen, nur
|
||
leise: Waere sie weg, saehe die Kachel aus, als fehle dort
|
||
eine Auskunft. */
|
||
zeile.dataset.leer = 'ja';
|
||
zeile.append(el('span', 'e-person__auftrag-leer', 'keine offene Aufgabe'));
|
||
return;
|
||
}
|
||
delete zeile.dataset.leer;
|
||
|
||
const spaet = meine.filter((x) => x.frist && x.frist < heute);
|
||
if (spaet.length) zeile.dataset.spaet = 'ja';
|
||
|
||
const kopf = el('span', 'e-person__auftrag-kopf');
|
||
kopf.append(el('span', 'e-person__auftrag-zahl', String(meine.length)));
|
||
kopf.append(el('span', 'e-person__auftrag-wort',
|
||
meine.length === 1 ? 'offene Aufgabe' : 'offene Aufgaben'));
|
||
if (spaet.length) {
|
||
kopf.append(el('span', 'e-person__auftrag-spaet',
|
||
spaet.length === 1 ? '1 überfällig' : `${spaet.length} überfällig`));
|
||
}
|
||
zeile.append(kopf);
|
||
|
||
/* DIE NAECHSTE MIT NAMEN, nicht nur als Zahl. "3 offen" sagt, dass
|
||
etwas da ist; "Clips vom Samstag schneiden, seit 2 Tagen
|
||
ueberfaellig" sagt, worum es geht -- und genau danach fragt man,
|
||
bevor man jemanden anspricht.
|
||
|
||
Ueberfaellige zuerst, dann nach Frist, und was keine Frist hat,
|
||
steht hinten: Eine Aufgabe ohne Termin draengt nicht. Dieselbe
|
||
Reihenfolge wie in der Liste unter der Karte. */
|
||
const naechste = [...meine].sort((x, y) =>
|
||
String(x.frist || '9999-99-99').localeCompare(String(y.frist || '9999-99-99')))[0];
|
||
if (naechste) {
|
||
const z = el('span', 'e-person__auftrag-naechste');
|
||
z.append(el('span', 'e-person__auftrag-titel', naechste.titel || 'Ohne Titel'));
|
||
if (naechste.frist) {
|
||
const wann = el('span', 'e-person__auftrag-frist', fristWort(naechste.frist, heute));
|
||
if (naechste.frist < heute) wann.dataset.spaet = 'ja';
|
||
z.append(wann);
|
||
}
|
||
zeile.append(z);
|
||
}
|
||
}
|
||
|
||
/** "heute fällig", "in 2 Tagen", "seit 3 Tagen überfällig".
|
||
*
|
||
* EIN DATUM MUSS MAN GEGEN HEUTE RECHNEN, ein Satz nicht. "bis
|
||
* 22. Sep. 2026" zwingt zum Nachdenken; "seit 2 Tagen überfällig"
|
||
* nicht.
|
||
*
|
||
* Gerechnet wird in ganzen Tagen ueber die Ortszeit --
|
||
* `window.heuteLokal()` ist die Stelle im Haus, die das richtig
|
||
* macht (Mitternacht hier, nicht in Greenwich). */
|
||
function fristWort(frist, heute) {
|
||
const a = new Date(`${frist}T00:00:00`);
|
||
const b = new Date(`${heute}T00:00:00`);
|
||
if (Number.isNaN(a.getTime()) || Number.isNaN(b.getTime())) return datum(frist);
|
||
const tage = Math.round((a - b) / 86400000);
|
||
if (tage === 0) return 'heute fällig';
|
||
if (tage === 1) return 'morgen fällig';
|
||
if (tage === -1) return 'seit gestern überfällig';
|
||
if (tage > 1) return `in ${tage} Tagen`;
|
||
return `seit ${Math.abs(tage)} Tagen überfällig`;
|
||
}
|
||
|
||
/** Alle Aufgabenzeilen der Kacheln auf den neuesten Stand bringen.
|
||
*
|
||
* Steht in `aufgabenHolen()` und nicht an den vier Aufrufstellen:
|
||
* Wer die Liste neu holt, hat auch den neuen Stand -- und eine
|
||
* Stelle, die das vergisst, gibt es dann nicht. */
|
||
function auftraegeNachziehen() {
|
||
for (const zeile of document.querySelectorAll('#personen .e-person__auftrag')) {
|
||
auftragZeichnen(zeile, zeile.dataset.fuer);
|
||
}
|
||
}
|
||
|
||
function personKarte(p, lage) {
|
||
const k = el('button', 'e-person');
|
||
k.type = 'button';
|
||
/* JEDE PERSON IST EINZELN WAEHLBAR -- UND MAN SIEHT ES.
|
||
Vorher war die Kachel ein Knopf ohne Zustand: Man klickte jemanden
|
||
an, die Karte ging auf, und die Kachel sah aus wie vorher. Wer
|
||
dann scrollte, wusste nicht mehr, wen er offen hat. */
|
||
k.setAttribute('aria-pressed', 'false');
|
||
k.dataset.person = String(p.id);
|
||
/* Der Name als Angabe, nicht nur als Text: Das Verteil-Band nennt
|
||
ihn ("Aufgabe fuer Kessi"), und `textContent` der ganzen Kachel
|
||
waere Name + Rolle + alles, was sonst noch draufsteht. */
|
||
k.dataset.name = p.name;
|
||
|
||
/* DAS GANZE IST DAS ZUGETRAGENE (25.09.2026).
|
||
|
||
Filipe: „es sollen nur die menge angezeigt werden die wir
|
||
zutragen." Hier stand `lage.gesamt_fremd` -- die 68 des
|
||
Katalogs, fuer jeden dieselbe Zahl. Bei jemandem, dem drei
|
||
Aufgaben zugetragen sind, sagte die Kachel „0 von 68" und der
|
||
Ring 0 Prozent, obwohl es um drei geht.
|
||
|
||
`p.gesetzt` ZAEHLT SEIT HEUTE NUR ZUGETRAGENES (der Verbund im
|
||
Server) -- sonst stuende hier „13 von 3", eine Zahl groesser als
|
||
ihr Ganzes. */
|
||
const gesamt = Number(p.zugeteilt) || 0;
|
||
const gesetzt = Number(p.gesetzt) || 0;
|
||
const anteil = gesamt > 0 ? Math.round(gesetzt / gesamt * 100) : 0;
|
||
|
||
/* ==================================================================
|
||
DIE STANDKARTE (24.09.2026)
|
||
==================================================================
|
||
|
||
Filipe: "mach das bitte viel krasser und viel geiler. die
|
||
aufgaben die wir verteilen sollen da auch zu sehen sein und so.
|
||
viel uebersichtlicher, viel moderner."
|
||
|
||
WAS VORHER DASTAND: sechs Kacheln mit je drei Zahlenkaesten --
|
||
und bei fuenf davon exakt dieselben drei Zahlen: 0 angesehen,
|
||
68 noch offen, 0 verschieden gesehen. Achtzehn Kaesten fuer eine
|
||
einzige Auskunft, naemlich: hier ist noch nichts passiert. Und
|
||
die Aufgaben, die Filipe eine Handbreit darunter verteilt,
|
||
kamen auf der Kachel derselben Person gar nicht vor.
|
||
|
||
DIE FRAGE AN DIESE LISTE ist nicht "wie viele Punkte hat wer".
|
||
Sie lautet: WEN NEHME ICH MIR JETZT VOR? Darauf antworten drei
|
||
Dinge, und in dieser Reihenfolge stehen sie jetzt da:
|
||
|
||
1. WER -- Ring, Name, Rolle. Der Ring traegt den Anteil in
|
||
seiner Mitte: Form und Zahl an derselben Stelle, statt
|
||
eines Balkens ueber die Breite und dreier Kaesten darunter.
|
||
2. WAS LIEGT AN -- offene Aufgaben, ueberfaellige, und die
|
||
naechste mit Namen und Frist. Der Teil, den Filipe
|
||
vermisst hat.
|
||
3. WIE WEIT -- "46 von 68 angesehen" als feine Zeile, und
|
||
"verschieden gesehen" nur dann, wenn es dort etwas gibt.
|
||
|
||
DIE DREI BILANZ-KAESTEN SIND WEG, nicht verschoben. Zwei davon
|
||
("angesehen", "noch offen") waren zwei Seiten derselben Zahl --
|
||
zusammen ergaben sie immer das Ganze. Der Ring zeigt dasselbe
|
||
ohne Rechnung, und "46 von 68" nennt beide Zahlen in einer
|
||
Zeile. Die dritte steht nur noch da, wenn sie etwas sagt.
|
||
================================================================= */
|
||
const kopf = el('span', 'e-person__kopf');
|
||
|
||
/* DER RING. Umfang 100 -- deshalb laesst sich der Anteil in Prozent
|
||
unmittelbar als Strichlaenge setzen ("68 100"), ohne Umrechnung
|
||
und ohne eine Grafik je Stand. r = 100 / (2 * PI) = 15,9155.
|
||
|
||
KEINE AMPELFARBE. Was "genug" ist, haengt davon ab, wie lange
|
||
jemand dabei ist; eine Schwelle waere geraten und bei der
|
||
naechsten Person falsch. Der Ring sagt WIE WEIT und urteilt
|
||
nicht. Geurteilt wird nur dort, wo es einen Massstab gibt, der
|
||
nicht geraten ist: bei einer ueberfaelligen Frist. */
|
||
const ring = el('span', 'e-ring');
|
||
ring.setAttribute('role', 'img');
|
||
ring.setAttribute('aria-label', gesamt > 0
|
||
? `${gesetzt} von ${gesamt} erledigt (${anteil} Prozent)`
|
||
: 'noch nichts zugetragen');
|
||
if (gesamt > 0 && gesetzt >= gesamt) ring.dataset.voll = 'ja';
|
||
/* BEI NULL PROZENT DARF NICHTS ZU SEHEN SEIN (gemessen am
|
||
Bildschirmfoto). Der Bogen hat runde Enden -- damit zeichnet
|
||
schon "0 100" einen kleinen Punkt auf zwoelf Uhr. Der sieht aus
|
||
wie ein Staubkorn auf dem Bildschirm oder wie ein Rest vom
|
||
Zeichnen, nicht wie "hier ist noch nichts". Die Ecke wird
|
||
deshalb stumpf, solange der Anteil null ist. */
|
||
if (anteil <= 0) ring.dataset.leer = 'ja';
|
||
const SVG_NS = 'http://www.w3.org/2000/svg';
|
||
const bild = document.createElementNS(SVG_NS, 'svg');
|
||
bild.setAttribute('viewBox', '0 0 36 36');
|
||
bild.setAttribute('aria-hidden', 'true');
|
||
bild.setAttribute('class', 'e-ring__bild');
|
||
const spur = document.createElementNS(SVG_NS, 'circle');
|
||
const bogen = document.createElementNS(SVG_NS, 'circle');
|
||
for (const [kreis, klasse] of [[spur, 'e-ring__spur'], [bogen, 'e-ring__bogen']]) {
|
||
kreis.setAttribute('class', klasse);
|
||
kreis.setAttribute('cx', '18');
|
||
kreis.setAttribute('cy', '18');
|
||
kreis.setAttribute('r', '15.9155');
|
||
kreis.setAttribute('fill', 'none');
|
||
bild.append(kreis);
|
||
}
|
||
bogen.setAttribute('stroke-dasharray', Math.max(0, Math.min(100, anteil)) + ' 100');
|
||
ring.append(bild);
|
||
const ringZahl = el('span', 'e-ring__zahl', gesamt > 0 ? String(anteil) : '–');
|
||
ring.append(ringZahl);
|
||
if (gesamt > 0) ring.append(el('span', 'e-ring__prozent', '%'));
|
||
kopf.append(ring);
|
||
|
||
const wer = el('span', 'e-person__wer');
|
||
wer.append(el('span', 'e-person__name', p.name));
|
||
wer.append(el('span', 'e-person__rolle', p.rolle_name));
|
||
kopf.append(wer);
|
||
k.append(kopf);
|
||
|
||
/* ==== WAS LIEGT BEI DIESER PERSON? ============================
|
||
|
||
Der Platz wird hier angelegt und erst gefuellt, wenn die
|
||
Aufgabenliste da ist: Die Kacheln entstehen frueher als der
|
||
Abruf. Stuende hier der Stand von jetzt, waere die Zeile beim
|
||
ersten Aufbau immer leer -- und fuellte sich erst beim
|
||
naechsten Klick. `aufgabenHolen()` zieht sie nach. */
|
||
const auftrag = el('span', 'e-person__auftrag');
|
||
auftrag.dataset.fuer = String(p.id);
|
||
k.append(auftrag);
|
||
auftragZeichnen(auftrag, p.id);
|
||
|
||
/* ==== DER FUSS: die feine Zeile ============================== */
|
||
const fuss = el('span', 'e-person__fuss');
|
||
const stand = el('span', 'e-person__stand');
|
||
const standZahl = el('span', 'e-person__stand-zahl', gesamt ? String(gesetzt) : '');
|
||
stand.append(standZahl);
|
||
/* NULL ZUGETRAGEN IST KEINE NULL, SONDERN EIN SATZ. „0 von 0
|
||
erledigt" liest sich wie ein Versaeumnis; „noch nichts
|
||
zugetragen" sagt, was zu tun ist -- und wer es zu tun hat. */
|
||
stand.append(el('span', 'e-person__stand-rest', gesamt
|
||
? ` von ${gesamt} erledigt`
|
||
: 'noch nichts zugetragen'));
|
||
if (gesamt > 0 && gesetzt >= gesamt) stand.dataset.voll = 'ja';
|
||
fuss.append(stand);
|
||
|
||
/* "verschieden gesehen" ist die einzige Zahl dieser Seite, die zu
|
||
einer HANDLUNG fuehrt -- dort lohnt das Gespraech. Deshalb faellt
|
||
sie auf, sobald es etwas gibt. Steht dort eine Null, steht sie
|
||
gar nicht mehr da: Eine Null, die aussieht wie eine Warnung, ist
|
||
Laerm -- und bei sechs Leuten sechsmal derselbe Laerm. */
|
||
const abw = Number(p.abweichungen) || 0;
|
||
if (abw) {
|
||
const marke = el('span', 'e-person__abw', `${abw} verschieden gesehen`);
|
||
marke.title = 'Hier lohnt das Gespräch – nicht dort, wo ihr einig seid.';
|
||
fuss.append(marke);
|
||
}
|
||
if (p.veraltet) {
|
||
const z = el('span', 'e-person__alt',
|
||
`${p.veraltet} älter als ${lage.verblasst_nach} Tage`);
|
||
z.title = 'Eine alte Einschätzung ist keine Einschätzung mehr.';
|
||
fuss.append(z);
|
||
}
|
||
k.append(fuss);
|
||
|
||
/* Die Kachel merkt sich ihre beweglichen Teile -- daran zieht
|
||
zaehlerNachziehen() sie nach, ohne die Liste neu zu holen. Ohne
|
||
das stuende auf der Kachel weiter die Zahl von vorhin, waehrend
|
||
die Karte darunter schon die neue zeigt: zwei Wahrheiten auf
|
||
einem Bildschirm, und die falsche oben. */
|
||
personZahlen.set(p.id, { ring, bogen, ringZahl, stand, standZahl });
|
||
|
||
/* KEIN SPRUNG MEHR (23.09.2026) -- die Seite ist umgeraeumt.
|
||
|
||
Hier stand "Wer eine Person anklickt, will zu ihrer Karte", und
|
||
das war richtig, SOLANGE die Karte direkt unter der Auswahl
|
||
stand. Seit die Seite nach dem Arbeitsablauf geordnet ist,
|
||
liegen dazwischen das Verteil-Formular, der Katalog und ihre
|
||
Aufgaben -- ein Sprung zur Karte wuerde also ueber genau die
|
||
drei Dinge hinwegspringen, die man nach der Wahl zuerst braucht.
|
||
|
||
Filipe, 15.09.2026: "die seite soll sich nicht immer bewegen
|
||
wenn ich auf was druecke."
|
||
|
||
Der Sprung bleibt dort, wo er gemeint war: Wer von der
|
||
Talentseite mit ?zeigen= kommt, will genau diese eine Karte --
|
||
dafuer ruft der Start weiter unten karte(wen, true). */
|
||
/* EIN ZWEITER DRUCK AUF DIESELBE PERSON MACHT WIEDER ZU
|
||
(24.09.2026). Filipe: "ich will das wenn ich wieder auf die
|
||
person drücke die liste unten dan wieder zu geht."
|
||
|
||
Dasselbe Muster, das die Stufenleiste weiter unten schon hat
|
||
("Ein zweiter Klick auf dieselbe Stufe hebt den Filter auf") --
|
||
nicht ein zweites, neu erfundenes. Der Weg zurueck ist derselbe
|
||
Knopf wie der Weg hin; einen eigenen Schliessen-Knopf daneben
|
||
zu setzen waere ein zweites Ziel fuer dieselbe Absicht.
|
||
|
||
Der Vergleich als Zeichenkette, weil `offen` die Kennung aus
|
||
der Antwort traegt und `p.id` aus der Liste -- mal Zahl, mal
|
||
Text, und "3" === 3 ist falsch. */
|
||
k.addEventListener('click', () => {
|
||
if (offen !== null && String(offen) === String(p.id)) zumachen();
|
||
else karte(p.id);
|
||
});
|
||
/* VON AUSSEN AUF EINE KARTE (17.09.2026).
|
||
|
||
Die Talentseite verweist seit heute hierher -- bis dahin oeffnete
|
||
sich eine Karte NUR durch Klick auf ihre Kachel. Wer dem Link
|
||
folgte, landete auf einer Liste und musste die Person suchen;
|
||
genau der Schritt, den der Link ersparen sollte.
|
||
|
||
`?zeigen=` ist der vorhandene Weg (kopf.js), nicht ein zweiter.
|
||
Er wird an der Kachel ausgewertet, waehrend sie entsteht --
|
||
vorher gibt es sie nicht, und ein Sprung ins Nichts scrollt
|
||
kommentarlos an den Seitenanfang. */
|
||
if (window.zielSchluessel?.() === `person-${p.id}`) {
|
||
gesucht = k;
|
||
}
|
||
return k;
|
||
}
|
||
|
||
/* Welche Kachel von aussen gesucht wurde (`?zeigen=person-…`).
|
||
Sie wird beim Bauen gemerkt und erst danach angesprungen -- vorher
|
||
haengt sie nicht im Dokument und hat keine Position. */
|
||
let gesucht = null;
|
||
|
||
/* ---- Die Karte einer Person ----------------------------------------- */
|
||
/* DIE SEITE BEWEGT SICH NUR NOCH, WENN MAN ES WILL (15.09.2026).
|
||
|
||
Filipe: „immer wenn ich auf was drücke dan geht das fenster wieder
|
||
hoch. die seite soll sich nicht immer wieder bewegen wenn ich auf
|
||
was drücke."
|
||
|
||
Hier stand ein `scrollIntoView` ohne Bedingung -- und diese Funktion
|
||
wird an VIER Stellen gerufen: beim Öffnen einer Person (da ist es
|
||
richtig), nach dem ersten Klick auf einen Punkt, beim Aufklappen
|
||
aller Kategorien und nach dem Anlegen eines Schritts. Dreimal von
|
||
vier war es falsch: Man tippt unten auf „Läuft“ und steht wieder
|
||
ganz oben.
|
||
|
||
`springen` ist deshalb VORGABE NEIN. Wer springen will, sagt es --
|
||
und das tut genau eine Stelle. Und selbst ohne Sprung wird die
|
||
Höhe gehalten: Der Neuaufbau kann die Karte kürzer oder länger
|
||
machen (eine Zeile „Schritt läuft“ kommt dazu), und dann rutscht
|
||
alles darunter weg. */
|
||
/* DAS VERTEIL-BAND (22.09.2026).
|
||
------------------------------------------------------------------
|
||
Es steht nur da, wenn die Person auch verteilen darf -- gefragt
|
||
wird `darf_verteilen` vom Server (ueber window.__ich, das kopf.js
|
||
ohnehin holt). Ein Rollenname stuende hier in einer Datei, die
|
||
jeder herunterlaedt, und waere am Tag der naechsten Aenderung
|
||
falsch. */
|
||
function verteilBandZeigen(personId, name) {
|
||
const band = $('verteilen-band');
|
||
if (!band) return;
|
||
/* `window.__ich` KOMMT SPAETER ALS DIESE SEITE (22.09.2026).
|
||
----------------------------------------------------------------
|
||
kopf.js holt die Auskunft ueber das Netz; beim ersten Aufruf hier
|
||
steht sie noch nicht. Gemessen: Das Band blieb unsichtbar, bis
|
||
man eine Person anklickte -- also genau dann, wenn man es zum
|
||
ersten Mal gebraucht haette.
|
||
|
||
Ein festes `setTimeout` waere eine Zahl aus dem Kopf. Gewartet
|
||
wird stattdessen darauf, dass die Auskunft DA ist, und laengstens
|
||
drei Sekunden. */
|
||
if (window.__ich === undefined && window.wennIchDaBin) {
|
||
/* Benutzt seit dem 22.09.2026 die gemeinsame Wartestelle aus
|
||
kopf.js. Vorher stand hier ein eigenes setInterval -- dasselbe
|
||
Warten ein zweites Mal geschrieben, und damit die Fassung, die
|
||
beim naechsten Umbau nur halb nachgezogen wird. */
|
||
window.wennIchDaBin().then((da) => {
|
||
if (da) verteilBandZeigen(personId, name);
|
||
});
|
||
return;
|
||
}
|
||
const darf = window.__ich?.darf_verteilen === true;
|
||
band.hidden = !darf;
|
||
if (!darf) return;
|
||
const knopf = $('verteilen-knopf');
|
||
const satz = $('verteilen-satz');
|
||
/* WER UND WIE HEISST ER -- fuer das Formular darunter. Es steht
|
||
hier und nicht im Formular selbst: Die Auswahl geschieht
|
||
oberhalb, und zwei Stellen, die sich dieselbe Person merken,
|
||
laufen auseinander. */
|
||
fuerPerson = personId || null;
|
||
fuerName = name || null;
|
||
|
||
/* DER KNOPF IST NIE AUS (22.09.2026 abends).
|
||
|
||
Vorher: Ohne vorherige Auswahl war er abgeschaltet, und
|
||
daneben stand „Wähl links jemanden aus". Gemessen steht die
|
||
Auswahl aber gar nicht links, sondern 206 px WEITER UNTEN, bei
|
||
derselben linken Kante. Wer der Anweisung folgte, schaute auf
|
||
eine leere Fläche -- und hatte einen Knopf, der nicht ging.
|
||
|
||
Filipe: „weil gerade hängt das und weiß nicht wieso."
|
||
|
||
Ein toter Knopf mit einer falschen Erklärung daneben ist die
|
||
schlechteste Kombination: Man sucht den Fehler bei sich. Die
|
||
Auswahl steht jetzt IM Formular; der Knopf öffnet es immer.
|
||
Wer unten schon jemanden angeklickt hat, findet ihn angehakt
|
||
wieder. */
|
||
knopf.disabled = false;
|
||
if (personId) {
|
||
knopf.textContent = name ? `Aufgabe für ${name}` : 'Aufgabe anlegen';
|
||
satz.textContent = name
|
||
? `Sie landet direkt bei ${name} — mit einer Meldung.`
|
||
: 'Sie landet direkt bei der gewählten Person.';
|
||
} else {
|
||
knopf.textContent = 'Aufgabe anlegen';
|
||
satz.textContent = 'Aufgabe anlegen und an eine oder mehrere Personen geben.';
|
||
}
|
||
/* Steht das Formular schon offen, zieht die Auswahl darin mit --
|
||
sonst haette man unten jemanden angeklickt und oben stuende
|
||
weiterhin der alte Haken. */
|
||
if (!$('e-neu')?.hidden) fuerLeuteBauen();
|
||
}
|
||
|
||
/* =====================================================================
|
||
AUFGABEN ANLEGEN -- HIER, NICHT AUF EINER ANDEREN SEITE (22.09.2026)
|
||
=====================================================================
|
||
Filipe: „dieser buttion da soll nicht einen zu der seite aufgaben
|
||
fuehren sondern da in dieser seite die aufgaben erstellen und
|
||
vergeben koennen. plus man soll die aufgaben hier in dieser seite
|
||
auch sehen."
|
||
|
||
DREI FELDER, NICHT ZWOELF. Wer hier steht, hat die Person schon
|
||
gewaehlt; was fehlt, ist was, bis wann und ob noch jemand
|
||
mitmacht. Das grosse Formular auf dem Brett bleibt, wo es ist --
|
||
fuer den Fall, dass man eine Aufgabe fuer irgendwen irgendwo
|
||
anlegt.
|
||
===================================================================== */
|
||
let fuerPerson = null;
|
||
let fuerName = null;
|
||
let mehrLeute = [];
|
||
let leute = [];
|
||
|
||
function formularZu() {
|
||
const f = $('e-neu');
|
||
if (!f) return;
|
||
f.hidden = true;
|
||
const auf = $('verteilen-knopf');
|
||
if (auf) auf.setAttribute('aria-expanded', 'false');
|
||
}
|
||
|
||
/* =====================================================================
|
||
DIE EINE PERSONENLISTE
|
||
|
||
Sie ersetzt zwei: „Für wen?" (oben, vorher gar nicht im Formular)
|
||
und „Noch jemanden dazunehmen" (unten, zugeklappt). Zwei Listen
|
||
für dieselbe Frage waren zwei Bedienungen -- und die
|
||
Bewerbungs-Option steckte ausgerechnet in der zugeklappten.
|
||
|
||
DIE REIHENFOLGE DER HAKEN ENTSCHEIDET, WER VORN STEHT: Der erste
|
||
Angehakte ist die Hauptperson, die weiteren kommen dazu. Das
|
||
steht hier an einer Stelle, damit `fuerPerson` und `mehrLeute`
|
||
nicht auseinanderlaufen können -- sie werden beide aus derselben
|
||
Liste abgeleitet und nirgends sonst gesetzt.
|
||
===================================================================== */
|
||
function auswahlLesen() {
|
||
const ziel = $('e-fuer-leute');
|
||
if (!ziel) return;
|
||
const an = [...ziel.querySelectorAll('input:checked')].map((x) => Number(x.value));
|
||
fuerPerson = an.length ? an[0] : null;
|
||
mehrLeute = an.slice(1);
|
||
/* DIE ART FRAGT NUR, WER MEHR ALS EINEN EINTRAEGT. Bei einer
|
||
Person gibt es nichts zu verteilen -- die Frage waere
|
||
Fuellwerk. */
|
||
$('e-art').hidden = an.length < 2;
|
||
if (an.length < 2) {
|
||
const einzeln = $('e-art').querySelector('input[value="mehrere"]');
|
||
if (einzeln) einzeln.checked = true;
|
||
}
|
||
/* DER ZAEHLER IST DER TEIL, DER UEBERSICHT MACHT. Bei acht
|
||
Kaestchen sieht man nicht mehr, wie viele angehakt sind -- man
|
||
zaehlt nach. Er steht in der Kopfzeile der Auswahl und sagt es. */
|
||
const zahlFeld = $('e-fuer-zahl');
|
||
if (zahlFeld) {
|
||
const gesamt = (leute || []).length;
|
||
zahlFeld.textContent = an.length
|
||
? `${an.length} von ${gesamt} ausgewählt`
|
||
: `niemand ausgewählt (${gesamt} zur Wahl)`;
|
||
zahlFeld.dataset.leer = an.length ? 'nein' : 'ja';
|
||
}
|
||
|
||
const knopf = $('verteilen-knopf');
|
||
const name = an.length
|
||
? (leute.find((x) => String(x.id) === String(fuerPerson))?.name || null) : null;
|
||
if (knopf) {
|
||
knopf.textContent = an.length > 1
|
||
? `Aufgabe für ${an.length} Personen`
|
||
: name ? `Aufgabe für ${name}` : 'Aufgabe anlegen';
|
||
}
|
||
const satz = $('verteilen-satz');
|
||
if (satz) {
|
||
satz.textContent = an.length > 1
|
||
? 'Sie geht an alle Angehakten — wie, sagst du gleich darunter.'
|
||
: name ? `Sie landet direkt bei ${name} — mit einer Meldung.`
|
||
: 'Aufgabe anlegen und an eine oder mehrere Personen geben.';
|
||
}
|
||
/* UND DER KATALOG DARUNTER ZIEHT MIT (23.09.2026).
|
||
|
||
Er zeigt den Namen auf jedem Uebernehmen-Knopf und markiert, was
|
||
DIESE Person schon hat. Beides haengt an "fuerPerson" -- und das
|
||
aendert sich genau hier. Ohne diese Zeile stuende auf dem Knopf
|
||
noch der Name von vorhin, waehrend das Formular darueber laengst
|
||
jemand anderen meint: zwei Antworten auf dieselbe Frage, auf
|
||
einem Bildschirm.
|
||
|
||
Zugeklappt kostet das nichts -- dann zeichnet der Katalog
|
||
ohnehin nur seine Kopfzeile. */
|
||
window.Vorlagenbrett?.zeigen();
|
||
}
|
||
|
||
/* =====================================================================
|
||
DIE AUSWAHL ALS KARTEN (23.09.2026)
|
||
=====================================================================
|
||
|
||
Filipe: „diese kachel soll viel geiler sein, also alles was drin
|
||
ist soll viel geiler gestaltet sein, viel hochwertiger, viel
|
||
profissineller, auch die möglichkeit alle auf einmal auszuwählen
|
||
und so. mach es hoch krass profissionel, übersichtlich"
|
||
|
||
Vorher: ein Kästchen und ein Name je Zeile, untereinander. Bei
|
||
sechs Leuten sechs gleiche Zeilen -- man liest sie alle, um eine
|
||
zu finden.
|
||
|
||
Jetzt eine Karte je Person: Anfangsbuchstabe im Kreis, Name,
|
||
Rolle darunter. Sie liegen NEBENEINANDER (Raster), damit acht
|
||
Leute nicht acht Bildschirmzeilen kosten -- auf dem Handy eine
|
||
Spalte, am Rechner drei.
|
||
|
||
ANGEHAKT WIRD DIE GANZE KARTE, nicht das Kästchen darin: Auf
|
||
einem Handy ist ein Kästchen 20 px, eine Karte 200. Das Kästchen
|
||
bleibt trotzdem da -- es ist das, was ein Vorleseprogramm
|
||
ansagt, und was die Tastatur bedient. */
|
||
function fuerLeuteBauen() {
|
||
const ziel = $('e-fuer-leute');
|
||
if (!ziel) return;
|
||
/* WAS SCHON ANGEHAKT WAR, BLEIBT ANGEHAKT. Die Liste wird auch
|
||
neu gebaut, wenn man unten eine andere Person anklickt --
|
||
wer dabei seine Haken verliert, fängt von vorn an. */
|
||
const warAn = new Set([...ziel.querySelectorAll('input:checked')].map((x) => x.value));
|
||
if (fuerPerson !== null && fuerPerson !== undefined) warAn.add(String(fuerPerson));
|
||
ziel.replaceChildren();
|
||
for (const p2 of (leute || [])) {
|
||
const karte2 = document.createElement('label');
|
||
karte2.className = 'e-fuer__karte';
|
||
const k = document.createElement('input');
|
||
k.type = 'checkbox';
|
||
k.value = String(p2.id);
|
||
k.checked = warAn.has(String(p2.id));
|
||
k.addEventListener('change', auswahlLesen);
|
||
|
||
const zeichen = el('span', 'e-fuer__zeichen',
|
||
String(p2.name || '?').trim().charAt(0).toUpperCase());
|
||
zeichen.setAttribute('aria-hidden', 'true');
|
||
|
||
const text = el('span', 'e-fuer__text');
|
||
text.append(el('span', 'e-fuer__name', p2.name || 'Jemand'));
|
||
/* DIE ROLLE KOMMT VOM SERVER als fertiges Wort (`rolle_name`).
|
||
Sie hier aus einem Rollenschlüssel zu übersetzen hieße, die
|
||
Namen der Rollen in eine Datei zu schreiben, die jeder
|
||
herunterlädt -- darüber wacht eine eigene Prüfung. */
|
||
if (p2.rolle_name) text.append(el('span', 'e-fuer__rolle', p2.rolle_name));
|
||
|
||
karte2.append(k, zeichen, text);
|
||
ziel.append(karte2);
|
||
}
|
||
schnellwahlBauen();
|
||
auswahlLesen();
|
||
}
|
||
|
||
/* =====================================================================
|
||
ALLE AUF EINMAL -- UND DIE GRUPPEN DAZU
|
||
|
||
„auch die möglichkeit alle auf einmal auszuwählen und so."
|
||
|
||
DIE GRUPPEN WERDEN GELESEN, NICHT AUFGEZÄHLT: Welche Rollen in
|
||
der Liste vorkommen, sagt die Liste. Eine feste Aufzählung hier
|
||
wäre die, die beim nächsten Rollenzuschnitt eine Gruppe
|
||
verschweigt -- und niemand merkte es, weil die Knöpfe ja
|
||
funktionieren.
|
||
|
||
„Keine" steht am Ende und nur, wenn etwas angehakt ist. Ein
|
||
Knopf, der nichts zurücknehmen kann, ist ein Knopf, der nichts
|
||
tut. */
|
||
function schnellwahlBauen() {
|
||
const ziel = $('e-fuer-schnell');
|
||
if (!ziel) return;
|
||
const rollen = [];
|
||
for (const p2 of (leute || [])) {
|
||
if (p2.rolle_name && !rollen.includes(p2.rolle_name)) rollen.push(p2.rolle_name);
|
||
}
|
||
ziel.replaceChildren();
|
||
|
||
const knopf = (wort, was) => {
|
||
const b = el('button', 'e-fuer__kurz', wort);
|
||
b.type = 'button';
|
||
b.addEventListener('click', () => {
|
||
for (const k of $('e-fuer-leute').querySelectorAll('input[type=checkbox]')) {
|
||
const p2 = (leute || []).find((x) => String(x.id) === k.value);
|
||
k.checked = was === 'alle' ? true
|
||
: was === 'keine' ? false
|
||
: (p2?.rolle_name === was);
|
||
}
|
||
auswahlLesen();
|
||
});
|
||
return b;
|
||
};
|
||
|
||
if ((leute || []).length > 1) ziel.append(knopf('Alle', 'alle'));
|
||
/* Eine Gruppe nur dann, wenn sie NICHT alle sind -- „Modis" neben
|
||
„Alle" bei einer Liste aus lauter Modis wären zwei Knöpfe für
|
||
dasselbe. */
|
||
if (rollen.length > 1) {
|
||
for (const r of rollen) ziel.append(knopf(r, r));
|
||
}
|
||
ziel.append(knopf('Keine', 'keine'));
|
||
}
|
||
|
||
function formularAuf() {
|
||
const f = $('e-neu');
|
||
if (!f) return;
|
||
f.hidden = false;
|
||
$('verteilen-knopf').setAttribute('aria-expanded', 'true');
|
||
$('e-neu-fehler').textContent = '';
|
||
fuerLeuteBauen();
|
||
/* OHNE AUSWAHL IST DIE ERSTE FRAGE „für wen" -- dorthin der
|
||
Blick. Steht sie schon fest, fehlt nur noch der Titel. */
|
||
if (fuerPerson) $('e-titel').focus();
|
||
else $('e-fuer-leute').querySelector('input')?.focus();
|
||
}
|
||
|
||
function aufgabenFormularAufbauen() {
|
||
const auf = $('verteilen-knopf');
|
||
if (!auf) return;
|
||
auf.setAttribute('aria-expanded', 'false');
|
||
auf.setAttribute('aria-controls', 'e-neu');
|
||
auf.addEventListener('click', () => {
|
||
if ($('e-neu').hidden) formularAuf(); else formularZu();
|
||
});
|
||
$('e-neu-abbrechen').addEventListener('click', () => {
|
||
$('e-neu').reset();
|
||
mehrLeute = [];
|
||
/* ZURUECK AUF DIE KACHEL, NICHT AUF NICHTS. Wer abbricht, hat
|
||
unten meist trotzdem jemanden ausgewaehlt -- der Knopf soll
|
||
dann wieder „Aufgabe fuer X" heissen und nicht so tun, als
|
||
waere die Auswahl auch weg. Sie wird aus der Kachel GELESEN
|
||
und nicht daneben gemerkt: zwei Stellen, die sich dieselbe
|
||
Person merken, laufen auseinander. */
|
||
const kachel = document.querySelector('#personen .e-person[aria-pressed="true"]');
|
||
verteilBandZeigen(kachel ? Number(kachel.dataset.person) : null,
|
||
kachel ? (kachel.dataset.name || null) : null);
|
||
formularZu();
|
||
});
|
||
/* Der Klapp-Knopf „Noch jemanden dazunehmen" ist weg -- es gibt
|
||
nur noch eine Liste, und sie ist von Anfang an offen. */
|
||
|
||
$('e-neu').addEventListener('submit', async (ev) => {
|
||
ev.preventDefault();
|
||
const melder = $('e-neu-fehler');
|
||
melder.textContent = '';
|
||
const titel = $('e-titel').value.trim();
|
||
if (titel.length < 2) {
|
||
melder.textContent = 'Schreib kurz, was zu tun ist.';
|
||
$('e-titel').focus();
|
||
return;
|
||
}
|
||
if (!fuerPerson) {
|
||
/* KEIN „links" MEHR. Dort steht nichts -- gemessen. Der
|
||
Hinweis zeigt jetzt auf die Zeile, die im selben Formular
|
||
direkt darüber steht. */
|
||
melder.textContent = 'Hak oben an, für wen die Aufgabe ist.';
|
||
$('e-fuer-leute')?.querySelector('input')?.focus();
|
||
return;
|
||
}
|
||
const knopf = $('e-neu-speichern');
|
||
knopf.disabled = true;
|
||
try {
|
||
const art = mehrLeute.length
|
||
? ($('e-art').querySelector('input[name="e-verteilart"]:checked')?.value || 'mehrere')
|
||
: 'einzeln';
|
||
const a = await fetch('/workspace/api/aufgaben', {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
titel,
|
||
beschreibung: $('e-text').value.trim() || null,
|
||
frist: $('e-frist').value || null,
|
||
/* DAUERHAFT (30.09.2026) -- siehe workspace-aufgaben.js.
|
||
Der Server nimmt es nur von denen an, die verteilen
|
||
duerfen, und loescht dann die Frist. */
|
||
dauerhaft: !!$('e-dauerhaft')?.checked,
|
||
/* DIE GEWAEHLTE PERSON STEHT VORN. `zuteilen` leitet die
|
||
Art ab; bei einer Person wird daraus „einzeln", und
|
||
genau das ist gemeint. */
|
||
zuteilung: [Number(fuerPerson), ...mehrLeute],
|
||
verteilart: art,
|
||
}),
|
||
});
|
||
const r = await a.json().catch(() => ({}));
|
||
if (!a.ok) {
|
||
melder.textContent = window.sagWas?.(r.fehler, 'Ging nicht.') || 'Ging nicht.';
|
||
knopf.disabled = false;
|
||
return;
|
||
}
|
||
/* WAS DER SERVER WIRKLICH GESETZT HAT, nicht was ich wollte.
|
||
`zuteilen` kann eine Person weglassen (gesperrter Zugang)
|
||
oder die Art aendern -- wer das verschweigt, laesst jemanden
|
||
in dem Glauben, er habe drei Leute eingetragen. */
|
||
const gesetzt = r?.zuteilung?.gesetzt;
|
||
const alleIds = [Number(fuerPerson), ...mehrLeute];
|
||
const fehlt = Array.isArray(gesetzt)
|
||
? alleIds.filter((x) => !gesetzt.includes(x)).length
|
||
: 0;
|
||
|
||
/* ERST MERKEN, DANN AUFRAEUMEN (22.09.2026 abends).
|
||
|
||
Hier wurde `fuerPerson` auf null gesetzt und danach
|
||
`aufgabenZeigen(fuerPerson, …)` aufgerufen -- die Liste,
|
||
die gerade zeigen soll, was eben entstanden ist, wurde
|
||
damit ausgeblendet. Und die Meldung las `fuerName`, das
|
||
nur beim Klick auf eine Kachel gefuellt wird; wer im
|
||
Formular ankreuzte, bekam „steht jetzt bei ihm".
|
||
|
||
Die Namen kommen jetzt aus derselben Liste wie die Haken
|
||
und werden festgehalten, BEVOR `reset()` sie abraeumt. */
|
||
const namenVon = (id) =>
|
||
(leute.find((x) => String(x.id) === String(id))?.name || null);
|
||
const ersterName = fuerName || namenVon(fuerPerson);
|
||
const wieViele = alleIds.length;
|
||
const zeigeFuer = Number(fuerPerson);
|
||
const zeigeName = ersterName;
|
||
const istPool = art === 'pool';
|
||
|
||
$('e-neu').reset();
|
||
mehrLeute = [];
|
||
fuerPerson = null;
|
||
formularZu();
|
||
|
||
/* DIE MELDUNG SAGT, WAS WIRKLICH PASSIERT IST -- nicht immer
|
||
denselben Satz. Bei einem Pool ist das Entscheidende, dass
|
||
jetzt eine Bewerbung kommt und nicht etwa schon jemand
|
||
dransitzt. */
|
||
melde(fehlt
|
||
? `Angelegt — aber ${fehlt} Person(en) konnten nicht eingetragen werden.`
|
||
: wieViele === 1
|
||
? `„${titel}“ steht jetzt bei ${ersterName || 'der gewählten Person'}.`
|
||
: istPool
|
||
? `„${titel}“ liegt für ${wieViele} Leute bereit — wer zuerst Zeit hat,`
|
||
+ ' bewirbt sich, und du entscheidest.'
|
||
: `„${titel}“ steht jetzt bei ${wieViele} Leuten — jede hat ihren`
|
||
+ ' eigenen Stand.',
|
||
!fehlt);
|
||
/* UND DIE LISTE DARUNTER ZEIGT DIE ERSTE PERSON. Sie ist die,
|
||
die im Knopf stand; wer mehrere eingetragen hat, findet den
|
||
Rest ueber deren Kacheln. */
|
||
await aufgabenZeigen(zeigeFuer, zeigeName);
|
||
} catch {
|
||
melder.textContent = 'Das hat gerade nicht geklappt. Versuch es noch einmal.';
|
||
knopf.disabled = false;
|
||
} finally {
|
||
knopf.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
/* ---------------------------------------------------------------------
|
||
UND DIE AUFGABEN DIESER PERSON, AUF DERSELBEN SEITE
|
||
---------------------------------------------------------------------
|
||
„plus man soll die aufgaben hier in dieser seite auch sehen."
|
||
|
||
GEFILTERT WIRD IM BROWSER, nicht mit einem eigenen Weg im Server:
|
||
Die Liste, die er ohnehin schickt, enthaelt die Zuteilung je
|
||
Aufgabe. Ein zweiter Weg waere eine zweite Rechnung fuer dieselbe
|
||
Frage -- und die Sichtbarkeitsregel muesste dort noch einmal
|
||
stehen.
|
||
--------------------------------------------------------------------- */
|
||
/** Die Aufgaben holen und festhalten -- EIN Weg fuer beide, die sie
|
||
* brauchen: die Liste unter dem Katalog und der Katalog selbst.
|
||
*
|
||
* WARUM DAS EINE EIGENE FUNKTION IST (23.09.2026): Der Abruf stand
|
||
* in "aufgabenZeigen", und das kehrt ohne gewaehlte Person sofort
|
||
* um. Ein Modi waehlt aber nie jemanden -- er sieht die Kacheln gar
|
||
* nicht. Also blieb die Liste bei ihm leer, und der Katalog konnte
|
||
* nicht mehr markieren, was er schon uebernommen hat. Genau diese
|
||
* Markierung ist die Sperre dagegen, dieselbe Vorlage ein zweites
|
||
* Mal zu holen; auf der alten Seite kam sie vom Brett, das immer
|
||
* geladen wurde.
|
||
*
|
||
* Gefunden von pruef-modi-katalog ("0 Karten tragen den dritten
|
||
* Fusseintrag") -- eine Zeile, die seit dem 16.09. dasteht und
|
||
* eigentlich die Optik einer engen Karte prueft. */
|
||
async function aufgabenHolen() {
|
||
try {
|
||
const a = await fetch('/workspace/api/aufgaben', { credentials: 'same-origin' });
|
||
if (!a.ok) return null;
|
||
const roh = await a.json();
|
||
alleAufgabenV = Array.isArray(roh) ? roh : (roh?.aufgaben || []);
|
||
/* UND DIE KACHELN ERFAHREN ES SOFORT (24.09.2026). Sie tragen
|
||
seit heute die offenen Aufgaben ihrer Person; diese Zeile
|
||
steht bewusst HIER und nicht an den vier Aufrufstellen -- wer
|
||
die Liste neu holt, hat auch den neuen Stand, und eine Stelle,
|
||
die das Nachziehen vergisst, gibt es dann nicht. */
|
||
auftraegeNachziehen();
|
||
return alleAufgabenV;
|
||
} catch { return null; }
|
||
}
|
||
|
||
async function aufgabenZeigen(personId, name) {
|
||
const block = $('e-aufgaben-block');
|
||
if (!block) return;
|
||
if (!personId) { block.hidden = true; return; }
|
||
try {
|
||
const alle = await aufgabenHolen();
|
||
if (!alle) { block.hidden = true; return; }
|
||
const nr = Number(personId);
|
||
const meine = alle.filter((x) => Number(x.verantwortlich_id) === nr
|
||
|| (x.zuteilung || []).some((z) => Number(z.person_id) === nr
|
||
&& z.zustand !== 'abgelehnt'));
|
||
|
||
block.hidden = false;
|
||
$('e-aufgaben-titel').textContent = name ? `Aufgaben von ${name}` : 'Ihre Aufgaben';
|
||
|
||
const offen = meine.filter((x) => x.status !== 'erledigt' && x.status !== 'abgebrochen');
|
||
$('e-aufgaben-satz').textContent = meine.length
|
||
? `${offen.length} offen, ${meine.length} insgesamt.`
|
||
: '';
|
||
|
||
const liste = $('e-aufgaben-liste');
|
||
liste.replaceChildren();
|
||
if (!meine.length) {
|
||
const p2 = document.createElement('p');
|
||
p2.className = 'e-aufgaben__leer';
|
||
p2.textContent = name
|
||
? `${name} hat gerade keine Aufgabe. Leg oben eine an, wenn etwas ansteht.`
|
||
: 'Gerade keine Aufgabe.';
|
||
liste.append(p2);
|
||
return;
|
||
}
|
||
/* OFFENE ZUERST. Was erledigt ist, interessiert danach -- und
|
||
eine Liste nach Anlegedatum haette die wichtigste Zeile
|
||
irgendwo in der Mitte. */
|
||
const sortiert = [...meine].sort((x, y) => {
|
||
const fertig = (z) => (z.status === 'erledigt' || z.status === 'abgebrochen' ? 1 : 0);
|
||
return fertig(x) - fertig(y)
|
||
|| String(x.frist || '9999').localeCompare(String(y.frist || '9999'));
|
||
});
|
||
for (const x of sortiert) {
|
||
const a2 = document.createElement('a');
|
||
a2.className = 'e-aufgabe';
|
||
a2.href = 'aufgaben.html#a' + x.id;
|
||
a2.dataset.status = x.status || 'offen';
|
||
const t = document.createElement('span');
|
||
t.className = 'e-aufgabe__titel';
|
||
t.textContent = x.titel;
|
||
a2.append(t);
|
||
const m = document.createElement('span');
|
||
m.className = 'e-aufgabe__stand';
|
||
m.textContent = STATUS_WORT[x.status] || x.status || 'offen';
|
||
a2.append(m);
|
||
if (x.frist) {
|
||
const f = document.createElement('span');
|
||
f.className = 'e-aufgabe__frist';
|
||
f.textContent = 'bis ' + datum(x.frist);
|
||
a2.append(f);
|
||
}
|
||
liste.append(a2);
|
||
}
|
||
} catch { block.hidden = true; }
|
||
}
|
||
|
||
const STATUS_WORT = {
|
||
offen: 'Offen', arbeit: 'In Arbeit', review: 'Zur Freigabe',
|
||
erledigt: 'Erledigt', abgebrochen: 'Abgebrochen',
|
||
};
|
||
|
||
/* =====================================================================
|
||
DER VORLAGENKATALOG (umgezogen am 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."
|
||
|
||
DER KATALOG SELBST STEHT IN assets/js/vorlagenbrett.js -- dieselbe
|
||
Datei bedient auch "Aufgaben", dort aber mit dem anderen der beiden
|
||
Kataloge. Was hier steht, ist nur die Verbindung zu DIESER Seite.
|
||
|
||
UND DAS IST DER GANZE GEWINN DES UMZUGS: Drueben musste der Block
|
||
eine eigene Personenwahl mitbringen, 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.
|
||
|
||
"fuerPerson" IST DIE EINE WAHRHEIT. Sie wird an genau zwei Stellen
|
||
gesetzt: beim Anklicken einer Kachel (ueber verteilBandZeigen) und
|
||
beim Anhaken im Formular (auswahlLesen). Der Katalog fragt
|
||
dieselbe Variable ab wie der Verteil-Knopf darueber -- damit KANN
|
||
nicht auseinanderlaufen, was beide behaupten. */
|
||
function katalogAnschliessen() {
|
||
if (!window.Vorlagenbrett) return;
|
||
window.Vorlagenbrett.einrichten({
|
||
zweig: 'team',
|
||
hole: (pfad, optionen) => window.holen(pfad, optionen),
|
||
/* "melde" nimmt hier zwei Angaben (Text, gut/schlecht), und der
|
||
Katalog braucht seit dem 23.09.2026 beide: Dass eine Vorlage
|
||
nicht doppelt angelegt wurde, ist eine gute Nachricht. Stuende
|
||
sie in Warnrot, suchte man einen Fehler, den es nicht gibt --
|
||
genau das ist am 22.09. an der Aufgabenanlage passiert. */
|
||
melde: (text, gut) => melde(text, gut),
|
||
statusName: STATUS_WORT,
|
||
ich: () => window.__ich || null,
|
||
aufgaben: () => alleAufgabenV,
|
||
ziel: () => fuerPerson || null,
|
||
/* DER NAME WIRD ABGELEITET, NICHT GEHALTEN. Es gibt "fuerName"
|
||
daneben -- aber das wird beim Anhaken im Formular nicht
|
||
nachgezogen, und ein Name, der zur falschen Nummer gehoert,
|
||
ist schlimmer als gar keiner: Der Knopf verspraeche dann
|
||
etwas, das nicht passiert. Aus der Nummer laesst sich der
|
||
Name jederzeit richtig herleiten. */
|
||
zielName: () => {
|
||
if (!fuerPerson) return null;
|
||
/* „alle" ist keine Nummer -- der Knopf unten soll trotzdem
|
||
sagen, was er tut („An alle" statt „Übernehmen"). */
|
||
if (String(fuerPerson) === 'alle') return 'alle';
|
||
const p = (leute || []).find((x) => String(x.id) === String(fuerPerson));
|
||
return p?.name || fuerName || null;
|
||
},
|
||
/* WER STEHT IN DER REIHE "AN WEN"? Die Leute aus den Kacheln
|
||
unter "Wen gehst du durch?" -- aber nur die, die aus dem
|
||
Katalog ueberhaupt etwas bekommen koennen.
|
||
|
||
WARUM DIESER FILTER (24.09.2026). Ohne ihn zeigte die Reihe
|
||
sechs Personen; der Server nahm bei Katalog-Vorlagen aber nur
|
||
vier an. "An alle 6" legte also vier Aufgaben an, und die
|
||
beiden Haende gingen leer aus, ohne ein Wort. Wer einzeln auf
|
||
"An Rechte Hand" drueckte, bekam "Das gibt es nicht mehr --
|
||
lade die Seite neu" zu einer Person, die es gibt.
|
||
|
||
ENTSCHIEDEN HAT DAS DER SERVER, nicht diese Zeile: `kann_katalog`
|
||
kommt aus derselben Menge, aus der auch die SQL-Abfrage und
|
||
die Annahme-Pruefung gebildet werden. Stuende hier eine eigene
|
||
Rollenliste, waere es beim naechsten Mal wieder dieselbe
|
||
Doppelung. Der Rueckfall auf `true` gilt nur fuer eine
|
||
Serverfassung, die das Feld noch nicht kennt -- dann ist die
|
||
Reihe wie vorher, statt ploetzlich leer zu sein. */
|
||
zielbare: () => (leute || []).filter((p) => p.kann_katalog !== false),
|
||
/* EIN TIPP IN DER REIHE IST EIN TIPP AUF DIE KACHEL.
|
||
|
||
Nicht "auch so aehnlich", sondern derselbe Weg: `karte()`
|
||
macht genau das, was ein Klick auf eine Kachel macht --
|
||
Person merken, Kacheln markieren, Verteil-Band setzen,
|
||
Aufgaben zeigen, Katalog neu zeichnen. Damit KANN die Reihe
|
||
nicht etwas anderes meinen als die Kacheln; frueher hatte sie
|
||
eine eigene Auswahl, und dann standen zwei Antworten auf
|
||
einem Bildschirm. */
|
||
/* „ALLE" IST KEINE KARTE (24.09.2026). Eine Person waehlen
|
||
heisst: ihre Karte aufschlagen. „Alle" hat keine -- hier wird
|
||
nur das Ziel gemerkt und der Katalog neu gezeichnet, damit
|
||
die Knoepfe „An alle" sagen. Die Kacheln oben bleiben
|
||
unberuehrt; es ist ja keine Person ausgewaehlt. */
|
||
zielWaehlen: (id) => {
|
||
if (String(id) === 'alle') {
|
||
fuerPerson = 'alle';
|
||
fuerName = 'alle';
|
||
for (const kachel of document.querySelectorAll('#personen .e-person')) {
|
||
kachel.setAttribute('aria-pressed', 'false');
|
||
}
|
||
window.Vorlagenbrett?.auffrischen();
|
||
return;
|
||
}
|
||
karte(Number(id));
|
||
},
|
||
/* Nach dem Uebernehmen: die Aufgaben der Person neu holen. Auf
|
||
der alten Seite hiess das "laden()" und zeichnete das ganze
|
||
Brett; hier ist es die Liste unter dem Katalog. */
|
||
nachAenderung: async () => {
|
||
if (fuerPerson == null) return;
|
||
/* Bei „alle" gibt es keine einzelne Liste zum Nachladen --
|
||
die Zahlen an den Knoepfen kommen aus "aufgaben()", und die
|
||
wird hier neu geholt. */
|
||
if (String(fuerPerson) === 'alle') {
|
||
await aufgabenHolen();
|
||
await window.Vorlagenbrett?.auffrischen();
|
||
return;
|
||
}
|
||
await aufgabenZeigen(fuerPerson, katalogName(fuerPerson));
|
||
},
|
||
});
|
||
}
|
||
|
||
/** Der Name zu einer Nummer -- aus "leute", der Liste, die die Seite
|
||
* ohnehin vom Server hat. */
|
||
function katalogName(id) {
|
||
return (leute || []).find((x) => String(x.id) === String(id))?.name || null;
|
||
}
|
||
|
||
/** WIEDER ZUMACHEN (24.09.2026).
|
||
*
|
||
* Filipe: "ich will das wenn ich wieder auf die person drücke die
|
||
* liste unten dan wieder zu geht."
|
||
*
|
||
* ES WIRD DER AUSGANGSZUSTAND WIEDERHERGESTELLT, nicht einfach
|
||
* eine Liste von Kaesten versteckt -- und das ist ein Unterschied,
|
||
* der sich messen laesst. Das Verteil-Band und das Vorlagenbrett
|
||
* stehen naemlich AUCH OHNE AUSWAHL da (der Start ruft
|
||
* "verteilBandZeigen(null, null)" selbst auf). Sie auszublenden
|
||
* waere kein Zumachen, sondern das Wegnehmen von etwas, das vorher
|
||
* da war. Sie fallen deshalb nur auf "niemand gewaehlt" zurueck.
|
||
*
|
||
* Wirklich weg gehen die beiden Kaesten, die es ohne Person gar
|
||
* nicht gibt: ihre Aufgaben und ihre Karte. */
|
||
function zumachen() {
|
||
offen = null;
|
||
letzteKarte = null;
|
||
for (const kachel of document.querySelectorAll('#personen .e-person')) {
|
||
kachel.setAttribute('aria-pressed', 'false');
|
||
}
|
||
/* Setzt zugleich fuerPerson und fuerName auf null -- daran haengt
|
||
alles Weitere, und zwei Stellen, die sich dieselbe Person
|
||
merken, laufen auseinander. */
|
||
verteilBandZeigen(null, null);
|
||
aufgabenZeigen(null, null);
|
||
window.Vorlagenbrett?.zeigen();
|
||
const ziel = $('karte');
|
||
if (ziel) { ziel.hidden = true; ziel.textContent = ''; }
|
||
}
|
||
|
||
/* ==== DIE AUSWAHL (25.09.2026) ===================================
|
||
|
||
Ein Dialog mit dem GANZEN Katalog und einem Haken an dem, was
|
||
gilt. Man waehlt nur aus, was man sieht -- eine Liste, die nur das
|
||
Ausgewaehlte zeigt, koennte man nie erweitern.
|
||
|
||
JE BLOCK EIN „ALLE"-SCHALTER: Wer einen Chat-Moderator einrichtet,
|
||
will die vierzehn Chat-Punkte am Stueck, nicht vierzehn Klicks.
|
||
Und er zeigt den Stand mit an (`indeterminate`), wenn nur ein Teil
|
||
gesetzt ist -- sonst muesste man zaehlen.
|
||
|
||
GESPEICHERT WIRD ALLES AUF EINMAL. Punkt fuer Punkt zu schicken
|
||
waere bequemer zu bauen und im Fehlerfall schlimmer: Bei einem
|
||
Abbruch mitten drin haette die Person eine halb gesetzte Liste,
|
||
und niemand saehe, welche Haelfte. */
|
||
async function punkteWaehlen(id, name) {
|
||
const a = await hole(`/workspace/api/entwicklung/person/${id}/punkte`);
|
||
if (!a || !a.ok) { melde('Die Punkte ließen sich nicht laden.'); return; }
|
||
const daten = await a.json();
|
||
|
||
const dlg = document.createElement('dialog');
|
||
dlg.className = 'dialog e-punktwahl';
|
||
const form = document.createElement('form');
|
||
form.method = 'dialog';
|
||
|
||
const titel = el('h2', 'dialog__titel', `Welche Punkte gelten für ${name}?`);
|
||
form.append(titel);
|
||
form.append(el('p', 'e-punktwahl__satz',
|
||
'Nur das Ausgewählte steht danach auf der Karte – und nur daran '
|
||
+ 'wird gemessen. Was hier keinen Haken hat, taucht bei ihr oder '
|
||
+ 'ihm gar nicht erst auf.'));
|
||
|
||
const zaehler = el('p', 'e-punktwahl__zahl');
|
||
form.append(zaehler);
|
||
|
||
const kaesten = [];
|
||
for (const b of daten.bloecke) {
|
||
const block = el('section', 'e-punktwahl__block');
|
||
const kopfZeile = el('div', 'e-punktwahl__kopf');
|
||
const alleId = `pw-alle-${b.schluessel}`;
|
||
const alle = document.createElement('input');
|
||
alle.type = 'checkbox';
|
||
alle.id = alleId;
|
||
alle.className = 'e-punktwahl__alle';
|
||
const alleSchild = document.createElement('label');
|
||
alleSchild.htmlFor = alleId;
|
||
alleSchild.className = 'e-punktwahl__blockname';
|
||
alleSchild.textContent = b.name;
|
||
kopfZeile.append(alle, alleSchild);
|
||
block.append(kopfZeile);
|
||
|
||
const liste = el('div', 'e-punktwahl__liste');
|
||
const eigene = [];
|
||
for (const pt of b.punkte) {
|
||
const zeile = document.createElement('label');
|
||
zeile.className = 'e-punktwahl__punkt';
|
||
const k = document.createElement('input');
|
||
k.type = 'checkbox';
|
||
k.value = pt.schluessel;
|
||
k.checked = pt.dabei;
|
||
k.addEventListener('change', () => { standZeigen(); });
|
||
zeile.append(k);
|
||
zeile.append(el('span', 'e-punktwahl__titel', pt.titel));
|
||
liste.append(zeile);
|
||
eigene.push(k);
|
||
kaesten.push(k);
|
||
}
|
||
block.append(liste);
|
||
alle.addEventListener('change', () => {
|
||
for (const k of eigene) k.checked = alle.checked;
|
||
standZeigen();
|
||
});
|
||
block.dataset.block = b.schluessel;
|
||
block._alle = alle;
|
||
block._eigene = eigene;
|
||
form.append(block);
|
||
}
|
||
|
||
function standZeigen() {
|
||
const n = kaesten.filter((k) => k.checked).length;
|
||
zaehler.textContent = n === 0
|
||
? 'Noch nichts ausgewählt – die Karte bliebe leer.'
|
||
: `${n} von ${kaesten.length} ausgewählt`;
|
||
zaehler.dataset.leer = n === 0 ? 'ja' : 'nein';
|
||
for (const block of form.querySelectorAll('.e-punktwahl__block')) {
|
||
const an = block._eigene.filter((k) => k.checked).length;
|
||
block._alle.checked = an === block._eigene.length && an > 0;
|
||
/* DER DRITTE ZUSTAND. Ohne ihn saehe „drei von vierzehn" aus
|
||
wie „keiner" -- und ein Klick auf den Schalter haette dann
|
||
alle vierzehn gesetzt, statt die drei zu loesen. */
|
||
block._alle.indeterminate = an > 0 && an < block._eigene.length;
|
||
}
|
||
}
|
||
standZeigen();
|
||
|
||
const knoepfe = el('div', 'dialog__knoepfe');
|
||
const ab = el('button', 'knopf-still', 'Abbrechen');
|
||
ab.type = 'button';
|
||
ab.addEventListener('click', () => { dlg.close(); dlg.remove(); });
|
||
const ok = el('button', 'knopf', 'Speichern');
|
||
ok.type = 'button';
|
||
ok.addEventListener('click', async () => {
|
||
ok.disabled = true;
|
||
const punkte = kaesten.filter((k) => k.checked).map((k) => k.value);
|
||
const s = await hole(`/workspace/api/entwicklung/person/${id}/punkte`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ punkte }),
|
||
});
|
||
if (!s || !s.ok) { melde('Das ließ sich nicht speichern.'); ok.disabled = false; return; }
|
||
dlg.close(); dlg.remove();
|
||
/* DIE KARTE NEU HOLEN und nicht im Browser nachziehen: Welche
|
||
Punkte danach dastehen, rechnet der Server -- und er weiss
|
||
auch, was schon gesetzt war. */
|
||
await karte(id);
|
||
});
|
||
knoepfe.append(ab, ok);
|
||
form.append(knoepfe);
|
||
dlg.append(form);
|
||
document.body.append(dlg);
|
||
dlg.showModal();
|
||
dlg.addEventListener('close', () => dlg.remove());
|
||
}
|
||
|
||
async function karte(id, springen = false) {
|
||
offen = id;
|
||
/* WER IST GERADE OFFEN? Die Antwort gehoert auf die Kacheln, nicht
|
||
nur in eine Variable. `aria-pressed` sagt es zugleich einem
|
||
Vorleseprogramm -- eine Umrandung allein kann das nicht. */
|
||
for (const kachel of document.querySelectorAll('#personen .e-person')) {
|
||
kachel.setAttribute('aria-pressed',
|
||
kachel.dataset.person === String(id) ? 'true' : 'false');
|
||
}
|
||
{
|
||
const k = document.querySelector(`#personen .e-person[data-person="${id}"]`);
|
||
const wieHeisst = k ? (k.dataset.name || k.textContent.trim()) : null;
|
||
verteilBandZeigen(id, wieHeisst);
|
||
/* UND IHRE AUFGABEN GLEICH MIT. „plus man soll die aufgaben
|
||
hier in dieser seite auch sehen." Sie kommen beim Auswaehlen,
|
||
nicht auf Knopfdruck: Wer jemanden durchgeht, will wissen,
|
||
was bei ihm liegt -- das ist keine Zusatzfrage. */
|
||
/* ERST DIE AUFGABEN, DANN DER KATALOG -- und zwar NACHEINANDER.
|
||
Der Katalog markiert "schon uebernommen" aus der Aufgabenliste,
|
||
und die holt "aufgabenZeigen" ueber das Netz. Beide nebeneinander
|
||
loszuschicken hiesse: Wer schneller ist, entscheidet, ob die
|
||
Markierung stimmt -- meistens zeichnete der Katalog zuerst und
|
||
zeigte die Aufgaben der VORHERIGEN Person. */
|
||
aufgabenZeigen(id, wieHeisst).then(() => window.Vorlagenbrett?.zeigen());
|
||
}
|
||
const ziel = $('karte');
|
||
const vorherY = window.scrollY;
|
||
ziel.hidden = false;
|
||
ziel.textContent = '';
|
||
ziel.append(el('p', 'leise', 'wird geladen …'));
|
||
/* HIER STAND DER SPRUNG -- und er kam zu frueh (24.09.2026).
|
||
|
||
In diesem Augenblick enthaelt die Karte einen einzigen Satz
|
||
("wird geladen …"). Die Seite ist damit so kurz, dass der
|
||
Browser ueberhaupt nicht scrollen KANN: Er klemmt bei 0 ab, und
|
||
wenn der Inhalt Sekundenbruchteile spaeter ankommt, bewegt sich
|
||
nichts mehr. Gemessen: scrollY 0, waehrend die Karte weit unten
|
||
stand.
|
||
|
||
Wer von der Talentseite mit "?zeigen=person-…" kam, landete
|
||
deshalb oben auf der Personenliste -- also genau dort, wo der
|
||
Link ihn NICHT hinbringen sollte. Derselbe Fehler, vor dem
|
||
oben an der Kachel gewarnt wird ("vorher haengt sie nicht im
|
||
Dokument und hat keine Position"), nur eine Ebene tiefer.
|
||
|
||
Gesprungen wird jetzt am Ende dieser Funktion, wenn die Karte
|
||
ihre Hoehe hat. Ein zweiter, frueher Sprung waere ausserdem eine
|
||
zweite Bewegung fuer denselben Klick -- und Bewegung ist genau
|
||
das, worueber Filipe sich beschwert hat. */
|
||
|
||
const a = await hole(`/workspace/api/entwicklung/person/${id}`);
|
||
if (!a.ok) { melde('Die Karte ließ sich nicht laden.'); return; }
|
||
const r = await a.json();
|
||
stufen = r.stufen;
|
||
letzteKarte = { id, daten: r };
|
||
ziel.textContent = '';
|
||
|
||
/* WIE DIESER MENSCH GEKOMMEN IST (17.09.2026).
|
||
|
||
Wer eine Karte aufschlaegt, sieht Namen und Punkte -- aber nicht,
|
||
dass derselbe Mensch vier Wochen lang beobachtet wurde und wer
|
||
ihn eingearbeitet hat. Beim ersten Beurteilen ist genau das die
|
||
Frage: „Woher kommt der eigentlich?"
|
||
|
||
NUR DER WEG, NICHT DIE DATEN. Die Merkmale aus dem Trichter
|
||
hierher zu kopieren waere bequem und falsch -- zwei Kataloge,
|
||
zwei Fragen (taugt er? / wie entwickelt er sich?). Eine Zahl aus
|
||
dem einen sieht im anderen aus wie eine Beurteilung und ist
|
||
keine. */
|
||
if (r.herkunft) {
|
||
const h = el('p', 'e-herkunft');
|
||
h.append(el('span', 'e-herkunft__schild', 'Kam über den Trichter'));
|
||
const hin = el('a', 'e-herkunft__weg', r.herkunft.titel);
|
||
hin.href = 'talente.html';
|
||
h.append(hin);
|
||
if (r.herkunft.buddy_name) {
|
||
h.append(el('span', 'e-herkunft__buddy',
|
||
'· eingearbeitet von ' + r.herkunft.buddy_name));
|
||
}
|
||
ziel.append(h);
|
||
}
|
||
|
||
const kopf = el('div', 'e-karte__kopf');
|
||
kopf.append(el('h2', 'e-karte__name', r.person.name));
|
||
kopf.append(el('span', 'e-karte__rolle', r.person.rolle_name));
|
||
kopf.append(el('span', 'e-karte__stand', r.gesamt
|
||
? `${r.gesetzt} von ${r.gesamt} erledigt`
|
||
: 'noch nichts zugetragen'));
|
||
/* Der Weg zum Notizbrett. Der Katalog ist das, was man regelmäßig
|
||
durchgeht; die Notiz das, was zwischendurch auffällt. Beides
|
||
nebeneinander, nicht statt. */
|
||
const brett = el('a', 'schritt e-karte__brett', 'Notizen zu dieser Person');
|
||
brett.href = 'bereich.html?b=entwicklung';
|
||
kopf.append(brett);
|
||
|
||
/* ==== WELCHE PUNKTE GELTEN FUER IHN? (25.09.2026) ==============
|
||
|
||
Filipe: „ich will dass die rechte hand und ich diese aufgaben
|
||
die es da gibt, individuel aussuchen koennen wer welche aufgabe
|
||
bekommt. bis dahin sollen die keine aufgaben sehen."
|
||
|
||
DER KNOPF SAGT DIE ZAHL. „Punkte auswaehlen" allein liesse
|
||
offen, ob schon jemand ausgesucht hat -- und genau das ist die
|
||
Frage, wenn die Karte leer aussieht. */
|
||
const waehlen = el('button', 'schritt e-karte__waehlen',
|
||
r.gesamt
|
||
? `Aufgaben zutragen · ${r.gesamt} von ${r.gesamt_katalog}`
|
||
: 'Aufgaben zutragen');
|
||
waehlen.type = 'button';
|
||
if (!r.gesamt) waehlen.dataset.leer = 'ja';
|
||
waehlen.addEventListener('click', () => punkteWaehlen(id, r.person.name));
|
||
kopf.append(waehlen);
|
||
ziel.append(kopf);
|
||
|
||
/* DER LEERE ZUSTAND IST WEG (korrigiert 25.09.2026).
|
||
|
||
Hier stand ein Kasten, der die ganze Karte ersetzte, solange
|
||
nichts zugetragen war. Filipe: „aber unsere sicht von dogfather
|
||
und rechte hand soll sich nicht aendern."
|
||
|
||
Er hat recht: Die 68 sind das, woran ihr euch entlanghangelt,
|
||
wenn ihr jemanden anseht. Ob ihm etwas als Aufgabe zugetragen
|
||
ist, ist eine andere Frage -- sie steht in der Zahl oben und als
|
||
Marke an den einzelnen Punkten, aber sie verdeckt die Liste
|
||
nicht mehr. */
|
||
|
||
/* ---- Abweichungen zuerst ------------------------------------------
|
||
Sie stehen oben, weil sie das Einzige sind, was diese Seite
|
||
liefert und eine Liste nicht: die Stelle, an der zwei Leute
|
||
dieselbe Person verschieden sehen. */
|
||
if (r.abweichungen.length) {
|
||
const box = el('div', 'e-abweichung');
|
||
box.append(el('h3', 'e-abweichung__titel',
|
||
`${r.abweichungen.length} × seht ihr das verschieden`));
|
||
box.append(el('p', 'e-abweichung__satz',
|
||
'Genau hier lohnt das Gespräch. Wo ihr einig seid, gibt es nichts zu besprechen.'));
|
||
const liste = el('ul', 'e-abweichung__liste');
|
||
for (const ab of r.abweichungen) {
|
||
const li = document.createElement('li');
|
||
li.append(el('span', 'e-abweichung__punkt', ab.titel));
|
||
const nn = (s) => stufen.find((x) => x.schluessel === s)?.name || s;
|
||
li.append(el('span', 'e-abweichung__gegen',
|
||
`du: ${nn(ab.meins)} · ${ab.von_name}: ${nn(ab.andere)}`));
|
||
liste.append(li);
|
||
}
|
||
box.append(liste);
|
||
ziel.append(box);
|
||
}
|
||
|
||
/* ---- Die Blöcke, auf- und zuklappbar --------------------------------
|
||
|
||
ACHTUNDZWANZIG PUNKTE SIND ZU VIEL FÜR EINEN BILDSCHIRM. Offen
|
||
sind es rund zwei Meter Seite; man scrollt, verliert die Stelle
|
||
und macht es beim nächsten Mal gar nicht mehr.
|
||
|
||
WELCHER ZUERST OFFEN STEHT, IST DIE EIGENTLICHE ENTSCHEIDUNG.
|
||
Alle zu hieße: jedes Mal erst suchen. Alle auf wäre der Zustand
|
||
von vorher. Offen steht deshalb die erste Kategorie, in der noch
|
||
etwas fehlt — man landet dort, wo die Arbeit ist. Ist alles
|
||
gesetzt, steht die erste offen.
|
||
================================================================ */
|
||
/* BEIM MENSCHENWECHSEL WIRD BEIDES NEU ENTSCHIEDEN (01.10.2026).
|
||
Der Filter blieb bisher stehen -- richtig, solange er eine
|
||
Erwartungsstufe meinte („wer Fortgeschritten gewählt hat, will
|
||
das weiter sehen"). „Zugetragen" ist dagegen eine Aussage ÜBER
|
||
DIESE PERSON; sie mit zur nächsten zu nehmen wäre die falsche
|
||
Frage. */
|
||
if (karteFuer !== id) { karteFuer = id; offeneBloecke = null; stufenFilter = null; }
|
||
if (offeneBloecke === null) {
|
||
offeneBloecke = new Set();
|
||
const ersteOffene = r.bloecke.find((b) => b.punkte.some((pt) => !pt.meins))
|
||
|| r.bloecke[0];
|
||
if (ersteOffene) offeneBloecke.add(ersteOffene.schluessel);
|
||
}
|
||
|
||
const leiste = el('div', 'e-bloecke__leiste');
|
||
const alleZu = () => r.bloecke.every((b) => offeneBloecke.has(b.schluessel));
|
||
const alleKnopf = el('button', 'e-bloecke__alle');
|
||
alleKnopf.type = 'button';
|
||
const alleBeschriften = () => {
|
||
alleKnopf.textContent = alleZu() ? 'Alle zuklappen' : 'Alle aufklappen';
|
||
};
|
||
alleBeschriften();
|
||
/* Jede Kategorie legt hier ihren eigenen Umschalter ab. */
|
||
const umschalter = [];
|
||
alleKnopf.addEventListener('click', () => {
|
||
/* AN ORT UND STELLE, NICHT ÜBER EINEN NEUAUFBAU. Vorher rief
|
||
dieser Knopf karte() -- die Karte wurde neu geholt, neu
|
||
gezeichnet, und die Seite sprang. Aufklappen ist eine Sache der
|
||
Anzeige; dafür braucht es den Server nicht. */
|
||
const zu = alleZu();
|
||
if (zu) offeneBloecke.clear();
|
||
else for (const b of r.bloecke) offeneBloecke.add(b.schluessel);
|
||
for (const u of umschalter) u(!zu);
|
||
alleBeschriften();
|
||
});
|
||
leiste.append(alleKnopf);
|
||
/* HIER STAND KURZ „2 von 28 angesehen" -- dieselbe Zahl, die zwei
|
||
Zeilen hoeher schon im Kopf der Karte steht. Zweimal dasselbe auf
|
||
einem Bildschirm laesst einen ueberlegen, ob es zwei verschiedene
|
||
Zahlen sind. Gesehen habe ich das im Bildschirmfoto, nicht im
|
||
Code. */
|
||
/* DIE STUFENLEITER (17.09.2026).
|
||
|
||
Sie wird aus r.erwartung GEBAUT und nicht hier aufgezählt --
|
||
eine zweite Liste wäre die Falle, die in diesem Haus schon
|
||
mehrfach zugeschlagen hat: Kommt eine fünfte Stufe dazu, fehlt
|
||
sie genau an der Stelle, an der niemand nachsieht.
|
||
|
||
Und jede Stufe sagt, wie viele Punkte sie hat. Ein Filter, der
|
||
auf eine leere Liste führt, sieht aus wie ein Fehler. */
|
||
const stufenLeiste = el('div', 'e-stufen');
|
||
stufenLeiste.setAttribute('role', 'group');
|
||
stufenLeiste.setAttribute('aria-label', 'Nach Stufe filtern');
|
||
const allePunkte = r.bloecke.flatMap((b) => b.punkte);
|
||
/* ==== WOMIT DIE KARTE AUFGEHT (01.10.2026) ===================
|
||
Hier und nicht oben bei der Erklärung: Erst hier weiß man, ob
|
||
dieser Mensch überhaupt etwas zugetragen bekommen hat. Ein
|
||
Filter, der auf eine leere Liste führt, sieht aus wie ein
|
||
Fehler -- dieselbe Überlegung wie bei der Stufenleiter
|
||
darunter, die ihre Zahlen mitführt. */
|
||
if (stufenFilter === null) {
|
||
stufenFilter = allePunkte.some((pt) => pt.zugetragen) ? 'zugetragen' : '';
|
||
}
|
||
/* ==== UND EIN KNOPF FÜR DAS ZUGETRAGENE (30.09.2026) ==========
|
||
|
||
VanVan aus dem Support: „steht in der Karte bei jedem Punkt
|
||
läuft obwohl auch wenn die Aufgaben darüber gar nicht
|
||
zugetragen wurde."
|
||
|
||
Gemessen stimmt das: Die vier Bewertungsknöpfe („✓ Läuft
|
||
↗ Wächst ! Da hakt es – Kann ich nicht sagen") stehen an allen
|
||
68 Punkten, auch an denen, die niemandem zugetragen sind. Wer
|
||
durchscrollt, liest deshalb an jedem Punkt das Wort „Läuft".
|
||
|
||
DIE 68 BLEIBEN. Filipe am 25.09.2026 ausdrücklich: „dogfather
|
||
und die rechte hand sollen immer noch die 68 sachen sehen wie
|
||
vorher … unsere sicht soll sich nicht ändern." Also kein
|
||
Ausblenden und keine neue Vorgabe — ein Knopf mehr in der
|
||
Leiste, die es schon gibt. „Alle" bleibt, was beim Aufmachen
|
||
gilt.
|
||
|
||
ER STEHT VORNE, gleich hinter „Alle": Es ist die Frage, die man
|
||
am häufigsten hat, wenn man eine bestimmte Person ansieht. */
|
||
const stufenWahl = [
|
||
{ schluessel: '', name: 'Alle', text: '' },
|
||
{ schluessel: 'zugetragen', name: 'Zugetragen',
|
||
text: 'Nur die Punkte, die diesem Menschen als Aufgabe gegeben wurden.' },
|
||
...(r.erwartung || []),
|
||
];
|
||
for (const e of stufenWahl) {
|
||
const wieViele = e.schluessel === 'zugetragen'
|
||
? allePunkte.filter((pt) => pt.zugetragen).length
|
||
: e.schluessel
|
||
? allePunkte.filter((pt) => pt.stufe === e.schluessel).length
|
||
: allePunkte.length;
|
||
const k = el('button', 'e-stufen__knopf');
|
||
k.type = 'button';
|
||
k.dataset.erwartung = e.schluessel;
|
||
k.setAttribute('aria-pressed', String(stufenFilter === e.schluessel));
|
||
if (e.text) k.title = e.text;
|
||
k.append(el('span', 'e-stufen__name', e.name));
|
||
k.append(el('span', 'e-stufen__zahl', String(wieViele)));
|
||
k.addEventListener('click', () => {
|
||
/* Ein zweiter Klick auf dieselbe Stufe hebt den Filter auf.
|
||
Sonst sucht man den Weg zurück zu „Alle" und findet ihn
|
||
nicht, weil man nicht mehr weiß, dass man gefiltert hat. */
|
||
stufenFilter = (stufenFilter === e.schluessel) ? '' : e.schluessel;
|
||
karte(id, false);
|
||
});
|
||
stufenLeiste.append(k);
|
||
}
|
||
leiste.append(stufenLeiste);
|
||
ziel.append(leiste);
|
||
|
||
for (const b of r.bloecke) {
|
||
/* GEFILTERT WIRD HIER, EINMAL -- und alles darunter rechnet mit
|
||
derselben Liste. Stünde der Filter nur beim Zeichnen der
|
||
Zeilen, zeigte der Kopf weiterhin „3 / 14", während vier
|
||
Punkte darunter stehen. Genau diese Sorte Unstimmigkeit
|
||
lässt einen an der Zahl zweifeln statt am Filter. */
|
||
const punkte = punkteGefiltert(b.punkte);
|
||
/* EINE KATEGORIE OHNE TREFFER WIRD GANZ AUSGELASSEN. Ein
|
||
Abschnitt mit „0 / 0" ist kein Ergebnis, sondern ein
|
||
Hindernis zwischen den echten. */
|
||
if (!punkte.length) continue;
|
||
const gesetzt = punkte.filter((pt) => pt.meins).length;
|
||
const offen = offeneBloecke.has(b.schluessel);
|
||
const haktHier = punkte.filter((pt) => pt.meins?.stufe === 'hakt').length;
|
||
|
||
const abschnitt = el('section', 'e-block');
|
||
/* Der Schlüssel am Element -- daran findet zaehlerNachziehen()
|
||
den richtigen Kopf wieder, ohne eine zweite Liste zu führen. */
|
||
abschnitt.dataset.block = b.schluessel;
|
||
abschnitt.dataset.offen = offen ? 'ja' : 'nein';
|
||
if (gesetzt === punkte.length) abschnitt.dataset.fertig = 'ja';
|
||
|
||
const koerper = el('div', 'e-block__koerper');
|
||
koerper.id = `block-${b.schluessel}`;
|
||
koerper.hidden = !offen;
|
||
|
||
const titel = el('h3', 'e-block__zeile');
|
||
const schalter = el('button', 'e-block__schalter');
|
||
schalter.type = 'button';
|
||
schalter.setAttribute('aria-expanded', String(offen));
|
||
schalter.setAttribute('aria-controls', koerper.id);
|
||
schalter.append(pfeil());
|
||
schalter.append(el('span', 'e-block__name', b.name));
|
||
/* DER STAND STEHT IM KOPF, damit man ihn ZUGEKLAPPT sieht. Eine
|
||
zugeklappte Kategorie, die nicht sagt, wie weit sie ist, klappt
|
||
man einmal auf und danach nie wieder zu. */
|
||
const stand = el('span', 'e-block__stand', `${gesetzt} / ${punkte.length}`);
|
||
if (gesetzt === punkte.length) stand.dataset.fertig = 'ja';
|
||
schalter.append(stand);
|
||
/* DER HAKT-HINWEIS STEHT IMMER DA, nur meistens verborgen: So
|
||
kann er nachgezogen werden, ohne den Kopf neu zu bauen -- und
|
||
ein neu gebauter Kopf wäre wieder eine Bewegung. */
|
||
const h = el('span', 'e-block__hakt');
|
||
h.hidden = !haktHier;
|
||
h.append(el('span', 'e-block__hakt-zahl', `${haktHier} ×`));
|
||
h.append(el('span', 'e-block__hakt-wort', 'hakt'));
|
||
h.title = 'Punkte, bei denen du „Da hakt es“ gesetzt hast.';
|
||
schalter.append(h);
|
||
|
||
const umschalten = (auf) => {
|
||
abschnitt.dataset.offen = auf ? 'ja' : 'nein';
|
||
koerper.hidden = !auf;
|
||
schalter.setAttribute('aria-expanded', String(auf));
|
||
};
|
||
umschalter.push(umschalten);
|
||
schalter.addEventListener('click', () => {
|
||
if (offeneBloecke.has(b.schluessel)) offeneBloecke.delete(b.schluessel);
|
||
else offeneBloecke.add(b.schluessel);
|
||
/* Nur DIESEN Abschnitt umschalten statt die Karte neu zu holen:
|
||
Ein Neuaufbau bei jedem Klick ließe die Seite unter den
|
||
Fingern springen -- und bei 28 Punkten sieht man das. */
|
||
umschalten(offeneBloecke.has(b.schluessel));
|
||
alleBeschriften();
|
||
});
|
||
titel.append(schalter);
|
||
abschnitt.append(titel);
|
||
|
||
/* Der erklärende Satz gehört in den Körper, nicht in den Kopf:
|
||
Zugeklappt will man die Überschrift lesen, nicht einen Absatz. */
|
||
koerper.append(el('p', 'e-block__satz', b.text));
|
||
for (const pt of punkte) koerper.append(punktZeile(id, pt, r.erwartung));
|
||
abschnitt.append(koerper);
|
||
ziel.append(abschnitt);
|
||
}
|
||
|
||
/* Die Zahl auf der Personenkachel mitziehen -- sie kommt aus einer
|
||
Liste, die nur beim Laden der Seite geholt wird. */
|
||
zaehlerNachziehen();
|
||
/* JETZT ERST SPRINGEN -- die Karte steht vollstaendig und hat eine
|
||
Hoehe, der Browser kann also wirklich dorthin scrollen.
|
||
|
||
UND SONST DIE HÖHE HALTEN. Ohne diese Zeile springt die Seite
|
||
auch ohne scrollIntoView: Der Neuaufbau ändert die Höhe der
|
||
Karte, und der Browser behält nur die Pixelzahl, nicht die
|
||
Stelle. */
|
||
if (springen) {
|
||
ziel.scrollIntoView({ behavior: window.sanft?.() || 'auto', block: 'start' });
|
||
} else {
|
||
window.scrollTo({ top: vorherY, behavior: 'instant' });
|
||
}
|
||
}
|
||
|
||
function punktZeile(personId, pt, erwartung) {
|
||
const z = el('article', 'e-punkt');
|
||
if (pt.meins) z.dataset.stufe = pt.meins.stufe;
|
||
if (pt.meins?.verblasst) z.dataset.verblasst = 'ja';
|
||
/* ZUGETRAGEN HEISST: er hat es als AUFGABE (25.09.2026).
|
||
|
||
Die Liste bleibt vollstaendig -- die Marke sagt, welche davon
|
||
diesem Menschen aufgetragen sind. Ohne sie muesste man das
|
||
Auswahlfenster aufmachen, um zu sehen, was man ihm gegeben hat.
|
||
|
||
DAS MERKMAL SETZT SEIT HEUTE `auftragZeigen()` weiter unten --
|
||
es muss sich ja beim Umschalten mitaendern. Hier stand es
|
||
zusaetzlich; zwei Stellen fuer denselben Zustand sind eine
|
||
Gelegenheit, dass eine davon stehenbleibt. */
|
||
|
||
const kopf = el('div', 'e-punkt__kopf');
|
||
kopf.append(el('h4', 'e-punkt__titel', pt.titel));
|
||
/* ==== DIREKT HIER ZUTRAGEN (25.09.2026) ========================
|
||
|
||
Filipe: „ich will sofort über diese aufgaben da spezifisch
|
||
zuteilen kann bitte. bei allen personnen."
|
||
|
||
Das Auswahlfenster bleibt -- es ist der Weg, wenn man jemanden
|
||
neu einrichtet und zwoelf Punkte auf einmal vergibt. Dieser
|
||
Knopf ist der andere Fall, und der haeufigere: Man liest eine
|
||
Beobachtung, denkt „das soll er machen", und will es in dem
|
||
Moment erledigen -- nicht ein Fenster aufmachen, in einer Liste
|
||
von 68 denselben Punkt suchen und wieder zumachen.
|
||
|
||
ER STEHT IMMER DA, nicht erst beim Ueberfahren: Auf einem Handy
|
||
gibt es kein Ueberfahren, und ein Knopf, den man dort nicht
|
||
erreicht, gibt es dort nicht. Im Ruhezustand ist er leise; ist
|
||
der Punkt zugetragen, traegt er Farbe und das Wort.
|
||
|
||
DAS WORT STEHT DA, NICHT NUR DIE FARBE. Wer Farben schlecht
|
||
unterscheidet, sieht sonst nur einen etwas anderen Kasten --
|
||
dieselbe Ueberlegung wie bei den Marken im Vorlagenbrett.
|
||
|
||
`aria-pressed` UND NICHT ZWEI KNOEPFE: Es ist ein Schalter mit
|
||
zwei Stellungen, und genau so liest ihn ein Vorleseprogramm
|
||
vor. */
|
||
const auftrag = el('button', 'e-punkt__auftrag');
|
||
auftrag.type = 'button';
|
||
const auftragZeigen = () => {
|
||
const an = !!pt.zugetragen;
|
||
auftrag.setAttribute('aria-pressed', String(an));
|
||
auftrag.textContent = an ? 'Aufgabe ✓' : '+ Aufgabe';
|
||
auftrag.title = an
|
||
? 'Zugetragen – noch einmal tippen nimmt es zurück'
|
||
: 'Diesem Menschen als Aufgabe zutragen';
|
||
auftrag.setAttribute('aria-label', (an ? 'Zurücknehmen: ' : 'Als Aufgabe zutragen: ')
|
||
+ pt.titel);
|
||
z.dataset.zugetragen = an ? 'ja' : 'nein';
|
||
};
|
||
auftragZeigen();
|
||
auftrag.addEventListener('click', async () => {
|
||
auftrag.disabled = true;
|
||
const soll = !pt.zugetragen;
|
||
try {
|
||
const a = await hole(
|
||
`/workspace/api/entwicklung/person/${personId}/punkt/${pt.schluessel}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ an: soll }),
|
||
});
|
||
if (!a.ok) { melde('Das ließ sich gerade nicht ändern.'); return; }
|
||
const d = await a.json().catch(() => ({}));
|
||
pt.zugetragen = d.zugetragen === true;
|
||
auftragZeigen();
|
||
/* DIE ZAHL KOMMT VOM SERVER, nicht aus einer eigenen Rechnung:
|
||
Bei zwei offenen Fenstern waere die eigene falsch, und
|
||
niemand saehe, warum. `zaehlerNachziehen` bringt Kopf, Ring
|
||
und Kachel auf denselben Stand -- ohne Neuladen und ohne
|
||
Sprung. */
|
||
if (letzteKarte && typeof d.gesamt === 'number') {
|
||
letzteKarte.daten.gesamt = d.gesamt;
|
||
zaehlerNachziehen();
|
||
}
|
||
melde('');
|
||
} catch { /* hole() hat schon gemeldet */ } finally {
|
||
auftrag.disabled = false;
|
||
}
|
||
});
|
||
kopf.append(auftrag);
|
||
const erw = erwartung.find((e) => e.schluessel === pt.stufe);
|
||
if (erw) {
|
||
const m = el('span', 'e-punkt__erwartung', erw.name);
|
||
m.dataset.erwartung = pt.stufe;
|
||
m.title = erw.text;
|
||
kopf.append(m);
|
||
}
|
||
z.append(kopf);
|
||
z.append(el('p', 'e-punkt__text', pt.text));
|
||
|
||
/* Das Anlassfeld. Es steht IMMER da, nicht erst nach dem Klick:
|
||
Ein Feld, das aufklappt, kostet einen zweiten Blick, und genau
|
||
an der Stelle hört man auf zu schreiben. */
|
||
const feld = el('textarea', 'e-punkt__anlass');
|
||
feld.rows = 2;
|
||
feld.value = pt.meins?.anlass || '';
|
||
/* DER HINWEIS STAND BISHER ACHTUNDZWANZIGMAL DA -- auch an Punkten,
|
||
an denen nichts hakt. Ein Satz, der überall steht, wird nirgends
|
||
gelesen. Jetzt sagt das Feld erst dann, dass es gebraucht wird,
|
||
wenn es gebraucht wird. */
|
||
const feldSetzen = (stufe) => {
|
||
const noetig = stufe === 'hakt';
|
||
feld.dataset.noetig = noetig ? 'ja' : 'nein';
|
||
feld.placeholder = noetig
|
||
? 'Woran hast du das gemerkt? Ein Satz genügt – ohne ihn wird daraus kein Schritt.'
|
||
: 'Woran hast du das gemerkt? (kann leer bleiben)';
|
||
};
|
||
feldSetzen(pt.meins?.stufe || null);
|
||
|
||
z.append(knopfsatz(pt.meins?.stufe || null, async (stufe, knopf) => {
|
||
const reihe = knopf.parentElement;
|
||
for (const b of reihe.querySelectorAll('.e-wahl__knopf')) {
|
||
b.setAttribute('aria-pressed', String(b === knopf));
|
||
}
|
||
z.dataset.stufe = stufe;
|
||
feldSetzen(stufe);
|
||
if (stufe === 'hakt' && !feld.value.trim()) feld.focus();
|
||
const a = await hole(`/workspace/api/entwicklung/person/${personId}/${pt.schluessel}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ stufe, anlass: feld.value }),
|
||
});
|
||
if (!a.ok) {
|
||
const j = await a.json().catch(() => ({}));
|
||
melde(sagWas(j.fehler, 'Ging nicht.'));
|
||
feld.focus();
|
||
return;
|
||
}
|
||
melde('');
|
||
/* DER STAND WANDERT SOFORT MIT -- ohne den Server noch einmal zu
|
||
fragen. Daran hängen drei sichtbare Zahlen: der Kopf der Karte,
|
||
der Kopf der Kategorie und die Personenkachel oben, die sonst
|
||
bei „0 von 28" stehen bliebe. */
|
||
const warNeu = !pt.meins;
|
||
pt.meins = { ...(pt.meins || {}), stufe, anlass: feld.value };
|
||
zaehlerNachziehen();
|
||
/* Neu laden NUR beim ersten Klick auf einem Punkt: Erst danach
|
||
darf der Stand des anderen überhaupt ausgeliefert werden, und
|
||
dann soll er auch sichtbar werden. Ohne Sprung -- karte()
|
||
hält seit heute die Höhe. */
|
||
if (warNeu) await karte(personId);
|
||
}));
|
||
|
||
z.append(feld);
|
||
|
||
if (pt.andere?.length) {
|
||
for (const a of pt.andere) {
|
||
const nn = stufen.find((x) => x.schluessel === a.stufe);
|
||
const zeile = el('p', 'e-punkt__andere',
|
||
`${a.von_name}: ${nn?.name || a.stufe}${a.anlass ? ' – ' + a.anlass : ''}`);
|
||
zeile.dataset.stufe = a.stufe;
|
||
z.append(zeile);
|
||
}
|
||
} else if (pt.andere_offen) {
|
||
/* WARUM DA ETWAS STEHT UND NICHT NICHTS: „Noch niemand" und „du
|
||
darfst es noch nicht sehen" sähen sonst gleich aus — und das
|
||
erste ist eine Auskunft, das zweite eine Täuschung. */
|
||
z.append(el('p', 'e-punkt__verdeckt',
|
||
`${pt.andere_offen} andere Einschätzung liegt vor – sichtbar, sobald du selbst gesetzt hast.`));
|
||
}
|
||
|
||
if (pt.meins) {
|
||
z.append(el('p', 'e-punkt__wann',
|
||
`von dir am ${datum(pt.meins.am)}${pt.meins.verblasst ? ' – älter als 90 Tage' : ''}`));
|
||
|
||
/* ==== KOMMT DAS BEI IHM AN? (30.09.2026) ====================
|
||
|
||
VanVan aus dem Support: Sie hatte einem Modi Punkte
|
||
zugetragen und bewertet — und bei ihm tauchte nichts auf.
|
||
|
||
Der Grund ist die Regel drüben in seiner Karte: Ein Punkt
|
||
erscheint dort erst, wenn ALLE Beurteiler gesetzt haben. Die
|
||
Regel bleibt, sie hat einen guten Grund — eine einzelne
|
||
Meinung soll bei einem Menschen nicht als Urteil des ganzen
|
||
Teams ankommen.
|
||
|
||
Falsch war, dass es niemand DENEN sagt, die schon gesetzt
|
||
haben. Sie hat getan, was sie tun sollte, es kam nicht an,
|
||
und nichts auf ihrem Bildschirm erklärte das. Ein Mensch, der
|
||
so etwas zweimal erlebt, hört auf zu setzen.
|
||
|
||
KEIN NAME. Wer noch fehlt, steht hier nicht — das wäre eine
|
||
Aufforderung, jemanden anzutreiben. Dass etwas offen ist,
|
||
genügt; wer es ist, weiß man bei zwei Beurteilern ohnehin. */
|
||
const se = pt.sieht_er;
|
||
if (se && se.noetig > 1 && se.da < se.noetig) {
|
||
const hin = el('p', 'e-punkt__wartet',
|
||
`Er sieht das noch nicht – es fehlt noch eine Einschätzung.`);
|
||
hin.title = 'Eine Beobachtung erscheint bei der Person erst, wenn alle '
|
||
+ 'sie gesetzt haben. Eine halbe Karte wäre schlimmer als keine.';
|
||
z.append(hin);
|
||
}
|
||
}
|
||
|
||
/* ==== AUS DER BEOBACHTUNG WIRD EIN SCHRITT (15.09.2026) ==========
|
||
|
||
Bis heute konnte diese Zeile genau eines: festhalten, dass etwas
|
||
hakt. Danach passierte nichts — beim nächsten Öffnen stand
|
||
dieselbe Beobachtung da, nur älter.
|
||
|
||
Ein Entwicklungsplan wirkt dann, wenn er an einer ECHTEN Aufgabe
|
||
hängt. Also wird aus „da hakt es" eine Aufgabe auf dem Brett,
|
||
mit Verantwortlichem und Frist — und wenn sie erledigt ist,
|
||
fragt die Karte dieselbe Frage noch einmal. Das ist der Kreis.
|
||
============================================================== */
|
||
z.append(schrittZeile(personId, pt));
|
||
return z;
|
||
}
|
||
|
||
const SCHRITT_ABSAGE = {
|
||
kein_hakt: 'Ein Schritt entsteht aus „Da hakt es“ – setz das zuerst.',
|
||
schon_da: 'Dazu läuft schon ein Schritt.',
|
||
};
|
||
|
||
function schrittZeile(personId, pt) {
|
||
const s = pt.schritt;
|
||
const zeile = el('div', 'e-schritt');
|
||
|
||
/* ---- Es läuft schon etwas ---- */
|
||
if (s && s.status !== 'erledigt' && s.status !== 'abgebrochen') {
|
||
zeile.dataset.lage = 'laeuft';
|
||
zeile.append(el('span', 'e-schritt__was', 'Schritt läuft'));
|
||
const a = el('a', 'e-schritt__hin', s.titel);
|
||
/* DIE SCHREIBWEISE DES HAUSES: `#a<nummer>`. Das Brett springt
|
||
damit wirklich zu der Aufgabe -- `?a=` hatte ich erfunden, und
|
||
das Brett liest es nicht. Ein Verweis, der nur die Seite
|
||
oeffnet und nichts zeigt, ist schlechter als keiner. */
|
||
a.href = 'aufgaben.html#a' + s.id;
|
||
zeile.append(a);
|
||
if (s.frist) zeile.append(el('span', 'e-schritt__frist', `bis ${datum(s.frist)}`));
|
||
return zeile;
|
||
}
|
||
|
||
/* ---- Er ist durch: dieselbe Frage noch einmal ----
|
||
DAS IST DER EIGENTLICHE GEWINN. Ohne diesen Satz wäre der Schritt
|
||
nur eine Aufgabe mehr; mit ihm wird aus der Beobachtung eine
|
||
Runde, die zu Ende geht. */
|
||
if (s && s.status === 'erledigt') {
|
||
zeile.dataset.lage = 'durch';
|
||
zeile.append(el('span', 'e-schritt__was', 'Schritt erledigt'));
|
||
zeile.append(el('span', 'e-schritt__satz',
|
||
s.erledigt_am
|
||
? `am ${datum(s.erledigt_am)} – sieh noch einmal hin und setz neu.`
|
||
: 'Sieh noch einmal hin und setz neu.'));
|
||
return zeile;
|
||
}
|
||
|
||
/* ---- Noch keiner: aber nur aus einem „da hakt es" ----
|
||
Der Knopf steht NUR dort. Ein Knopf, der überall steht und fast
|
||
überall „geht nicht" antwortet, ist schlechter als keiner. */
|
||
if (pt.meins?.stufe !== 'hakt') return zeile;
|
||
|
||
zeile.dataset.lage = 'offen';
|
||
const b = el('button', 'schritt e-schritt__knopf', 'Daraus einen Schritt machen');
|
||
b.type = 'button';
|
||
b.title = 'Legt eine Aufgabe auf dem Brett an – mit dieser Person als '
|
||
+ 'Verantwortlicher und einer Frist in zwei Wochen.';
|
||
b.addEventListener('click', async () => {
|
||
b.disabled = true;
|
||
const a = await hole(
|
||
`/workspace/api/entwicklung/person/${personId}/${pt.schluessel}/schritt`,
|
||
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
|
||
b.disabled = false;
|
||
if (!a.ok) {
|
||
const j = await a.json().catch(() => ({}));
|
||
melde(SCHRITT_ABSAGE[j.fehler] || 'Ging nicht.');
|
||
return;
|
||
}
|
||
const neu = await a.json();
|
||
melde(`Steht auf dem Aufgabenbrett: „${neu.titel}“, bis ${datum(neu.frist)}.`);
|
||
await karte(personId);
|
||
});
|
||
zeile.append(b);
|
||
return zeile;
|
||
}
|
||
|
||
/* ---- Deine eigene Karte -----------------------------------------------
|
||
|
||
WAS HIER STEHT UND WAS NICHT: die Antworten, nicht die Namen, und
|
||
nicht der Anlass. „2 × Läuft" oder „1 × Läuft, 1 × Wächst gerade".
|
||
|
||
Der Anlass ist für ein Gespräch aufgeschrieben, nicht zum Lesen
|
||
ohne den Menschen dazu — deshalb steht hier stattdessen der Satz,
|
||
dass man danach fragen soll. Ein Satz aus einer Notiz, aus dem
|
||
Zusammenhang gerissen, richtet mehr an, als er erklärt. */
|
||
/** Welche Punkte zeigt der Filter gerade?
|
||
*
|
||
* AN EINER STELLE, weil an zwei Stellen gefiltert wird: beim
|
||
* Zeichnen und beim Nachziehen der Zähler. Standen dort zwei
|
||
* Abschriften, zeigte der Kopf „3 / 14", während vier Punkte
|
||
* darunter stehen — genau die Sorte Unstimmigkeit, die einen an
|
||
* der Zahl zweifeln lässt statt am Filter. */
|
||
function punkteGefiltert(punkte) {
|
||
if (!stufenFilter) return punkte;
|
||
if (stufenFilter === 'zugetragen') return punkte.filter((pt) => pt.zugetragen);
|
||
return punkte.filter((pt) => pt.stufe === stufenFilter);
|
||
}
|
||
|
||
/** Welche Abschnitte der eigenen Karte stehen offen?
|
||
* Im Gedaechtnis und nicht im Speicher des Browsers -- genau wie
|
||
* bei der Karte der Leitung nebenan. Eine zweite Art, sich etwas
|
||
* zu merken, waere eine zweite Art, es falsch zu merken. */
|
||
let offeneEigene = null;
|
||
|
||
/** Ein einklappbarer Abschnitt -- Kopf mit Pfeil, Zahl und Koerper.
|
||
*
|
||
* DERSELBE BAUSTEIN WIE IN DER KARTE DER LEITUNG (30.09.2026).
|
||
* VanVan aus dem Support: „Deine Karte … kann man noch nicht
|
||
* einklappen, was die Seite unnötig lang zieht." Gemessen war die
|
||
* Karte 6186 px hoch — 85 Prozent der ganzen Seite — und hatte
|
||
* keinen einzigen Schalter.
|
||
*
|
||
* Die Klassen sind dieselben wie nebenan, damit beide Karten
|
||
* gleich aussehen und gleich bedient werden. Ein zweiter
|
||
* Mechanismus daneben wäre der, der beim nächsten Umbau vergessen
|
||
* wird.
|
||
*
|
||
* @param {string} schluessel Wofür der offene Zustand gemerkt wird
|
||
* @param {string} name Die Überschrift
|
||
* @param {string} zahl Was zugeklappt sichtbar bleibt
|
||
* @param {boolean} offen Anfangszustand
|
||
* @returns {{abschnitt: HTMLElement, koerper: HTMLElement,
|
||
* umschalten: Function}}
|
||
*/
|
||
function klappAbschnitt(schluessel, name, zahl, offen) {
|
||
const abschnitt = el('section', 'e-block');
|
||
abschnitt.dataset.block = schluessel;
|
||
abschnitt.dataset.offen = offen ? 'ja' : 'nein';
|
||
|
||
const koerper = el('div', 'e-block__koerper');
|
||
koerper.id = `eigen-${schluessel}`;
|
||
koerper.hidden = !offen;
|
||
|
||
const titel = el('h3', 'e-block__zeile');
|
||
const schalter = el('button', 'e-block__schalter');
|
||
schalter.type = 'button';
|
||
schalter.setAttribute('aria-expanded', String(offen));
|
||
schalter.setAttribute('aria-controls', koerper.id);
|
||
schalter.append(pfeil());
|
||
schalter.append(el('span', 'e-block__name', name));
|
||
/* DIE ZAHL STEHT IM KOPF, damit man sie ZUGEKLAPPT sieht. Eine
|
||
zugeklappte Kategorie, die nicht sagt, wie viel in ihr steckt,
|
||
klappt man einmal auf und danach nie wieder zu. */
|
||
if (zahl) schalter.append(el('span', 'e-block__stand', zahl));
|
||
|
||
const umschalten = (auf) => {
|
||
abschnitt.dataset.offen = auf ? 'ja' : 'nein';
|
||
koerper.hidden = !auf;
|
||
schalter.setAttribute('aria-expanded', String(auf));
|
||
};
|
||
schalter.addEventListener('click', () => {
|
||
const jetztOffen = !offeneEigene.has(schluessel);
|
||
if (jetztOffen) offeneEigene.add(schluessel);
|
||
else offeneEigene.delete(schluessel);
|
||
/* NUR DIESEN ABSCHNITT, kein Neuaufbau: Bei 68 Zeilen sieht
|
||
man jeden Sprung. */
|
||
umschalten(jetztOffen);
|
||
if (eigeneLeisteNachziehen) eigeneLeisteNachziehen();
|
||
});
|
||
titel.append(schalter);
|
||
abschnitt.append(titel);
|
||
abschnitt.append(koerper);
|
||
return { abschnitt, koerper, umschalten };
|
||
}
|
||
/* Wird beim Zeichnen gesetzt -- der Knopf „Alle" muss nach jedem
|
||
einzelnen Klick seine Aufschrift pruefen. */
|
||
let eigeneLeisteNachziehen = null;
|
||
|
||
/** Die Antworten zu einem fertigen Punkt.
|
||
*
|
||
* ALS EIGENE FUNKTION, seit die Aufgaben oben dieselbe Reihe
|
||
* brauchen (30.09.2026). Zweimal abgeschrieben waere sie die, bei
|
||
* der eine Seite irgendwann anders aussieht als die andere. */
|
||
function antwortReihe(pt) {
|
||
const antw = el('div', 'e-meine__antworten');
|
||
for (const x of pt.antworten || []) {
|
||
const chip = el('span', 'e-meine__antwort', `${x.anzahl} × ${x.name}`);
|
||
chip.dataset.stufe = x.stufe;
|
||
antw.append(chip);
|
||
}
|
||
if (!pt.einig) {
|
||
const hin = el('span', 'e-meine__uneinig', 'darüber seid ihr euch nicht einig');
|
||
hin.title = 'Zwei Leute haben dasselbe verschieden gesehen. Das ist normal – '
|
||
+ 'und ein guter Anlass, danach zu fragen.';
|
||
antw.append(hin);
|
||
}
|
||
return antw;
|
||
}
|
||
|
||
async function meineKarte() {
|
||
let a;
|
||
try { a = await hole('/workspace/api/entwicklung/meine-karte'); } catch { return; }
|
||
if (!a.ok) return;
|
||
const r = await a.json();
|
||
if (!r.hat_karte) return; // DogFather hat keine
|
||
|
||
$('karte-block').hidden = false;
|
||
const satz = $('karte-satz');
|
||
const ziel = $('meine-karte');
|
||
ziel.textContent = '';
|
||
ziel.setAttribute('aria-busy', 'false');
|
||
|
||
if (!r.beurteiler) {
|
||
satz.textContent = 'Es gibt gerade niemanden, der sie führt. Sobald jemand '
|
||
+ 'da ist, steht hier etwas.';
|
||
return;
|
||
}
|
||
/* ==== ZUERST: WAS SIND MEINE AUFGABEN? (30.09.2026) ===========
|
||
|
||
VanVan aus dem Support: „dann sieht der Modi zwar die
|
||
Auswertung im Bereich Entwicklung, aber bei ihm taucht nichts
|
||
im Bereich eure Aufgaben unten in der Karte auf."
|
||
|
||
Nachgestellt und bestaetigt: Waren die Bewertungen noch nicht
|
||
vollstaendig, stieg diese Funktion hier mit `return` aus — und
|
||
damit sah er GAR NICHTS, obwohl ihm zwei Aufgaben zugetragen
|
||
worden waren.
|
||
|
||
DAS WAR DIE FALSCHE REIHENFOLGE. Eine Aufgabe gibt es, bevor
|
||
sie beurteilt wird. Wer sie erst sieht, wenn sich zwei Leute
|
||
ueber ihre Ausfuehrung einig geworden sind, kann sie nicht
|
||
angehen.
|
||
|
||
Die Regel fuer die BEWERTUNG bleibt: Ein Punkt erscheint erst,
|
||
wenn alle Beurteiler gesetzt haben. Eine einzelne Meinung soll
|
||
bei einem Menschen nicht als Urteil des Teams ankommen. */
|
||
const meine = r.bloecke.flatMap((b) => b.punkte).filter((pt) => pt.zugetragen);
|
||
|
||
/* ==== WELCHE ABSCHNITTE STEHEN OFFEN? (30.09.2026) ============
|
||
Dieselbe Überlegung wie bei der Karte der Leitung: Alle zu
|
||
hieße „jedes Mal erst suchen", alle auf wäre der Zustand von
|
||
vorher — gemessen 6186 px, 85 Prozent der Seite.
|
||
|
||
Offen steht, was ich TUN soll, und von den Beobachtungen die
|
||
erste Kategorie. Man landet dort, wo etwas zu sehen ist. */
|
||
const mitInhalt = r.bloecke.filter((b) =>
|
||
b.punkte.some((pt) => !pt.offen && !pt.zugetragen));
|
||
if (offeneEigene === null) {
|
||
offeneEigene = new Set(['auftrag']);
|
||
if (mitInhalt[0]) offeneEigene.add(mitInhalt[0].schluessel);
|
||
}
|
||
|
||
/* Die Leiste mit „Alle auf-/zuklappen" — dieselbe wie nebenan.
|
||
Sie steht nur da, wenn es überhaupt etwas zu klappen gibt: Ein
|
||
Knopf, der auf einer leeren Karte „Alle aufklappen" anbietet,
|
||
ist eine Auskunft über nichts. */
|
||
const klappbar = [
|
||
...(meine.length ? ['auftrag'] : []),
|
||
...mitInhalt.map((b) => b.schluessel),
|
||
];
|
||
const umschalter = [];
|
||
let alleKnopf = null;
|
||
if (klappbar.length > 1) {
|
||
const leiste = el('div', 'e-bloecke__leiste');
|
||
alleKnopf = el('button', 'e-bloecke__alle');
|
||
alleKnopf.type = 'button';
|
||
eigeneLeisteNachziehen = () => {
|
||
const alleAuf = klappbar.every((k) => offeneEigene.has(k));
|
||
alleKnopf.textContent = alleAuf ? 'Alle zuklappen' : 'Alle aufklappen';
|
||
};
|
||
eigeneLeisteNachziehen();
|
||
alleKnopf.addEventListener('click', () => {
|
||
const alleAuf = klappbar.every((k) => offeneEigene.has(k));
|
||
if (alleAuf) for (const k of klappbar) offeneEigene.delete(k);
|
||
else for (const k of klappbar) offeneEigene.add(k);
|
||
for (const u of umschalter) u(!alleAuf);
|
||
eigeneLeisteNachziehen();
|
||
});
|
||
leiste.append(alleKnopf);
|
||
ziel.append(leiste);
|
||
} else {
|
||
eigeneLeisteNachziehen = null;
|
||
}
|
||
|
||
if (meine.length) {
|
||
const k = klappAbschnitt('auftrag',
|
||
meine.length === 1 ? 'Deine Aufgabe' : 'Deine Aufgaben',
|
||
String(meine.length), offeneEigene.has('auftrag'));
|
||
const auf = k.abschnitt;
|
||
auf.classList.add('e-block--auftrag');
|
||
umschalter.push(k.umschalten);
|
||
k.koerper.append(el('p', 'e-block__satz',
|
||
'Das ist dir zugetragen. Daran arbeitest du – unabhängig davon, '
|
||
+ 'ob schon jemand etwas dazu notiert hat.'));
|
||
for (const pt of meine) {
|
||
const zeile = el('article', 'e-punkt e-punkt--meine');
|
||
zeile.dataset.zugetragen = 'ja';
|
||
zeile.append(el('h4', 'e-punkt__titel', pt.titel));
|
||
if (pt.offen) {
|
||
/* „NOCH NIEMAND" UND „EINER VON ZWEIEN" SIND NICHT DASSELBE.
|
||
Das erste heisst, es hat noch keiner hingesehen; das
|
||
zweite, dass es laeuft. Eine ZAHL steht hier bewusst
|
||
nicht — sie machte aus einer Auskunft einen Countdown. */
|
||
zeile.append(el('p', 'e-punkt__wann', pt.davon_da
|
||
? 'Einschätzung läuft – sie erscheint, sobald alle hingesehen haben.'
|
||
: 'Noch nicht eingeschätzt.'));
|
||
} else {
|
||
zeile.append(antwortReihe(pt));
|
||
}
|
||
k.koerper.append(zeile);
|
||
}
|
||
ziel.append(auf);
|
||
}
|
||
|
||
if (!r.fertig) {
|
||
/* EINE ZAHL IN EINEM SATZ MUSS SICH VORLESEN LASSEN. „wenn alle 1
|
||
sie gesetzt haben" ist keine Auskunft, sondern eine Rechnung --
|
||
und wer einen Satz liest, der falsch klingt, glaubt auch dem
|
||
Rest der Seite weniger. Derselbe Fall wie bei der Ampel und
|
||
beim Takt auf befinden.html. */
|
||
satz.textContent = (r.beurteiler === 1
|
||
? 'Noch keine Einschätzung durch. Eine Beobachtung erscheint hier, sobald sie '
|
||
+ 'gesetzt ist – eine halbe Karte wäre schlimmer als keine.'
|
||
: `Noch keine Einschätzung durch. Eine Beobachtung erscheint hier erst, `
|
||
+ `wenn alle ${r.beurteiler} sie gesetzt haben – eine halbe Karte ist `
|
||
+ `schlimmer als keine.`)
|
||
/* „DARUNTER" UND NICHT „OBEN". Der Satz steht ueber der Liste
|
||
(`karte-satz` vor `#meine-karte`) -- am Bildschirm
|
||
nachgesehen, nicht aus dem Quelltext geschlossen. Ein
|
||
Verweis in die falsche Richtung laesst einen an der
|
||
falschen Stelle suchen. */
|
||
+ (meine.length ? ' Was dir zugetragen ist, steht darunter.' : '');
|
||
return;
|
||
}
|
||
|
||
satz.textContent = `${r.fertig} von ${r.gesamt} Beobachtungen sind durch. `
|
||
+ 'Du siehst die Antworten, nicht wer was gesetzt hat – und nicht, was '
|
||
+ 'danebengeschrieben steht. Das gehört ins Gespräch: frag danach.';
|
||
|
||
for (const b of mitInhalt) {
|
||
/* WAS OBEN SCHON STEHT, STEHT NICHT NOCH EINMAL. Eine Aufgabe
|
||
zweimal auf derselben Seite liest sich wie zwei Aufgaben. */
|
||
const fertige = b.punkte.filter((pt) => !pt.offen && !pt.zugetragen);
|
||
const k = klappAbschnitt(b.schluessel, b.name, String(fertige.length),
|
||
offeneEigene.has(b.schluessel));
|
||
umschalter.push(k.umschalten);
|
||
/* Der erklärende Satz gehört in den Körper, nicht in den Kopf:
|
||
Zugeklappt will man die Überschrift lesen, nicht einen
|
||
Absatz. Dieselbe Entscheidung wie bei der Karte nebenan. */
|
||
if (b.text) k.koerper.append(el('p', 'e-block__satz', b.text));
|
||
for (const pt of fertige) {
|
||
const zeile = el('article', 'e-punkt e-punkt--meine');
|
||
if (pt.einig) zeile.dataset.stufe = pt.antworten[0].stufe;
|
||
zeile.append(el('h4', 'e-punkt__titel', pt.titel));
|
||
zeile.append(antwortReihe(pt));
|
||
k.koerper.append(zeile);
|
||
}
|
||
ziel.append(k.abschnitt);
|
||
}
|
||
}
|
||
|
||
/* ---- Start ------------------------------------------------------------ */
|
||
(async () => {
|
||
try {
|
||
const a = await hole('/workspace/api/entwicklung/lage');
|
||
if (!a.ok) { melde('Ging nicht.'); return; }
|
||
const lage = await a.json();
|
||
stufen = lage.stufen;
|
||
|
||
/* DER KATALOG WIRD ZUERST EINGERICHTET (vorgezogen am 24.09.2026).
|
||
|
||
Er stand weiter unten, hinter dem ganzen Leitungsteil. Das ging
|
||
gut, solange niemand vorher eine Karte oeffnete -- "karte()"
|
||
ruft am Ende aber "Vorlagenbrett.zeigen()", und auf dem Weg
|
||
ueber "?zeigen=person-…" passiert genau das, bevor der Katalog
|
||
eingerichtet ist. In der Browserkonsole stand dann
|
||
"Vorlagenbrett: zeigen() vor einrichten()", und der Katalog
|
||
blieb beim ersten Zeichnen leer.
|
||
|
||
Gefunden am 24.09.2026, weil die Gegenprobe in pruef-schritt
|
||
neu gefasst wurde und seither ueber diesen Weg geht. Vorher
|
||
ging KEINE Pruefung ueber "?zeigen=" -- der Weg von der
|
||
Talentseite war ungeprueft.
|
||
|
||
Einrichten legt nur die Rueckrufe an (alle "() => …", also
|
||
traege); es hat mit der Leitungssicht nichts zu tun und darf
|
||
deshalb fuer alle und als Erstes laufen. */
|
||
/* DER VORLAGENKATALOG STEHT FUER BEIDE DA -- Leitung und Modi.
|
||
|
||
Er gehoert ausdruecklich NICHT in den Leitungszweig darueber.
|
||
Filipe: "die modis sollen bei all diesen voschlaegen auch nur
|
||
bewerben koennen." Genau dafuer muessen sie ihn sehen. Was
|
||
jemand damit tun darf -- uebernehmen oder sich bewerben --,
|
||
entscheidet der Server und nicht diese Zeile.
|
||
|
||
Vor der Personenwahl: Der Katalog steht trotzdem schon da und
|
||
sagt, dass noch niemand gewaehlt ist. Ein Block, der erst nach
|
||
einem Klick erscheint, wird uebersehen. */
|
||
katalogAnschliessen();
|
||
|
||
if (lage.leitung) {
|
||
/* DIE LEUTE FUER „NOCH JEMANDEN DAZUNEHMEN" kommen aus
|
||
derselben Antwort wie die Kacheln. Ein eigener Abruf waere
|
||
eine zweite Liste, die im Zweifel eine andere ist. */
|
||
leute = lage.personen || [];
|
||
/* Das Verteil-Band gehoert zur Leitungssicht -- ein Modi sieht
|
||
hier nur sich selbst und verteilt nichts. */
|
||
aufgabenFormularAufbauen();
|
||
verteilBandZeigen(null, null);
|
||
$('personen-block').hidden = false;
|
||
const ziel = $('personen');
|
||
ziel.textContent = '';
|
||
ziel.setAttribute('aria-busy', 'false');
|
||
if (!lage.personen.length) {
|
||
ziel.append(el('p', 'leise',
|
||
'Noch niemand im Team. Zugänge legst du in „Personen & Zugänge“ an.'));
|
||
}
|
||
gesucht = null;
|
||
for (const p of lage.personen) ziel.append(personKarte(p, lage));
|
||
/* ERST JETZT SPRINGEN. Die Kachel entsteht in der Schleife
|
||
darueber; waehrenddessen haengt sie noch nicht im Dokument
|
||
und hat keine Position. Ein Sprung dorthin waere ein Sprung
|
||
an den Seitenanfang -- lautlos, und man haelt den Link fuer
|
||
kaputt.
|
||
|
||
Die Karte wird MIT geoeffnet: Wer von der Talentseite kommt,
|
||
will die Karte sehen und nicht die Kachel dazu. Genau der
|
||
Schritt, den der Link ersparen soll. */
|
||
if (gesucht) {
|
||
const wen = Number(gesucht.dataset.person);
|
||
window.zielMarkieren?.([gesucht], 'diese eine Karte');
|
||
if (wen) await karte(wen, true);
|
||
}
|
||
await ampel();
|
||
} else {
|
||
/* Für die Beurteilten ist DIESE Seite die eigene Karte — das,
|
||
was die Leitung über ihn notiert hat, und was er selbst
|
||
dazu sagt. Nicht „Entwicklung" als Überschrift: Das liest
|
||
sich wie eine Beurteilung, und darum geht es hier nicht.
|
||
|
||
Bis zum 15.09.2026 stand hier „Wie geht's dir?" — damals
|
||
bestand die Seite für ihn aus den sechs Fragen. Die haben
|
||
jetzt ihre eigene Seite, also stimmte der Titel nicht mehr.
|
||
Ein Titel, der etwas anderes verspricht als die Seite zeigt,
|
||
ist derselbe Fehler wie der an der Kachel. */
|
||
/* NICHT 'Deine Entwicklung' (geaendert 19.09.2026): So hiess bis
|
||
heute AUCH werdegang.html fuer denselben Menschen. Wer aus der
|
||
Glocke oder einem Link kam, konnte nicht sagen, auf welcher der
|
||
beiden Seiten er stand. Jetzt traegt jede den Namen ihrer
|
||
Kachel: hier 'Deine Aufgaben', dort 'Deine Entwicklung'. */
|
||
$('titel').textContent = 'Deine Aufgaben';
|
||
$('unterzeile').textContent =
|
||
'Was im Team über deine Arbeit notiert ist – und was du selbst dazu sagst. '
|
||
+ 'Keine Noten, keine Punktzahl.';
|
||
}
|
||
|
||
/* ERST DIE AUFGABEN, DANN DER KATALOG. Er markiert daran, was
|
||
schon uebernommen wurde -- ohne diesen Abruf stuende die
|
||
Markierung beim ersten Zeichnen nicht da, und fuer einen Modi
|
||
nie, weil bei ihm nie eine Person gewaehlt wird. */
|
||
await aufgabenHolen();
|
||
await window.Vorlagenbrett?.zeigen();
|
||
|
||
await meineKarte();
|
||
/* Die sechs Fragen stehen seit dem 15.09.2026 auf einer eigenen
|
||
Seite. Hier bleibt nur der Verweis -- er wird fuer alle
|
||
gezeigt, die dort ueberhaupt etwas zu beantworten haben. */
|
||
if ($('meins-block')) $('meins-block').hidden = false;
|
||
} catch {
|
||
/* NICHT MEHR STUMM (20.09.2026). Scheitert hier etwas am Netz,
|
||
blieben die Ladeplatzhalter fuer immer stehen -- gemessen auf
|
||
13 von 21 Seiten. Umgeleitet wird nur bei 401; alles andere
|
||
ist ein Ladefehler und gehoert gesagt. */
|
||
window.ladefehler?.();
|
||
}
|
||
})();
|
||
})();
|