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]>
1272 lines
52 KiB
JavaScript
1272 lines
52 KiB
JavaScript
/* =====================================================================
|
||
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 */ }
|
||
})();
|
||
})();
|