/* ===================================================================== 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 = []; 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 ---------------------------------------------------- */ for (const b of r.bloecke) { const abschnitt = el('section', 'e-block'); abschnitt.append(el('h3', 'e-block__titel', b.name)); abschnitt.append(el('p', 'e-block__satz', b.text)); for (const pt of b.punkte) abschnitt.append(punktZeile(id, pt, r.erwartung)); 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 || ''; feld.placeholder = 'Woran hast du das gemerkt? (bei „Da hakt es" nötig)'; 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; 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' : ''}`)); } return z; } /* ---- 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) { satz.textContent = `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); } } /* ---- Die eigenen sechs Fragen ---------------------------------------- */ async function meins() { const a = await hole('/workspace/api/entwicklung/mich'); if (!a.ok) return; const r = await a.json(); stufen = stufen.length ? stufen : r.stufen; $('meins-block').hidden = false; $('meins-titel').textContent = r.block.name; $('meins-satz').textContent = r.block.text; const ziel = $('meins'); ziel.textContent = ''; ziel.setAttribute('aria-busy', 'false'); for (const pt of r.punkte) { const z = el('article', 'e-punkt e-punkt--meins'); if (pt.meins) z.dataset.stufe = pt.meins.stufe; z.append(el('h4', 'e-punkt__titel', pt.titel)); z.append(el('p', 'e-punkt__text', pt.text)); z.append(knopfsatz(pt.meins?.stufe || null, async (stufe, knopf) => { for (const b of knopf.parentElement.querySelectorAll('.e-wahl__knopf')) { b.setAttribute('aria-pressed', String(b === knopf)); } z.dataset.stufe = stufe; const s = await hole(`/workspace/api/entwicklung/mich/${pt.schluessel}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ stufe }), }); if (!s.ok) melde('Ging nicht.'); })); ziel.append(z); } } /* ---- 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 die sechs Fragen. Der Titel sagt das auch — sonst steht über sechs Fragen das Wort „Entwicklung", und das liest sich wie eine Beurteilung. */ $('titel').textContent = "Wie geht's dir?"; $('unterzeile').textContent = 'Sechs Fragen über dich. Niemand aus dem Team sieht deine Antworten.'; } await meineKarte(); await meins(); } catch { /* umgeleitet */ } })(); })();