diff --git a/server/pruef-bewerbung-aufgaben.mjs b/server/pruef-bewerbung-aufgaben.mjs
index c2b360d0..46440bf0 100644
--- a/server/pruef-bewerbung-aufgaben.mjs
+++ b/server/pruef-bewerbung-aufgaben.mjs
@@ -848,8 +848,127 @@ melde("=== 6. Am echten Bildschirm ===");
await kasten2.screenshot({ path: "server/pruef-bewerbung-antwort.png" });
console.log(" -- Bild: server/pruef-bewerbung-antwort.png");
}
+
+ /* ==== „VERSTANDEN" RAEUMT AUF (03.10.2026) ==============
+
+ VanVan im Support, Meldung #15: „Die Modis können die
+ Antworten auf die Bewerbungen im Bereich eure Aufgaben noch
+ nicht einklappen. Das wird mit der Zeit unübersichtlich."
+
+ GEMESSEN WIRD DER GANZE WEG, nicht nur die Route: Knopf
+ druecken, Zeile rutscht in die zugeklappte Gruppe, Gruppe
+ laesst sich oeffnen, Text ist noch da -- und nach dem
+ Neuladen ist sie immer noch gelesen. Der letzte Punkt ist
+ der eigentliche: Haette ich es im Browserspeicher gemerkt,
+ waere dieselbe Antwort auf dem naechsten Geraet wieder neu. */
+ const stand = () => m5.s.evaluate(() => {
+ const neuB = document.querySelector('.vorlagenbrett__antwort:not([data-alt])');
+ const altB = document.querySelector('.vorlagenbrett__antwort[data-alt="ja"]');
+ return {
+ neuZeilen: neuB ? neuB.querySelectorAll(".vorlagenbrett__antwort-zeile").length : 0,
+ altKopf: altB
+ ? (altB.querySelector(".vorlagenbrett__antwort-kopf")?.innerText || "").trim()
+ : null,
+ altZeilen: altB ? altB.querySelectorAll(".vorlagenbrett__antwort-zeile").length : 0,
+ knopf: !!document.querySelector(".vorlagenbrett__antwort-weg"),
+ };
+ });
+
+ const vorher = await stand();
+ ok(vorher.neuZeilen >= 1 && vorher.knopf === true,
+ `die ungelesene Antwort steht offen da, mit „Verstanden" (${vorher.neuZeilen})`);
+ ok(vorher.altKopf === null,
+ `und es gibt noch keine aeltere Gruppe (${vorher.altKopf})`);
+
+ await m5.s.click(".vorlagenbrett__antwort-weg");
+ await m5.s.waitForTimeout(1200);
+ const danach = await stand();
+ /* EINE WENIGER, NICHT KEINE. Mein erster Anlauf erwartete hier
+ null -- Frida hat aber ZWEI Antworten (eine Zusage von
+ weiter oben und diese Absage), und „Verstanden" gilt je
+ Zeile. Die Pruefung hatte recht, die Erwartung war falsch;
+ dass der Knopf nur seine eigene Zeile nimmt, ist genau das
+ gewollte Verhalten und steht jetzt hier als Aussage. */
+ ok(danach.neuZeilen === vorher.neuZeilen - 1,
+ `nach „Verstanden" ist genau diese eine Zeile weg `
+ + `(${vorher.neuZeilen} → ${danach.neuZeilen})`);
+ /* SIE IST NICHT WEG, SIE IST NUR ZU. Eine Absage samt
+ Begruendung zu loeschen, weil jemand sie einmal gelesen
+ hat, waere das Gegenteil von dem, was am 30.09. gebaut
+ wurde. */
+ ok(/1 ältere Antwort/.test(danach.altKopf || ""),
+ `sondern hinter einer zugeklappten Zeile („${danach.altKopf}")`);
+ ok(danach.altZeilen === 0,
+ ` und zugeklappt wird sie gar nicht erst gebaut (${danach.altZeilen})`);
+
+ await m5.s.click(".vorlagenbrett__antwort-kopf");
+ await m5.s.waitForTimeout(800);
+ const auf = await m5.s.evaluate(() => {
+ const altB = document.querySelector('.vorlagenbrett__antwort[data-alt="ja"]');
+ return {
+ zeilen: altB ? altB.querySelectorAll(".vorlagenbrett__antwort-zeile").length : 0,
+ text: altB ? (altB.innerText || "").replace(/\s+/g, " ").trim() : "",
+ };
+ });
+ ok(auf.zeilen === 1, `aufgeklappt steht sie wieder da (${auf.zeilen})`);
+ ok(/schon drei offene/.test(auf.text),
+ "mitsamt der Begruendung — nachlesen geht weiter");
+
+ /* ---- UND DER KNOPF FUER ALLE AUF EINMAL ----------------
+ Wer nach dem Urlaub sieben Antworten vorfindet, soll nicht
+ siebenmal tippen. Er steht erst ab zwei da -- bei einer
+ waere er dasselbe wie der daneben, nur laenger. */
+ await m5.s.click(".vorlagenbrett__antwort-kopf"); /* wieder zu */
+ await m5.s.waitForTimeout(500);
+ const vorAlle = await m5.s.evaluate(() =>
+ !!document.querySelector(".vorlagenbrett__antwort-alle"));
+ if (vorAlle) {
+ await m5.s.click(".vorlagenbrett__antwort-alle");
+ await m5.s.waitForTimeout(1200);
+ } else {
+ /* Nur noch eine offen -- dann nimmt sie ihr eigener Knopf. */
+ await m5.s.click(".vorlagenbrett__antwort-weg");
+ await m5.s.waitForTimeout(1200);
+ }
+ const leer = await stand();
+ ok(leer.neuZeilen === 0,
+ `danach steht oben gar nichts mehr (${leer.neuZeilen})`);
+ ok(/2 ältere Antworten/.test(leer.altKopf || ""),
+ `und beide stehen in der zugeklappten Gruppe („${leer.altKopf}")`);
+
+ /* UND DER SERVER HAT ES SICH GEMERKT. */
+ await m5.s.reload({ waitUntil: "networkidle" });
+ await m5.s.waitForTimeout(1800);
+ const nachLaden = await stand();
+ ok(nachLaden.neuZeilen === 0 && /2 ältere Antworten/.test(nachLaden.altKopf || ""),
+ `nach dem Neuladen gelten sie weiter als gelesen („${nachLaden.altKopf}")`);
+ /* UND SIE IST WIEDER ZU. Das Aufklappen ist eine Frage des
+ Augenblicks, kein Zustand, den man mitschleppt. */
+ ok(nachLaden.altZeilen === 0,
+ `und wieder zugeklappt (${nachLaden.altZeilen} Zeilen)`);
} finally { await m5.ctx.close(); }
+ /* GEGENPROBE AM SERVER: Eine fremde Antwort laesst sich nicht
+ als gelesen melden -- und eine, auf die noch niemand
+ geantwortet hat, auch nicht. Ohne diese beiden waere
+ „gesehen" ein Knopf, mit dem man in fremden Zeilen schreibt. */
+ {
+ const fremd = await roh("/workspace/api/vorlagen/bewerbung/gesehen", {
+ methode: "POST", keks: hand.keks, body: { id: bw2?.id } });
+ ok(fremd.code === 404,
+ `eine fremde Antwort kann niemand als gelesen melden (${fremd.code})`);
+
+ const erfunden = await roh("/workspace/api/vorlagen/bewerbung/gesehen", {
+ methode: "POST", keks: modi.keks, body: { id: 999999 } });
+ ok(erfunden.code === 404, `eine erfundene Nummer auch nicht (${erfunden.code})`);
+
+ /* UND ZWEIMAL „VERSTANDEN" AENDERT NICHTS MEHR. */
+ const nochmal = await roh("/workspace/api/vorlagen/bewerbung/gesehen", {
+ methode: "POST", keks: modi.keks, body: { id: bw2?.id } });
+ ok(nochmal.code === 200 && nochmal.json?.wieviele === 0,
+ `ein zweites „Verstanden" zaehlt nicht noch einmal (${nochmal.json?.wieviele})`);
+ }
+
const h2 = await seiteAls("hand", "CODE-HAND-0001", 1280);
try {
await h2.s.goto(`${SEITE}/workspace/entwicklung.html`, { waitUntil: "networkidle" });
diff --git a/server/workspace-vorlagen.js b/server/workspace-vorlagen.js
index 9b6944f8..b1ca6f88 100644
--- a/server/workspace-vorlagen.js
+++ b/server/workspace-vorlagen.js
@@ -1678,7 +1678,7 @@ 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
+ b.entschieden_am, b.aufgabe_id, b.gesehen_am, e.name AS von_name
FROM vorlagen_bewerbungen b
LEFT JOIN personen e ON e.id = b.entschieden_von
WHERE b.person_id = ?
@@ -1686,7 +1686,16 @@ function vorlagenAntwortenFuer(person) {
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 }));
+ /* `gesehen` ALS JA/NEIN und nicht der Zeitstempel: Die
+ Oberflaeche fragt „stand das schon offen da?", nicht „wann".
+ Ein Datum dorthin zu schicken hiesse, dass sie es
+ vergleichen muss -- und beim naechsten Mal anders. */
+ .map((z) => ({
+ ...z,
+ gesehen: !!z.gesehen_am,
+ gesehen_am: undefined,
+ titel: vorlageZuSchluessel(z.vorlage)?.titel || z.vorlage,
+ }));
} catch (fehler) {
console.error("[vorlagen] Antworten:", fehler?.message);
return [];
@@ -1979,6 +1988,60 @@ vorlagenRouter.post("/workspace/api/vorlagen/bewerbung-zurueck", gleicheHerkunft
}
});
+/* ---- „Verstanden" -- die Antwort zur Kenntnis nehmen -----------------
+
+ VanVan im Support, Meldung #15: „Die Modis können die Antworten auf
+ die Bewerbungen im Bereich eure Aufgaben noch nicht einklappen. Das
+ wird mit der Zeit unübersichtlich."
+
+ NUR DIE EIGENEN, und nur die BEANTWORTETEN. Eine Bewerbung, auf die
+ noch niemand geantwortet hat, kann man nicht gelesen haben -- der
+ Versuch bekommt deshalb 404 und nicht etwa ein stilles „ok".
+
+ EINMAL IST EINMAL: Ein zweiter Druck aendert nichts. `gesehen_am`
+ wird nur gesetzt, wo es noch leer ist -- sonst wanderte die Zeile
+ bei jedem Klick ans Ende der Liste und man saehe nicht mehr, wann
+ man sie wirklich gelesen hat.
+
+ `alle: true` NIMMT ALLE AUF EINMAL. Wer nach dem Urlaub sieben
+ Antworten vorfindet, soll nicht siebenmal tippen muessen -- und
+ sieben einzelne Anfragen waeren sieben Gelegenheiten, dass eine
+ davon unterwegs verlorengeht. */
+vorlagenRouter.post("/workspace/api/vorlagen/bewerbung/gesehen", gleicheHerkunft,
+ express.json({ limit: "2kb" }), (req, res) => {
+ try {
+ const nun = jetzt();
+ if (req.body?.alle === true) {
+ const n = db().prepare(`UPDATE vorlagen_bewerbungen
+ SET gesehen_am = ?
+ WHERE person_id = ? AND gesehen_am IS NULL
+ AND zustand IN ('angenommen','abgelehnt')`)
+ .run(nun, req.person.id).changes;
+ return res.json({ ok: true, wieviele: n,
+ antworten: vorlagenAntwortenFuer(req.person) });
+ }
+ const id = Number(req.body?.id);
+ if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
+ const b = db().prepare("SELECT * FROM vorlagen_bewerbungen WHERE id = ?").get(id);
+ /* NICHT MEINE = GIBT ES NICHT -- dieselbe Antwort wie beim
+ Zurueckziehen eine Route weiter oben. Ein 403 verriete, dass
+ es sie gibt und wem sie gehoert. */
+ if (!b || b.person_id !== req.person.id
+ || !["angenommen", "abgelehnt"].includes(b.zustand)) {
+ return res.status(404).json({ fehler: "nicht_gefunden" });
+ }
+ if (!b.gesehen_am) {
+ db().prepare("UPDATE vorlagen_bewerbungen SET gesehen_am = ? WHERE id = ?")
+ .run(nun, id);
+ }
+ res.json({ ok: true, wieviele: b.gesehen_am ? 0 : 1,
+ antworten: vorlagenAntwortenFuer(req.person) });
+ } catch (f) {
+ console.error("[vorlagen] bewerbung/gesehen:", f?.message);
+ res.status(503).json({ fehler: "nicht_verfuegbar" });
+ }
+ });
+
/* ---- Entscheiden: annehmen oder ablehnen ------------------------------
EINE SCHLEIFE FUER BEIDE WEGE. Zwei fast gleiche Routen waeren zwei
diff --git a/server/workspace.js b/server/workspace.js
index 4ec28e0b..5a5ab76c 100644
--- a/server/workspace.js
+++ b/server/workspace.js
@@ -4384,6 +4384,40 @@ function umstellungen(d) {
console.error("[workspace] Spalte dauerhaft:", fehler?.message);
}
+ /* ==== GELESEN IST GELESEN (03.10.2026) =============================
+
+ VanVan im Support, Meldung #15: „Die Modis können die Antworten
+ auf die Bewerbungen im Bereich eure Aufgaben noch nicht
+ einklappen. Das wird mit der Zeit unübersichtlich."
+
+ Am 30.09. habe ich diese Antworten absichtlich NICHT einklappbar
+ gemacht, mit der Begruendung: „Eine Antwort, die man erst
+ aufklappen muss, ist wieder keine." Das stimmt -- fuer eine
+ Antwort, die man noch nicht gelesen hat. Danach ist es falsch
+ herum, und genau das meldet sie.
+
+ DESHALB NICHT „EINKLAPPBAR", SONDERN „GELESEN". Ein Schalter,
+ der alles wegklappt, haette die naechste Absage mitversteckt --
+ die Zeile, die man unbedingt sehen soll. Eine Antwort, die man
+ mit „Verstanden" quittiert hat, rutscht dagegen hinter eine
+ zugeklappte Zeile und bleibt nachlesbar. Beides zusammen loest
+ ihr Problem UND behaelt den Grund von vorgestern.
+
+ AM MENSCHEN UND NICHT AM GERAET. Sie haette auch in den
+ Browserspeicher gekonnt; dann waere dieselbe Antwort auf dem
+ Handy wieder neu. „Habe ich das gelesen?" ist eine Frage ueber
+ die Person, nicht ueber das Geraet. */
+ try {
+ const spalten = d.prepare("PRAGMA table_info(vorlagen_bewerbungen)").all()
+ .map((x) => x.name);
+ if (spalten.length && !spalten.includes("gesehen_am")) {
+ d.exec("ALTER TABLE vorlagen_bewerbungen ADD COLUMN gesehen_am TEXT");
+ console.log("[workspace] Spalte gesehen_am an vorlagen_bewerbungen angelegt.");
+ }
+ } catch (fehler) {
+ console.error("[workspace] Spalte gesehen_am:", fehler?.message);
+ }
+
/* =====================================================================
WER VERANTWORTLICH IST, BRAUCHT EINE ZUTEILUNGSZEILE (30.09.2026)
=====================================================================
@@ -5480,6 +5514,11 @@ export function db() {
entschieden_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
entschieden_am TEXT,
aufgabe_id INTEGER,
+ /* Wann die Person die Antwort darauf zur Kenntnis genommen
+ hat (03.10.2026, VanVan #15). NULL heisst: noch nicht --
+ dann steht sie offen da. Siehe die ausfuehrliche
+ Begruendung bei der Nachruestung weiter oben. */
+ gesehen_am TEXT,
erstellt TEXT NOT NULL
);
/* EINMAL BEWERBEN, NICHT DREIMAL -- und zwar von der Datenbank
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index 1908039b..2c7e85b0 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`. -->
-
-
+
+
-
+