/* ===================================================================== ENTWICKLUNG — die Auswertung je Person (19.09.2026) Zwei Gesichter, und welches man sieht, entscheidet der Server: Wer das Team führt, bekommt die Karten; wer dazugehört, bekommt ausschließlich die Bewegung in der eigenen. --------------------------------------------------------------------- DIE DIAGRAMME SIND SELBST GEZEICHNET Kein Diagrammpaket. Zwei Formen, beide aus wenigen Zeilen: BEWEGUNG Balken um eine Nulllinie, nach oben „nach vorn“, nach unten „zurück“. Sechs Monate, mehr passt auf ein Handy nicht und sagt auch nichts. STREIFEN Ein Raster: Zeile = Punkt, Spalte = Monat, Zelle = die Stufe, die damals galt. Dieselbe Bauform wie der Befinden-Verlauf — wer die eine kennt, liest die andere. Ein Paket dafür wären 60 kB über eine Leitung, auf der jemand mit dem Handy im Stream steht. Und es brächte seine eigenen Farben mit, die dann neben den Hausfarben stünden. --------------------------------------------------------------------- DIE FARBE IST NIE DIE AUSKUNFT Jede Zelle trägt das Zeichen ihrer Stufe als Text (✓ ↗ ! –), jeder Balken seine Zahl. Wer Farben schlecht unterscheidet, liest genau dasselbe. Das ist im Haus seit dem Befinden-Verlauf so. Und es gibt hier kein Rot. „Da hakt es“ ist eine Beobachtung, kein Alarm; ein Rückschritt heißt oft nur, dass jemand genauer hingesehen hat als beim letzten Mal. ===================================================================== */ (() => { 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; } }; async function hole(pfad) { const a = await fetch(pfad, { credentials: 'same-origin' }); if (a.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); } return a; } /* Die vier Stufen, wie der Server sie schickt — Zeichen inklusive. Eine eigene Liste hier wäre die, die beim nächsten Nachschärfen auseinanderläuft. */ let stufen = []; const zeichenVon = (s) => stufen.find((x) => x.schluessel === s)?.zeichen || '·'; const namenVon = (s) => stufen.find((x) => x.schluessel === s)?.name || s; const MONATSNAME = ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez']; function monatKurz(m) { const [j, mm] = String(m).split('-').map(Number); if (!j || !mm) return m; return `${MONATSNAME[mm - 1]}${mm === 1 ? ' ' + String(j).slice(2) : ''}`; } function datumKurz(iso) { const t = Date.parse(iso || ''); if (!Number.isFinite(t)) return ''; return new Date(t).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit' }); } /* ===================================================================== DIAGRAMM 1 — BEWEGUNG ZWEI RICHTUNGEN UM EINE NULLLINIE. „Nach vorn“ und „zurück“ sind dieselbe Größe mit verschiedenem Vorzeichen; zwei getrennte Diagramme würden dazu verleiten, sie wie zwei Themen zu lesen. EINE ACHSE, EIN MASSSTAB. Der höchste Balken bestimmt ihn, und er gilt für oben und unten gleichermaßen — sonst sähe ein einzelner Rückschritt so groß aus wie fünf Fortschritte. KEINE ZAHL AN JEDEM BALKEN. Nur dort, wo etwas steht. Eine Null an jedem leeren Monat wäre Lärm; die leere Spalte sagt dasselbe. ===================================================================== */ function bewegungBauen(bewegung, spalten) { const wurzel = el('div', 'w-bewegung'); const hoechst = Math.max(1, ...bewegung.map( (m) => Math.max(m.vor + m.geklaert + m.neu, m.zurueck + m.offen))); const gitter = el('div', 'w-bewegung__gitter'); gitter.style.setProperty('--spalten', String(spalten.length)); for (const m of bewegung) { const spalte = el('div', 'w-bewegung__spalte'); const oben = el('div', 'w-bewegung__halb w-bewegung__halb--oben'); const obenWert = m.vor + m.geklaert + m.neu; if (obenWert) { const b = el('div', 'w-bewegung__balken'); b.dataset.richtung = 'vor'; b.style.height = `${Math.round((obenWert / hoechst) * 100)}%`; b.title = [ m.vor ? `${m.vor}× nach vorn` : null, m.geklaert ? `${m.geklaert}× geklärt` : null, m.neu ? `${m.neu}× zum ersten Mal` : null, ].filter(Boolean).join(', '); oben.append(el('span', 'w-bewegung__zahl', String(obenWert)), b); } spalte.append(oben); spalte.append(el('div', 'w-bewegung__null')); const unten = el('div', 'w-bewegung__halb w-bewegung__halb--unten'); const untenWert = m.zurueck + m.offen; if (untenWert) { const b = el('div', 'w-bewegung__balken'); b.dataset.richtung = 'zurueck'; b.style.height = `${Math.round((untenWert / hoechst) * 100)}%`; b.title = [ m.zurueck ? `${m.zurueck}× zurückgesetzt` : null, m.offen ? `${m.offen}× auf „Kann ich nicht sagen“` : null, ].filter(Boolean).join(', '); unten.append(b, el('span', 'w-bewegung__zahl', String(untenWert))); } spalte.append(unten); spalte.append(el('div', 'w-bewegung__monat', monatKurz(m.monat))); gitter.append(spalte); } wurzel.append(gitter); /* DIE LEGENDE IST PFLICHT, nicht Zierde: Ohne sie trägt allein die Farbe die Bedeutung, und das ist genau die Stelle, an der ein Diagramm für manche Leute aufhört zu funktionieren. */ const legende = el('div', 'w-legende'); for (const [richtung, text] of [ ['vor', 'nach vorn · geklärt · zum ersten Mal'], ['zurueck', 'zurückgesetzt · wieder offen'], ]) { const p = el('span', 'w-legende__punkt'); const fleck = el('span', 'w-legende__fleck'); fleck.dataset.richtung = richtung; p.append(fleck, el('span', 'w-legende__text', text)); legende.append(p); } wurzel.append(legende); return wurzel; } /* ===================================================================== DIAGRAMM 2 — DER STREIFEN ===================================================================== */ function streifenBauen(streifen, spalten) { const gitter = el('div', 'w-streifen__gitter'); const kopf = el('div', 'w-streifen__zeile w-streifen__zeile--kopf'); kopf.append(el('span', 'w-streifen__punkt-name', '')); for (const m of spalten) kopf.append(el('span', 'w-streifen__monat', monatKurz(m))); gitter.append(kopf); for (const z of streifen) { const zeile = el('div', 'w-streifen__zeile'); zeile.append(el('span', 'w-streifen__punkt-name', z.titel)); for (const c of z.zellen) { const zelle = el('span', 'w-streifen__zelle', c.stufe ? zeichenVon(c.stufe) : '·'); if (c.stufe) { zelle.dataset.stufe = c.stufe; if (c.alt) zelle.dataset.alt = 'ja'; zelle.title = `${monatKurz(c.monat)}: ${namenVon(c.stufe)}` + (c.seit ? ` (gesetzt am ${datumKurz(c.seit)})` : '') + (c.alt ? ' – älter als drei Monate' : ''); } else { zelle.dataset.stufe = 'leer'; zelle.title = `${monatKurz(c.monat)}: noch keine Einschätzung`; } zeile.append(zelle); } gitter.append(zeile); } return gitter; } /* ===================================================================== DIE TEXTE ===================================================================== */ function texteBauen(texte) { const wurzel = el('div', 'w-texte'); for (const t of texte) { const kasten = el('section', 'w-text'); kasten.dataset.art = t.art; kasten.append(el('h4', 'w-text__titel', t.titel)); kasten.append(el('p', 'w-text__satz', t.satz)); if (t.punkte?.length) { const liste = el('ul', 'w-text__liste'); for (const p of t.punkte) { const zeile = el('li', 'w-text__zeile'); zeile.append(el('span', 'w-text__punkt', p.titel)); const rechts = el('span', 'w-text__rechts'); if (p.von && p.zu) { rechts.append(el('span', 'w-text__stufe', namenVon(p.von))); rechts.append(el('span', 'w-text__pfeil', '→')); } if (p.zu) { const s = el('span', 'w-text__stufe', namenVon(p.zu)); s.dataset.stufe = p.zu; rechts.append(s); } if (p.erwartung) rechts.append(el('span', 'w-text__stufe', p.erwartung)); if (p.tage != null) rechts.append(el('span', 'w-text__tage', `seit ${p.tage} T.`)); else if (p.am) rechts.append(el('span', 'w-text__tage', datumKurz(p.am))); zeile.append(rechts); liste.append(zeile); } kasten.append(liste); } wurzel.append(kasten); } return wurzel; } /* ===================================================================== DIE KARTE EINER PERSON ===================================================================== */ async function karteZeigen(id) { const a = await hole(`/workspace/api/werdegang/person/${id}`); if (!a.ok) { melde('Diese Karte lässt sich gerade nicht öffnen.'); return; } const d = await a.json(); stufen = d.stufen || []; $('liste-block').hidden = true; $('mich-block').hidden = true; $('karte-block').hidden = false; $('karte-name').textContent = `${d.person.name} · ${d.person.rolle_name}`; $('karte-satz').textContent = d.satz; /* ==== DER DRITTE AUSGANG ======================================== Nicht „in Ordnung“ und nicht „nicht in Ordnung“, sondern: KONNTE NOCH NICHT NACHSEHEN — mit Grund. Ein Diagramm über einen einzigen Zeitpunkt wäre kein Diagramm, sondern ein Balken, und er sähe aus wie eine Aussage. ============================================================= */ const bBlock = $('bewegung-block'), sBlock = $('streifen-block'); if (!d.verlauf_moeglich) { bBlock.hidden = false; sBlock.hidden = true; $('bewegung').replaceChildren(); $('bewegung-satz').textContent = d.gesetzt === 0 ? 'Zu dieser Person ist noch nichts eingetragen. Sobald die erste ' + 'Einschätzung steht, beginnt hier der Verlauf.' : `Es gibt bisher ${d.monate_mit_inhalt === 1 ? 'einen Monat' : d.monate_mit_inhalt + ' Monate'} ` + `mit Einschätzungen. Ein Verlauf braucht mindestens ${d.verlauf_ab} – ` + 'vorher stünde hier eine Linie, die nichts bedeutet.'; } else { bBlock.hidden = false; sBlock.hidden = false; $('bewegung-satz').textContent = `Die letzten ${d.spalten.length} Monate. Nach oben: weitergekommen, ` + 'geklärt oder zum ersten Mal beantwortet. Nach unten: zurückgesetzt ' + 'oder wieder offen. Beides ist eine Auskunft, keines ist eine Note.'; $('bewegung').replaceChildren(bewegungBauen(d.bewegung, d.spalten)); $('streifen-satz').textContent = 'Was in einem Monat galt, gilt auch im nächsten, solange niemand etwas ' + `ändert – deshalb sind die Zellen durchgezogen. Blasse Zellen sind älter ` + `als ${d.verblasst_nach_tagen} Tage und zählen nicht mehr mit.`; $('streifen').replaceChildren(streifenBauen(d.streifen, d.spalten)); } const tBlock = $('texte-block'); if (d.texte?.length) { tBlock.hidden = false; $('texte').replaceChildren(texteBauen(d.texte)); } else { tBlock.hidden = true; } /* Die Adresse merkt sich, welche Karte offen ist — damit „Zurück“ auf die Liste führt und nicht aus der Seite heraus. */ history.replaceState(null, '', `?p=${id}`); window.scrollTo({ top: 0, behavior: 'instant' }); } /* ===================================================================== DIE LISTE KEINE SORTIERUNG NACH AKTIVITÄT. Eine Liste, die die mit der meisten Bewegung nach oben stellt, ist eine Rangliste mit einem anderen Namen. Der Server sortiert nach Namen, und dabei bleibt es. ===================================================================== */ function listeBauen(d) { const wurzel = el('div', 'w-liste__gitter'); for (const p of d.liste) { const karte = el('button', 'w-karte'); karte.type = 'button'; karte.append(el('span', 'w-karte__name', p.name)); karte.append(el('span', 'w-karte__rolle', p.rolle_name)); const zeilen = el('span', 'w-karte__zeilen'); zeilen.append(el('span', 'w-karte__zeile', p.gesetzt === 0 ? 'Noch nichts eingetragen' : `${p.gesetzt} von ${p.gesamt} Punkten beantwortet`)); zeilen.append(el('span', 'w-karte__zeile', p.bewegt === 0 ? `In ${d.monate} Monaten keine Änderung` : `${p.bewegt} Änderung${p.bewegt === 1 ? '' : 'en'} in ${d.monate} Monaten` + (p.vor ? ` · ${p.vor} nach vorn` : ''))); if (p.zuletzt_tage != null) { const z = el('span', 'w-karte__zeile', p.zuletzt_tage === 0 ? 'Zuletzt heute angesehen' : `Zuletzt vor ${p.zuletzt_tage} Tag${p.zuletzt_tage === 1 ? '' : 'en'} angesehen`); /* DIE EINZIGE FARBE IN DER LISTE, und sie sagt etwas über UNS: Wer seit über drei Monaten nicht hingesehen hat, hat keine Auskunft über diesen Menschen, sondern über sich. */ if (p.zuletzt_tage >= 90) z.dataset.alt = 'ja'; zeilen.append(z); } karte.append(zeilen); karte.addEventListener('click', () => karteZeigen(p.id).catch(() => {})); wurzel.append(karte); } return wurzel; } async function listeZeigen() { const a = await hole('/workspace/api/werdegang/liste'); if (a.status === 404) return false; /* kein Leitungszugang */ if (!a.ok) { melde('Die Karten lassen sich gerade nicht laden.'); return true; } const d = await a.json(); $('karte-block').hidden = true; $('mich-block').hidden = true; $('liste-block').hidden = false; $('liste').removeAttribute('aria-busy'); $('liste-hinweis').textContent = d.hinweis; if (!d.liste.length) { $('liste').replaceChildren(el('p', 'leise', 'Es gibt noch niemanden mit einer Karte. Sobald jemand ins Team kommt, ' + 'steht er hier.')); } else { $('liste').replaceChildren(listeBauen(d)); } history.replaceState(null, '', location.pathname); return true; } /* ===================================================================== DIE EIGENE SICHT OHNE NAMEN, OHNE ANLASS, OHNE STREIFEN. Sie sieht, DASS sich etwas bewegt hat und wann — nicht, wer was gesetzt hat und was danebensteht. Der Anlass ist für ein Gespräch aufgeschrieben, nicht zum Lesen ohne den Menschen dazu. ===================================================================== */ async function michZeigen() { const a = await hole('/workspace/api/werdegang/mich'); if (!a.ok) { melde('Das lässt sich gerade nicht laden.'); return; } const d = await a.json(); $('liste-block').hidden = true; $('karte-block').hidden = true; $('mich-block').hidden = false; $('titel').textContent = 'Deine Entwicklung'; $('unterzeile').textContent = 'Was sich über die Zeit bewegt hat. Keine Note, keine Punktzahl.'; if (!d.hat_karte) { $('mich-satz').textContent = 'Für deine Rolle wird keine Entwicklungskarte geführt. Das ist keine ' + 'Lücke – es gibt schlicht nichts, was hier stehen müsste.'; $('mich-hinweis').textContent = ''; return; } stufen = d.stufen || stufen; $('mich-satz').textContent = d.satz; $('mich-hinweis').textContent = d.hinweis; const etwas = d.bewegung?.some((m) => m.vor || m.zurueck || m.neu || m.geklaert); $('mich-bewegung').replaceChildren( etwas ? bewegungBauen(d.bewegung, d.spalten) : el('span')); } /* ---- Start ---------------------------------------------------------- */ $('zur-liste').addEventListener('click', () => { listeZeigen().catch(() => {}); }); (async () => { try { /* ERST DIE LISTE FRAGEN. Antwortet der Server mit 404, führt diese Person das Team nicht — dann ist die eigene Sicht die richtige. Die Rolle hier selbst zu prüfen hieße, dieselbe Entscheidung an zwei Stellen zu treffen, und die zweite ist immer die, die irgendwann etwas durchlässt. */ const leitung = await listeZeigen(); if (!leitung) { await michZeigen(); return; } const p = new URLSearchParams(location.search).get('p'); if (p) await karteZeigen(Number(p)); } catch (f) { if (String(f?.message || '') !== 'abgemeldet') { melde('Die Seite lässt sich gerade nicht laden.'); } } })(); })();