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:
@@ -85,6 +85,13 @@ const jetzt = () => new Date().toISOString();
|
||||
const TAG = 86400000;
|
||||
const ANLASS_MAX = 1200;
|
||||
|
||||
/* WIE KURZ EIN ABSCHIEDSSATZ SEIN DARF.
|
||||
Zehn Zeichen -- dieselbe Grenze wie bei einer abgelehnten Anfrage
|
||||
(NACHRICHT_MIN, workspace-bewerbung-fragen.js), und aus demselben
|
||||
Grund: Sie verhindert nicht das schlechte Argument, sie verhindert
|
||||
das leere Feld. "Passt nicht" hat neun Zeichen. */
|
||||
const BEENDEN_GRUND_MIN = 10;
|
||||
|
||||
/* WANN EINE EINSCHAETZUNG VERBLASST.
|
||||
Nach 90 Tagen zaehlt ein Stand nicht mehr als aktuell. Er wird NICHT
|
||||
geloescht -- man soll sehen, dass etwas alt ist, statt dass es
|
||||
@@ -230,6 +237,31 @@ export function tabellen() {
|
||||
for (const [spalte, typ] of [
|
||||
["buddy_id", "INTEGER REFERENCES personen(id) ON DELETE SET NULL"],
|
||||
["erste_schicht", "TEXT"],
|
||||
/* DER AUSGANG (17.09.2026).
|
||||
|
||||
Der Trichter hatte bis heute genau EINEN: "Ins Team holen". Die
|
||||
Stufenbeschreibung versprach daneben "oder sauber beenden" --
|
||||
den Weg dazu gab es nicht. Ein Kandidat, bei dem es nicht passt,
|
||||
blieb damit fuer immer auf seiner Stufe stehen, mit wachsender
|
||||
Standzeit und rotem Hinweis. Das ist schlimmer als keine
|
||||
Funktion: Die Liste fuellt sich mit Leuten, bei denen laengst
|
||||
entschieden ist, und irgendwann liest niemand mehr die
|
||||
Standzeiten.
|
||||
|
||||
ZWEI SPALTEN UND KEINE NEUE STUFE, und das ist Absicht:
|
||||
|
||||
Eine Stufe "beendet" haette die Regel in talent_stufe umbauen
|
||||
muessen (CHECK) -- ein Tabellenneubau an lebenden Daten fuer
|
||||
etwas, das sich mit zwei Spalten sagen laesst.
|
||||
|
||||
Und sie haette eine Auskunft VERNICHTET: So bleibt stehen, WO
|
||||
es geendet hat. "Beendet in der Probe" und "beendet nach dem
|
||||
ersten Hinsehen" sind zwei verschiedene Saetze ueber dieselbe
|
||||
Person -- und wenn sie in einem halben Jahr wieder auffaellt,
|
||||
ist genau das die Frage. */
|
||||
["beendet_am", "TEXT"],
|
||||
["beendet_grund", "TEXT"],
|
||||
["beendet_von", "INTEGER"],
|
||||
]) {
|
||||
try {
|
||||
const da = db().prepare("PRAGMA table_info(talent_stufe)").all().map((s) => s.name);
|
||||
@@ -890,7 +922,11 @@ function uebergangVon(stand) {
|
||||
/** Die Karte eines Kandidaten -- Stufe, Standzeit, was zu tun ist. */
|
||||
function talentKarte(e, merkmale) {
|
||||
const stand = db().prepare(
|
||||
"SELECT stufe, seit, notiz, buddy_id, erste_schicht FROM talent_stufe WHERE eintrag_id = ?")
|
||||
`SELECT s.stufe, s.seit, s.notiz, s.buddy_id, s.erste_schicht,
|
||||
s.beendet_am, s.beendet_grund, p.name AS beendet_von_name
|
||||
FROM talent_stufe s
|
||||
LEFT JOIN personen p ON p.id = s.beendet_von
|
||||
WHERE s.eintrag_id = ?`)
|
||||
.get(e.id);
|
||||
const stufe = talentStufe(stand?.stufe) || TALENT_STUFEN[0];
|
||||
const seit = stand?.seit || e.erstellt;
|
||||
@@ -906,7 +942,11 @@ function talentKarte(e, merkmale) {
|
||||
/* LIEGENGEBLIEBEN IST EINE AUSKUNFT UEBER UNS, nicht ueber den
|
||||
Kandidaten. Deshalb steht sie an der Karte und nicht in einer
|
||||
Erinnerungsmail, die man wegklickt. */
|
||||
liegt: stufe.frist !== null && tage !== null && tage > stufe.frist,
|
||||
/* Ein beendeter Kandidat liegt nicht -- er ist fertig. Ohne diese
|
||||
Ausnahme wuerde die Standzeit weiterlaufen und die Karte nach
|
||||
einer Woche rot werden, obwohl niemand mehr etwas zu tun hat. */
|
||||
liegt: !stand?.beendet_am && stufe.frist !== null
|
||||
&& tage !== null && tage > stufe.frist,
|
||||
merkmale: gesetzt.filter((s) => !TALENT_NACH_SCHLUESSEL.get(s)?.warnung),
|
||||
warnzeichen: warn,
|
||||
notiz: stand?.notiz || null,
|
||||
@@ -935,6 +975,23 @@ function talentKarte(e, merkmale) {
|
||||
Abruf -- sonst holt man sie erst, wenn man schon entschieden
|
||||
hat. */
|
||||
probe: probeVon(e.id, stufe.schluessel),
|
||||
/* DER AUSGANG (17.09.2026).
|
||||
|
||||
Die Stufe bleibt stehen -- dass jemand raus ist, steht daneben,
|
||||
nicht anstelle. "Beendet in der Probe" und "beendet nach dem
|
||||
ersten Hinsehen" sind zwei verschiedene Auskuenfte, und bei
|
||||
einem zweiten Anlauf in einem halben Jahr ist genau der
|
||||
Unterschied die Frage.
|
||||
|
||||
DER GRUND WIRD MITGELIEFERT, NICHT NUR DAS DATUM. Ein Ausgang
|
||||
ohne Satz waere dieselbe Sackgasse, gegen die der Satz beim
|
||||
Ablehnen einer Anfrage verlangt wird. */
|
||||
beendet: stand?.beendet_am ? {
|
||||
am: stand.beendet_am,
|
||||
grund: stand.beendet_grund || null,
|
||||
von: stand.beendet_von_name || null,
|
||||
tage: tageSeit(stand.beendet_am),
|
||||
} : null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -983,7 +1040,22 @@ entwicklungRouter.get("/workspace/api/talente/lage", (req, res) => {
|
||||
.all(...eintraege.map((e) => e.id))
|
||||
: [];
|
||||
|
||||
const karten = eintraege.map((e) => talentKarte(e, merkmale));
|
||||
/* BEENDETE KOMMEN AUS DER ARBEITSLISTE HERAUS -- aber nicht aus
|
||||
dem Haus (17.09.2026).
|
||||
|
||||
Wer einmal abgesagt hat, steht sonst fuer immer im Trichter,
|
||||
mit wachsender Standzeit und rotem "liegt"-Zeichen. Nach einem
|
||||
halben Jahr sind das mehr als die echten Kandidaten, und dann
|
||||
schaut niemand mehr hin -- eine Liste, in der 20 von 25 Eintraege
|
||||
erledigt sind, ist keine Liste mehr.
|
||||
|
||||
SIE WERDEN TROTZDEM MITGELIEFERT, in einem eigenen Feld. Das
|
||||
Loeschen waere die bequeme Variante und die, nach der in sechs
|
||||
Monaten niemand mehr weiss, dass es schon einmal einen Anlauf
|
||||
gab. */
|
||||
const alle = eintraege.map((e) => talentKarte(e, merkmale));
|
||||
const karten = alle.filter((k) => !k.beendet);
|
||||
const beendete = alle.filter((k) => k.beendet);
|
||||
res.json({
|
||||
stufen: TALENT_STUFEN,
|
||||
gruppen: TALENT_GRUPPEN.map((g) => ({ ...g, merkmale: TALENT_MERKMALE[g.schluessel] || [] })),
|
||||
@@ -1002,6 +1074,9 @@ entwicklungRouter.get("/workspace/api/talente/lage", (req, res) => {
|
||||
dazu steht; siehe ZUGANG_ROLLE oben. */
|
||||
zugang: { rolle: ZUGANG_ROLLE, name: ROLLEN_NAME[ZUGANG_ROLLE] ?? ZUGANG_ROLLE },
|
||||
kandidaten: karten,
|
||||
/* Nach Ausgangsdatum, das juengste zuerst -- wer gerade abgesagt
|
||||
hat, ist der, an den man sich erinnert. */
|
||||
beendete: beendete.sort((x, y) => (y.beendet.am > x.beendet.am ? 1 : -1)),
|
||||
/* Wie viele auf jeder Stufe liegen -- der Trichter als Zahlen.
|
||||
Abgeleitet, damit er nicht danebenliegen kann. */
|
||||
trichter: TALENT_STUFEN.map((s) => ({
|
||||
@@ -1098,6 +1173,133 @@ entwicklungRouter.put("/workspace/api/talente/:id/probe/:schluessel", gleicheHer
|
||||
}
|
||||
});
|
||||
|
||||
/* =====================================================================
|
||||
DEN TRICHTER SAUBER VERLASSEN (17.09.2026)
|
||||
|
||||
Filipe: „wie talente bewertet werden, aufgaben bekommen, analysiert
|
||||
werden ... alles moegliche, wie gesagt ich will hoch profissionel
|
||||
arbeiten."
|
||||
|
||||
EIN GRUND IST PFLICHT, und zwar aus zwei Richtungen:
|
||||
|
||||
Nach vorn -- wenn dieselbe Person in einem halben Jahr wieder
|
||||
auffaellt, ist die erste Frage: Was war damals? Ohne Satz
|
||||
beantwortet sie niemand, und man faengt bei null an.
|
||||
|
||||
Nach innen -- wer einen Satz schreiben muss, entscheidet anders
|
||||
als wer einen Knopf drueckt. "Passt nicht" laesst sich nicht
|
||||
aufschreiben, ohne dass auffaellt, dass es keine Begruendung ist.
|
||||
|
||||
DER EINTRAG BLEIBT. Er wird nicht geloescht, sondern bekommt ein
|
||||
Datum -- und verschwindet damit aus der Arbeitsliste, nicht aus dem
|
||||
Gedaechtnis. Loeschen waere die bequeme Variante und die, nach der
|
||||
in sechs Monaten niemand mehr weiss, dass es schon einmal einen
|
||||
Anlauf gab.
|
||||
===================================================================== */
|
||||
entwicklungRouter.put("/workspace/api/talente/:id/beenden", gleicheHerkunft,
|
||||
express.json({ limit: "8kb" }), (req, res) => {
|
||||
try {
|
||||
const id = nummer(req.params.id);
|
||||
const e = id && db().prepare(
|
||||
"SELECT id, bereich FROM eintraege WHERE id = ?").get(id);
|
||||
if (!e || e.bereich !== "talente") return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
|
||||
const grund = text(req.body?.grund, 600);
|
||||
if (grund.length < BEENDEN_GRUND_MIN) {
|
||||
return res.status(400).json({
|
||||
fehler: "Schreib kurz dazu, woran es lag – in einem halben Jahr "
|
||||
+ "weiß es sonst niemand mehr.",
|
||||
});
|
||||
}
|
||||
/* WER IM TEAM IST, WIRD NICHT HIER BEENDET (17.09.2026).
|
||||
|
||||
Gefunden am Bildschirmfoto, nicht an einer Zahl: Auf einer
|
||||
Karte auf "Im Team" stand woertlich "Nichts mehr zu tun -- der
|
||||
Zugang steht" und direkt darunter "Passt doch nicht?".
|
||||
|
||||
Ein Beenden haette dort die KARTE stillgelegt und den ZUGANG
|
||||
bestehen lassen -- die Person koennte sich weiter anmelden,
|
||||
waehrend im Trichter steht, sie sei raus. Das ist schlimmer
|
||||
als gar kein Ausgang: Man glaubt, gehandelt zu haben.
|
||||
|
||||
Der Riegel steht hier und nicht nur im Browser. Was der Browser
|
||||
prueft, ist Hoeflichkeit; was der Server prueft, ist die Regel. */
|
||||
const wo = db().prepare("SELECT stufe FROM talent_stufe WHERE eintrag_id = ?").get(id);
|
||||
if (wo?.stufe === "imteam") {
|
||||
return res.status(409).json({
|
||||
fehler: "Der ist schon im Team. Wer aufhört, wird unter "
|
||||
+ "„Personen & Zugänge“ abgeschaltet – sonst bleibt der "
|
||||
+ "Zugang offen, während hier steht, er sei raus.",
|
||||
});
|
||||
}
|
||||
const jetztZeit = jetzt();
|
||||
/* Die STUFE bleibt stehen. Wo es geendet hat, ist die Auskunft --
|
||||
"beendet in der Probe" und "beendet nach dem ersten Hinsehen"
|
||||
sind zwei verschiedene Saetze. */
|
||||
const da = db().prepare("SELECT eintrag_id FROM talent_stufe WHERE eintrag_id = ?").get(id);
|
||||
if (da) {
|
||||
db().prepare(`UPDATE talent_stufe
|
||||
SET beendet_am = ?, beendet_grund = ?, beendet_von = ?
|
||||
WHERE eintrag_id = ?`).run(jetztZeit, grund, req.person.id, id);
|
||||
} else {
|
||||
db().prepare(`INSERT INTO talent_stufe
|
||||
(eintrag_id, stufe, seit, von_id, beendet_am, beendet_grund, beendet_von)
|
||||
VALUES (?, 'aufgefallen', ?, ?, ?, ?, ?)`)
|
||||
.run(id, jetztZeit, req.person.id, jetztZeit, grund, req.person.id);
|
||||
}
|
||||
protokolliere("talent_beendet", {
|
||||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||||
detail: `#${id}`,
|
||||
});
|
||||
res.json({ ok: true, beendet: true });
|
||||
} catch (f) {
|
||||
console.error("[talente] Beenden:", f?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
/* WIEDER AUFNEHMEN -- und der alte Grund bleibt stehen.
|
||||
|
||||
Menschen aendern sich; ein "damals zu wenig Zeit" ist in einem Jahr
|
||||
keine Aussage mehr. Wer wieder aufgenommen wird, startet deshalb
|
||||
wieder bei "aufgefallen" -- aber der Satz von damals wandert in die
|
||||
Notiz, statt geloescht zu werden. Genau er ist beim zweiten Anlauf
|
||||
das Wertvollste, was man hat. */
|
||||
entwicklungRouter.put("/workspace/api/talente/:id/wieder", gleicheHerkunft,
|
||||
(req, res) => {
|
||||
try {
|
||||
const id = nummer(req.params.id);
|
||||
const stand = id && db().prepare(
|
||||
`SELECT s.eintrag_id, s.beendet_am, s.beendet_grund, s.notiz
|
||||
FROM talent_stufe s JOIN eintraege e ON e.id = s.eintrag_id
|
||||
WHERE s.eintrag_id = ? AND e.bereich = 'talente'`).get(id);
|
||||
if (!stand) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
if (!stand.beendet_am) {
|
||||
return res.status(409).json({ fehler: "Der läuft doch noch." });
|
||||
}
|
||||
const alt = stand.beendet_grund
|
||||
? `Beim ersten Anlauf beendet: ${stand.beendet_grund}` : null;
|
||||
const notiz = [stand.notiz, alt].filter(Boolean).join("\n\n").slice(0, 2000) || null;
|
||||
db().prepare(`UPDATE talent_stufe
|
||||
SET stufe = 'aufgefallen', seit = ?, von_id = ?, notiz = ?,
|
||||
beendet_am = NULL, beendet_grund = NULL, beendet_von = NULL,
|
||||
buddy_id = NULL, erste_schicht = NULL
|
||||
WHERE eintrag_id = ?`)
|
||||
.run(jetzt(), req.person.id, notiz, id);
|
||||
/* Die Probe faengt neu an -- alte Haken wuerden sonst eine
|
||||
Probezeit beschreiben, die vier Wochen zurueckliegt. */
|
||||
db().prepare("DELETE FROM talent_probe WHERE eintrag_id = ?").run(id);
|
||||
protokolliere("talent_wieder", {
|
||||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||||
detail: `#${id}`,
|
||||
});
|
||||
res.json({ ok: true });
|
||||
} catch (f) {
|
||||
console.error("[talente] Wieder:", f?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
/** Buddy und Datum prüfen. Gibt `{ fehlt: [...] }` oder `{ buddyId, schicht }`.
|
||||
*
|
||||
* EINE STELLE, ZWEI AUFRUFER: der Stufenwechsel auf „Probe" und das
|
||||
|
||||
Reference in New Issue
Block a user