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`. --> - - + + - +