Files
dogfather-universe/workspace/assets/js/entwicklung.js
T
DogFatherGitandClaude Opus 5 45f5a35afe Die Kategorien lassen sich auf- und zuklappen
Filipe: "mach das viel geiler alles bitte, ich will auch dass man die
kategorien von den fragen, also die liste auf und zu druecken kann mit
einem button."

ACHTUNDZWANZIG PUNKTE OFFEN SIND RUND ZWEI METER SEITE. Man scrollt,
verliert die Stelle und macht es beim naechsten Mal gar nicht mehr.

  - Jede Kategorie hat einen Knopf. Ein Klick wechselt nur DIESEN
    Abschnitt; die Karte wird nicht neu geholt, sonst springt die Seite
    bei 28 Punkten unter den Fingern weg.
  - Der Zustand liegt im Skript, nicht an den Elementen: Nach dem
    ersten Klick auf einen Punkt zeichnet die Karte sich neu (erst
    danach darf der Stand der anderen ausgeliefert werden). Am Element
    klappte dabei alles wieder zu -- mitten in der Arbeit.
  - Welche zuerst offen steht, ist die eigentliche Entscheidung: die
    erste, in der noch etwas fehlt. Alle zu hiesse jedes Mal suchen,
    alle auf waere der Zustand von vorher.
  - Der Kopf sagt ZUGEKLAPPT, was drin ist: "2 / 8" und, wenn etwas
    hakt, "1 x hakt". Eine zugeklappte Kategorie, die nichts sagt,
    klappt man einmal auf und danach nie wieder zu.
  - Daneben "Alle aufklappen" / "Alle zuklappen" fuer den Durchgang.

WAS DAS BILDSCHIRMFOTO AUSSERDEM GEZEIGT HAT, und was ich im Code nicht
gesehen haette: Die Karte hatte gar keine eigene Flaeche. "Frida · Modi
· 2 von 28 angesehen" lag quer ueber dem Spicy-Media-Schriftzug, der
Knopf darunter ueber einer Chilischote. Genau der Fall, fuer den die
Kontrastmessung ihren dritten Ausgang hat ("konnte nicht nachsehen") --
ueber einem Foto laesst sich Lesbarkeit nicht ausrechnen. Die Antwort
darauf ist keine hellere Schrift, sondern eine deckende Flaeche.

Und der Hinweis "(bei 'Da hakt es' noetig)" stand bisher 28 Mal da,
auch an Punkten, an denen nichts hakt. Ein Satz, der ueberall steht,
wird nirgends gelesen. Jetzt meldet sich das Feld erst, wenn es
gebraucht wird -- mit Rahmen in derselben Farbe wie die Antwort, und
der Mauszeiger springt hinein.

Beim Nachsehen im Bild gefunden: "2 von 28 angesehen" stand danach
zweimal auf demselben Bildschirm. Zweimal dieselbe Zahl laesst einen
ueberlegen, ob es zwei verschiedene sind.

  server/pruef-schritt.mjs   57 Pruefungen (vorher 46), 0 Fehler
  Der Knopf wird wirklich gedrueckt -- ein Knopf, den niemand betaetigt
  hat, ist kein geprueter Knopf.

  pruef-css-klassen, pruef-entwicklung 43, pruef-neue-seiten 93,
  pruef-nachwuchs 123 -- alle 0 Fehler

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

