diff --git a/server/pruef-modi-katalog.mjs b/server/pruef-modi-katalog.mjs index 82fd2a82..1cf7d968 100644 --- a/server/pruef-modi-katalog.mjs +++ b/server/pruef-modi-katalog.mjs @@ -860,6 +860,129 @@ console.log(""); } finally { await browser.close(); } } +/* ======================================================================= + 6c. EINE BEWERBUNG, DIE NIEMAND SIEHT, IST KEINE (23.09.2026) + ======================================================================= + + Beide Bewerbungswege waren gebaut und beide funktionierten -- und + beide waren STUMM. Nachgemessen vor dem Bauen: `benachrichtige` kam + weder in workspace-zuteilung.js noch in workspace-vorlagen.js ein + einziges Mal vor. + + Wer sich bewirbt, wartet. Warten ohne Rueckmeldung fuehlt sich nach + zwei Tagen an wie "interessiert keinen" -- und genau das soll eine + Bewerbung verhindern. + + GEPRUEFT WIRD, WAS SICH OHNE PUSH-DIENST PRUEFEN LAESST, und das ist + fast alles: WER etwas erfaehrt (abgeleitet, nicht aufgezaehlt), WAS + darin steht (reine Funktionen), und DASS es zwei getrennte Schalter + gibt. + ======================================================================= */ +console.log("\n=== 6c. Erfaehrt es auch jemand? ===\n"); +{ + const melder = await import("./workspace-bewerbung-melden.js"); + const { ARTEN } = await import("./workspace-push.js"); + + /* ---- DIE SCHALTER --------------------------------------------- */ + const neu2 = ARTEN.find((a) => a.schluessel === "bewerbung_neu"); + const antwort = ARTEN.find((a) => a.schluessel === "bewerbung_antwort"); + ok(!!neu2 && !!antwort, + "beide Arten stehen in den Benachrichtigungs-Einstellungen"); + ok(neu2?.vorgabe === true && antwort?.vorgabe === true, + "und sind von sich aus an"); + /* ZWEI SCHALTER, ZWEI ENTSCHEIDUNGEN. Wer an einem lebhaften Tag + viele Bitten bekommt, will die stumm stellen koennen -- die + Antwort auf die EIGENE Bewerbung will niemand stumm stellen. Eine + gemeinsame Art hiesse: beides zusammen abschalten oder beides + zusammen ertragen. */ + ok(neu2?.schluessel !== antwort?.schluessel, + "getrennt voneinander — die eine trifft den, der antwortet, die andere den, der wartet"); + + /* ---- WER ES ERFAEHRT ------------------------------------------- */ + const wer = melder.entscheiderFuer({ id: idModi, name: "Marina" }); + const rollen = wer.map((x) => x.rolle).sort(); + ok(wer.length > 0, `die Bewerbung eines Modis erreicht ${wer.length} Leute`); + ok(rollen.includes("admin") && rollen.includes("hand"), + `DogFather und die rechte Hand sind dabei (${rollen.join(", ")})`); + /* DIE LINKE HAND NICHT -- sie darf verteilen, aber nicht ueber + Bewerbungen entscheiden. Eine Meldung ueber etwas, das man nicht + beantworten kann, ist Laerm. */ + ok(!rollen.includes("linke"), + "die linke Hand nicht — sie entscheidet hier nicht mit"); + /* UND DIE AGENTUR ERST RECHT NICHT. Ohne die Haeuser-Bedingung + erfuehre ein Manager ueber eine Push-Nachricht, dass es Team Dogi + ueberhaupt gibt. */ + ok(!rollen.includes("manager") && !rollen.includes("creator"), + `niemand aus dem anderen Haus (${rollen.join(", ") || "niemand"})`); + /* Und der Bewerber nicht sich selbst. */ + ok(!wer.some((x) => x.id === idModi), "und der Bewerber nicht sich selbst"); + + /* GEGENPROBE: Die Liste darf nicht einfach "alle Aktiven" sein -- + sonst waere jede Zeile oben zufaellig richtig. */ + const alleAktiven = (await import("node:sqlite")).DatabaseSync; + const d3 = new alleAktiven(process.env.WORKSPACE_DB, { readOnly: true }); + const anzahl = d3.prepare("SELECT COUNT(*) AS n FROM personen WHERE aktiv = 1").get().n; + d3.close(); + ok(wer.length < anzahl, + `Gegenprobe: es sind nicht einfach alle (${wer.length} von ${anzahl} Aktiven)`); + + /* ---- WAS DRINSTEHT --------------------------------------------- */ + const b1 = melder.bewerbungText({ + name: "Marina", titel: "Die Regeln des Rudels einmal ganz lesen", + wort: "ich habe Freitag Zeit" }); + ok(b1.titel === "Marina bewirbt sich", `der Titel nennt den Menschen (${b1.titel})`); + ok(b1.text.includes("Regeln des Rudels"), "und die Zeile darunter die Aufgabe"); + /* DAS WORT IST OFT DIE GANZE AUSKUNFT. Wer es schon auf dem + Sperrbildschirm liest, kann entscheiden, ohne zu oeffnen. */ + ok(b1.text.includes("ich habe Freitag Zeit"), + `samt dem Wort des Bewerbers (${b1.text})`); + + const b2 = melder.bewerbungText({ name: "Marina", titel: "Kurz", wort: "" }); + ok(!b2.text.includes("„"), `ohne Wort bleibt es sauber (${b2.text})`); + ok(b2.text === "Kurz", "und nur die Aufgabe steht da"); + + /* NAMENLOS DARF NICHT "undefined bewirbt sich" ergeben. */ + const b3 = melder.bewerbungText({ name: "", titel: "X", wort: "" }); + ok(b3.titel === "Jemand bewirbt sich", `ohne Namen steht ein Wort da (${b3.titel})`); + + const a1 = melder.antwortText({ titel: "Die Regeln", ja: true, notiz: "mach das gern" }); + const a2 = melder.antwortText({ titel: "Die Regeln", ja: false, notiz: "diesmal jemand anders" }); + /* DAS ERGEBNIS STEHT IM TITEL -- man soll es lesen koennen, ohne zu + oeffnen. Auch die gute Nachricht. */ + ok(/angenommen/.test(a1.titel), `die Zusage sagt es im Titel (${a1.titel})`); + ok(/nicht/.test(a2.titel) && a2.titel !== a1.titel, + `die Absage auch, und anders (${a2.titel})`); + /* DIE NOTIZ IST DER GRUND, WARUM FILIPE SIE VERLANGT HAT ("mit + einem text als notiz"). Sie hier wegzulassen hiesse, sie an genau + der Stelle zu verstecken, an der man sie liest. */ + ok(a1.text.includes("mach das gern") && a2.text.includes("diesmal jemand anders"), + "und die Notiz steht in beiden"); + + /* LANGES WIRD GEKUERZT, ABER NICHT MITTEN IM WORT. + + DIE ERSTE FASSUNG DIESER ZEILE WAR FALSCH und hat mich das + Richtige gelehrt: Sie nahm einen 68 Zeichen langen Titel und + erwartete eine Kuerzung bei 50 -- die gilt aber nur, wenn eine + Notiz danebensteht und Platz wegnimmt. Ohne Notiz darf der Titel + die ganzen 90 haben. Die Pruefung war rot, das Programm richtig. + Gemessen wird jetzt mit etwas, das JEDE Grenze reisst. */ + const langerSatz = "Eine ganze Sendung neben einem Neuen sitzen und im Zweifel " + + "eingreifen und danach noch drei Zeilen aufschreiben, damit am naechsten " + + "Tag jemand weiss, was eigentlich los war"; + const lang = melder.antwortText({ titel: langerSatz, ja: true, notiz: "" }); + ok(lang.text.length < langerSatz.length && /…$/.test(lang.text), + `zu Langes endet mit Auslassung (${lang.text})`); + /* KEIN SCHNITT MITTEN IM WORT -- ein "eingrei…" liest sich wie ein + Fehler, nicht wie eine Kuerzung. */ + ok(/\s…$/.test(lang.text) || /[a-zA-Zäöüß]{3,}\s?…$/.test(lang.text), + `und nicht mitten im Wort (${lang.text.slice(-24)})`); + /* GEGENPROBE: Kurzes bleibt unangetastet -- sonst bewiese die Zeile + darueber nur, dass immer gekuerzt wird. */ + const kurzerSatz = melder.antwortText({ titel: "Kurzer Titel", ja: true, notiz: "" }); + ok(kurzerSatz.text === "Kurzer Titel", + `Gegenprobe: Kurzes bleibt unangetastet (${kurzerSatz.text})`); +} + /* ---------- 7. Gegenprobe -------------------------------------------- */ console.log("\n=== 7. Gegenprobe: kann das hier ueberhaupt scheitern? ===\n"); { diff --git a/server/pruef-push-ziel.mjs b/server/pruef-push-ziel.mjs index c647bae2..2292181a 100644 --- a/server/pruef-push-ziel.mjs +++ b/server/pruef-push-ziel.mjs @@ -115,19 +115,42 @@ d.close(); melde(""); melde("=== 1. Das Team ==="); { + /* ==== DIE CALLS-SEITE GEHOERT INZWISCHEN DAZU (berichtigt 23.09.2026) + + Hier stand, dass ein Modi von "/workspace/calls.html" auf die + Startseite umgelenkt wird -- die Seite gab es fuer ihn nicht. + + Seither hat Filipe das Gegenteil bestellt: "jeder der einen + kalender hat soll auch sowas haben." Hinter jedem Kalender steht + jetzt eine Calls-Kachel (mitCalls in workspace.js), und zwar fuer + Modis, rechte und linke Hand. Nachgemessen: je eine Kachel, alle + drei. + + Die Pruefung war damit seit dem Umbau ROT -- und zwar mit einem + Satz, der wie ein Befund aussah ("aber nicht auf eine Seite, die + es fuer ihn nicht gibt") und in Wahrheit eine erfuellte Bestellung + meldete. Sie steht jetzt andersherum: Die Calls-Seite MUSS stehen + bleiben. So schuetzt dieselbe Zeile das, was sie vorher verboten + hat -- und wenn die Kachel je wieder verschwindet, wird sie rot. */ const a = await push.benachrichtige(idModi, "test", { titel: "Call gleich", text: "In zehn Minuten", ziel: "/workspace/calls.html" }); ok(a.verschickt === 1, `die Benachrichtigung geht raus (${a.verschickt})`); - /* DAS IST DER PUNKT: Sie wird NICHT unterdrueckt. Ein Modi, der in - einem Call steht, soll erfahren, dass er gleich anfaengt. */ - ok(a.ziel === "/workspace/start.html", - `aber nicht auf eine Seite, die es fuer ihn nicht gibt (${a.ziel})`); + ok(a.ziel === "/workspace/calls.html", + `und fuehrt auf die Calls-Seite, die es fuer ihn seit dem 22.09. gibt (${a.ziel})`); + /* Das Umlenken selbst muss trotzdem greifen -- sonst bewiese die + Zeile darueber nur, dass jedes Ziel stehen bleibt. Das Scouting + ist und bleibt Agentur. */ const b = await push.benachrichtige(idHand, "test", { titel: "Scouting", text: "Follow-up faellig", ziel: "/workspace/scouting.html" }); ok(b.ziel === "/workspace/start.html", `auch die rechte Hand landet nicht im Scouting (${b.ziel})`); + const b2 = await push.benachrichtige(idModi, "test", + { titel: "Scouting", text: "Follow-up faellig", ziel: "/workspace/scouting.html" }); + ok(b2.ziel === "/workspace/start.html", + `und ein Modi ebenso wenig (${b2.ziel})`); + /* UND DIE GEGENPROBE, ohne die die zwei Zeilen darueber nichts beweisen: Ein Ziel, das es auf crew. GIBT, bleibt stehen. Sonst hiesse "wird zur Startseite" nur, dass jedes Ziel dorthin faellt. */ diff --git a/server/workspace-bewerbung-melden.js b/server/workspace-bewerbung-melden.js new file mode 100644 index 00000000..94420116 --- /dev/null +++ b/server/workspace-bewerbung-melden.js @@ -0,0 +1,185 @@ +/* ===================================================================== + EINE BEWERBUNG, DIE NIEMAND SIEHT, IST KEINE (23.09.2026) + ===================================================================== + + Filipe, 22.09.2026: „die option dass die modis sich für aufgaben + bewerben können." + Filipe, 23.09.2026: „nur dogfather und die rechte hand sollen + annehmen oder ablehnen können, mit einem text als notiz." + + --------------------------------------------------------------------- + WAS GEFEHLT HAT -- und es ist der ganze Grund für diese Datei + + Beide Wege waren gebaut und beide funktionierten. Nur: Wenn Frida + sich bewirbt, erfährt DogFather das ausschließlich dann, wenn er von + sich aus das Brett aufmacht. Und wenn er antwortet, erfährt Frida es + ausschließlich dann, wenn SIE von sich aus nachsieht. + + Das ist keine Kleinigkeit, das ist die Funktion. Wer sich bewirbt, + wartet -- und Warten ohne Rückmeldung fühlt sich nach zwei Tagen an + wie „interessiert keinen". Genau das Gefühl soll eine Bewerbung + verhindern. + + Nachgemessen, bevor gebaut wurde: In workspace-zuteilung.js kam + `benachrichtige` kein einziges Mal vor, in workspace-vorlagen.js + auch nicht. Es war keine vergessene Zeile, es war ein fehlendes + Stück. + + --------------------------------------------------------------------- + WARUM EINE EIGENE DATEI + + Es gibt ZWEI Bewerbungswege -- auf eine Aufgabe, die es schon gibt + (workspace-zuteilung.js), und auf eine Vorlage, aus der erst eine + wird (workspace-vorlagen.js). Beide sollen dasselbe melden, sonst + liest dieselbe Person zweimal etwas Verschiedenes über denselben + Vorgang. + + Zwei Fassungen wären zwei Gelegenheiten, dass eine davon beim + nächsten Umbau die Ruhezeit, die Abschaltbarkeit oder die + Häusertrennung vergisst. + + --------------------------------------------------------------------- + WER ERFÄHRT ES -- ABGELEITET, NICHT AUFGEZÄHLT + + Die naheliegende Zeile wäre `rolle IN ('admin','hand')` gewesen; so + steht sie in workspace-hilfe.js. Sie ist eine Abschrift, und + Abschriften altern: Käme morgen eine Rolle dazu, die entscheiden + darf, bekäme sie keine einzige Meldung -- und niemand merkte es, + weil ja alles funktioniert. + + Gefragt wird deshalb die Regel selbst (`entscheidetUeberAufgaben`), + Person für Person. Und zusätzlich `darfSchreibenMit`: Wer den + Bewerber gar nicht sehen darf, bekommt auch keine Meldung über ihn. + Das ist keine Vorsicht um ihrer selbst willen -- ohne diese Zeile + erführe die Agentur über eine Push-Nachricht, dass es Team Dogi + gibt. + ===================================================================== */ + +import { + db, entscheidetUeberAufgaben, darfSchreibenMit, +} from "./workspace.js"; + +/** Wer über die Bewerbung DIESER Person entscheiden kann und darf. + * + * ZWEI BEDINGUNGEN, ZWEI GRÜNDE: + * entscheidetUeberAufgaben -- er darf antworten + * darfSchreibenMit -- er darf den Bewerber überhaupt sehen + * + * Der Bewerber selbst fällt heraus: Wer entscheiden darf, bewirbt + * sich nicht (die Routen lehnen das ab) -- aber falls doch einmal, + * wäre eine Meldung an sich selbst Unsinn. + */ +export function entscheiderFuer(bewerber) { + try { + const alle = db().prepare( + "SELECT id, rolle, name FROM personen WHERE aktiv = 1").all(); + return alle + .filter((p) => p.id !== bewerber.id) + .filter((p) => entscheidetUeberAufgaben(p)) + .filter((p) => darfSchreibenMit(p, bewerber.id)); + } catch (fehler) { + console.error("[bewerbung] Entscheider nicht ermittelbar:", fehler?.message); + return []; + } +} + +/** Träge geladen -- wie überall im Haus. + * + * Der Bewerbungsweg soll auch dann laufen, wenn am Push-Teil etwas + * klemmt. Eine Bewerbung, die wegen eines Schlüsselproblems gar nicht + * erst gespeichert wird, wäre der schlechtere Tausch. + */ +async function schicken(personId, art, inhalt) { + try { + const { benachrichtige } = await import("./workspace-push.js"); + await benachrichtige(personId, art, inhalt); + } catch (fehler) { + console.error("[bewerbung] Benachrichtigung:", fehler?.message); + } +} + +/** Kurz halten, aber nicht abschneiden mitten im Wort. + * + * Eine Benachrichtigung steht auf einem Sperrbildschirm; was dort + * nicht hinpasst, verschluckt das Gerät ohnehin. Ein hartes `slice` + * endet dabei gern mitten im Wort, und das liest sich wie ein Fehler. + */ +function kurz(text, max = 90) { + const t = String(text || "").trim().replace(/\s+/g, " "); + if (t.length <= max) return t; + const schnitt = t.slice(0, max); + const luecke = schnitt.lastIndexOf(" "); + return (luecke > max * 0.6 ? schnitt.slice(0, luecke) : schnitt) + " …"; +} + +/* ===================================================================== + WAS DRINSTEHT -- als eigene, reine Funktionen + ===================================================================== + + ALS EIGENE FUNKTIONEN UND EXPORTIERT, damit eine Prüfung sie lesen + kann, ohne einen Push-Dienst nachzubauen. Genau an dieser Stelle + steckte am 18.09.2026 ein Fehler, den Filipe gemeldet hat und der + von außen nicht messbar war: Der Wortlaut entstand tief in einer + Funktion, die nur der Push-Weg aufruft, und lautete am Ende + „Nachricht von [object Object]". + + Sie bekommen nichts als Text und geben Text zurück -- keine + Datenbank, kein Netz. Eine Prüfung kann sie deshalb mit einer Zeile + an fünfzehn Proben halten. + ===================================================================== */ + +/** + * „Jemand bewirbt sich" -- für alle, die antworten können. + * + * DER TITEL NENNT DEN MENSCHEN, nicht die Aufgabe. Wer drei Meldungen + * untereinander sieht, will wissen, WER etwas von ihm will; welche + * Aufgabe es war, steht in der Zeile darunter. + */ +export function bewerbungText({ name, titel, wort }) { + return { + titel: `${String(name || "Jemand").trim() || "Jemand"} bewirbt sich`, + /* DAS WORT DES BEWERBERS GEHT MIT, wenn es eines gibt. Es ist oft + die ganze Auskunft („ich habe Freitag Zeit") -- und wer sie + schon auf dem Sperrbildschirm liest, kann entscheiden, ohne die + Seite zu öffnen. */ + text: wort ? `${kurz(titel, 60)} — „${kurz(wort, 70)}“` : kurz(titel), + }; +} + +/** + * „Deine Bewerbung ist beantwortet" -- für den, der gewartet hat. + * + * DIE ANTWORT IST DER WICHTIGERE TEIL. Wer sich bewirbt, wartet; eine + * Absage, die man drei Tage später zufällig entdeckt, ist schlimmer + * als eine sofortige. + * + * ZWEI VERSCHIEDENE TITEL, und das ist Absicht: „angenommen" und + * „diesmal nicht" sagen das Ergebnis, bevor man öffnet. Ein + * gemeinsames „Antwort auf deine Bewerbung" zwänge jedes Mal zum + * Nachsehen -- auch bei der guten Nachricht. + * + * DIE NOTIZ STEHT DRIN -- genau dafür hat Filipe sie verlangt („mit + * einem text als notiz"). Sie erst zu verlangen und dann an der + * Stelle zu verschweigen, an der man sie liest, wäre die halbe + * Funktion. + */ +export function antwortText({ titel, ja, notiz }) { + return { + titel: ja ? "Deine Bewerbung: angenommen" : "Deine Bewerbung: diesmal nicht", + text: notiz ? `${kurz(titel, 50)} — „${kurz(notiz, 80)}“` : kurz(titel), + }; +} + +/** Schickt „jemand bewirbt sich" an alle, die antworten können. + * Gibt zurück, an wie viele -- die Zahl braucht die Prüfung. */ +export async function meldeBewerbung({ bewerber, titel, wort, ziel }) { + const leute = entscheiderFuer(bewerber); + const inhalt = bewerbungText({ name: bewerber.name, titel, wort }); + for (const p of leute) await schicken(p.id, "bewerbung_neu", { ...inhalt, ziel }); + return leute.length; +} + +/** Schickt die Antwort an den, der gewartet hat. */ +export async function meldeBewerbungsantwort({ anWen, titel, ja, notiz, ziel }) { + await schicken(anWen, "bewerbung_antwort", { ...antwortText({ titel, ja, notiz }), ziel }); +} diff --git a/server/workspace-push.js b/server/workspace-push.js index fd1d35a6..018b3e73 100644 --- a/server/workspace-push.js +++ b/server/workspace-push.js @@ -107,6 +107,40 @@ export const ARTEN = [ liest es um sieben -- das reicht. */ { schluessel: "chat_erwaehnung", name: "Jemand erwähnt dich im Chat (@)", vorgabe: true }, + /* ---- BEWERBUNGEN (23.09.2026) ----------------------------------- + + Filipe, 22.09.: „die option dass die modis sich für aufgaben + bewerben können." + Filipe, 23.09.: „nur dogfather und die rechte hand sollen annehmen + oder ablehnen können, mit einem text als notiz." + + BEIDE WEGE WAREN GEBAUT UND BEIDE STUMM. Wer sich beworben hatte, + erfuhr die Antwort nur, wenn er von sich aus nachsah -- und wer + antworten sollte, erfuhr von der Bewerbung nur, wenn er von sich + aus das Brett aufmachte. Nachgemessen: `benachrichtige` kam in + workspace-zuteilung.js und workspace-vorlagen.js kein einziges Mal + vor. Das war keine vergessene Zeile, das war ein fehlendes Stück. + + ZWEI ARTEN UND NICHT EINE, aus demselben Grund wie beim Chat: Die + beiden treffen verschiedene Menschen in verschiedenen Lagen. Wer + entscheidet, bekommt an einem lebhaften Tag mehrere Bitten -- + das kann man stumm stellen wollen. Die ANTWORT auf die eigene + Bewerbung will niemand stumm stellen; sie kommt einmal, und man + wartet darauf. Eine gemeinsame Art hieße: beides zusammen + abschalten oder beides zusammen ertragen. + + `vorgabe: true` bei beiden -- wie alles, was auf ein Ereignis + antwortet, statt es von sich aus zu erzeugen. + + KEINE AUSNAHME VON DER RUHEZEIT. Eine Bewerbung wartet, ein Anruf + nicht. Wer um halb vier eine Absage bekommt, liest sie um sieben; + das ist früh genug und sehr viel besser, als nachts geweckt zu + werden. */ + { schluessel: "bewerbung_neu", + name: "Jemand bewirbt sich für eine Aufgabe", vorgabe: true }, + { schluessel: "bewerbung_antwort", + name: "Antwort auf deine eigene Bewerbung", vorgabe: true }, + /* ER IST LIVE (22.09.2026). Filipe: "die leute sollen auch automatisch eine benarichtigung diff --git a/server/workspace-vorlagen.js b/server/workspace-vorlagen.js index a5bc3b27..ff8f075f 100644 --- a/server/workspace-vorlagen.js +++ b/server/workspace-vorlagen.js @@ -50,6 +50,13 @@ import { darfAufgabenVerteilen, } from "./workspace.js"; +/* Dieselbe Meldung wie beim anderen Bewerbungsweg -- eine Stelle, + damit dieselbe Person nicht zweimal etwas Verschiedenes ueber + denselben Vorgang liest. */ +import { + meldeBewerbung, meldeBewerbungsantwort, +} from "./workspace-bewerbung-melden.js"; + export const vorlagenRouter = express.Router(); const jetzt = () => new Date().toISOString(); @@ -1621,6 +1628,16 @@ vorlagenRouter.post("/workspace/api/vorlagen/bewerben", gleicheHerkunft, VALUES (?,?,?,?,'beworben',?)`) .run(v.schluessel, v.kategorie, req.person.id, text, jetzt()); + /* ES ERFAEHRT JEMAND (23.09.2026) -- siehe die Begruendung in + workspace-bewerbung-melden.js. Ohne diese Zeile waere die + Bewerbung ein Zettel in einer Schublade, die niemand oeffnet. */ + void meldeBewerbung({ + bewerber: { id: req.person.id, name: req.person.name }, + titel: v.titel, + wort: text, + ziel: "/workspace/aufgaben.html", + }); + protokolliere("vorlage_beworben", { personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), detail: `${v.schluessel} ${v.titel}`.slice(0, 120), @@ -1715,6 +1732,20 @@ for (const [weg, ja] of [["annehmen", true], ["ablehnen", false]]) { .run(ja ? "angenommen" : "abgelehnt", kommentar, req.person.id, nun, aufgabeId, id); + /* DIE ANTWORT AN DEN, DER GEWARTET HAT -- mit der Notiz. + Bei einer Zusage fuehrt das Antippen auf die neue Aufgabe, + bei einer Absage auf das Brett: Dort steht die Vorlage + wieder offen, und man kann sich spaeter erneut bewerben. */ + void meldeBewerbungsantwort({ + anWen: wer.id, + titel: v.titel, + ja, + notiz: kommentar, + ziel: aufgabeId + ? `/workspace/aufgaben.html?aufgabe=${aufgabeId}` + : "/workspace/aufgaben.html", + }); + protokolliere(ja ? "vorlage_bewerbung_angenommen" : "vorlage_bewerbung_abgelehnt", { personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), detail: `${b.vorlage} fuer ${wer.name}`.slice(0, 120), diff --git a/server/workspace-zuteilung.js b/server/workspace-zuteilung.js index b741e41d..7d6b9465 100644 --- a/server/workspace-zuteilung.js +++ b/server/workspace-zuteilung.js @@ -58,6 +58,11 @@ import { sitzungLesen, } from "./workspace.js"; import { TEAM_DOGI_ROLLEN } from "./crew-adresse.js"; +/* WER ES ERFAEHRT -- an einer Stelle fuer beide Bewerbungswege. + Begruendung samt Messung steht in der Datei selbst. */ +import { + meldeBewerbung, meldeBewerbungsantwort, +} from "./workspace-bewerbung-melden.js"; export const zuteilungRouter = express.Router(); @@ -577,6 +582,25 @@ zuteilungRouter.post("/workspace/api/aufgaben/:id/bewerben", (req, res) => { .run(a.id, req.person.id, text || null, nun, nun); } + /* ==== UND JETZT ERFAEHRT ES AUCH JEMAND (23.09.2026) ========= + + Bis heute endete eine Bewerbung hier: gespeichert, protokolliert + -- und still. DogFather erfuhr davon nur, wenn er von sich aus + das Brett aufmachte. Wer sich bewirbt, wartet aber; Warten ohne + Rueckmeldung fuehlt sich nach zwei Tagen an wie "interessiert + keinen", und genau das soll eine Bewerbung verhindern. + + NICHT ABWARTEN (`void`): Ob die Meldung durchgeht, haengt am + Push-Dienst, an der Ruhezeit und an den Einstellungen des + Empfaengers. Nichts davon darf entscheiden, ob die Bewerbung + selbst gespeichert ist -- die ist es laengst. */ + void meldeBewerbung({ + bewerber: { id: req.person.id, name: req.person.name }, + titel: a.titel, + wort: text, + ziel: `/workspace/aufgaben.html?aufgabe=${a.id}`, + }); + protokolliere("aufgabe_beworben", { personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), detail: `#${a.id} ${a.titel}`.slice(0, 120), @@ -664,6 +688,20 @@ for (const [weg, entscheidung] of [["annehmen", "ja"], ["ablehnen", "nein"]]) { WHERE id = ?`).run(nun, kommentar, req.person.id, nun, z.id); } + /* DIE ANTWORT IST DER WICHTIGERE TEIL. Eine Absage, die man + drei Tage spaeter zufaellig entdeckt, ist schlimmer als eine + sofortige -- und die NOTIZ geht mit, denn genau dafuer hat + Filipe sie verlangt ("mit einem text als notiz"). Sie erst zu + verlangen und dann an der Stelle zu verschweigen, an der man + sie liest, waere die halbe Funktion. */ + void meldeBewerbungsantwort({ + anWen: wer, + titel: a.titel, + ja: entscheidung === "ja", + notiz: kommentar, + ziel: `/workspace/aufgaben.html?aufgabe=${a.id}`, + }); + protokolliere(entscheidung === "ja" ? "bewerbung_angenommen" : "bewerbung_abgelehnt", { personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), detail: `#${a.id} fuer ${wer}`.slice(0, 120),