diff --git a/server/pruef-bewerbung-aufgaben.mjs b/server/pruef-bewerbung-aufgaben.mjs index 2d867fef..6ec29b99 100644 --- a/server/pruef-bewerbung-aufgaben.mjs +++ b/server/pruef-bewerbung-aufgaben.mjs @@ -696,6 +696,175 @@ melde("=== 6. Am echten Bildschirm ==="); ok(!!b && b.knoepfe.some((t) => /fange an/i.test(t)), `sondern „Ich fange an" (${b?.knoepfe.join(" | ")})`); } finally { await m4.ctx.close(); } + + /* =============================================================== + UND JETZT EINE ABSAGE -- VanVans vierte Meldung (30.09.2026) + + „dann bekommt der Modi keine Benachrichtigung darüber, dass + die Aufgabe abgelehnt wurde und sieht somit auch eine + eventuelle Begründung nicht. + Außerdem haben auch wir nirgendwo eine Übersicht, bei wem wir + welche Aufgaben schon abgelehnt haben." + + Bei einer ZUSAGE entsteht eine Aufgabe, und an ihr steht die + Antwort. Bei einer ABSAGE entsteht nichts -- die Begruendung + wird in `vorlagen_bewerbungen.entscheid_text` geschrieben und + danach von niemandem mehr ausgeliefert: + `vorlagenBewerbungenFuer` fragt `WHERE zustand = 'beworben'`. + + Der Satz wird also verlangt, geschrieben und weggesperrt. + =============================================================== */ + const v2 = katalogAufgaben.at(-2); + const vb2 = await roh("/workspace/api/vorlagen/bewerben", { + methode: "POST", keks: modi.keks, + body: { vorlage: v2.schluessel, text: "Die würde ich auch gern machen." } }); + ok(vb2.code === 201 || vb2.code === 200, + `Frida bewirbt sich auf „${v2.titel}" (${vb2.code})`); + + const offen3 = await roh("/workspace/api/vorlagen/bewerbungen", { keks: hand.keks }); + const bw2 = (offen3.json?.bewerbungen || []).find((x) => x.vorlage === v2.schluessel); + const GRUND = "Diese Woche nicht — du hast schon drei offene."; + const vab = await roh("/workspace/api/vorlagen/bewerbung/ablehnen", { + methode: "POST", keks: hand.keks, body: { id: bw2?.id, kommentar: GRUND } }); + ok(vab.code === 200, `die rechte Hand lehnt ab, mit Begruendung (${vab.code})`); + + /* ---- 1. SIEHT FRIDA DIE ANTWORT? ------------------------- */ + const beiFrida = await roh("/workspace/api/vorlagen", { keks: modi.keks }); + /* `antworten` UND NICHT `bewerbungen`. Das alte Feld heisst im + Browser „wartet auf Antwort" -- beantwortete Zeilen darin + haetten bei einer Absage genau das angezeigt. */ + const ihre = beiFrida.json?.antworten || []; + const abgelehnt = ihre.find((x) => x.vorlage === v2.schluessel); + ok(!!abgelehnt, + `Frida sieht ihre beantwortete Bewerbung (${ihre.length} insgesamt)`); + ok(abgelehnt?.zustand === "abgelehnt", + `und zwar als abgelehnt (${abgelehnt?.zustand})`); + ok(abgelehnt?.entscheid_text === GRUND, + `mit der Begruendung, die geschrieben wurde (${JSON.stringify(abgelehnt?.entscheid_text)})`); + + /* ---- 2. SIEHT DIE LEITUNG, WAS SIE ABGELEHNT HAT? -------- */ + const beiHand = await roh("/workspace/api/vorlagen", { keks: hand.keks }); + const absagen = beiHand.json?.absagen; + const verlauf = absagen?.liste || []; + ok(verlauf.length >= 1, + `die Leitung sieht die Absagen der letzten ${absagen?.seit_tagen} Tage ` + + `(${verlauf.length})`); + const eintrag = verlauf.find((x) => x.kennung === v2.schluessel); + ok(!!eintrag && eintrag.person_name === "Frida", + `mit Namen und Aufgabe (${eintrag?.person_name} / ${eintrag?.titel})`); + ok(eintrag?.entscheid_text === GRUND, + `und der eigenen Begruendung (${JSON.stringify(eintrag?.entscheid_text)})`); + + /* VANVANS EIGENTLICHE FRAGE: „Falls sich jemand immer wieder auf + Aufgaben bewirbt die immer wieder abgelehnt werden, wäre das + sinnvoll." Eine Liste beantwortet das nicht, eine Zahl je + Mensch schon. */ + const frida = (absagen?.je_mensch || []).find((x) => x.name === "Frida"); + ok(!!frida && frida.anzahl >= 1, + `und wie oft es je Mensch war (Frida: ${frida?.anzahl})`); + + /* BEIDE WEGE STEHEN DRIN. Weiter oben hat DogFather zwei + Bewerbungen auf AUFGABEN abgelehnt. Eine Uebersicht, die nur + die Vorlagen zeigt, waere eine Auskunft, die stimmt und + taeuscht. */ + ok(verlauf.some((x) => x.quelle === "aufgabe") + && verlauf.some((x) => x.quelle === "vorlage"), + `aus beiden Wegen (${[...new Set(verlauf.map((x) => x.quelle))].join(", ")})`); + + /* GEGENPROBE: Frida selbst bekommt die Uebersicht NICHT -- sie + entscheidet nicht, und eine Liste der Absagen an andere geht + sie nichts an. */ + ok(beiFrida.json?.absagen === null, + `ein Modi bekommt die Uebersicht nicht (${JSON.stringify(beiFrida.json?.absagen)})`); + + /* ---- UND AM ECHTEN BILDSCHIRM ----------------------------- + „Der Server liefert es" waere ein gruener Haken ueber etwas, + das niemand sieht. Das Vorlagenbrett steht auf der + Aufgabenseite -- also aufmachen und nachlesen. */ + const liesBrett = (seite) => seite.evaluate(() => { + const a = document.querySelector(".vorlagenbrett__antwort"); + const u = document.querySelector(".vorlagenbrett__absagen"); + return { + antwort: a ? (a.innerText || "").replace(/\s+/g, " ").trim().slice(0, 220) : null, + zeilen: a ? a.querySelectorAll(".vorlagenbrett__antwort-zeile").length : 0, + absagen: u ? (u.innerText || "").replace(/\s+/g, " ").trim().slice(0, 220) : null, + zugeklappt: u ? !u.open : null, + leute: u ? [...u.querySelectorAll(".vorlagenbrett__absagen-mensch")] + .map((x) => x.textContent.trim()) : [], + }; + }); + + /* AUF „EURE AUFGABEN" UND NICHT AUF DEM AUFGABENBRETT. Dort + bewirbt man sich, dort steht der Vorlagenkatalog + (`zweig: 'team'` in entwicklung.js). Das Aufgabenbrett zeigt + den anderen Zweig (`creator`, den Aufbauplan) -- beim ersten + Anlauf habe ich dort gemessen und nichts gefunden. Ein + Messfehler, der wie ein Befund aussieht. */ + const m5 = await seiteAls("modi", "CODE-MODI-0001", 390); + try { + await m5.s.goto(`${SEITE}/workspace/entwicklung.html`, { waitUntil: "networkidle" }); + await m5.s.waitForTimeout(1800); + const w = await liesBrett(m5.s); + ok(w.zeilen >= 1, `Frida sieht die Antwort auf ihrem Brett (${w.zeilen} Zeilen)`); + /* OHNE RUECKSICHT AUF GROSS UND KLEIN. `innerText` liefert den + Text SO, WIE ER DASTEHT -- und das Schild traegt + `text-transform: uppercase`. Die Zeile suchte „Diesmal + nicht" und fand „DIESMAL NICHT": rot, obwohl alles stimmte. + `textContent` haette den Rohtext gegeben, aber `innerText` + ist hier richtig: Gemessen wird, was ein Mensch liest. */ + ok(/diesmal nicht/i.test(w.antwort || ""), + `und dass es diesmal nichts wurde („${(w.antwort || "").slice(0, 60)}…")`); + ok(!!w.antwort && w.antwort.includes("schon drei offene"), + "mitsamt der Begruendung"); + /* GEGENPROBE: Die Uebersicht der Absagen sieht sie NICHT. */ + ok(w.absagen === null, + `und die Uebersicht der Absagen nicht (${JSON.stringify(w.absagen)})`); + /* UND EIN BILD DAVON -- am Handy, denn dort liest ein Modi. */ + const kasten2 = await m5.s.$(".vorlagenbrett__antwort"); + if (kasten2) { + await kasten2.screenshot({ path: "server/pruef-bewerbung-antwort.png" }); + console.log(" -- Bild: server/pruef-bewerbung-antwort.png"); + } + } finally { await m5.ctx.close(); } + + const h2 = await seiteAls("hand", "CODE-HAND-0001", 1280); + try { + await h2.s.goto(`${SEITE}/workspace/entwicklung.html`, { waitUntil: "networkidle" }); + await h2.s.waitForTimeout(1800); + /* DAS BRETT AUFKLAPPEN. Die Uebersicht ist ein + Nachschlagewerk und liegt im Koerper des Vorlagenbretts -- + der wird zugeklappt gar nicht erst gebaut. Die ANTWORT auf + die eigene Bewerbung steht dagegen davor: Sie ist eine + Nachricht, und eine Nachricht, die man aufklappen muss, + ist keine. */ + await h2.s.evaluate(() => { + const k = document.querySelector("#vorlagenbrett .vorlagenbrett__kopf"); + if (k && k.getAttribute("aria-expanded") !== "true") k.click(); + }); + await h2.s.waitForTimeout(1200); + const w = await liesBrett(h2.s); + ok(!!w.absagen, `die rechte Hand sieht die Uebersicht („${(w.absagen || "").slice(0, 50)}…")`); + /* ZUGEKLAPPT. Eine Liste von Absagen, die immer offen steht, + ist eine Sammlung von Nein-Sagen ueber Menschen, mit denen + man morgen wieder arbeitet. */ + ok(w.zugeklappt === true, `und zwar zugeklappt (${w.zugeklappt})`); + ok(w.leute.some((t) => /Frida/.test(t)), + `mit der Zahl je Mensch (${w.leute.join(" | ")})`); + + /* EIN BILD VON DEM, WAS NEU IST. Zahlen sagen, dass etwas da + ist; ob es auch auszuhalten ist, sieht man nur. Aufgeklappt + -- zugeklappt waere ein Bild von einer Zeile. */ + await h2.s.evaluate(() => { + const u = document.querySelector(".vorlagenbrett__absagen"); + if (u) u.open = true; + }); + await h2.s.waitForTimeout(300); + const kasten = await h2.s.$("#vorlagenbrett"); + if (kasten) { + await kasten.screenshot({ path: "server/pruef-bewerbung-absagen.png" }); + console.log(" -- Bild: server/pruef-bewerbung-absagen.png"); + } + } finally { await h2.ctx.close(); } } /* ================================================================= diff --git a/server/workspace-vorlagen.js b/server/workspace-vorlagen.js index 064c70bf..273f9836 100644 --- a/server/workspace-vorlagen.js +++ b/server/workspace-vorlagen.js @@ -48,6 +48,11 @@ import { MODI_STUFEN, entscheidetUeberAufgaben, darfAufgabenVerteilen, + /* Fuer die Absagen-Uebersicht (30.09.2026): Wer den Menschen gar + nicht sehen darf, sieht auch nicht, dass ihm abgesagt wurde. + Ohne diese Zeile erfuehre die Agentur ueber eine Uebersicht, + dass es Team Dogi gibt. */ + darfSchreibenMit, KATALOG_EMPFAENGER, KATALOG_EMPFAENGER_SQL, } from "./workspace.js"; @@ -1468,6 +1473,18 @@ vorlagenRouter.get("/workspace/api/vorlagen", (req, res) => { bekommt jeder, der die Seite oeffnet. */ bewerbungen: kategorienFuer(req.person).length ? vorlagenBewerbungenFuer(req.person) : null, + /* ==== UND WAS SCHON BEANTWORTET IST (30.09.2026) ============= + + ZWEI NEUE FELDER UND NICHT `bewerbungen` ERWEITERT. Dort heisst + eine Zeile im Browser „wartet auf Antwort" -- daraus wird ein + Schild und ein Zurueckziehen-Knopf. Beantwortete Zeilen + hineinzumischen hiesse, bei einer Absage „wartet auf Antwort" + zu zeigen. Ein Feld, dessen Bedeutung sich aendert, bricht + seine Leser lautlos. */ + antworten: kategorienFuer(req.person).length + ? vorlagenAntwortenFuer(req.person) : null, + absagen: kategorienFuer(req.person).length + ? absagenUebersicht(req.person) : null, darf_entscheiden: entscheidetUeberAufgaben(req.person), darf_verteilen: darfAufgabenVerteilen(req.person), }); @@ -1562,6 +1579,146 @@ function katalogAufgabeAnlegen(v, zielId, vonId, zustand = "offen") { drueben bei den Aufgaben -- ein Satz, kein Aufsatz. */ const BEWERBUNG_TEXT_MAX = 600; +/* ===================================================================== + WAS NACH EINER ANTWORT UEBRIG BLEIBT (30.09.2026) + + VanVan im Support: „dann bekommt der Modi keine Benachrichtigung + darüber, dass die Aufgabe abgelehnt wurde und sieht somit auch eine + eventuelle Begründung nicht. Außerdem haben auch wir nirgendwo eine + Übersicht, bei wem wir welche Aufgaben schon abgelehnt haben." + + Gemessen, bevor gebaut wurde: Frida sah nach der Absage NULL + beantwortete Bewerbungen, die Leitung NULL Absagen. + + Der Grund steht eine Funktion weiter unten: `WHERE zustand = + 'beworben'`. Sobald jemand antwortet, faellt die Zeile aus jeder + Ansicht heraus -- mitsamt der Begruendung, die man ausdruecklich + verlangt hat. + + Bei einer ZUSAGE fiel das nicht auf: Dort entsteht eine Aufgabe, + und an ihrer Karte steht die Antwort. Bei einer ABSAGE entsteht + nichts. + ===================================================================== */ + +/** Wie lange eine beantwortete Bewerbung beim Bewerber stehen bleibt. + * Zwei Wochen: lang genug, dass man sie sieht, kurz genug, dass das + * Brett nicht zur Ablage wird. */ +const ANTWORT_TAGE = 14; + +/** Wie weit die Absagen-Uebersicht zurueckreicht. + * Drei Monate -- VanVans Frage ist „bewirbt sich jemand immer wieder + * auf etwas, das wir immer wieder ablehnen?", und die beantwortet + * eine Woche nicht. */ +const ABSAGEN_TAGE = 90; + +const vorTagen = (n) => new Date(Date.now() - n * 86400_000).toISOString(); + +/** Meine eigenen beantworteten Bewerbungen -- mit der Begruendung. + * + * NUR DIE EIGENEN, ausdruecklich. Wer entscheidet, sieht die Absagen + * in der Uebersicht darunter; hier geht es um die Antwort, auf die + * man selbst gewartet hat. + */ +function vorlagenAntwortenFuer(person) { + try { + return db().prepare(` + SELECT b.id, b.vorlage, b.zustand, b.text, b.entscheid_text, + b.entschieden_am, b.aufgabe_id, e.name AS von_name + FROM vorlagen_bewerbungen b + LEFT JOIN personen e ON e.id = b.entschieden_von + WHERE b.person_id = ? + AND b.zustand IN ('angenommen','abgelehnt') + AND COALESCE(b.entschieden_am, '') >= ? + ORDER BY b.entschieden_am DESC`) + .all(person.id, vorTagen(ANTWORT_TAGE)) + .map((z) => ({ ...z, titel: vorlageZuSchluessel(z.vorlage)?.titel || z.vorlage })); + } catch (fehler) { + console.error("[vorlagen] Antworten:", fehler?.message); + return []; + } +} + +/** Was die Leitung schon abgelehnt hat -- aus BEIDEN Wegen. + * + * AUS BEIDEN, und das ist der Punkt. Man kann sich auf eine Vorlage + * bewerben und auf eine Aufgabe, die es schon gibt. Eine Uebersicht, + * die nur die Haelfte zeigt, ist schlimmer als keine: „Frida wurde + * nie abgelehnt" waere dann eine Auskunft, die stimmt und taeuscht. + * + * `entschieden_von IS NOT NULL` TRENNT ZWEI DINGE, die beide + * `abgelehnt` heissen: + * · die Leitung hat eine Bewerbung abgelehnt <- das hier + * · die Person hat eine Zuteilung abgelehnt <- ihre Entscheidung + * · „Von jemand anderem uebernommen" <- gar keine Absage + * Die letzten beiden tragen keinen Entscheider und fallen heraus. + * + * UND DIE HAUSTRENNUNG GILT: `darfSchreibenMit` -- wer den Menschen + * gar nicht sehen darf, sieht auch nicht, dass ihm abgesagt wurde. + */ +function absagenUebersicht(person) { + if (!entscheidetUeberAufgaben(person)) return null; + try { + const seit = vorTagen(ABSAGEN_TAGE); + const zeilen = db().prepare(` + SELECT 'vorlage' AS quelle, b.person_id, p.name AS person_name, + b.vorlage AS kennung, NULL AS aufgabe_id, + b.text AS wort, b.entscheid_text, b.entschieden_am, + e.name AS von_name + FROM vorlagen_bewerbungen b + LEFT JOIN personen p ON p.id = b.person_id + LEFT JOIN personen e ON e.id = b.entschieden_von + WHERE b.zustand = 'abgelehnt' + AND b.entschieden_von IS NOT NULL + AND COALESCE(b.entschieden_am, '') >= ? + UNION ALL + SELECT 'aufgabe', z.person_id, p.name, + a.titel, a.id, + z.grund, z.entscheid_text, z.entschieden_am, + e.name + FROM aufgaben_zuteilung z + JOIN aufgaben a ON a.id = z.aufgabe_id + LEFT JOIN personen p ON p.id = z.person_id + LEFT JOIN personen e ON e.id = z.entschieden_von + WHERE z.zustand = 'abgelehnt' + AND z.entschieden_von IS NOT NULL + AND COALESCE(z.entschieden_am, '') >= ? + ORDER BY entschieden_am DESC`).all(seit, seit); + + const sichtbar = zeilen + .filter((z) => z.person_id && darfSchreibenMit(person, z.person_id)) + .map((z) => ({ + ...z, + titel: z.quelle === "vorlage" + ? (vorlageZuSchluessel(z.kennung)?.titel || z.kennung) + : z.kennung, + })); + + /* WIE OFT JE MENSCH -- das ist VanVans eigentliche Frage. Eine + Liste von zwanzig Zeilen beantwortet „bewirbt sich jemand immer + wieder auf etwas, das wir immer wieder ablehnen?" nicht; eine + Zahl je Person schon. + + ABGELEITET AUS DERSELBEN LISTE und nicht zweitgezaehlt: Zwei + Rechnungen fuer dieselbe Frage laufen auseinander. */ + const jeMensch = new Map(); + for (const z of sichtbar) { + const t = jeMensch.get(z.person_id) + || { person_id: z.person_id, name: z.person_name, anzahl: 0, zuletzt: null }; + t.anzahl += 1; + if (!t.zuletzt || String(z.entschieden_am) > t.zuletzt) t.zuletzt = z.entschieden_am; + jeMensch.set(z.person_id, t); + } + return { + seit_tagen: ABSAGEN_TAGE, + liste: sichtbar, + je_mensch: [...jeMensch.values()].sort((a, b) => b.anzahl - a.anzahl), + }; + } catch (fehler) { + console.error("[vorlagen] Absagen:", fehler?.message); + return null; + } +} + /** Die offenen Bewerbungen, so wie sie die Seite braucht. * * WER WAS SIEHT, ENTSCHEIDET HIER DER SERVER: Wer entscheidet, sieht diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index 60402299..7224f03c 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,8 +16,8 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +