diff --git a/server/pruef-bewerbung-aufgaben.mjs b/server/pruef-bewerbung-aufgaben.mjs
index df930559..33d16058 100644
--- a/server/pruef-bewerbung-aufgaben.mjs
+++ b/server/pruef-bewerbung-aufgaben.mjs
@@ -867,6 +867,89 @@ melde("=== 6. Am echten Bildschirm ===");
} finally { await h2.ctx.close(); }
}
+ /* =================================================================
+ EINE BEWERBUNG AUF EINE ERLEDIGTE AUFGABE IST KEINE (01.10.2026)
+
+ Beim Durchsehen der echten Daten gefunden: Zwei Bewerbungen
+ standen auf „beworben" an Aufgaben, die laengst durch waren.
+ Bei der Person stand weiter „wartet auf Antwort", bei der
+ Leitung eine Entscheidung, die es nicht mehr gibt.
+
+ DAS MUSTER GIBT ES IM HAUS SCHON -- `uebernahmeAbschliessen`
+ raeumt genauso auf, wenn jemand anderes eine Pool-Aufgabe
+ bekommt. Derselbe Fall, ein anderer Ausloeser.
+ ================================================================= */
+ console.log("");
+ console.log("=== Bewerbungen auf durchgelaufene Aufgaben ===");
+ {
+ /* UEBER DIE LISTE, nicht ueber die Einzelaufgabe. Der erste
+ Anlauf las `/api/aufgaben/:id` und bekam `undefined` -- die
+ Antwort dort hat eine andere Form. Die Liste ist in dieser
+ Datei erprobt, und „erprobt" schlaegt „naheliegend". */
+ const stand = async (id, wer) => {
+ const a = await roh("/workspace/api/aufgaben", { keks: hand.keks });
+ const auf = (a.json?.aufgaben || a.json?.liste || []).find((x) => x.id === id);
+ return (auf?.zuteilung || []).find((x) => x.person_id === wer) || null;
+ };
+
+ /* ---- Weg 1: die Aufgabe wird ERLEDIGT ---------------------- */
+ const e1 = await roh("/workspace/api/aufgaben", {
+ methode: "POST", keks: hand.keks,
+ body: { titel: "Kurz die Liste sortieren", prioritaet: "niedrig" } });
+ const eId = e1.json?.aufgabe?.id ?? e1.json?.id;
+ const b1 = await roh(`/workspace/api/aufgaben/${eId}/bewerben`, {
+ methode: "POST", keks: modi.keks, body: { text: "Kann ich machen." } });
+ ok(b1.code === 200, `Frida bewirbt sich (${b1.code})`);
+ ok((await stand(eId, idModi))?.zustand === "beworben",
+ "und steht auf „beworben\"");
+
+ const fertig = await roh(`/workspace/api/aufgaben/${eId}`, {
+ methode: "PATCH", keks: hand.keks, body: { status: "erledigt" } });
+ ok(fertig.code === 200, `die rechte Hand erledigt sie (${fertig.code})`);
+
+ const nach1 = await stand(eId, idModi);
+ ok(nach1?.zustand !== "beworben",
+ `danach wartet Frida nicht mehr (${nach1?.zustand})`);
+ ok(/inzwischen erledigt/.test(nach1?.entscheid_text || ""),
+ `und liest, warum (${JSON.stringify(nach1?.entscheid_text)})`);
+ /* NIEMAND HAT ENTSCHIEDEN. Dadurch faellt die Zeile aus der
+ Absagen-Uebersicht heraus -- es ist keine Absage an sie. */
+ ok(!nach1?.entschieden_von,
+ `niemand hat ihr abgesagt (entschieden_von=${JSON.stringify(nach1?.entschieden_von)})`);
+
+ /* ---- Weg 2: die Aufgabe wird ABGEBROCHEN ------------------- */
+ const e2 = await roh("/workspace/api/aufgaben", {
+ methode: "POST", keks: hand.keks,
+ body: { titel: "Etwas, das sich erledigt hat", prioritaet: "niedrig" } });
+ const aId = e2.json?.aufgabe?.id ?? e2.json?.id;
+ await roh(`/workspace/api/aufgaben/${aId}/bewerben`, {
+ methode: "POST", keks: modi.keks, body: { text: "Mach ich." } });
+ const ab = await roh(`/workspace/api/aufgaben/${aId}/abbrechen`, {
+ methode: "POST", keks: chef.keks,
+ body: { grund: "Brauchen wir doch nicht." } });
+ const nach2 = await stand(aId, idModi);
+ ok(ab.code === 200 && nach2?.zustand !== "beworben",
+ `beim Abbrechen ebenso (${ab.code}, ${nach2?.zustand})`);
+ /* DER SATZ IST EIN ANDERER -- „abgebrochen" ist nicht
+ „erledigt", und wer gewartet hat, soll den Unterschied
+ lesen koennen. */
+ ok(/abgebrochen/.test(nach2?.entscheid_text || ""),
+ `mit dem passenden Satz (${JSON.stringify(nach2?.entscheid_text)})`);
+
+ /* ---- GEGENPROBE: eine OFFENE Aufgabe behaelt ihre Bewerbung.
+ Ohne sie hiesse „faellt weg" womoeglich nur, dass jede
+ Bewerbung wegfaellt. */
+ const e3 = await roh("/workspace/api/aufgaben", {
+ methode: "POST", keks: hand.keks,
+ body: { titel: "Laeuft noch", prioritaet: "niedrig" } });
+ const oId = e3.json?.aufgabe?.id ?? e3.json?.id;
+ await roh(`/workspace/api/aufgaben/${oId}/bewerben`, {
+ methode: "POST", keks: modi.keks, body: { text: "Gern." } });
+ const nach3 = await stand(oId, idModi);
+ ok(nach3?.zustand === "beworben",
+ `eine offene Aufgabe behaelt ihre Bewerbung (${nach3?.zustand})`);
+ }
+
/* =================================================================
DAUERHAFTE AUFGABEN (30.09.2026)
diff --git a/server/workspace-aufgaben.js b/server/workspace-aufgaben.js
index 14024845..e7801120 100644
--- a/server/workspace-aufgaben.js
+++ b/server/workspace-aufgaben.js
@@ -28,7 +28,7 @@ const TEXT_MAX = 4000;
const jetzt = () => new Date().toISOString();
-import { zuteilen, mitZuteilung } from "./workspace-zuteilung.js";
+import { zuteilen, mitZuteilung, bewerbungenSchliessen } from "./workspace-zuteilung.js";
/* ---------- Schranke ---------------------------------------------------- */
@@ -1064,6 +1064,17 @@ aufgabenRouter.patch("/workspace/api/aufgaben/:id", gleicheHerkunft, (req, res)
if (aus.status === "erledigt" && aufgabe.status !== "erledigt") {
setz.push("erledigt_am = ?"); daten.push(jetzt());
+ /* WER SICH BEWORBEN HAT, WARTET NICHT WEITER (01.10.2026).
+ Eine erledigte Aufgabe hat keine offenen Bewerbungen mehr --
+ sonst steht bei dem Menschen „wartet auf Antwort" auf etwas,
+ das es nicht mehr gibt, und bei der Leitung eine
+ Entscheidung, die sich erledigt hat. */
+ const weg = bewerbungenSchliessen(aufgabe.id,
+ "Die Aufgabe wurde inzwischen erledigt.");
+ if (weg) {
+ console.log(`[aufgaben] ${weg} offene Bewerbung(en) zu #${aufgabe.id} `
+ + "sind mit der Erledigung weggefallen.");
+ }
} else if (aus.status && aus.status !== "erledigt") {
setz.push("erledigt_am = NULL");
}
@@ -1161,6 +1172,19 @@ aufgabenRouter.post("/workspace/api/aufgaben/:id/abbrechen", gleicheHerkunft, (r
WHERE id = ?`)
.run(aufgabe.status, grund, jetzt(), req.person.id, jetzt(), id);
+ /* AUCH HIER FALLEN DIE OFFENEN BEWERBUNGEN WEG (01.10.2026).
+ Zwei Wege fuehren in den Endzustand -- erledigen und abbrechen
+ --, und beide brauchen dieselbe Aufraeumung. Nur einen zu
+ bedienen waere die Haelfte, die man spaeter sucht.
+
+ DER SATZ IST EIN ANDERER: „abgebrochen" ist nicht „erledigt",
+ und wer gewartet hat, soll den Unterschied lesen koennen. */
+ const wegAb = bewerbungenSchliessen(id, "Die Aufgabe wurde abgebrochen.");
+ if (wegAb) {
+ console.log(`[aufgaben] ${wegAb} offene Bewerbung(en) zu #${id} `
+ + "sind mit dem Abbruch weggefallen.");
+ }
+
protokolliere("aufgabe_abgebrochen", {
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
detail: `#${id} (${aufgabe.status}) ${grund}`.slice(0, 120),
diff --git a/server/workspace-zuteilung.js b/server/workspace-zuteilung.js
index ee9a03e9..870d7671 100644
--- a/server/workspace-zuteilung.js
+++ b/server/workspace-zuteilung.js
@@ -334,6 +334,42 @@ function holen(req, res) {
return a;
}
+/** Eine erledigte oder abgebrochene Aufgabe hat keine offenen
+ * Bewerbungen mehr (01.10.2026).
+ *
+ * GEFUNDEN AN DEN ECHTEN DATEN: Zwei Bewerbungen standen auf
+ * `beworben` an Aufgaben, die laengst durch waren. Bei den
+ * Bewerbern stand weiter „wartet auf Antwort", und in der Liste der
+ * Leitung stand eine Entscheidung an, die es nicht mehr gibt.
+ *
+ * DAS MUSTER GIBT ES HIER SCHON, eine Funktion weiter oben:
+ * `uebernahmeAbschliessen` raeumt genau so auf, wenn jemand anderes
+ * eine Pool-Aufgabe bekommt. Derselbe Fall, ein anderer Ausloeser --
+ * deshalb dieselbe Behandlung und dasselbe Feld.
+ *
+ * KEIN `entschieden_von`: Niemand hat entschieden, die Frage hat
+ * sich erledigt. Dadurch faellt die Zeile auch aus der
+ * Absagen-Uebersicht heraus (die fragt `entschieden_von IS NOT
+ * NULL`) -- richtig, denn es ist keine Absage an diesen Menschen.
+ *
+ * @returns {number} wie viele weggefallen sind -- die Zahl braucht
+ * die Pruefung, und der Aufrufer soll nicht raten muessen.
+ */
+export function bewerbungenSchliessen(aufgabeId, grund) {
+ try {
+ const nun = jetzt();
+ const e = db().prepare(`UPDATE aufgaben_zuteilung
+ SET zustand = 'abgelehnt', entscheid_text = ?,
+ entschieden_am = ?, geantwortet_am = ?
+ WHERE aufgabe_id = ? AND zustand = 'beworben'`)
+ .run(grund, nun, nun, Number(aufgabeId));
+ return Number(e?.changes) || 0;
+ } catch (fehler) {
+ console.error("[zuteilung] Bewerbungen schliessen:", fehler?.message);
+ return 0;
+ }
+}
+
/** Meine eigene Zuteilung zu dieser Aufgabe — oder nichts. */
function meine(aufgabeId, personId) {
return db().prepare(
diff --git a/server/workspace.js b/server/workspace.js
index ded06aa7..15201b37 100644
--- a/server/workspace.js
+++ b/server/workspace.js
@@ -4445,6 +4445,53 @@ function umstellungen(d) {
console.error("[workspace] Zuteilung nachtragen:", fehler?.message);
}
+ /* =====================================================================
+ BEWERBUNGEN AUF DURCHGELAUFENE AUFGABEN (01.10.2026)
+ =====================================================================
+
+ Beim Durchsehen der echten Daten am 30.09. gefunden: Zwei
+ Bewerbungen standen auf `beworben` an Aufgaben, die laengst
+ `erledigt` bzw. `review` waren. Bei der Person stand weiter
+ „wartet auf Antwort", und in der Liste der Leitung stand eine
+ Entscheidung an, die es nicht mehr gibt.
+
+ Die Ursache ist behoben (`bewerbungenSchliessen` laeuft jetzt an
+ beiden Wegen in den Endzustand). Dieser Block holt nach, was
+ schon dasteht.
+
+ OHNE `entschieden_von`: Niemand hat entschieden, die Frage hat
+ sich erledigt. Dadurch faellt die Zeile auch aus der
+ Absagen-Uebersicht heraus -- richtig, es ist keine Absage an
+ diesen Menschen.
+
+ WIEDERHOLBAR: Beim zweiten Start gibt es nichts mehr mit
+ `zustand = 'beworben'` an einer durchgelaufenen Aufgabe. */
+ try {
+ const vorher = d.prepare(`SELECT COUNT(*) AS n FROM aufgaben_zuteilung z
+ JOIN aufgaben a ON a.id = z.aufgabe_id
+ WHERE z.zustand = 'beworben'
+ AND a.status IN ('erledigt', 'abgebrochen', 'review')`).get().n;
+ if (vorher > 0) {
+ const nun = new Date().toISOString();
+ d.prepare(`UPDATE aufgaben_zuteilung
+ SET zustand = 'abgelehnt',
+ entscheid_text = (
+ SELECT CASE a.status WHEN 'abgebrochen'
+ THEN 'Die Aufgabe wurde abgebrochen.'
+ ELSE 'Die Aufgabe wurde inzwischen erledigt.' END
+ FROM aufgaben a WHERE a.id = aufgaben_zuteilung.aufgabe_id),
+ entschieden_am = ?, geantwortet_am = ?
+ WHERE zustand = 'beworben'
+ AND aufgabe_id IN (SELECT id FROM aufgaben
+ WHERE status IN ('erledigt', 'abgebrochen', 'review'))`)
+ .run(nun, nun);
+ console.log(`[workspace] ${vorher} Bewerbung(en) auf durchgelaufene `
+ + "Aufgaben sind weggefallen.");
+ }
+ } catch (fehler) {
+ console.error("[workspace] Bewerbungen aufraeumen:", fehler?.message);
+ }
+
loeschspurenAufraeumen(d);
/* =====================================================================
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index 982bbeb5..1debf2ab 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`. -->
-
-
+
+
-
+