/* ===================================================================== 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 }); }