From 5e26df788b1d17187b7a91a4dd90005555feb9b2 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Wed, 30 Sep 2026 18:34:24 +0200 Subject: [PATCH] Bewerbungen: die Absage bleibt lesbar, und ihr seht, was ihr absagt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VanVan im Support: „Wenn sich ein Modi auf eine Aufgabe bewirbt und man diese ablehnt, 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 -- die Pruefungen standen zuerst da und waren rot: Frida sieht ihre beantwortete Bewerbung 0 Die Leitung sieht die Absagen 0 DIE URSACHE. `vorlagenBewerbungenFuer` fragt `WHERE zustand = 'beworben'`. Sobald jemand antwortet, faellt die Zeile aus JEDER Ansicht heraus -- mitsamt der Begruendung, die Filipe am 23.09. ausdruecklich verlangt hat („mit einem text als notiz"). Der Satz wird also verlangt, geschrieben und weggesperrt. Bei einer ZUSAGE fiel das nicht auf: Dort entsteht eine Aufgabe, und an ihrer Karte steht die Antwort. Bei einer ABSAGE entsteht nichts. Die Benachrichtigung selbst gab es (`bewerbung_antwort`, vorgabe an, fuer ja und nein). Sie war nur das EINZIGE -- wer sie wegwischt oder kein Geraet angemeldet hat, erfuhr nie, warum. WAS JETZT DASTEHT * BEIM BEWERBER: „Antwort auf deine Bewerbung" mit dem Ergebnis, dem Satz und dem Namen dessen, der geantwortet hat. Beide Ausgaenge -- eine Liste, die nur Absagen sammelt, waere eine andere Sache. Nach 14 Tagen verschwindet sie von selbst. SIE STEHT VOR DEM ZUKLAPPEN. Beim ersten Anlauf lag sie im Koerper des Vorlagenbretts, und der wird nur gebaut, wenn das Brett aufgeklappt ist -- zugeklappt ist die Vorgabe. Eine Antwort, die man erst aufklappen muss, ist wieder keine. * BEI DER LEITUNG: „Schon abgesagt", 90 Tage, mit der Zahl JE MENSCH vorn. Das ist VanVans eigentliche Frage („falls sich jemand immer wieder bewirbt und immer wieder abgelehnt wird"), und die beantwortet eine Liste von zwanzig Zeilen nicht. AUS BEIDEN WEGEN -- Vorlage und Aufgabe. Eine Uebersicht, die nur die Haelfte zeigt, ist schlimmer als keine: „Frida wurde nie abgelehnt" waere dann eine Auskunft, die stimmt und taeuscht. ZUGEKLAPPT. Eine Liste von Absagen, die immer offen steht, ist eine Sammlung von Nein-Sagen ueber Menschen, mit denen man morgen wieder arbeitet. KEIN ROT. Eine Absage ist kein Fehler; „diesmal nicht" ist keine Aussage ueber den Menschen. Gedeckter Bernstein, und ab zwei Malen faellt die ZAHL auf -- nicht nur die Farbe. ZWEI EIGENE FELDER UND NICHT `bewerbungen` ERWEITERT. Dort heisst eine Zeile im Browser „wartet auf Antwort", samt Zurueckziehen-Knopf. Beantwortete Zeilen hineinzumischen haette bei einer Absage genau das gezeigt. Ein Feld, dessen Bedeutung sich aendert, bricht seine Leser lautlos. `entschieden_von IS NOT NULL` TRENNT DREI DINGE, die alle `abgelehnt` heissen: die Leitung hat eine Bewerbung abgelehnt (das hier), die Person hat eine Zuteilung abgelehnt (ihre Entscheidung), und „von jemand anderem uebernommen" (gar keine Absage). Haustrennung gilt: `darfSchreibenMit` je Zeile. GEPRUEFT pruef-bewerbung-aufgaben 144/0 (weitere 10 Pruefungen plus zwei Bilder). Die Gegenprobe steht in der Vorgeschichte: Dieselben Pruefungen waren vor dem Bau rot. Zwei eigene Messfehler, beide im Text festgehalten: Ich habe zuerst auf dem AUFGABENbrett gemessen -- der Vorlagenkatalog steht aber auf „Eure Aufgaben" (`zweig: 'team'`). Und `innerText` liefert den Text so, wie er DASTEHT: Die Zeile suchte „Diesmal nicht" und fand „DIESMAL NICHT", weil das Schild `text-transform: uppercase` traegt. pruef-vorlagen 24/0, pruef-aufgaben-vorlagen, pruef-zuteilung, pruef-aufgabenbrett, pruef-entwicklung 79/0, pruef-css-klassen, pruef-zwischenspeicher 34/0. Co-Authored-By: Claude Opus 5 --- server/pruef-bewerbung-aufgaben.mjs | 169 +++++++++++++++++++++++++++ server/workspace-vorlagen.js | 157 +++++++++++++++++++++++++ workspace/anruf-probe.html | 10 +- workspace/app.webmanifest | 8 +- workspace/assets/css/aufgaben.css | 127 ++++++++++++++++++++ workspace/assets/js/vorlagenbrett.js | 118 ++++++++++++++++++- workspace/aufgaben.html | 38 +++--- workspace/automation.html | 32 ++--- workspace/befinden.html | 34 +++--- workspace/bereich.html | 44 +++---- workspace/bewerben.html | 38 +++--- workspace/bewerbungen.html | 36 +++--- workspace/buehne.html | 6 +- workspace/calls.html | 34 +++--- workspace/chat.html | 40 +++---- workspace/content.html | 34 +++--- workspace/crew-index.html | 12 +- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 34 +++--- workspace/entwicklung.html | 36 +++--- workspace/hilfe.html | 30 ++--- workspace/index.html | 10 +- workspace/kalender.html | 34 +++--- workspace/leistung.html | 32 ++--- workspace/material.html | 32 ++--- workspace/notizen.html | 28 ++--- workspace/personen.html | 32 ++--- workspace/profil.html | 36 +++--- workspace/reaktion.html | 38 +++--- workspace/rechte.html | 34 +++--- workspace/report.html | 36 +++--- workspace/scouting.html | 32 ++--- workspace/start.html | 32 ++--- workspace/startcheck.html | 34 +++--- workspace/steckbrief.html | 36 +++--- workspace/support.html | 28 ++--- workspace/tafel.html | 6 +- workspace/talente.html | 36 +++--- workspace/team.html | 32 ++--- workspace/teamlage.html | 34 +++--- workspace/teilen.html | 30 ++--- workspace/treff-moderation.html | 32 ++--- workspace/treff-regeln.html | 32 ++--- workspace/uebersicht.html | 32 ++--- workspace/unsere-seiten.html | 30 ++--- workspace/unterstuetzen.html | 32 ++--- workspace/werdegang.html | 32 ++--- workspace/willkommen.html | 30 ++--- workspace/wissen.html | 34 +++--- 49 files changed, 1240 insertions(+), 671 deletions(-) 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`. --> - - + + - +