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:
@@ -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; }
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user