Talente: der Trichter bekommt einen Ausgang

Gemessen vor dem Bau: talentNaechste kettet aufgefallen -> beobachtet ->
angesprochen -> probe -> imteam, und danach kommt nichts. Eine Suche nach
"beenden|abgesagt|verworfen|archiv" in workspace-entwicklung.js fand
nichts. Der Trichter hatte genau einen Ausgang -- obwohl der Text der
Probe-Stufe woertlich verspricht: "Nach 30 Tagen: uebernehmen -- der
Zugang entsteht dabei -- oder sauber beenden."

Wer nicht passte, blieb also auf seiner Stufe stehen, mit wachsender
Standzeit und rotem "liegt"-Zeichen. Nach einem Jahr hat die Liste mehr
Karteileichen als Kandidaten, und dann schaut niemand mehr hin.

DREI SPALTEN, KEINE NEUE STUFE. Eine neue Stufe haette einen
CHECK-Umbau auf laufenden Daten verlangt -- und eine Auskunft zerstoert:
Die Stufe haelt fest, WO es geendet hat. "Beendet in der Probe" und
"beendet nach dem ersten Hinsehen" sind zwei verschiedene Saetze, und
genau der Unterschied ist die Frage, wenn dieselbe Person in einem
halben Jahr wieder auffaellt.

DER SATZ IST PFLICHT (10 Zeichen, wie beim Ablehnen einer Anfrage) --
aus zwei Richtungen: Nach vorn, weil beim zweiten Anlauf niemand mehr
weiss, was damals war. Nach innen, weil wer schreiben muss, anders
entscheidet als wer klickt. "Passt nicht" hat neun Zeichen.

WIEDER AUFNEHMEN setzt auf "aufgefallen" zurueck -- Menschen aendern
sich, ein Urteil von damals gilt nicht weiter -- und rettet den alten
Satz in die Notiz, statt ihn zu loeschen.

AM BILDSCHIRMFOTO GEFUNDEN, nicht an einer Zahl: Auf einer Karte auf
"Im Team" stand "Nichts mehr zu tun -- der Zugang steht" und direkt
darunter "Passt doch nicht?". Ein Beenden dort haette die Karte
stillgelegt und den ZUGANG offengelassen -- eine halbe Handlung, die
sich wie eine ganze anfuehlt. Jetzt steht dort der Weg zu "Personen &
Zugaenge", und der Server weist es mit 409 ab.

pruef-nachwuchs 152 -> 200, davon 21 im echten Browser (der erste
Browserblick auf diese Seite ueberhaupt -- mit einer Messung, ob Name
und Datum bei 390 px uebereinanderliegen, statt einer Zahl im CSS).
Sechs Gegenproben, jede genau auf ihr Ziel:
  Grenze auf 0          -> 3 rot
  Beendete bleiben drin -> 2 rot
  beendete: []          -> 5 rot
  alter Grund verworfen -> 1 rot
  Serverriegel imteam   -> 5 rot
  Browserzweig imteam   -> 3 rot
Dazu gruen: css-klassen, struktur, entwicklung, uebergang.

