Files
dogfather-universe/workspace/assets/js/talente.js
T
DogFatherGitandClaude Opus 5 95c4599baa Anfuehrungszeichen: 199 falsche Schlusszeichen im sichtbaren Text
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]>
2026-10-01 01:56:15 +02:00

1272 lines
52 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.
/* =====================================================================
Talente — der Trichter (11.09.2026)
Fünf Stufen, und jede hat GENAU EINEN nächsten Schritt. Das ist der
ganze Unterschied zu einer Liste: Eine Liste wächst und wird nie
wieder angesehen; ein Trichter fragt bei jeder Karte „und jetzt?".
DREI DINGE, DIE DIESE SEITE ABSICHTLICH TUT:
1. SIE ZEIGT DIE STANDZEIT. „Seit 41 Tagen beobachtet, nie
angesprochen" ist eine Auskunft über UNS, nicht über den
Kandidaten. Ohne sie wird der Trichter wieder zur Liste.
2. SIE TRENNT WARNZEICHEN VON MERKMALEN. Bei Moderatoren sagt das
Fehlverhalten mehr voraus als das Können — ein Warnzeichen neben
zwanzig Haken ginge unter.
3. SIE RECHNET NICHT. Keine Punktzahl, keine Eignung in Prozent.
Zwanzig Haken und ein Warnzeichen ergeben keine Zahl, sondern ein
Gespräch.
===================================================================== */
(() => {
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; } };
/* 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);
/* TAGESDATUM IN ORTSZEIT. Nicht toISOString().slice(0,10) -- das
rechnet in UTC und liefert in Deutschland zwischen Mitternacht und
zwei Uhr morgens den Vortag. Ein Termin, der beim Eintragen einen
Tag zurueckspringt, ist genau die Sorte Fehler, die man erst
bemerkt, wenn jemand umsonst gewartet hat. */
const tag = (d) => {
const z = (n) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${z(d.getMonth() + 1)}-${z(d.getDate())}`;
};
const heute = () => tag(new Date());
const inTagen = (n) => { const d = new Date(); d.setDate(d.getDate() + n); return tag(d); };
const tageBis = (s) => {
if (!s) return null;
const a = new Date(heute() + 'T12:00:00');
const b = new Date(s + 'T12:00:00');
return Number.isFinite(b.getTime()) ? Math.round((b - a) / 86400000) : null;
};
let lage = null;
/* WAS ICH ANLEGEN DARF -- die rohe Liste vom Server.
Ausgewertet wird sie erst beim Zeichnen: Welcher Zugang hier
entsteht, sagt `lage`, und die wird NACH dieser Liste geholt.
Ein Wert, der zu frueh gerechnet wird, ist dauerhaft falsch --
der Knopf waere nie erschienen. */
let darfListe = null;
/* WER BIN ICH? Für die Unterscheidung „Du kümmerst dich" gegen
„jemand anderes kümmert sich". Verglichen wird über die NUMMER,
nicht über den Namen: Zwei Leute dürfen gleich heißen, und dann
nimmt der eine dem anderen den Kandidaten aus der Hand. */
let ich = null;
const darfAnlegen = () => Array.isArray(darfListe)
&& !!lage?.zugang?.rolle && darfListe.includes(lage.zugang.rolle);
const offen = new Set(); // welche Karten aufgeklappt sind
const offenStart = new Set(); // wo der Kasten mit den ersten Aufgaben offen ist
/* ---- Der Trichter oben ---------------------------------------------- */
function trichter() {
const ziel = $('trichter');
ziel.textContent = '';
ziel.setAttribute('aria-busy', 'false');
for (const s of lage.trichter) {
const k = el('div', 't-stufe');
k.dataset.stufe = s.schluessel;
k.append(el('span', 't-stufe__zahl', String(s.anzahl)));
k.append(el('span', 't-stufe__name', s.name));
if (s.liegt) {
/* LIEGENGEBLIEBEN IST UNSERE ZAHL, nicht die des Kandidaten.
Sie steht deshalb am Trichter und nicht als Mahnung. */
const z = el('span', 't-stufe__liegt', `${s.liegt} liegt`);
z.title = 'So lange, dass es eine Auskunft über uns ist – nicht über sie.';
k.append(z);
}
ziel.append(k);
}
}
/* ---- Eine Karte ------------------------------------------------------ */
function karte(k) {
const a = el('article', 't-karte');
a.dataset.stufe = k.stufe;
if (k.liegt) a.dataset.liegt = 'ja';
const kopf = el('div', 't-karte__kopf');
kopf.append(el('h2', 't-karte__titel', k.titel));
kopf.append(el('span', 't-karte__stufe', k.stufe_name));
kopf.append(el('span', 't-karte__zeit',
k.tage === null ? '' : `seit ${k.tage} ${k.tage === 1 ? 'Tag' : 'Tagen'}`));
if (k.warnzeichen.length) {
const w = el('span', 't-karte__warn', `${k.warnzeichen.length} × hinsehen`);
w.title = 'Kein Ausschluss – ein Grund, länger hinzusehen.';
kopf.append(w);
}
a.append(kopf);
/* ---- WER SICH KÜMMERT (17.09.2026) --------------------------------
Dieselbe Lücke wie bei den Anfragen, nur teurer: Ein Kandidat,
für den sich niemand zuständig fühlt, bleibt auf seiner Stufe
liegen. Die Standzeit oben sagt, DASS etwas liegt -- nicht, wer
es aufheben sollte.
MAN NIMMT SELBST, MAN BEKOMMT NICHT ZUGETEILT. Ein „DogFather
weist zu" wäre eine Rangordnung, und die gehört hier nicht hin.
AN JEDER STUFE, AUCH IM TEAM. Gerade in den ersten Wochen nach
der Aufnahme ist die Frage „wer schaut nach ihm?" die wichtigste
-- und sie ist etwas anderes als der Buddy, der die Schicht
begleitet. */
if (!k.beendet) {
const kum = el('div', 't-kuemmern');
const meins = k.zustaendig?.id === ich?.id;
if (k.zustaendig) kum.dataset.wer = meins ? 'ich' : 'anderer';
const knopf = el('button', 't-kuemmern__knopf');
knopf.type = 'button';
knopf.textContent = k.zustaendig
? (meins ? 'Du kümmerst dich · abgeben' : `${k.zustaendig.name} kümmert sich`)
: 'Ich kümmere mich';
knopf.disabled = !!k.zustaendig && !meins;
knopf.addEventListener('click', async () => {
knopf.disabled = true;
melde('');
try {
const r = await hole(`/workspace/api/talente/${k.id}/kuemmern`, { method: 'PUT' });
if (!r.ok) {
melde(absage(await r.json().catch(() => ({}))));
knopf.disabled = false;
return;
}
await laden();
} catch { melde('Ging nicht.'); knopf.disabled = false; }
});
kum.append(knopf);
if (k.zustaendig?.tage !== null && k.zustaendig?.tage !== undefined) {
kum.append(el('span', 't-kuemmern__seit',
k.zustaendig.tage === 0 ? 'seit heute'
: `seit ${k.zustaendig.tage} ${k.zustaendig.tage === 1 ? 'Tag' : 'Tagen'}`));
}
a.append(kum);
}
if (k.einsatz) a.append(el('p', 't-karte__wo', k.einsatz));
if (k.text) a.append(el('p', 't-karte__text', k.text));
/* DER EINE NÄCHSTE SCHRITT. Nicht drei Möglichkeiten — einer.
Wer eine Karte öffnet, soll in zwei Sekunden wissen, was dran
ist, und nicht erst eine Entscheidung treffen müssen. */
const schritt = el('div', 't-schritt');
schritt.append(el('p', 't-schritt__satz', k.schritt));
/* DER LETZTE SCHRITT IST KEIN SCHRITT, SONDERN EINE ENTSCHEIDUNG.
Aus einem Namen wird ein Mensch mit einem Zugang. Deshalb haengt
genau daran das Anlegen selbst -- und nicht ein Satz, den man
danach von Hand nachbaut. Bis heute stand hier "Zugang in
Personen & Zugaenge anlegen", und wer das vergass,
hatte eine Karte auf "Im Team" und keinen Menschen darin. */
const zugangNoetig = k.naechste === 'imteam';
if (k.knopf && k.naechste && (!zugangNoetig || darfAnlegen())) {
const b = el('button', 'schritt t-schritt__knopf',
zugangNoetig ? 'Ins Team holen · Zugang anlegen' : k.knopf);
b.type = 'button';
b.addEventListener('click', async () => {
b.disabled = true;
/* ZUERST DIE ZWEI FELDER, DANN DIE STUFE. Andersherum stünde
die Karte auf „Probe", während der Buddy noch offen ist —
und genau dieser Zustand war bisher der Normalfall. */
let uebergang = {};
if (k.naechste === 'probe') {
const antwort = await uebergangFragen(k, a, {
titel: 'Probe: zwei Dinge, dann läuft es',
satz: 'Wer ihn einarbeitet und wann die erste Schicht ist. Beides jetzt, '
+ 'nicht „melden wir uns“ — zwischen Zusage und erster Schicht springen '
+ 'die meisten ab.',
knopf: 'Auf Probe nehmen',
});
if (!antwort) { b.disabled = false; return; }
uebergang = antwort;
}
let zugang = null;
if (zugangNoetig) {
zugang = await zugangAnlegen(k);
if (!zugang) { b.disabled = false; return; }
}
const r = await hole(`/workspace/api/talente/${k.id}/stufe`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
/* DIE NUMMER DES NEUEN ZUGANGS WANDERT MIT (17.09.2026).
Bis heute entstand hier ein Mensch, sein Code wurde einmal
gezeigt -- und danach wusste die Karte nicht mehr, wer aus
ihr geworden ist. Damit ging weder der Sprung zur Person
noch die Frage, aus welchem Kandidaten eigentlich welches
Teammitglied wurde.
`zugang?.id` und nicht `zugang.id`: Bei jedem anderen
Schritt ist `zugang` null, und der Server laesst ein
fehlendes Feld ausdruecklich stehen, statt zu loeschen. */
body: JSON.stringify({ stufe: k.naechste, ...uebergang, person_id: zugang?.id }),
});
b.disabled = false;
if (!r.ok) {
/* ZWEI DINGE SIND PASSIERT, EINES DAVON HAT GEKLAPPT. Ein
blosses "ging nicht" waere hier die gefaehrlichste
Auskunft von allen -- der Zugang steht ja, und niemand
wuesste es. */
melde(zugang
? `Der Zugang für ${zugang.name} steht – der Code steht oben. Nur die Karte `
+ 'blieb auf „Probe“. Bitte den Schritt gleich noch einmal setzen.'
: absage(await r.json().catch(() => ({}))));
return;
}
offen.add(k.id);
await laden();
});
schritt.append(b);
} else if (zugangNoetig && k.knopf) {
/* KEIN KNOPF INS LEERE. Die rechte Hand sieht den Trichter
vollstaendig, aber Zugaenge vergibt nur DogFather. Ein Knopf,
der ihr jedes Mal "darfst du nicht" antwortet, waere schlechter
als gar keiner -- er verspricht etwas. */
schritt.append(el('p', 't-schritt__liegt',
'Den Zugang legt DogFather an – sag ihm Bescheid. Die Karte bleibt so '
+ 'lange auf „Probe“.'));
}
/* DER VORLAGENTEXT -- genau auf der Stufe, auf der er gebraucht
wird. Eine Schaltfläche „Text anzeigen" auf jeder Stufe wäre auf
vier von fünf davon eine Ablenkung. */
if (k.stufe === 'beobachtet' && k.ansprechtext) {
const b = el('button', 'schritt t-schritt__text', 'Vorlagentext');
b.type = 'button';
b.addEventListener('click', () => textZeigen(k));
schritt.append(b);
}
if (k.liegt) {
schritt.append(el('p', 't-schritt__liegt',
'Das liegt länger, als es sollte. Entweder weiter – oder bewusst beenden.'));
}
a.append(schritt);
/* ---- Buddy und erste Schicht ---------------------------------------
Sie stehen direkt unter dem nächsten Schritt und über allem
anderen: Während einer Probe sind sie die Karte. */
if (k.uebergang) {
const u = el('div', 't-uebergang');
if (k.uebergang.buddy_weg) u.dataset.weg = 'ja';
const zeile = el('p', 't-uebergang__zeile');
zeile.append(el('span', 't-uebergang__was', 'Arbeitet ein'));
zeile.append(el('span', 't-uebergang__wer',
k.uebergang.buddy_name || 'niemand mehr – bitte neu bestimmen'));
u.append(zeile);
if (k.uebergang.satz) u.append(el('p', 't-uebergang__satz', k.uebergang.satz));
/* WAS JEMAND AM ERSTEN TAG BEKOMMT. Drei Dinge, keine Haken.
Sie stehen hier, damit der Buddy sie liest — eine Liste zum
Abhaken wäre die zweite neben der im Wissensbereich. */
if (k.stufe === 'probe' && (lage.erster_tag || []).length) {
const liste = el('ul', 't-uebergang__tag');
for (const p of lage.erster_tag) {
const li = el('li', 't-uebergang__punkt');
li.append(el('strong', null, p.titel));
li.append(document.createTextNode(' — ' + p.text));
liste.append(li);
}
u.append(el('p', 't-uebergang__was', 'Am ersten Tag braucht er drei Dinge'));
u.append(liste);
}
if (k.stufe === 'probe') {
const aendern = el('button', 'schritt t-uebergang__aendern', 'Buddy oder Termin ändern');
aendern.type = 'button';
aendern.addEventListener('click', async () => {
const antwort = await uebergangFragen(k, a, {
titel: 'Buddy oder Termin ändern',
satz: 'Ein Buddy hört auf, ein Termin verschiebt sich. Die Standzeit der '
+ 'Karte bleibt dabei stehen — sie zählt, seit wann die Probe läuft.',
knopf: 'Übernehmen',
buddy: k.uebergang.buddy_id,
datum: k.uebergang.erste_schicht,
});
if (!antwort) return;
const r = await hole(`/workspace/api/talente/${k.id}/uebergang`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(antwort),
});
if (!r.ok) { melde(absage(await r.json().catch(() => ({})))); return; }
await laden();
});
u.append(aendern);
}
a.append(u);
}
/* ---- WER DARAUS GEWORDEN IST (17.09.2026) -------------------------
Steht die Karte auf „Im Team", gehoert der Mensch dazu, der
daraus entstanden ist -- mit dem Weg zu seinen Zugaengen. Ohne
ihn ist die Karte ein Verlauf, der genau an der Stelle aufhoert,
an der es interessant wird.
IST DER ZUGANG ABGESCHALTET, steht das dabei. Eine Karte auf „Im
Team" mit einem stillgelegten Zugang daneben ist die ehrliche
Auskunft; eine, die so tut, als liefe alles, ist es nicht. */
if (k.person) {
const wer = el('div', 't-person');
if (!k.person.aktiv) wer.dataset.still = 'ja';
wer.append(el('span', 't-person__schild', 'Daraus wurde'));
const name = el('a', 't-person__name', k.person.name);
name.href = 'personen.html';
wer.append(name);
if (!k.person.aktiv) {
wer.append(el('span', 't-person__still', 'Zugang abgeschaltet'));
}
/* WAS SIE SCHON HAT. Ohne diese Zahl legt man beim zweiten
Hinsehen dasselbe noch einmal an. */
const schon = lage.schon_vergeben?.[k.person.id] || 0;
if (schon) {
wer.append(el('span', 't-person__schon',
`${schon} ${schon === 1 ? 'offene Aufgabe' : 'offene Aufgaben'}`));
}
/* DER WEG ZUR ENTWICKLUNGSKARTE (17.09.2026).
Der Satz dieser Stufe sagt zweimal „ab hier zaehlt die
Entwicklungskarte" -- und es gab keinen Weg dorthin. Wer ihn
las, musste die Seite selbst finden und die Person dort
heraussuchen. Genau dasselbe Muster wie beim fehlenden Ausgang
aus dem Trichter: ein Text, der einen Weg verspricht, den es
nicht gibt.
`?zeigen=` ist der Weg, den das Haus dafuer hat (kopf.js) --
keine zweite Mechanik. */
if (k.person.aktiv) {
const zurKarte = el('a', 't-person__weiter', 'Entwicklungskarte');
zurKarte.href = `entwicklung.html?zeigen=person-${k.person.id}`;
wer.append(zurKarte);
}
a.append(wer);
if (k.person.aktiv) startAufgaben(a, k, schon);
}
/* ---- Die Probezeit --------------------------------------------------
Sie steht direkt unter dem Uebergang: waehrend einer Probe ist sie
das Wichtigste auf der Karte. Und sie erscheint NUR dann -- an
einem Kandidaten, der noch beobachtet wird, waere eine leere
Zwoelferliste Laerm, und Laerm liest man auch dann weg, wenn er
gilt. */
if (k.probe) probeZeigen(a, k);
/* ---- Merkmale, aufklappbar ------------------------------------------
Zugeklappt steht dort eine Zahl, aufgeklappt die 21 Merkmale.
Immer offen wären es bei zehn Kandidaten 270 Kästchen auf einer
Seite — und dann klickt niemand mehr eines an. */
const auf = offen.has(k.id);
const schalter = el('button', 't-karte__auf',
auf ? 'Merkmale zuklappen'
: `Merkmale ansehen (${k.merkmale.length} gesetzt${k.warnzeichen.length
? `, ${k.warnzeichen.length} Warnzeichen` : ''})`);
schalter.type = 'button';
schalter.setAttribute('aria-expanded', String(auf));
schalter.addEventListener('click', () => {
if (offen.has(k.id)) offen.delete(k.id); else offen.add(k.id);
zeichne();
});
a.append(schalter);
if (auf) {
for (const g of lage.gruppen) {
const block = el('section', 't-gruppe');
block.append(el('h3', 't-gruppe__titel', g.name));
block.append(el('p', 't-gruppe__satz', g.text));
const reihe = el('div', 't-merkmale');
for (const m of g.merkmale) reihe.append(merkmal(k, m, false));
block.append(reihe);
a.append(block);
}
const warn = el('section', 't-gruppe t-gruppe--warn');
warn.append(el('h3', 't-gruppe__titel', 'Hinsehen'));
warn.append(el('p', 't-gruppe__satz',
'Keines davon ist ein Ausschluss. Bei Moderatoren sagt das Verhalten, '
+ 'das stört, mehr voraus als das, was jemand kann.'));
const reihe = el('div', 't-merkmale');
for (const m of lage.warnzeichen) reihe.append(merkmal(k, m, true));
warn.append(reihe);
a.append(warn);
}
/* ---- Der Ausgang ----------------------------------------------------
Filipe: „alles mögliche, wie gesagt ich will hoch profissionel
arbeiten."
ER IST LEISE UND ER IST DA. Bis heute hatte der Trichter fünf
Stufen und genau einen Ausgang: ins Team. Wer nicht passte, blieb
auf seiner Stufe stehen, mit wachsender Standzeit und rotem
Zeichen -- obwohl die Stufe selbst verspricht: „übernehmen oder
sauber beenden".
ER STEHT UNTEN UND SIEHT NICHT AUS WIE EIN KNOPF. Oben steht der
eine nächste Schritt; wenn daneben gleich groß „Beenden" stünde,
wären es wieder zwei Möglichkeiten und keine Anweisung.
UND ES GIBT KEINE ZUSÄTZLICHE RÜCKFRAGE. Der Satz IST die
Rückfrage -- wer einen Grund tippen muss, hat länger nachgedacht
als jemand, der „Wirklich beenden?" wegklickt. Ein Dialog obendrauf
wäre ein Klick mehr und würde genau das trainieren: wegklicken. */
a.append(ausgangZeigen(k));
return a;
}
/* Der Ausgang einer Karte: erst ein leiser Satz, dann das Feld. */
function ausgangZeigen(k) {
const kasten = el('div', 't-ausgang');
/* WER IM TEAM IST, HAT EINEN ZUGANG -- und der wird woanders
abgeschaltet (17.09.2026, am Bildschirmfoto gefunden).
Auf einer Karte auf „Im Team" stand „Nichts mehr zu tun – der
Zugang steht" und direkt darunter „Passt doch nicht?". Wer das
geklickt hätte, hätte die Karte stillgelegt und den Zugang
offengelassen: eine halbe Handlung, die sich wie eine ganze
anfühlt.
Statt des Ausgangs steht deshalb der Weg, der wirklich hilft.
Nicht weglassen -- wer nichts findet, sucht, und wer nichts
findet und aufgibt, lässt den Zugang offen. */
if (k.stufe === 'imteam') {
const hin = el('p', 't-ausgang__anderswo');
hin.append(document.createTextNode('Hört jemand auf? Der Zugang wird unter '));
const a = el('a', null, 'Personen & Zugänge');
a.href = 'personen.html';
hin.append(a);
hin.append(document.createTextNode(' abgeschaltet – diese Karte bleibt als '
+ 'Verlauf stehen.'));
kasten.append(hin);
return kasten;
}
const oeffnen = el('button', 't-ausgang__auf', 'Passt doch nicht?');
oeffnen.type = 'button';
oeffnen.setAttribute('aria-expanded', 'false');
const form = el('form', 't-ausgang__form');
form.hidden = true;
form.append(el('p', 't-ausgang__satz',
'Schreib auf, woran es lag. Der Satz bleibt an der Karte – '
+ 'wenn dieselbe Person in einem halben Jahr wieder auffällt, '
+ 'ist er das Einzige, was dann noch hilft.'));
const id = 'aus-' + k.id;
const schild = el('label', 't-ausgang__schild', 'Woran lag es?');
schild.htmlFor = id;
const feld = document.createElement('textarea');
feld.id = id;
feld.className = 't-ausgang__feld';
feld.rows = 3;
feld.maxLength = 600;
feld.required = true;
feld.placeholder = 'z. B. hat sich nach der zweiten Schicht nicht mehr gemeldet';
form.append(schild, feld);
const knopf = el('button', 'schritt t-ausgang__knopf', 'Sauber beenden');
knopf.type = 'submit';
form.append(knopf);
oeffnen.addEventListener('click', () => {
const zu = form.hidden;
form.hidden = !zu;
oeffnen.setAttribute('aria-expanded', String(zu));
if (zu) feld.focus();
});
form.addEventListener('submit', async (e) => {
e.preventDefault();
melde('');
const grund = feld.value.trim();
/* DIESELBE GRENZE WIE AUF DEM SERVER -- und sie steht dort, nicht
hier: Was der Browser prüft, ist Höflichkeit, was der Server
prüft, ist die Regel. */
if (grund.length < 10) {
melde('Ein Satz gehört dazu – „passt nicht“ ist keiner.');
feld.focus();
return;
}
knopf.disabled = true;
try {
const r = await hole(`/workspace/api/talente/${k.id}/beenden`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ grund }),
});
if (!r.ok) {
melde(absage(await r.json().catch(() => ({}))));
knopf.disabled = false;
return;
}
await laden();
} catch {
melde('Ging nicht.');
knopf.disabled = false;
}
});
kasten.append(oeffnen, form);
return kasten;
}
/* ---- WOMIT FAENGT ER AN? (17.09.2026) -------------------------------
Filipe: „wie talente bewertet werden, aufgaben bekommen, analysiert
werden von vanvan und mir, alles moegliche."
Bis heute endete der Trichter beim Zugang: ein Code und keine
einzige Aufgabe. Um ihm welche zu geben, musste man auf eine andere
Seite, die Person heraussuchen, Kategorie und Stufe waehlen. Vier
Schritte, die niemand macht, waehrend er gerade jemanden aufnimmt.
EIN ANGEBOT, KEINE AUTOMATIK -- dieselbe Entscheidung wie bei der
Live-Checkliste: Was ungefragt Dinge anlegt, ist schwer wieder
loszuwerden. Deshalb steht hier, WAS es gaebe; angelegt wird auf
Klick, eine Kategorie auf einmal.
ZUGEKLAPPT, WENN SCHON ETWAS DA IST. Wer bereits Aufgaben hat,
braucht den Kasten nicht offen -- er wuerde dann zum Nachlegen
einladen, und das ist selten gemeint. */
function startAufgaben(ziel, k, schon) {
const kategorien = lage.start_kategorien;
if (!Array.isArray(kategorien) || !kategorien.length) return;
const kasten = el('section', 't-start');
const auf = offenStart.has(k.id) || !schon;
const schalter = el('button', 't-start__auf',
auf ? 'Aufgaben zuklappen' : 'Aufgaben für den Anfang geben');
schalter.type = 'button';
schalter.setAttribute('aria-expanded', String(auf));
schalter.addEventListener('click', () => {
if (offenStart.has(k.id)) offenStart.delete(k.id); else offenStart.add(k.id);
zeichne();
});
kasten.append(schalter);
if (!auf) { ziel.append(kasten); return; }
kasten.append(el('p', 't-start__satz',
'Such aus, womit er anfangen soll. Ein Klick legt die Aufgaben '
+ 'dieses Bereichs an – mit Frist, und änderbar wie jede andere.'));
const reihe = el('div', 't-start__reihe');
for (const g of kategorien) {
const knopf = el('button', 't-start__kat');
knopf.type = 'button';
knopf.title = g.text || '';
knopf.append(el('span', 't-start__kat-name', g.name));
knopf.append(el('span', 't-start__kat-zahl',
`${g.anzahl} ${g.anzahl === 1 ? 'Aufgabe' : 'Aufgaben'}`));
knopf.addEventListener('click', async () => {
melde('');
knopf.disabled = true;
try {
const r = await hole('/workspace/api/vorlagen/uebernehmen', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
art: 'katalog_alle', gruppe: g.wert, stufe: 'neu',
person_id: k.person.id,
}),
});
if (!r.ok) {
melde(absage(await r.json().catch(() => ({}))));
knopf.disabled = false;
return;
}
const d = await r.json().catch(() => ({}));
/* DIE ZAHL AUS DER ANTWORT, nicht die erwartete. Legt der
Server weniger an, als der Knopf versprach, soll das
dastehen und nicht verschwiegen werden.
`d.angelegt` ist eine ZAHL (workspace-vorlagen.js:
`res.json({ angelegt: angelegt.length, titel: angelegt })`).
Der erste Anlauf stand auf `d.angelegt?.length` -- das ist
bei einer Zahl `undefined` und waere still auf die
erwartete Menge zurueckgefallen. Der Knopf haette dann „6
Aufgaben stehen bereit" gemeldet, auch wenn null entstanden
sind. */
const n = Number.isFinite(d.angelegt) ? d.angelegt : g.anzahl;
melde('');
offenStart.delete(k.id);
await laden();
if (!n) {
melde('Dabei ist nichts Neues entstanden – vermutlich steht es schon da.');
return;
}
const hin = el('p', 't-start__fertig');
hin.append(document.createTextNode(
`${n} ${n === 1 ? 'Aufgabe steht' : 'Aufgaben stehen'} für `
+ `${k.person.name} bereit. `));
/* DER WEG DORTHIN GEHOERT DAZU. Eine Meldung „sechs Aufgaben
stehen bereit" ohne Link laesst einen raten, wo. */
const zu = el('a', 'schritt', 'Aufgaben ansehen');
zu.href = 'aufgaben.html';
hin.append(zu);
$('liste').prepend(hin);
} catch {
melde('Ging nicht.');
knopf.disabled = false;
}
});
reihe.append(knopf);
}
kasten.append(reihe);
ziel.append(kasten);
}
/* ---- Aus der Probe wird ein Zugang ---------------------------------
DERSELBE WEG WIE IN "PERSONEN & ZUGAENGE", nicht ein zweiter.
/workspace/api/verwaltung/personen ist die einzige Stelle im ganzen
Haus, an der ein Zugang entsteht -- mit ihrer Rechtepruefung, ihrer
Rollenpruefung, ihrem Protokolleintrag und ihrem genau einmal
angezeigten Code. Ein eigener Weg von hier waere eine zweite Tuer
zu genau dem, was am wenigsten zwei Tueren haben darf.
NICHT ueber hole(): Diese Adresse antwortet Nicht-DogFather mit
404, und hole() versteht ein 404 als "Seite gibt es nicht" und
schickt zur Startseite. Ein Rechtefehler wuerde die rechte Hand
also aus dem Trichter werfen. Hier wird deshalb selbst gelesen.
@returns {object|null} die angelegte Person, oder null bei Abbruch. */
async function zugangAnlegen(k) {
if (!darfAnlegen()) { melde('Zugänge legt DogFather an.'); return null; }
const vorschlag = String(k.titel || '').split(/[\s,(–-]+/)[0] || '';
const an = await frageNach({
titel: 'Zugang anlegen',
was: 'Die Person bekommt ein eigenes Konto im Workspace.',
bleibt: 'Gleich danach siehst du einmalig ihren Zugangscode – danach '
+ 'nie wieder. Schreib ihn dir vorher auf oder hab den Chat offen.',
ja: 'Zugang anlegen',
eingabe: 'Wie soll die Person im Workspace heißen?',
eingabeWert: vorschlag,
eingabeMin: 2,
});
if (!an) return null; // abgebrochen
const name = an.eingabe;
if (String(name).trim().length < 2) { melde('Der Name ist zu kurz.'); return null; }
let a;
try {
a = await fetch('/workspace/api/verwaltung/personen', {
credentials: 'same-origin',
method: 'POST',
headers: { 'Content-Type': 'application/json' },
/* WELCHE ROLLE, SAGT DER SERVER (16.09.2026). Hier stand ein
fester Text -- und diese Datei liefert der Server an jeden
aus, der die Adresse kennt. Nachgemessen: ohne Anmeldung
200, 25 KB Quelltext. */
body: JSON.stringify({ name: String(name).trim(), rolle: lage.zugang.rolle }),
});
} catch { melde('Keine Verbindung – der Zugang wurde nicht angelegt.'); return null; }
if (a.status === 404 || a.status === 403) {
melde('Zugänge legt DogFather an – sag ihm Bescheid.');
return null;
}
if (!a.ok) {
const grund = (await a.json().catch(() => ({}))).fehler;
melde(grund || 'Der Zugang ließ sich nicht anlegen.');
return null;
}
const neu = await a.json();
codeZeigen(neu);
return neu;
}
/* Der Code steht in einem Kasten, den man SCHLIESSEN muss -- nicht in
einer Meldung, die von selbst verschwindet. Er ist das Einzige auf
dieser Seite, das es nur ein einziges Mal gibt. */
function codeZeigen(neu) {
$('code-name').textContent = `${neu.name} · ${lage.zugang.name}`;
$('code-wert').textContent = neu.code;
$('code-kasten').hidden = false;
$('code-kasten').scrollIntoView({ behavior: window.sanft?.() || 'auto', block: 'center' });
$('code-kopieren').focus();
}
$('code-kopieren')?.addEventListener('click', async () => {
const code = $('code-wert').textContent;
try { await navigator.clipboard.writeText(code); melde('Code kopiert.'); }
catch {
/* Ohne Zwischenablage-Recht: markieren, dann geht Strg+C. Der
Code steht ja da -- "ging nicht" waere hier unbrauchbar. */
getSelection()?.removeAllRanges();
const r = document.createRange();
r.selectNodeContents($('code-wert'));
getSelection()?.addRange(r);
melde('Markiert – mit Strg+C kopieren.');
}
});
$('code-schliessen')?.addEventListener('click', () => {
$('code-kasten').hidden = true;
$('code-wert').textContent = ''; // auch aus der Seite raus
melde('');
});
/* DER TEXT WIRD GEZEIGT, NICHT VERSCHICKT.
Eine Nachricht, die ein Programm abschickt, liest sich wie eine
Nachricht, die ein Programm abgeschickt hat — und genau das soll
dieser Text nicht sein. Er steht da zum Lesen, Ändern und
Kopieren, in einem Feld, das man bearbeiten kann. */
function textZeigen(k) {
const alt = document.getElementById('t-textfeld');
if (alt) alt.closest('.t-text')?.remove();
const kasten = el('div', 't-text');
kasten.append(el('h3', 't-text__titel', `Vorschlag für ${k.titel}`));
kasten.append(el('p', 't-text__satz',
'Gebaut aus den Merkmalen, die du angeklickt hast. Ändere ihn ruhig – '
+ 'er soll nach dir klingen, nicht nach einer Vorlage. Abgeschickt wird '
+ 'hier nichts.'));
const feld = el('textarea', 't-text__feld');
feld.id = 't-textfeld';
feld.rows = 14;
feld.value = k.ansprechtext;
kasten.append(feld);
const reihe = el('div', 't-text__knoepfe');
const kopieren = el('button', 'schritt', 'Kopieren');
kopieren.type = 'button';
kopieren.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(feld.value);
melde('Kopiert. Jetzt einfügen, wo du ihn ansprichst.');
} catch {
/* Ohne Zwischenablage-Recht: markieren, dann geht Strg+C.
Eine Fehlermeldung „ging nicht" wäre hier unbrauchbar — der
Text ist ja da. */
feld.select();
melde('Markiert – mit Strg+C kopieren.');
}
});
reihe.append(kopieren);
const zu = el('button', 'schritt', 'Schließen');
zu.type = 'button';
zu.addEventListener('click', () => kasten.remove());
reihe.append(zu);
kasten.append(reihe);
const karte = [...document.querySelectorAll('.t-karte')]
.find((x) => x.querySelector('.t-karte__titel')?.textContent === k.titel);
(karte || $('liste')).append(kasten);
feld.focus();
}
/* =====================================================================
DER ÜBERGANG — ein Name und ein Datum (15.09.2026)
Filipe: „ich will die noch viel besser […] so einfach wie möglich
für jeden."
Die Auswahl war der gut gebaute Teil dieser Seite. Verloren gehen
Leute dahinter: zwischen dem „ja" und der ersten echten Schicht.
Wer zusagt und danach zwei Wochen nichts hört, hat innerlich schon
abgesagt — und auf der Karte steht weiter „angesprochen".
KEIN prompt() FÜR EIN DATUM. Der Zugang nebenan benutzt eins für
einen Namen, und dort geht das. Ein Datum, das jemand in ein
Textfeld tippt, kommt als „nächsten Samstag" zurück. Hier steht
deshalb eine Auswahl mit Namen und ein echter Datumsknopf — auf
dem Handy der Kalender des Geräts.
===================================================================== */
function uebergangFragen(k, karteEl, art) {
return new Promise((fertig) => {
document.querySelector('.t-uebergang--frage')?.remove();
const kasten = el('form', 't-uebergang t-uebergang--frage');
kasten.append(el('h3', 't-uebergang__titel', art.titel));
kasten.append(el('p', 't-uebergang__satz', art.satz));
const w1 = el('label', 't-uebergang__feld');
w1.append(el('span', 't-uebergang__was', 'Wer arbeitet ein'));
const wahl = el('select', 't-uebergang__wahl');
const leer = el('option', null, 'bitte wählen');
leer.value = '';
wahl.append(leer);
for (const p of (lage.buddys || [])) {
const o = el('option', null, p.name);
o.value = String(p.id);
if (art.buddy && art.buddy === p.id) o.selected = true;
wahl.append(o);
}
w1.append(wahl);
kasten.append(w1);
const w2 = el('label', 't-uebergang__feld');
w2.append(el('span', 't-uebergang__was', 'Erste Schicht'));
const datum = el('input', 't-uebergang__wahl');
datum.type = 'date';
datum.min = heute();
/* DREI TAGE ALS VORSCHLAG, nicht heute und nicht leer. Heute wäre
eine Behauptung, leer wäre eine Aufgabe. Drei Tage sind nah
genug, dass es nicht verläuft, und weit genug, dass man sich
abstimmen kann. */
datum.value = art.datum || inTagen(3);
w2.append(datum);
kasten.append(w2);
const hinweis = el('p', 't-uebergang__hinweis');
kasten.append(hinweis);
const nachsehen = () => {
const bis = tageBis(datum.value);
const weit = lage.schicht_weit_tage || 14;
hinweis.textContent = bis !== null && bis > weit
? `Das sind ${bis} Tage hin. Bis dahin hört man nichts voneinander – `
+ 'wenn es nicht anders geht, meldet euch zwischendurch einmal.'
: '';
hinweis.hidden = !hinweis.textContent;
};
datum.addEventListener('change', nachsehen);
nachsehen();
const reihe = el('div', 't-uebergang__knoepfe');
const ja = el('button', 'schritt t-uebergang__ok', art.knopf);
ja.type = 'submit';
reihe.append(ja);
const ab = el('button', 'schritt', 'Abbrechen');
ab.type = 'button';
ab.addEventListener('click', () => { kasten.remove(); fertig(null); });
reihe.append(ab);
kasten.append(reihe);
kasten.addEventListener('submit', (e) => {
e.preventDefault();
if (!wahl.value) { melde('Ohne einen Namen geht die Probe nicht los.'); wahl.focus(); return; }
if (!datum.value) { melde('Und ohne Datum auch nicht.'); datum.focus(); return; }
melde('');
kasten.remove();
fertig({ buddy_id: Number(wahl.value), erste_schicht: datum.value });
});
karteEl.append(kasten);
wahl.focus();
});
}
/* Was der Server absagt, in einem Satz, der sagt WAS fehlt.
„Ging nicht" wäre hier die unbrauchbarste Auskunft überhaupt — die
beiden Felder standen ja gerade noch auf dem Bildschirm. */
const UEBERGANG_ABSAGE = {
buddy: 'Ohne einen Namen geht die Probe nicht los.',
buddy_unbekannt: 'Diese Person kann gerade niemanden einarbeiten.',
erste_schicht: 'Es fehlt das Datum der ersten Schicht.',
datum_unlesbar: 'Das Datum konnte ich nicht lesen.',
datum_zu_alt: 'Das Datum liegt zu weit zurück.',
datum_zu_weit: 'Das Datum liegt zu weit in der Zukunft – das ist kein Termin mehr.',
};
/* Erst die bekannten Fehlstellen, dann der Satz des Servers, dann
erst der allgemeine. Bis heute fiel ein erklärender Satz vom Server
hier lautlos auf „Ging nicht." zurück -- die Auskunft war da und
wurde weggeworfen. */
const absage = (antwort) => UEBERGANG_ABSAGE[(antwort?.fehlt || [])[0]]
|| antwort?.fehler || 'Ging nicht.';
function merkmal(k, m, istWarnung) {
const gesetzt = (istWarnung ? k.warnzeichen : k.merkmale).includes(m.schluessel);
const b = el('button', 't-merkmal');
b.type = 'button';
if (istWarnung) b.dataset.warnung = 'ja';
b.setAttribute('aria-pressed', String(gesetzt));
/* WER ES GESEHEN HAT, steht im Titel -- nicht als weitere Zeile
auf einem Knopf, der in 27 Stueck nebeneinander steht.
Filipe: „analysiert werden von vanvan und mir." Bei zwei Leuten,
die unabhaengig beobachten, ist genau das die Auskunft: Wer hat
es gesehen? Bis heute stand `von_id` in der Tabelle und wurde nie
ausgeliefert. */
const wer = k.gesehen_von?.[m.schluessel];
b.title = m.text + (gesetzt && wer ? ' — gesehen von ' + wer + '.' : '');
if (gesetzt && wer) b.dataset.wer = wer;
b.append(el('span', 't-merkmal__haken', gesetzt ? '✓' : '+'));
b.append(el('span', 't-merkmal__titel', m.titel));
if (gesetzt && wer) b.append(el('span', 't-merkmal__wer', wer));
b.addEventListener('click', async () => {
b.disabled = true;
const r = await hole(`/workspace/api/talente/${k.id}/merkmal/${m.schluessel}`,
{ method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: '{}' });
b.disabled = false;
if (!r.ok) { melde('Ging nicht.'); return; }
await laden();
});
return b;
}
/* ---- Zeichnen --------------------------------------------------------- */
/* =====================================================================
DIE PROBEZEIT (17.09.2026)
Filipe: „wie talente bewertet werden, aufgaben bekommen, analysiert
werden von vanvan und mir."
Zwoelf Schritte in drei Phasen -- mitlaufen, selbst machen, allein.
Jeder mit einem Zustaendigen, denn ohne den wandern in jeder
Probezeit dieselben drei Schritte an niemanden.
KEINE PUNKTZAHL, KEIN PROZENT. Die Zahl sagt, wie viel man GESEHEN
hat -- nicht, wie gut jemand ist. Ein Prozentwert ueber einem
Menschen wird gelesen, verglichen und weitererzaehlt.
===================================================================== */
const kurzDatum = (iso) => {
const d = new Date(iso || '');
return Number.isNaN(d.getTime()) ? ''
: d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
};
function probeZeigen(ziel, k) {
const pr = k.probe;
const kasten = el('section', 't-probe');
if (pr.entscheidbar) kasten.dataset.reif = 'ja';
const kopf = el('div', 't-probe__kopf');
kopf.append(el('h3', 't-probe__titel', 'Die Probezeit'));
kopf.append(el('span', 't-probe__stand', pr.getan + ' von ' + pr.gesamt + ' gesehen'));
kasten.append(kopf);
/* DER SATZ, DER DIE ENTSCHEIDUNG TRAEGT. Er steht oben und nicht
unter zwoelf Zeilen: Wer die Karte aufmacht, will wissen, ob man
schon entscheiden kann. */
if (pr.entscheidbar) {
kasten.append(el('p', 't-probe__reif',
'Genug gesehen, um zu entscheiden. Die drei Fragen stehen unten.'));
} else {
const fehlt = pr.fehlt_noetig
.map((s) => (pr.schritte.find((x) => x.schluessel === s) || {}).titel)
.filter(Boolean);
kasten.append(el('p', 't-probe__offen',
'Noch nicht entscheidbar – dafür fehlt: ' + fehlt.join(' · ')));
}
for (const ph of pr.phasen) {
const block = el('div', 't-phase');
const pk = el('div', 't-phase__kopf');
pk.append(el('h4', 't-phase__titel', ph.name));
pk.append(el('span', 't-phase__zeit', ph.zeit + ' · ' + ph.fertig + '/' + ph.gesamt));
block.append(pk);
block.append(el('p', 't-phase__satz', ph.text));
for (const sch of pr.schritte.filter((x) => x.phase === ph.schluessel)) {
const zeile = el('div', 't-pschritt');
if (sch.fertig) zeile.dataset.fertig = 'ja';
const knopf = el('button', 't-pschritt__haken', sch.fertig ? '✓' : '');
knopf.type = 'button';
knopf.setAttribute('aria-pressed', String(!!sch.fertig));
knopf.setAttribute('aria-label',
(sch.fertig ? 'Zurücknehmen: ' : 'Abhaken: ') + sch.titel);
knopf.addEventListener('click', async () => {
knopf.disabled = true;
const r = await hole('/workspace/api/talente/' + k.id + '/probe/' + sch.schluessel,
{ method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: '{}' });
if (!r.ok) {
const d = await r.json().catch(() => ({}));
melde(sagWas(d.fehler, absage(d)));
knopf.disabled = false;
return;
}
await laden();
});
zeile.append(knopf);
const text = el('div', 't-pschritt__text');
const oben = el('p', 't-pschritt__titel');
oben.append(document.createTextNode(sch.titel));
oben.append(el('span', 't-pschritt__wer', sch.wer_name));
text.append(oben);
text.append(el('p', 't-pschritt__satz', sch.text));
/* WOFUER der Schritt da ist. Ein Haken, dessen Zweck man nicht
kennt, wird gesetzt, weil er dasteht. */
text.append(el('p', 't-pschritt__wofuer', sch.wofuer));
if (sch.fertig && sch.von) {
text.append(el('p', 't-pschritt__von',
'abgehakt von ' + sch.von + (sch.am ? ' · ' + kurzDatum(sch.am) : '')));
}
zeile.append(text);
block.append(zeile);
}
kasten.append(block);
}
/* DIE DREI FRAGEN. Sie erscheinen erst, wenn man sie beantworten
kann -- vorher waeren sie eine Einladung, aus dem Bauch zu
entscheiden. */
if (pr.entscheidbar) {
const f = el('div', 't-probe__fragen');
f.append(el('h4', 't-phase__titel', 'Und jetzt – drei Fragen'));
for (const q of pr.fragen) {
const z = el('div', 't-frage');
z.append(el('p', 't-frage__frage', q.frage));
z.append(el('p', 't-frage__satz', q.text));
f.append(z);
}
f.append(el('p', 't-probe__schluss',
'Drei Mal Ja: übernehmen. Ein Mal Nein: sauber beenden und sagen, '
+ 'warum. Ein „weiß nicht“ heißt, dass noch etwas fehlt – dann '
+ 'nicht raten, sondern nachholen.'));
kasten.append(f);
}
ziel.append(kasten);
}
function zeichne() {
trichter();
const ziel = $('liste');
ziel.textContent = '';
ziel.setAttribute('aria-busy', 'false');
if (!lage.kandidaten.length) {
/* DER LEERE ZUSTAND ARBEITET (17.09.2026).
Vorher stand hier ein Satz, der sagte, dass nichts da ist,
und ein Knopf, der woanders hinführt. Das ist keine
Auskunft, sondern eine Absage -- und genau der Eindruck,
den Filipe im Bildschirmfoto beschrieben hat.
Jetzt steht da, WORAUF man achtet: die fünf Gruppen aus
lage.gruppen mit der Zahl ihrer Anzeichen -- 21 Stück, die
es längst gibt und die auf der leeren Seite nie jemand zu
sehen bekam. Abgeleitet, nicht abgeschrieben: Kommt eine
sechste Gruppe dazu, steht sie hier von selbst.
UND ER BLEIBT EHRLICH: Es werden keine erfundenen Namen in
den Trichter gesetzt, damit er voll aussieht. Ein Talent ist
ein Mensch, kein Inhalt. */
const leer = el('div', 't-leer');
leer.append(el('h3', 't-leer__titel', 'Wonach du suchst'));
leer.append(el('p', null,
'Noch niemand auf der Liste – das ist der Normalzustand am Anfang. '
+ 'Ein Talent fällt zuerst im Chat auf, nicht in einer Bewerbung. '
+ 'Das hier sind die fünf Dinge, die zählen:'));
const merkmale = el('ul', 't-leer__merkmale');
for (const g of lage.gruppen || []) {
const li = el('li', 't-leer__merkmal');
li.dataset.art = g.schluessel;
li.append(el('span', 't-leer__merkmal-name', g.name));
if (g.merkmale?.length) {
li.append(el('span', 't-leer__merkmal-zahl',
`${g.merkmale.length} Anzeichen`));
}
merkmale.append(li);
}
if (merkmale.children.length) leer.append(merkmale);
leer.append(el('p', 't-leer__weg',
'Wer dir auffällt, kommt als Eintrag ins Brett „Talente“. '
+ 'Von dort wird hier ein Weg daraus: aufgefallen, beobachtet, '
+ 'angesprochen, auf Probe – und am Ende im Team.'));
const knoepfe = el('div', 't-leer__knoepfe');
const a = el('a', 'schritt', 'Jemanden eintragen');
a.href = 'bereich.html?b=talente';
knoepfe.append(a);
/* DER WEG ZURÜCK ZUM TREFF. Dort fällt jemand auf -- das ist
der Anfang der Kette, und ohne ihn beginnt diese Seite im
Nichts. */
const t = el('a', 'schritt schritt--leise', 'Im Rudel umsehen');
t.href = 'bereich.html?b=treff';
knoepfe.append(t);
leer.append(knoepfe);
ziel.append(leer);
/* AUCH AUF DER LEEREN SEITE. Wer gerade den letzten Kandidaten
beendet hat, sähe sonst „Noch niemand auf der Liste" und
müsste glauben, seine Arbeit sei verschwunden. */
beendeteZeigen(ziel);
return;
}
/* LIEGENGEBLIEBENE ZUERST. Die Reihenfolge ist die eigentliche
Arbeitsanweisung dieser Seite: Was liegt, ist das, was dran ist. */
const sortiert = [...lage.kandidaten].sort((a, b) =>
(b.liegt ? 1 : 0) - (a.liegt ? 1 : 0) || (b.tage || 0) - (a.tage || 0));
for (const k of sortiert) ziel.append(karte(k));
const fuss = el('p', 't-fuss');
fuss.append('Ein neues Talent legst du im Brett an – ');
const a = el('a', null, 'Talente-Brett öffnen');
a.href = 'bereich.html?b=talente';
fuss.append(a);
fuss.append('. Von dort landet es hier automatisch auf „Aufgefallen“.');
ziel.append(fuss);
beendeteZeigen(ziel);
}
/* ---- Was beendet wurde ------------------------------------------------
ZUGEKLAPPT, UNTEN, MIT ZAHL. Nicht gelöscht und nicht oben.
Gelöscht wäre bequem -- und in sechs Monaten wüsste niemand mehr,
dass es schon einmal einen Anlauf gab. Oben wäre es die Liste, die
die Arbeitsliste erdrückt: nach einem Jahr stehen hier mehr
Beendete als Kandidaten, und dann schaut niemand mehr auf die
Seite.
Aufgeklappt steht an jedem, WO er ausgestiegen ist und WARUM.
Beides zusammen -- „beendet in der Probe, hat sich nicht mehr
gemeldet" ist eine Auskunft, „beendet" ist keine. */
const beendeteOffen = { ja: false };
function beendeteZeigen(ziel) {
const liste = lage.beendete || [];
if (!liste.length) return;
const kasten = el('section', 't-beendet');
const schalter = el('button', 't-beendet__auf',
beendeteOffen.ja
? 'Beendete zuklappen'
: `Beendet: ${liste.length} ${liste.length === 1 ? 'Kandidat' : 'Kandidaten'}`);
schalter.type = 'button';
schalter.setAttribute('aria-expanded', String(beendeteOffen.ja));
schalter.addEventListener('click', () => {
beendeteOffen.ja = !beendeteOffen.ja;
zeichne();
});
kasten.append(schalter);
if (beendeteOffen.ja) {
for (const k of liste) kasten.append(beendetKarte(k));
}
ziel.append(kasten);
}
function beendetKarte(k) {
const a = el('article', 't-beendet__karte');
const kopf = el('div', 't-beendet__kopf');
kopf.append(el('h3', 't-beendet__titel', k.titel));
/* WO ES GEENDET HAT, nicht nur DASS. Die Stufe blieb deshalb
stehen, statt auf einen eigenen Wert „beendet" umgeschrieben zu
werden -- „nach dem ersten Hinsehen" und „in der Probe" sind
zwei verschiedene Sätze. */
kopf.append(el('span', 't-beendet__stufe', 'in „' + k.stufe_name + '“'));
kopf.append(el('span', 't-beendet__zeit', kurzDatum(k.beendet.am)));
a.append(kopf);
if (k.beendet.grund) a.append(el('p', 't-beendet__grund', k.beendet.grund));
if (k.beendet.von) {
a.append(el('p', 't-beendet__wer', 'beendet von ' + k.beendet.von));
}
const wieder = el('button', 't-beendet__wieder', 'Wieder aufnehmen');
wieder.type = 'button';
wieder.title = 'Beginnt neu bei „Aufgefallen“. Der Satz von damals '
+ 'wandert in die Notiz.';
wieder.addEventListener('click', async () => {
wieder.disabled = true;
melde('');
try {
const r = await hole(`/workspace/api/talente/${k.id}/wieder`, { method: 'PUT' });
if (!r.ok) {
melde(absage(await r.json().catch(() => ({}))));
wieder.disabled = false;
return;
}
await laden();
} catch {
melde('Ging nicht.');
wieder.disabled = false;
}
});
a.append(wieder);
return a;
}
async function laden() {
const a = await hole('/workspace/api/talente/lage');
if (!a.ok) { melde('Ging nicht.'); return; }
lage = await a.json();
zeichne();
}
/* WER ZUGAENGE ANLEGEN DARF, SAGT DER SERVER -- dieselbe Auskunft,
aus der auch "Personen & Zugaenge" ihre Rollenknoepfe baut. Eine
eigene Rollenabfrage hier waere eine zweite Meinung darueber, und
die zweite ist die, die irgendwann mehr durchlaesst.
VOR dem ersten Zeichnen, nicht daneben: Sonst steht der Knopf
einen Wimpernschlag lang da und verschwindet wieder. */
async function ichHolen() {
try {
const a = await hole('/workspace/api/ich');
if (!a.ok) return;
ich = await a.json();
window.werZeigen?.(ich);
darfListe = ich?.darf_anlegen;
} catch { /* umgeleitet */ }
}
/* DIE ANFRAGEN -- nur die Zahl, nicht der Inhalt (17.09.2026).
Eine eigene, winzige Auskunft. Den vollen Eingang zu laden hiesse,
vertrauliche Saetze in eine Seite zu holen, die sie gar nicht
anzeigt -- im Zwischenspeicher des Browsers landen sie trotzdem.
SCHWEIGT BEI 404. Wer diese Seite sieht, darf die Anfragen zwar in
aller Regel auch sehen; aber die Rechtetafel ist umstellbar, und
ein Kasten, der dann ins Leere fuehrt, ist schlechter als keiner. */
async function anfragenZeigen() {
const kasten = $('anfragen-tor');
if (!kasten) return;
let d;
try {
const a = await hole('/workspace/api/bewerbungen-zahl');
if (!a.ok) return;
d = await a.json();
} catch { return; }
kasten.textContent = '';
kasten.hidden = false;
if (d.neu > 0) kasten.dataset.neu = 'ja'; else delete kasten.dataset.neu;
const text = document.createElement('div');
const wort = document.createElement('p');
wort.className = 'bw-tor__wort';
/* DIE ZAHL STEHT IM SATZ, nicht als nackte Ziffer daneben. „2 neue
Anfragen" sagt, was zu tun ist; eine 2 in einem Kreis sagt es
nicht. */
wort.textContent = d.neu > 0
? (d.neu === 1 ? 'Eine neue Anfrage wartet.' : d.neu + ' neue Anfragen warten.')
: (d.offen > 0
? (d.offen === 1 ? 'Eine Anfrage ist noch offen.' : d.offen + ' Anfragen sind noch offen.')
: 'Keine offenen Anfragen.');
text.append(wort);
const leise = document.createElement('p');
leise.className = 'leise';
leise.textContent = 'Wer sich selbst gemeldet hat. Nimmst du an, '
+ 'entsteht daraus hier ein Talent auf der Stufe „Angesprochen“.';
text.append(leise);
const hin = document.createElement('a');
hin.className = d.neu > 0 ? 'knopf' : 'schritt';
hin.href = 'bewerbungen.html';
hin.textContent = d.neu > 0 ? 'Ansehen' : 'Anfragen öffnen';
kasten.append(text, hin);
}
(async () => {
try {
await ichHolen();
await laden();
await anfragenZeigen();
} catch { /* umgeleitet */ }
})();
})();