Filipe: "ich will dass diese zwei kategorien perfektionniert werden, ich
will dass fertige aufgaben da stehen und so ... so dass dogfather und
rechte hand es so einfach wie möglich haben aber so professionell wie
auch nur möglich."
Bis heute waren beide Kacheln ein leeres Formular: Titel tippen, Art
wählen, Text schreiben. Ein leeres Feld beantwortet keine Frage — es
stellt sie noch einmal.
ENTWICKLUNG: 34 Beobachtungen in fünf Blöcken, vier Antworten je Zeile
(Läuft · Wächst gerade · Da hakt es · Kann ich nicht sagen). Es sind
BEOBACHTUNGEN, keine Eigenschaften — "Absagen kommen rechtzeitig" statt
"ist zuverlässig". Eine Eigenschaft kann man nur bestätigen oder
bestreiten; ein Verhalten kann man gesehen haben oder nicht.
TALENTE: 21 Merkmale in fünf Gruppen, 6 Warnzeichen getrennt geführt,
fünf Trichterstufen mit je GENAU EINEM nächsten Schritt. Eine Liste
wächst und wird nie wieder angesehen; ein Trichter fragt bei jeder Karte
"und jetzt?". Die Standzeit steht dabei — "seit 41 Tagen beobachtet"
ist eine Auskunft über uns, nicht über den Kandidaten.
VIER RIEGEL, JEDER MIT GEGENPROBE:
· Block 5 ("Wie geht es dir damit?") gehört der Person — lesend UND
schreibend, auch vor DogFather. Er kommt gar nicht erst aus dem
Server; Schreiben gibt dasselbe 404 wie ein erfundener Schlüssel.
Nach außen dringt eine Ampel OHNE Namen, und die schweigt, solange
das Team so klein ist, dass "jemandem geht es zu viel" dasselbe wäre
wie ein Name.
· Der Stand des anderen kommt erst nach dem eigenen Klick — sonst
klickt man dasselbe. Sichtbar ist nur, DASS es einen gibt: sonst
sähen "noch niemand" und "du darfst es nicht sehen" gleich aus.
· "Da hakt es" ohne Anlass wird abgelehnt. "Läuft" nicht — 28
Pflichtfelder je Person wären das Gegenteil von "so einfach wie
möglich".
· Talente gehört DogFather und der rechten Hand. Ein Modi bekommt 404
auf Seite UND Schnittstelle.
WAS NICHT GEBAUT IST, UND WARUM: keine Punktzahl, kein Durchschnitt,
keine Rangliste, keine Gesamtnote. Eine Zahl über einem Menschen wird
gelesen, verglichen und weitererzählt. Und kein Rot — "da hakt es" ist
eine Beobachtung, kein Alarm.
DER PLAN LAG AN EINER STELLE FALSCH, gemessen statt vermutet: Er sagte
"keine neue Tabelle, der Stand liegt in punkt_stand". Dort ist der
Schlüssel (bereich, schluessel, creator_id) — genau EIN Stand je Punkt
und Person. Zwei unabhängige Augenpaare passen da nicht hinein, ohne
eine lebende Tabelle neu zu bauen. Also eine eigene.
EIN RÜCKSCHLAG, DEN NUR DIE PRÜFUNG GEFUNDEN HAT: Die Liste der
Bretter, die ein Modi oder die rechte Hand öffnen darf, wird aus den
KACHELZIELEN abgeleitet. Als die Kacheln auf die neuen Seiten zeigten,
fielen beide Bretter heraus — und die Seite verlinkte auf ein 404. Kein
Fehler im Riegel, sondern in seiner Quelle.
Dazu zwei kleinere Funde vom Hinsehen statt vom Messen: Die Ampel sagte
"Solange ihr zu 1 seid" (jetzt drei Sätze für drei Lagen), und die
Fußzeile der Talente-Seite lag auf der Bühne — die Kontrastprüfung hatte
sie übersprungen, weil ein Link darin steht. Beide behoben, und die
Prüfung hat den stillen Aussetzer gleich mit verloren.
Geprüft: rollen 314 · start-ansicht 147 · crew-adresse 132 · neue-seiten
70 · nachwuchs 69 · entwicklung 40 · css-klassen 30 · zwischenspeicher
21 · alle-wege 19 · rechtetafel 19 — alles grün.
Was noch fehlt, steht in der Notiz, Abschnitt 9: die eigene Karte für
die Person, der Übergang "Probe" → Zugang, der Vorlagentext zum
Ansprechen.
Co-Authored-By: Claude Opus 5 <[email protected]>
312 lines
12 KiB
JavaScript
312 lines
12 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;
|
||
};
|
||
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;
|
||
}
|
||
|
||
/* ---- 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 meins();
|
||
} catch { /* umgeleitet */ }
|
||
})();
|
||
})();
|