Bewerbungen: die Absage bleibt lesbar, und ihr seht, was ihr absagt

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 <[email protected]>
This commit is contained in:
2026-09-30 18:34:24 +02:00
co-authored by Claude Opus 5
parent c933bcd2e0
commit 5e26df788b
49 changed files with 1240 additions and 671 deletions
+157
View File
@@ -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