Talente: der Uebergang, an dem die meisten verlorengehen

Filipe: "ich will dass du die 2 kategorien perfektionnierst. ich will
die noch viel besser, perfektionniert, viel geiler und krasser. es
soll so einfach wie moeglich sein fuer jeden."

Die AUSWAHL war der gut gebaute Teil dieser Seite: 21 Merkmale, sechs
Warnzeichen, ein Trichter mit Standzeiten. Die Stelle, an der Teams
tatsaechlich Leute verlieren, liegt dahinter -- zwischen dem "ja" und
der ersten echten Schicht. Wer zusagt und danach zwei Wochen nichts
hoert, hat innerlich schon abgesagt, und auf der Karte steht weiter
"angesprochen".

In der Beschreibung der Stufe "Probe" stand seit dem ersten Tag
"fester Buddy". Ein Feld dafuer gab es nicht. Ein Versprechen im Text
ist keine Eigenschaft des Systems -- erst eine Regel, die NEIN sagen
kann, ist eine.

Ab jetzt:

  - Auf die Probe kommt niemand ohne einen Namen und ein Datum. Die
    Absage sagt, welches von beiden fehlt, und die Karte bleibt dabei
    stehen, wo sie war.
  - Die Karte zeigt beides: wer einarbeitet, wann die erste Schicht
    ist -- in einem Satz, den man laut vorlesen kann ("Erste Schicht
    ist morgen."). Ein Termin weiter als 14 Tage ist erlaubt, wird
    aber benannt.
  - Drei Dinge fuer den ersten Tag stehen auf der Probekarte: was
    gilt, was du darfst, wen du fragst. Keine Haken -- der Buddy soll
    sie lesen, nicht abarbeiten.
  - Buddy und Termin lassen sich spaeter aendern, OHNE die Standzeit
    zurueckzusetzen: Sie ist die Auskunft ueber uns, nicht ueber den
    Kandidaten.
  - Wer geht, ist kein Buddy mehr -- stillgelegt wie geloescht. Die
    Karte sagt dann "bitte neu bestimmen" und ist am Rand markiert.

Recherche (Quellen im Kopf von workspace-talent-punkte.js): Discord
nennt Buddy und Mentoring als die beiden Trainingswege, die
funktionieren; aus der Freiwilligenarbeit kommt dieselbe Aussage mit
Zahlen. Und: Am ersten Tag braucht jemand drei Dinge, nicht dreissig.

Was die Pruefung gefunden hat: buddy_weg fragte "buddy_id gesetzt UND
Person weg" -- damit war der zweite Weg blind. Wird ein Mensch
geloescht, setzt ON DELETE SET NULL die Spalte auf NULL, die Karte sah
unauffaellig aus, und eine laufende Probe stand ohne jeden Buddy da.

  server/pruef-uebergang.mjs   61 Pruefungen, 0 Fehler (Port 4420)
  pruef-nachwuchs 123, pruef-entwicklung 43, pruef-css-klassen -- 0 Fehler

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-15 13:55:26 +02:00
co-authored by Claude Opus 5
parent d4730deb30
commit b85740dc20
35 changed files with 1355 additions and 367 deletions
+79
View File
@@ -499,3 +499,82 @@
.b-verlauf__zeile { grid-template-columns: minmax(9ch, 1fr) repeat(auto-fit, minmax(2rem, 2rem)); }
.b-verlauf__punkt { width: 1.9rem; }
}
/* =====================================================================
DER ÜBERGANG — Buddy und erste Schicht (15.09.2026)
Er steht unter dem nächsten Schritt und sieht anders aus als der
Rest der Karte: ein durchgehender linker Rand statt eines Kastens.
Während einer Probe ist er die Karte, nicht eine Angabe darauf.
Augenschonend heißt hier vor allem: keine zweite Signalfarbe. Das
Orange auf dieser Seite bedeutet „liegt zu lange" — wenn der
Übergang es auch benutzt, bedeutet es bald gar nichts mehr.
===================================================================== */
.t-uebergang {
margin: 11px 0 0; padding: 12px 14px;
background: color-mix(in srgb, var(--tinte) 90%, #ffffff 10%);
border: 1px solid var(--rand);
border-left: 3px solid color-mix(in srgb, #668e6e 55%, transparent);
}
/* Kein Buddy mehr: die einzige Lage, die eine Farbe verdient. */
.t-uebergang[data-weg="ja"] { border-left-color: #cc9451; }
.t-uebergang__titel {
margin: 0 0 5px; font-size: .95rem; font-weight: 640; color: var(--text);
}
.t-uebergang__zeile {
margin: 0; display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px;
}
.t-uebergang__was {
font-size: .72rem; letter-spacing: .05em; text-transform: uppercase;
color: var(--text-still);
}
.t-uebergang__wer { font-size: .9rem; font-weight: 620; color: var(--text); }
.t-uebergang[data-weg="ja"] .t-uebergang__wer { color: #d8a76a; font-weight: 500; }
.t-uebergang__satz {
margin: 7px 0 0; max-width: 66ch;
font-size: .82rem; line-height: 1.6; color: var(--text-leise);
}
.t-uebergang__hinweis {
margin: 8px 0 0; max-width: 66ch;
font-size: .8rem; line-height: 1.55; color: #d8a76a;
}
.t-uebergang__tag { margin: 6px 0 0; padding-left: 18px; }
.t-uebergang__punkt {
margin: 3px 0; max-width: 66ch;
font-size: .81rem; line-height: 1.55; color: var(--text-leise);
}
.t-uebergang__punkt strong { color: var(--text); font-weight: 620; }
/* ---- Das Fragefeld ----------------------------------------------------
Zwei Felder untereinander, nicht nebeneinander: Auf einem Handy
werden aus zwei Spalten zwei zu schmale Spalten, und ein Datumsfeld,
das nicht vollständig hineinpasst, öffnet den Gerätekalender an der
falschen Stelle. */
.t-uebergang--frage { border-left-color: color-mix(in srgb, #8ec9ff 50%, transparent); }
.t-uebergang__feld {
display: grid; gap: 4px;
margin: 10px 0 0; max-width: 34ch;
}
.t-uebergang__wahl {
font: inherit; font-size: .88rem;
padding: 8px 10px;
color: var(--text);
background: var(--tinte);
border: 1px solid var(--rand-hell);
border-radius: var(--radius);
/* Der eigene Kalenderknopf des Browsers ist auf Dunkel hell gezeichnet.
`color-scheme` sagt ihm, dass er dunkel zeichnen soll — sonst sitzt
ein weißes Kästchen im dunklen Feld. */
color-scheme: dark;
}
.t-uebergang__wahl:focus-visible {
outline: 2px solid var(--akzent); outline-offset: 1px;
}
.t-uebergang__knoepfe {
display: flex; flex-wrap: wrap; gap: 9px; margin: 13px 0 0;
}
.t-uebergang__aendern { margin: 11px 0 0; }
+194 -2
View File
@@ -36,6 +36,24 @@
return a;
}
/* 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;
let darfAnlegen = false; // darf ich hier einen Modi-Zugang anlegen?
const offen = new Set(); // welche Karten aufgeklappt sind
@@ -100,6 +118,21 @@
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);
@@ -108,7 +141,7 @@
const r = await hole(`/workspace/api/talente/${k.id}/stufe`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ stufe: k.naechste }),
body: JSON.stringify({ stufe: k.naechste, ...uebergang }),
});
b.disabled = false;
if (!r.ok) {
@@ -119,7 +152,7 @@
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.'
: 'Ging nicht.');
: absage(await r.json().catch(() => ({}))));
return;
}
offen.add(k.id);
@@ -151,6 +184,62 @@
}
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);
}
/* ---- Merkmale, aufklappbar ------------------------------------------
Zugeklappt steht dort eine Zahl, aufgeklappt die 21 Merkmale.
Immer offen wären es bei zehn Kandidaten 270 Kästchen auf einer
@@ -325,6 +414,109 @@
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.',
};
const absage = (antwort) => UEBERGANG_ABSAGE[(antwort?.fehlt || [])[0]] || 'Ging nicht.';
function merkmal(k, m, istWarnung) {
const gesetzt = (istWarnung ? k.warnzeichen : k.merkmale).includes(m.schluessel);
const b = el('button', 't-merkmal');