From e54c6829f19f71595fd820477a3f35f74eda0c23 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Thu, 17 Sep 2026 18:39:06 +0200 Subject: [PATCH] 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 --- server/pruef-nachwuchs.mjs | 318 ++++++++++++++++++++++++++- server/workspace-entwicklung.js | 208 +++++++++++++++++- workspace/app.webmanifest | 8 +- workspace/assets/css/entwicklung.css | 137 ++++++++++++ workspace/assets/js/talente.js | 210 +++++++++++++++++- workspace/aufgaben.html | 24 +- workspace/automation.html | 26 +-- workspace/befinden.html | 24 +- workspace/bereich.html | 36 +-- workspace/bewerben.html | 30 +-- workspace/bewerbungen.html | 28 +-- workspace/calls.html | 28 +-- workspace/chat.html | 26 +-- workspace/content.html | 30 +-- workspace/crew-index.html | 10 +- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 26 +-- workspace/entwicklung.html | 24 +- workspace/index.html | 8 +- workspace/kalender.html | 28 +-- workspace/leistung.html | 26 +-- workspace/personen.html | 26 +-- workspace/profil.html | 28 +-- workspace/rechte.html | 24 +- workspace/report.html | 28 +-- workspace/scouting.html | 26 +-- workspace/start.html | 24 +- workspace/startcheck.html | 28 +-- workspace/steckbrief.html | 28 +-- workspace/talente.html | 26 +-- workspace/team.html | 26 +-- workspace/teamlage.html | 24 +- workspace/teilen.html | 22 +- workspace/treff-moderation.html | 24 +- workspace/treff-regeln.html | 26 +-- workspace/uebersicht.html | 26 +-- workspace/wissen.html | 26 +-- 37 files changed, 1269 insertions(+), 406 deletions(-) diff --git a/server/pruef-nachwuchs.mjs b/server/pruef-nachwuchs.mjs index f048175c..c22ddb10 100644 --- a/server/pruef-nachwuchs.mjs +++ b/server/pruef-nachwuchs.mjs @@ -49,6 +49,13 @@ process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):" let fehler = 0, geprueft = 0; const melde = (t) => console.log(t); const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; +/* DER DRITTE AUSGANG: nicht in Ordnung ist etwas anderes als "konnte + nicht nachsehen". Ohne ihn wuerde ein fehlendes Playwright entweder + als Fehler gemeldet (falscher Alarm) oder stillschweigend + uebersprungen (schlimmer -- die Zahl saenke, und niemand wuesste + warum). */ +let offen = 0; +const unklar = (t) => { offen++; console.log(" -- " + t); }; const CREW = "crew.dogfather-universe.com"; const HAUS = "workspace.dogfather-universe.com"; @@ -920,9 +927,318 @@ melde("=== 13. Die Probezeit ==="); } } +/* ======================================================================= + 14. DER AUSGANG AUS DEM TRICHTER (17.09.2026) + + Filipe: „alles moegliche, wie gesagt ich will hoch profissionel + arbeiten." + + Gemessen vor dem Bau: talentNaechste kettet aufgefallen -> beobachtet + -> angesprochen -> probe -> imteam, und danach kommt nichts. Ein + Kandidat, der nicht passt, blieb auf seiner Stufe stehen -- mit + wachsender Standzeit und rotem "liegt"-Zeichen -- obwohl der Text der + Stufe woertlich verspricht: "uebernehmen ... oder sauber beenden". + + Geprueft wird hier DREIERLEI, und der dritte Punkt ist der, wegen dem + es die Spalten und nicht eine neue Stufe geworden ist: + 1. Der Satz ist Pflicht -- und die Grenze beisst wirklich. + 2. Beendete verschwinden aus der Arbeitsliste, nicht aus dem Haus. + 3. Die STUFE bleibt stehen: Man sieht, WO es geendet hat. + ======================================================================= */ +melde(""); +melde("=== 14. Der Ausgang ==="); +{ + /* Jonas steht aus Abschnitt 13 in der Probe -- genau der interessante + Fall: Wer in der Probe aussteigt, hat eine andere Geschichte als + wer nach dem ersten Hinsehen aussteigt. */ + const vorher = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi })); + const jonas = (vorher.kandidaten || []).find((k) => k.titel === "Jonas aus dem Chat"); + ok(!!jonas && jonas.stufe === "probe", + `Jonas steht in der Arbeitsliste, Stufe ${jonas?.stufe}`); + const stufeVorher = (vorher.trichter || []).find((x) => x.schluessel === "probe")?.anzahl; + + /* ---- Der Satz ist Pflicht ---- */ + { + const r = await roh(`/workspace/api/talente/${jonas.id}/beenden`, HAUS, + { cookie: kDogi }, { grund: "" }, "PUT"); + ok(r.code === 400, `ohne Satz geht es nicht (${r.code})`); + ok((json(r).fehler || "").length > 20, + "und es steht dabei, warum der Satz verlangt wird"); + } + { + /* DIE GRENZE BEISST WIRKLICH. "Passt nicht" hat neun Zeichen -- die + haeufigste Nicht-Begruendung ueberhaupt, und genau die soll + auffallen. Ohne diese Zeile wuesste man nur, dass ein LEERES Feld + abgewiesen wird, und das ist eine andere, viel schwaechere + Aussage. */ + const r = await roh(`/workspace/api/talente/${jonas.id}/beenden`, HAUS, + { cookie: kDogi }, { grund: "passt nix" }, "PUT"); + ok(r.code === 400, `neun Zeichen sind noch kein Satz (${r.code})`); + } + + /* ---- Beenden ---- */ + const GRUND = "Hat sich nach der zweiten Schicht nicht mehr gemeldet."; + { + const r = await roh(`/workspace/api/talente/${jonas.id}/beenden`, HAUS, + { cookie: kDogi }, { grund: GRUND }, "PUT"); + ok(r.code === 200 && json(r).beendet === true, `mit Satz geht es (${r.code})`); + } + { + const l = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi })); + ok(!(l.kandidaten || []).some((k) => k.id === jonas.id), + "er steht nicht mehr in der Arbeitsliste"); + const b = (l.beendete || []).find((k) => k.id === jonas.id); + ok(!!b, "aber er ist da -- unter den Beendeten"); + ok(b?.beendet?.grund === GRUND, `der Satz steht dabei (${b?.beendet?.grund})`); + ok(b?.beendet?.von === "Filipe", `und wer es war (${b?.beendet?.von})`); + /* DER EIGENTLICHE PUNKT: Die Stufe blieb stehen. Waere daraus ein + eigener Stufenwert "beendet" geworden, stuende hier "beendet" -- + und die Frage "wie weit war er gekommen?" waere fuer immer + unbeantwortbar. */ + ok(b?.stufe === "probe", + `und WO es geendet hat: ${b?.stufe_name} -- nicht bloss "beendet"`); + ok(b?.liegt === false, + "er liegt nicht mehr -- eine beendete Karte wird nicht rot"); + const nachher = (l.trichter || []).find((x) => x.schluessel === "probe")?.anzahl; + ok(nachher === stufeVorher - 1, + `der Trichter zaehlt ihn nicht mehr mit (${stufeVorher} -> ${nachher})`); + } + + /* ---- Wieder aufnehmen ---- */ + { + const r = await roh(`/workspace/api/talente/${jonas.id}/wieder`, HAUS, + { cookie: kDogi }, null, "PUT"); + ok(r.code === 200, `wieder aufnehmen geht (${r.code})`); + const l = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi })); + const k = (l.kandidaten || []).find((x) => x.id === jonas.id); + ok(!!k, "er steht wieder in der Arbeitsliste"); + ok(k?.stufe === "aufgefallen", + `und faengt vorne an (${k?.stufe}) -- ein Jahr altes Urteil gilt nicht weiter`); + /* DER SATZ VON DAMALS BLEIBT. Er ist beim zweiten Anlauf das + Wertvollste, was man hat -- und genau er wuerde beim Zuruecksetzen + am leichtesten verschwinden. */ + ok((k?.notiz || "").includes(GRUND), + "der Grund von damals steht in der Notiz"); + ok(!k?.beendet, "und die Karte ist nicht mehr als beendet gezeichnet"); + /* Die Probe faengt neu an -- alte Haken wuerden eine Probezeit + beschreiben, die vier Wochen zurueckliegt. */ + ok(!(l.beendete || []).some((x) => x.id === jonas.id), + "aus der Sammlung der Beendeten ist er heraus"); + } + { + const r = await roh(`/workspace/api/talente/${jonas.id}/wieder`, HAUS, + { cookie: kDogi }, null, "PUT"); + ok(r.code === 409, `wer laeuft, kann nicht wieder aufgenommen werden (${r.code})`); + } + + /* ---- Wer im Team ist, wird nicht hier beendet ---- + + Gefunden am Bildschirmfoto: 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 -- und das ist schlimmer als gar kein + Ausgang, weil man glaubt, gehandelt zu haben. */ + { + const l = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi })); + const drin = (l.kandidaten || []).find((k) => k.stufe === "imteam"); + ok(!!drin, `es gibt einen im Team zum Gegenprobieren (${drin?.titel})`); + const r = await roh(`/workspace/api/talente/${drin.id}/beenden`, HAUS, + { cookie: kDogi }, { grund: GRUND }, "PUT"); + ok(r.code === 409, `wer im Team ist, wird nicht hier beendet (${r.code})`); + ok((json(r).fehler || "").includes("Zug"), + `und es steht dabei, wo stattdessen (${(json(r).fehler || "").slice(0, 40)}...)`); + const l2 = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi })); + ok((l2.kandidaten || []).some((k) => k.id === drin.id), + "er steht unveraendert in der Liste -- nichts halb passiert"); + } + + /* ---- Und die Gegenproben ---- */ + { + const r = await roh("/workspace/api/talente/999999/beenden", HAUS, + { cookie: kDogi }, { grund: GRUND }, "PUT"); + ok(r.code === 404, `eine erfundene Nummer wird abgewiesen (${r.code})`); + } + { + const r = await roh(`/workspace/api/talente/${jonas.id}/beenden`, CREW, + { cookie: kModi }, { grund: GRUND }, "PUT"); + ok(r.code === 404, + `ein Modi kann niemanden beenden -- und erfaehrt nicht, dass es das gibt (${r.code})`); + } + { + const r = await roh(`/workspace/api/talente/${jonas.id}/wieder`, CREW, + { cookie: kModi }, null, "PUT"); + ok(r.code === 404, `dasselbe beim Wiederaufnehmen (${r.code})`); + } +} + +/* ======================================================================= + 15. DIE TALENTE-SEITE IM ECHTEN BROWSER (17.09.2026) + + Der erste Browserblick auf diese Seite ueberhaupt. Er ist faellig: + + Heute Mittag hat ein Bildschirmfoto zwei Dinge gefunden, die keine + Zahl gefunden haette -- ein Wasserzeichen, das durch vertraulichen + Text hindurchschien (color-mix mit DURCHSICHTIG statt mit Weiss), + und eine Klassenkollision, bei der .t-schritt fuer zwei verschiedene + Dinge stand. Beides war gruen in jeder Prueflinie und falsch auf dem + Schirm. + + GEKLICKT WIRD WIRKLICH, nicht ueber die Schnittstelle. Der ganze Weg + -- aufklappen, tippen, absenden -- ist der, der kaputtgehen kann; was + darunter liegt, ist in Abschnitt 14 schon bewiesen. + ======================================================================= */ +melde(""); +melde("=== 15. Im echten Browser ==="); +{ + let chromium; + try { + ({ chromium } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs")); + } catch { chromium = null; } + + if (!chromium) { + unklar("kein Playwright gefunden -- die Oberflaeche wurde nicht angesehen"); + } else { + const browser = await chromium.launch(); + try { + /* 390 x 844 -- ein Handy. Filipe arbeitet am Handy, und die + Kopfleiste ist genau dort im September zerbrochen. */ + const sicht = await browser.newContext({ viewport: { width: 390, height: 844 } }); + await sicht.addCookies(kDogi.split("; ").map((c) => { + const [name, ...rest] = c.split("="); + return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" }; + })); + const seite = await sicht.newPage(); + const konsole = []; + seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); }); + await seite.goto(`http://127.0.0.1:${PORT}/workspace/talente.html`, + { waitUntil: "domcontentloaded" }); + await seite.waitForTimeout(1200); + + const karten = await seite.locator(".t-karte").count(); + ok(karten > 0, `die Seite zeigt ${karten} Karten`); + /* ABGELEITET, NICHT ABGESCHRIEBEN: An jeder Karte IM TRICHTER + steht ein Ausgang -- an denen im Team ausdruecklich nicht. + Feste Zahlen waeren hier die Rechnung von gestern: Kommt ein + Kandidat dazu, muesste man sie nachziehen. */ + const imTeam = await seite.locator('.t-karte[data-stufe="imteam"]').count(); + const ausgaenge = await seite.locator(".t-ausgang__auf").count(); + ok(ausgaenge === karten - imTeam, + `an jeder Karte im Trichter ein Ausgang (${ausgaenge} von ${karten} ` + + `- ${imTeam} im Team)`); + ok(imTeam > 0 && await seite.locator(".t-ausgang__anderswo").count() === imTeam, + `und an denen im Team steht stattdessen der Weg zum Zugang (${imTeam})`); + ok(await seite.locator('.t-karte[data-stufe="imteam"] .t-ausgang__auf').count() === 0, + "dort gibt es das Feld nicht einmal zum Aufklappen"); + + /* ---- Der Weg: aufklappen, zu kurz, dann richtig ---- + Gezielt eine Karte AUS DEM TRICHTER, nicht einfach die erste: + Welche oben steht, entscheidet die Sortierung, und die haengt + an Standzeiten. */ + const erste = seite.locator('.t-karte:not([data-stufe="imteam"])').first(); + const titel = (await erste.locator(".t-karte__titel").textContent())?.trim(); + ok(await erste.locator(".t-ausgang__form").isHidden(), + "das Feld ist zugeklappt -- der Ausgang draengt sich nicht auf"); + await erste.locator(".t-ausgang__auf").click(); + await seite.waitForTimeout(200); + ok(await erste.locator(".t-ausgang__form").isVisible(), + "nach einem Klick steht es da"); + + { + /* ZU KURZ -- und zwar im Browser, nicht nur am Server. Was der + Browser prueft, ist Hoeflichkeit; faellt sie aus, sieht man + statt eines Satzes eine rohe Fehlermeldung. */ + await erste.locator(".t-ausgang__feld").fill("passt nix"); + await erste.locator(".t-ausgang__knopf").click(); + await seite.waitForTimeout(400); + const meldung = (await seite.locator("#fehler").textContent())?.trim() || ""; + ok(meldung.length > 10, `ein zu kurzer Satz wird abgewiesen ("${meldung}")`); + ok(await seite.locator(".t-karte").count() === karten, + "und die Karte bleibt stehen"); + } + + const GRUND_B = "Kam dreimal nicht zur Schicht, ohne Bescheid zu sagen."; + { + await erste.locator(".t-ausgang__feld").fill(GRUND_B); + await erste.locator(".t-ausgang__knopf").click(); + await seite.waitForTimeout(900); + const uebrig = await seite.locator(".t-karte").count(); + ok(uebrig === karten - 1, + `mit Satz verschwindet sie aus der Arbeitsliste (${karten} -> ${uebrig})`); + ok(await seite.locator(".t-beendet__auf").count() === 1, + "und unten steht die Sammlung der Beendeten"); + const wort = (await seite.locator(".t-beendet__auf").textContent())?.trim() || ""; + ok(/[0-9]/.test(wort), `zugeklappt mit Zahl ("${wort}")`); + } + + { + await seite.locator(".t-beendet__auf").click(); + await seite.waitForTimeout(300); + const karte = seite.locator(".t-beendet__karte").first(); + ok(await karte.isVisible(), "aufgeklappt steht die Karte da"); + ok((await karte.locator(".t-beendet__titel").textContent())?.trim() === titel, + "mit dem richtigen Namen"); + ok((await karte.locator(".t-beendet__grund").textContent())?.includes(GRUND_B), + "und dem Satz von damals"); + const stufe = (await karte.locator(".t-beendet__stufe").textContent())?.trim() || ""; + ok(stufe.length > 3, `und WO es geendet hat (${stufe})`); + /* GEGENPROBE ZUR SICHTBARKEIT: An einer beendeten Karte steht + KEIN zweiter Ausgang -- sonst koennte man beenden, was schon + beendet ist, und der Grund wuerde ueberschrieben. */ + ok(await karte.locator(".t-ausgang__auf").count() === 0, + "an einer beendeten Karte steht kein zweiter Ausgang"); + ok(await karte.locator(".t-beendet__wieder").count() === 1, + "sondern der Weg zurueck"); + } + + { + /* DIE KOPFZEILE BEI 390 px. margin-inline-start: auto schiebt + das Datum nach rechts -- genau die Bauart, die im September + in der Kopfleiste zwei Knoepfe uebereinandergelegt hat. Eine + Messung statt einer Zahl im CSS. */ + const k = seite.locator(".t-beendet__karte").first(); + const a = await k.locator(".t-beendet__titel").boundingBox(); + const b = await k.locator(".t-beendet__zeit").boundingBox(); + const ueber = a && b + && a.x < b.x + b.width && b.x < a.x + a.width + && a.y < b.y + b.height && b.y < a.y + a.height; + ok(!!a && !!b && !ueber, + `Name und Datum liegen nicht uebereinander (${Math.round(a?.x ?? -1)}+` + + `${Math.round(a?.width ?? 0)} | ${Math.round(b?.x ?? -1)})`); + /* Und der Weg zurueck muss mit einem Finger zu treffen sein. */ + const w = await k.locator(".t-beendet__wieder").boundingBox(); + ok(!!w && w.height >= 28 && w.width >= 100, + `der Weg zurueck ist treffbar (${Math.round(w?.width ?? 0)} x ${Math.round(w?.height ?? 0)})`); + } + + { + await seite.locator(".t-beendet__wieder").first().click(); + await seite.waitForTimeout(900); + const zurueck = await seite.locator(".t-karte").count(); + ok(zurueck === karten, `wieder aufnehmen bringt sie zurueck (${zurueck})`); + ok(await seite.locator(".t-beendet__auf").count() === 0, + "und die Sammlung verschwindet, wenn nichts mehr drin ist"); + } + + ok(konsole.length === 0, + konsole.length ? `Konsolenfehler: ${konsole[0]}` : "kein einziger Konsolenfehler"); + + /* Das Bildschirmfoto -- der Teil, den keine Zahl ersetzt. */ + await seite.locator('.t-karte:not([data-stufe="imteam"]) .t-ausgang__auf') + .first().click().catch(() => {}); + await seite.waitForTimeout(300); + await seite.screenshot({ path: "server/pruef-nachwuchs-ausgang.png", fullPage: true }); + melde(" -- Bildschirmfoto: server/pruef-nachwuchs-ausgang.png"); + } finally { + await browser.close(); + } + } +} + /* ------------------------------------------------------------------- */ melde(""); -melde(`${geprueft} Pruefungen, ${fehler} Fehler`); +melde(`${geprueft} Pruefungen, ${fehler} Fehler` + + (offen ? `, ${offen} nicht nachsehbar` : "")); melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG"); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ } process.exit(fehler ? 1 : 0); diff --git a/server/workspace-entwicklung.js b/server/workspace-entwicklung.js index 684244a6..bbaed146 100644 --- a/server/workspace-entwicklung.js +++ b/server/workspace-entwicklung.js @@ -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 diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 6e1117e4..a8c8d1eb 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -22,25 +22,25 @@ }, "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609171614", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609171838", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609171614", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609171838", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609171614", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609171838", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609171614", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609171838", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/entwicklung.css b/workspace/assets/css/entwicklung.css index 8e4ae84c..ccb21cb4 100644 --- a/workspace/assets/css/entwicklung.css +++ b/workspace/assets/css/entwicklung.css @@ -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; } + diff --git a/workspace/assets/js/talente.js b/workspace/assets/js/talente.js index b163ce44..1cb0ac29 100644 --- a/workspace/assets/js/talente.js +++ b/workspace/assets/js/talente.js @@ -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() { diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index c85b0b55..9a939535 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -12,19 +12,19 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + - + @@ -291,10 +291,10 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index f797c94d..47ac4bcc 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -168,10 +168,10 @@ - - - - - + + + + + diff --git a/workspace/befinden.html b/workspace/befinden.html index 4d7e5e66..da5f5351 100644 --- a/workspace/befinden.html +++ b/workspace/befinden.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -105,9 +105,9 @@ - - - - + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index 788ba515..da12fe24 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -12,24 +12,24 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + - - + + - + @@ -237,14 +237,14 @@ - - - - - - - - - + + + + + + + + + diff --git a/workspace/bewerben.html b/workspace/bewerben.html index a6c34ed0..1e5f8af3 100644 --- a/workspace/bewerben.html +++ b/workspace/bewerben.html @@ -7,17 +7,17 @@ - - - - - - - - - + + + + + + + + + - + @@ -107,10 +107,10 @@ - - - - - + + + + + diff --git a/workspace/bewerbungen.html b/workspace/bewerbungen.html index 5458f9d0..b59a1831 100644 --- a/workspace/bewerbungen.html +++ b/workspace/bewerbungen.html @@ -7,16 +7,16 @@ - - - - - - - - + + + + + + + + - + @@ -80,10 +80,10 @@ - - - - - + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index c61c6555..39322709 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -89,11 +89,11 @@ - - - - - - + + + + + + diff --git a/workspace/chat.html b/workspace/chat.html index ee5859a4..e8ba1bce 100644 --- a/workspace/chat.html +++ b/workspace/chat.html @@ -12,26 +12,26 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + - + @@ -341,10 +341,10 @@ - - - - - + + + + + diff --git a/workspace/content.html b/workspace/content.html index 5f88637c..57ec962d 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -179,12 +179,12 @@ - - - - - - - + + + + + + + diff --git a/workspace/crew-index.html b/workspace/crew-index.html index cdc5861d..3ba1527d 100644 --- a/workspace/crew-index.html +++ b/workspace/crew-index.html @@ -36,15 +36,15 @@ - - - + + + - + @@ -339,6 +339,6 @@ - + diff --git a/workspace/crew.webmanifest b/workspace/crew.webmanifest index ad76ffef..9aa7db06 100644 --- a/workspace/crew.webmanifest +++ b/workspace/crew.webmanifest @@ -22,25 +22,25 @@ }, "icons": [ { - "src": "/assets/img/app-symbole/crew-192.png?v=202609171614", + "src": "/assets/img/app-symbole/crew-192.png?v=202609171838", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-512.png?v=202609171614", + "src": "/assets/img/app-symbole/crew-512.png?v=202609171838", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609171614", + "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609171838", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609171614", + "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609171838", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/dateien.html b/workspace/dateien.html index ad50f7ae..676f8889 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -108,10 +108,10 @@ - - - - - + + + + + diff --git a/workspace/entwicklung.html b/workspace/entwicklung.html index ca14e58d..433145ed 100644 --- a/workspace/entwicklung.html +++ b/workspace/entwicklung.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -102,9 +102,9 @@ - - - - + + + + diff --git a/workspace/index.html b/workspace/index.html index dc55d869..7c5088dc 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -30,9 +30,9 @@ - - - + + + @@ -401,6 +401,6 @@ - + diff --git a/workspace/kalender.html b/workspace/kalender.html index e382913b..4bab1650 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -377,11 +377,11 @@ - - - - - - + + + + + + diff --git a/workspace/leistung.html b/workspace/leistung.html index 12c45f71..6d1b9ad7 100644 --- a/workspace/leistung.html +++ b/workspace/leistung.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -444,10 +444,10 @@ - - - - - + + + + + diff --git a/workspace/personen.html b/workspace/personen.html index 3d2745c3..a754e345 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -423,10 +423,10 @@ - - - - - + + + + + diff --git a/workspace/profil.html b/workspace/profil.html index 10f29216..04159e11 100644 --- a/workspace/profil.html +++ b/workspace/profil.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -214,11 +214,11 @@ - - - - - - + + + + + + diff --git a/workspace/rechte.html b/workspace/rechte.html index 0bd32402..764abfb6 100644 --- a/workspace/rechte.html +++ b/workspace/rechte.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -134,9 +134,9 @@ Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen, die seit dem 07.09. jede Seite danach absucht. --> - - - - + + + + diff --git a/workspace/report.html b/workspace/report.html index 291ca2b3..8b796c60 100644 --- a/workspace/report.html +++ b/workspace/report.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -106,11 +106,11 @@ - - - - - - + + + + + + diff --git a/workspace/scouting.html b/workspace/scouting.html index 0197955d..4649e36d 100644 --- a/workspace/scouting.html +++ b/workspace/scouting.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -173,10 +173,10 @@ - - - - - + + + + + diff --git a/workspace/start.html b/workspace/start.html index 0c1726ca..3c7c055d 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -12,21 +12,21 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - + + - + @@ -674,10 +674,10 @@

- - - - - + + + + + diff --git a/workspace/startcheck.html b/workspace/startcheck.html index a848e7a0..0b932d81 100644 --- a/workspace/startcheck.html +++ b/workspace/startcheck.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -85,11 +85,11 @@ - - - - - - + + + + + + diff --git a/workspace/steckbrief.html b/workspace/steckbrief.html index bac542da..8dfadeb7 100644 --- a/workspace/steckbrief.html +++ b/workspace/steckbrief.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -146,11 +146,11 @@ - - - - - - + + + + + + diff --git a/workspace/talente.html b/workspace/talente.html index f21ae29f..194a88ba 100644 --- a/workspace/talente.html +++ b/workspace/talente.html @@ -7,18 +7,18 @@ - - - - - - + + + + + + - - - + + + @@ -111,9 +111,9 @@ - - - - + + + + diff --git a/workspace/team.html b/workspace/team.html index e77e8f4a..56b116aa 100644 --- a/workspace/team.html +++ b/workspace/team.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + @@ -110,10 +110,10 @@ - - - - - + + + + + diff --git a/workspace/teamlage.html b/workspace/teamlage.html index 8eaad048..5ec81a61 100644 --- a/workspace/teamlage.html +++ b/workspace/teamlage.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + - - - - + + + + diff --git a/workspace/teilen.html b/workspace/teilen.html index c111afe2..3e1168c9 100644 --- a/workspace/teilen.html +++ b/workspace/teilen.html @@ -7,13 +7,13 @@ - - - - - - - + + + + + + + @@ -112,9 +112,9 @@ - - - - + + + + diff --git a/workspace/treff-moderation.html b/workspace/treff-moderation.html index a1487f2c..c005bacf 100644 --- a/workspace/treff-moderation.html +++ b/workspace/treff-moderation.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -126,9 +126,9 @@ - - - - + + + + diff --git a/workspace/treff-regeln.html b/workspace/treff-regeln.html index fbd638b4..c9be0f94 100644 --- a/workspace/treff-regeln.html +++ b/workspace/treff-regeln.html @@ -7,18 +7,18 @@ - - - - - - - + + + + + + + - + @@ -211,10 +211,10 @@ Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen, die seit dem 07.09. jede Seite danach absucht. --> - - - - - + + + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index c03f8932..056cd2b5 100644 --- a/workspace/uebersicht.html +++ b/workspace/uebersicht.html @@ -12,22 +12,22 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + - + @@ -149,11 +149,11 @@ - + - - - - + + + + diff --git a/workspace/wissen.html b/workspace/wissen.html index 58093d00..d22450a7 100644 --- a/workspace/wissen.html +++ b/workspace/wissen.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -190,10 +190,10 @@ - - - - - + + + + +