572 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
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;
};
const melde = (t) => { const f = $('fehler'); if (f) { f.textContent = t; f.hidden = !t; } };
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' });
};
async function hole(pfad, optionen = {}) {
const a = await fetch(pfad, { credentials: 'same-origin', ...optionen });
if (a.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); }
if (a.status === 404) { location.assign('/workspace/start.html'); throw new Error('unbekannt'); }
return a;
}
let stufen = [];
/* 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
/* ---- 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; // für einen Modi gibt es sie nicht
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 sechs Fragen schon beantwortet. `
+ 'Namen stehen hier nicht – auch für dich nicht.'));
}
}
/* ---- Die Personen --------------------------------------------------- */
function personKarte(p, lage) {
const k = el('button', 'e-person');
k.type = 'button';
k.append(el('span', 'e-person__name', p.name));
k.append(el('span', 'e-person__rolle', p.rolle_name));
const zahlen = el('span', 'e-person__zahlen');
zahlen.append(el('span', 'e-person__zahl',
`${p.gesetzt} von ${lage.gesamt_fremd} angesehen`));
/* DIE ABWEICHUNG STEHT VORNE UND NICHT IM DETAIL. Sie ist das
Ergebnis dieser Seite: Wo zwei Leute dasselbe sehen, gibt es
nichts zu besprechen. */
if (p.abweichungen) {
const z = el('span', 'e-person__zahl e-person__zahl--wichtig',
`${p.abweichungen} × verschieden gesehen`);
z.title = 'Hier lohnt das Gespräch – nicht dort, wo ihr einig seid.';
zahlen.append(z);
}
if (p.veraltet) {
const z = el('span', 'e-person__zahl e-person__zahl--alt',
`${p.veraltet} älter als ${lage.verblasst_nach} Tage`);
z.title = 'Eine alte Einschätzung ist keine Einschätzung mehr.';
zahlen.append(z);
}
k.append(zahlen);
k.addEventListener('click', () => karte(p.id));
return k;
}
/* ---- Die Karte einer Person ----------------------------------------- */
async function karte(id) {
offen = id;
const ziel = $('karte');
ziel.hidden = false;
ziel.textContent = '';
ziel.append(el('p', 'leise', 'wird geladen …'));
ziel.scrollIntoView({ behavior: 'smooth', block: 'start' });
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;
ziel.textContent = '';
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.gesetzt} von ${r.gesamt} angesehen`));
/* 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);
ziel.append(kopf);
/* ---- 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.
================================================================ */
if (karteFuer !== id) { karteFuer = id; offeneBloecke = 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();
alleKnopf.addEventListener('click', () => {
if (alleZu()) offeneBloecke.clear();
else for (const b of r.bloecke) offeneBloecke.add(b.schluessel);
karte(id);
});
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. */
ziel.append(leiste);
for (const b of r.bloecke) {
const gesetzt = b.punkte.filter((pt) => pt.meins).length;
const offen = offeneBloecke.has(b.schluessel);
const haktHier = b.punkte.filter((pt) => pt.meins?.stufe === 'hakt').length;
const abschnitt = el('section', 'e-block');
abschnitt.dataset.offen = offen ? 'ja' : 'nein';
if (gesetzt === b.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} / ${b.punkte.length}`);
if (gesetzt === b.punkte.length) stand.dataset.fertig = 'ja';
schalter.append(stand);
if (haktHier) {
const h = el('span', 'e-block__hakt', `${haktHier} ×`);
h.title = haktHier === 1 ? 'Hier hakt ein Punkt.' : `Hier haken ${haktHier} Punkte.`;
h.append(el('span', 'e-block__hakt-wort', 'hakt'));
schalter.append(h);
}
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. */
const jetztOffen = offeneBloecke.has(b.schluessel);
abschnitt.dataset.offen = jetztOffen ? 'ja' : 'nein';
koerper.hidden = !jetztOffen;
schalter.setAttribute('aria-expanded', String(jetztOffen));
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 b.punkte) koerper.append(punktZeile(id, pt, r.erwartung));
abschnitt.append(koerper);
ziel.append(abschnitt);
}
}
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';
const kopf = el('div', 'e-punkt__kopf');
kopf.append(el('h4', 'e-punkt__titel', pt.titel));
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(j.fehler || 'Ging nicht.');
feld.focus();
return;
}
melde('');
/* Neu laden, sobald der erste Klick auf einem Punkt sitzt: Erst
danach darf der Stand des anderen überhaupt ausgeliefert
werden, und dann soll er auch sichtbar werden. */
if (!pt.meins) 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' : ''}`));
}
/* ==== 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. */
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;
}
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 nichts fertig. Eine Beobachtung erscheint hier, sobald sie gesetzt ist '
+ '– eine halbe Karte wäre schlimmer als keine.'
: `Noch nichts fertig. Eine Beobachtung erscheint hier erst, `
+ `wenn alle ${r.beurteiler} sie gesetzt haben – eine halbe Karte ist `
+ `schlimmer als keine.`;
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 r.bloecke) {
const fertige = b.punkte.filter((pt) => !pt.offen);
if (!fertige.length) continue;
const abschnitt = el('section', 'e-block');
abschnitt.append(el('h3', 'e-block__titel', b.name));
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));
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);
}
zeile.append(antw);
abschnitt.append(zeile);
}
ziel.append(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;
if (lage.leitung) {
$('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.'));
}
for (const p of lage.personen) ziel.append(personKarte(p, lage));
await ampel();
} else {
/* Für einen Modi ist DIESE Seite seine 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. */
$('titel').textContent = 'Deine Entwicklung';
$('unterzeile').textContent =
'Was im Team über deine Arbeit notiert ist – und was du selbst dazu sagst. '
+ 'Keine Noten, keine Punktzahl.';
}
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 { /* umgeleitet */ }
})();
})();