Nebenbefund behoben: absage() im Browser warf einen erklaerenden Satz
des Servers lautlos weg und zeigte "Ging nicht." -- die Auskunft war da.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-17 18:39:06 +02:00
co-authored by Claude Opus 5
parent 3a49e30aa4
commit e54c6829f1
37 changed files with 1269 additions and 406 deletions
+137
View File
@@ -1111,3 +1111,140 @@
/* Wer ein Merkmal gesehen hat. Steht am Knopf, klein -- 27 Knoepfe
nebeneinander vertragen keine zweite Zeile in Normalgroesse. */
.t-merkmal__wer { font-size: .72rem; opacity: .6; margin-inline-start: 6px; }
/* =====================================================================
DER AUSGANG AUS DEM TRICHTER (17.09.2026)
LEISE, UNTEN, OHNE ROT. Ein "Beenden" in Warnfarbe waere eine
Drohung -- gemeint ist das Gegenteil: Eine Absage ist ein normaler,
ordentlicher Ausgang, kein Schaden. Rot gehoert an Dinge, die
schiefgehen, nicht an Entscheidungen, die getroffen werden.
Erst der Satz als Knopf, dann das Feld. Zugeklappt nimmt der ganze
Block eine Zeile.
===================================================================== */
.t-ausgang {
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid color-mix(in srgb, var(--rand) 60%, transparent);
}
.t-ausgang__auf {
background: none; border: 0; padding: 2px 0;
color: var(--text-still);
font: inherit; font-size: .82rem;
cursor: pointer;
text-decoration: underline;
text-underline-offset: 3px;
text-decoration-color: color-mix(in srgb, var(--text-still) 45%, transparent);
}
.t-ausgang__auf:hover { color: var(--text-leise); }
.t-ausgang__auf:focus-visible {
outline: 2px solid var(--akzent); outline-offset: 3px; border-radius: 4px;
}
.t-ausgang__form { margin-top: 10px; }
/* Der Hinweis anstelle des Ausgangs -- an Karten, die schon im Team
sind. Gleiche Groesse wie der leise Knopf daneben, damit er nicht
wie eine Warnung wirkt. */
.t-ausgang__anderswo {
margin: 0; font-size: .82rem; line-height: 1.55;
color: var(--text-still);
}
.t-ausgang__anderswo a { color: var(--text-leise); }
.t-ausgang__satz {
margin: 0 0 8px; font-size: .82rem; line-height: 1.55;
color: var(--text-leise);
}
.t-ausgang__schild {
display: block; margin-bottom: 4px;
font-size: .82rem; font-weight: 600;
}
.t-ausgang__feld {
width: 100%; box-sizing: border-box;
/* Undurchsichtig gemischt, nicht mit `transparent` (17.09.2026):
`color-mix(..., transparent)` laesst den Hintergrund durch -- auf
einer Seite mit Wasserzeichen steht er dann im Text. */
background: color-mix(in srgb, var(--tinte) 88%, #ffffff 12%);
color: var(--text);
border: 1px solid var(--rand-hell);
border-radius: 8px;
padding: 8px 10px;
font: inherit; font-size: .88rem; line-height: 1.5;
resize: vertical;
}
.t-ausgang__feld:focus-visible {
outline: 2px solid var(--akzent); outline-offset: 2px;
border-color: var(--akzent);
}
.t-ausgang__knopf { margin-top: 8px; }
/* =====================================================================
WAS BEENDET WURDE
Eine eigene Sammlung unter der Arbeitsliste. Zugeklappt eine Zeile
mit einer Zahl; aufgeklappt Karten, die deutlich stiller sind als
die oben -- sie sind Gedaechtnis, keine Arbeit.
===================================================================== */
.t-beendet { margin-top: 26px; }
.t-beendet__auf {
width: 100%;
background: color-mix(in srgb, var(--tinte) 94%, #ffffff 6%);
border: 1px solid var(--rand);
border-radius: 10px;
padding: 10px 14px;
color: var(--text-leise);
font: inherit; font-size: .86rem;
text-align: start;
cursor: pointer;
}
.t-beendet__auf:hover { border-color: var(--rand-hell); color: var(--text); }
.t-beendet__auf:focus-visible { outline: 2px solid var(--akzent); outline-offset: 2px; }
.t-beendet__karte {
margin-top: 10px;
padding: 12px 14px;
background: color-mix(in srgb, var(--tinte) 96%, #ffffff 4%);
border: 1px solid color-mix(in srgb, var(--rand) 70%, transparent);
border-radius: 10px;
}
.t-beendet__kopf {
display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 10px;
}
.t-beendet__titel {
margin: 0; font-size: .96rem; font-weight: 600;
/* Nicht per `opacity` gedaempft -- das macht den Kasten durchsichtig
und laesst den Hintergrund durch den Text. Eine leisere FARBE tut
dasselbe, ohne die Flaeche anzufassen. */
color: var(--text-leise);
}
.t-beendet__stufe { font-size: .8rem; color: var(--text-still); }
.t-beendet__zeit { font-size: .78rem; color: var(--text-still); margin-inline-start: auto; }
.t-beendet__grund {
margin: 6px 0 0; font-size: .86rem; line-height: 1.55;
color: var(--text);
}
.t-beendet__wer { margin: 4px 0 0; font-size: .76rem; color: var(--text-still); }
.t-beendet__wieder {
margin-top: 10px;
background: none;
border: 1px solid var(--rand-hell);
border-radius: 999px;
padding: 5px 12px;
color: var(--text-leise);
font: inherit; font-size: .8rem;
cursor: pointer;
}
.t-beendet__wieder:hover { color: var(--text); border-color: var(--akzent); }
.t-beendet__wieder:focus-visible { outline: 2px solid var(--akzent); outline-offset: 2px; }
.t-beendet__wieder[disabled] { opacity: .5; cursor: default; }
+209 -1
View File
@@ -294,9 +294,127 @@
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;
}
/* ---- Aus der Probe wird ein Zugang ---------------------------------
DERSELBE WEG WIE IN "PERSONEN & ZUGAENGE", nicht ein zweiter.
@@ -535,7 +653,12 @@
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.';
/* 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);
@@ -740,6 +863,10 @@
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
@@ -755,6 +882,87 @@
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() {