diff --git a/server/pruef-modi-katalog.mjs b/server/pruef-modi-katalog.mjs index 9af1bb0b..82fd2a82 100644 --- a/server/pruef-modi-katalog.mjs +++ b/server/pruef-modi-katalog.mjs @@ -76,6 +76,14 @@ const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001"); const idMan = anlegen("Schulle", "manager", "CODE-MAN-0001"); const idCreat = anlegen("Tili", "creator", "CODE-ANNA-0001"); const idModi = anlegen("Marina", "modi", "CODE-MODI-0001"); +/* DIE BEIDEN HAENDE (23.09.2026). Ohne sie laesst sich die Grenze, die + Filipe gezogen hat, gar nicht messen: "nur dogfather und die rechte + hand sollen annehmen oder ablehnen koennen". Die linke Hand darf + verteilen -- und genau deshalb ist sie die schaerfste Gegenprobe: + Wer nur "darf verteilen" prueft, laesst sie mitentscheiden. */ +const idHand = anlegen("Rieke", "hand", "CODE-HAND-0001"); +const idLinke = anlegen("Funny", "linke", "CODE-LINK-0001"); +void idHand; void idLinke; d.close(); void idDogi; void idMan; @@ -100,6 +108,10 @@ const modiAn = await anmelden("creator", "CODE-MODI-0001"); const dogiAn = await anmelden("admin", "CODE-DOGI-0001"); const manAn = await anmelden("manager", "CODE-MAN-0001"); const creatAn = await anmelden("creator", "CODE-ANNA-0001"); +/* Ueber den stillen Zugang -- welche Kachel man anfasst, ist + gleichgueltig, der Code allein entscheidet (siehe die Anmelderoute). */ +const handAn = await anmelden("creator", "CODE-HAND-0001"); +const linkeAn = await anmelden("creator", "CODE-LINK-0001"); /* ---------- 1. Ist er vollstaendig da? ------------------------------- */ console.log("\n=== 1. Der Katalog, wie ihn das Dokument beschreibt ===\n"); @@ -196,42 +208,223 @@ for (const [name, an] of [["Manager", manAn], ["Creator", creatAn]]) { "und der Modi bekommt umgekehrt den Creator-Katalog nicht"); } -/* ---------- 3. Uebernehmen ------------------------------------------- */ -console.log("\n=== 3. Aus der Vorlage wird eine echte Aufgabe ===\n"); +/* ---------- 3. Ein Modi kann sich nur BEWERBEN ------------------------ + + Filipe, 23.09.2026: "die modis sollen bei all diesen voschlaegen + auch nur bewerben koennen. die aufgaben aus der vorlage, da sollen + die modis sich nur bewerben koennen und nur dogfather und die rechte + hand sollen annehmen oder ablehnen koennen, mit einem text als + notiz." + + BIS HEUTE STAND HIER DAS GEGENTEIL: "uebernommen (200)", "und er + selbst ist verantwortlich". Diese Pruefung ist beim Umbau ROT + geworden -- richtig so, sie hat die Aenderung bemerkt. + + DREI DINGE WERDEN GEMESSEN, und das dritte ist das, was Filipe + ausdruecklich wollte: + 1. Der Modi kommt an die alte Tuer nicht mehr heran. + 2. Die Bewerbung kommt an -- und legt NOCH KEINE Aufgabe an. + 3. Erst die Zusage macht daraus Arbeit, mit Notiz daran. + --------------------------------------------------------------------- */ +console.log("\n=== 3. Ein Modi kann sich nur bewerben ===\n"); + +let bewerbungsNr = null, bewerbungsVorlage = null; { const nr = katalog.aufgaben.findIndex((v) => v.gruppe === "chat"); const vorlage = katalog.aufgaben[nr]; - const a = await hole(modiAn.keks, "/workspace/api/vorlagen/uebernehmen", { + bewerbungsVorlage = vorlage; + + /* ---- 1. Die alte Tuer ist zu -------------------------------------- */ + const alt = await hole(modiAn.keks, "/workspace/api/vorlagen/uebernehmen", { method: "POST", headers: jsonKopf, body: JSON.stringify({ art: "katalog", nr, gruppe: "chat" }), }); - ok(a.status === 200 && a.daten?.angelegt === 1, `übernommen (${a.status})`); + ok(alt.status === 403, `selbst uebernehmen geht nicht mehr (${alt.status})`); + /* DER SATZ IST EIN WEGWEISER, KEINE ABSAGE. Wer "Das darfst du + nicht" liest, haelt sich fuer ausgeschlossen -- es gibt aber einen + Weg, er fuehrt nur ueber jemanden. */ + ok(alt.daten?.fehler === "erst_bewerben", + ` und sagt, was stattdessen geht (${alt.daten?.fehler})`); - const b = await hole(modiAn.keks, "/workspace/api/aufgaben"); - const neu = (b.daten?.aufgaben || []).find((x) => x.vorlage === vorlage.schluessel); - ok(!!neu, `sie liegt auf seinem Brett (${neu?.titel || "nicht gefunden"})`); - /* DIE KATEGORIE IST DER HALBE WERT DER VORLAGE. Ginge sie unterwegs - verloren, muesste man 60-mal von Hand einsortieren, was im Dokument - danebensteht -- und niemand merkte es, weil die Aufgabe ja da ist. */ - /* DIE KATEGORIE IST DER HALBE WERT DER VORLAGE -- und sie heisst im - ausgelieferten Katalog `gruppe`, weil die Oberflaeche damit - zeichnet. Ginge sie unterwegs verloren, muesste man von Hand - einsortieren, was danebensteht. */ - ok(neu?.kategorie === vorlage.gruppe, - `mit ihrer Kategorie (${neu?.kategorie} / erwartet ${vorlage.gruppe})`); - ok(!!neu?.frist, `und mit Frist (${neu?.frist})`); - ok(neu?.verantwortlich_id === idModi, "und er selbst ist verantwortlich"); + const keine = await hole(modiAn.keks, "/workspace/api/aufgaben"); + const gibtsNicht = (keine.daten?.aufgaben || []) + .some((x) => x.vorlage === vorlage.schluessel); + ok(!gibtsNicht, " und es liegt nichts auf seinem Brett"); + + /* ---- 2. Die Bewerbung ---------------------------------------------- */ + const b = await hole(modiAn.keks, "/workspace/api/vorlagen/bewerben", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ vorlage: vorlage.schluessel, text: "ich habe Freitag Zeit" }), + }); + ok(b.status === 201, `die Bewerbung kommt an (${b.status})`); + + /* DAS IST DER KERN DER BAUWEISE: Bis jemand ja sagt, entsteht keine + Arbeit. Waere die Aufgabe schon da, stuenden nach zwoelf Absagen + zwoelf herrenlose Aufgaben auf dem Brett -- und das Ablehnen + muesste Aufgaben LOESCHEN, um das wieder einzufangen. */ + const nochNicht = await hole(modiAn.keks, "/workspace/api/aufgaben"); + ok(!(nochNicht.daten?.aufgaben || []).some((x) => x.vorlage === vorlage.schluessel), + " und legt noch KEINE Aufgabe an — das tut erst die Zusage"); + + const meine = await hole(modiAn.keks, "/workspace/api/vorlagen"); + const meineB = (meine.daten?.bewerbungen || []) + .find((x) => x.vorlage === vorlage.schluessel); + ok(!!meineB, "er sieht seine eigene Bewerbung"); + ok(meineB?.selbst === true && meineB?.text === "ich habe Freitag Zeit", + ` samt seinem Wort dazu ("${meineB?.text}")`); + ok(meine.daten?.darf_entscheiden === false, + "und der Server sagt ihm, dass er nicht entscheidet"); + bewerbungsNr = meineB?.id ?? null; + + /* Zweimal dieselbe Bitte ist eine Bitte. */ + const doppelt = await hole(modiAn.keks, "/workspace/api/vorlagen/bewerben", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ vorlage: vorlage.schluessel }), + }); + ok(doppelt.status === 409 && doppelt.daten?.fehler === "schon_beworben", + `zweimal dieselbe Bewerbung wird abgewiesen (${doppelt.status})`); } -/* Eine ganze Etappe auf einmal. */ +/* ---------- 3b. Wer entscheidet -- und wer nicht ---------------------- + + DIE LINKE HAND IST HIER DIE SCHAERFSTE PROBE. Sie darf verteilen und + gehoert sonst ueberall dazu; Filipe hat bei der Entscheidung aber + genau zwei genannt. Wer statt "darf entscheiden" nur "darf + verteilen" prueft, laesst sie durch -- und niemand merkt es, weil + alles funktioniert. + --------------------------------------------------------------------- */ +console.log("\n=== 3b. Wer über die Bewerbung entscheidet ===\n"); { - const wieViele = katalog.aufgaben.filter((v) => v.gruppe === "technik").length; - const a = await hole(modiAn.keks, "/workspace/api/vorlagen/uebernehmen", { + ok(Number.isInteger(bewerbungsNr), `die Bewerbung hat eine Nummer (${bewerbungsNr})`); + + const alsLinke = await hole(linkeAn.keks, "/workspace/api/vorlagen"); + ok(alsLinke.daten?.darf_verteilen === true, + "die linke Hand darf verteilen (sie bekommt den Übernehmen-Weg)"); + ok(alsLinke.daten?.darf_entscheiden === false, + " aber NICHT entscheiden — woertlich nach Filipes Aufzaehlung"); + const versuch = await hole(linkeAn.keks, "/workspace/api/vorlagen/bewerbung/annehmen", { method: "POST", headers: jsonKopf, - body: JSON.stringify({ art: "katalog_alle", gruppe: "technik" }), + body: JSON.stringify({ id: bewerbungsNr, kommentar: "mach ich" }), }); - ok(a.status === 200 && a.daten?.angelegt === wieViele, - `eine ganze Kategorie auf einmal: ${a.daten?.angelegt} von ${wieViele}`); + ok(versuch.status === 403, + ` und am Knopf vorbei geht es auch nicht (${versuch.status})`); + + /* GEGENPROBE: Der Modi selbst erst recht nicht -- sonst naehme er + seine eigene Bewerbung an. */ + const selbst = await hole(modiAn.keks, "/workspace/api/vorlagen/bewerbung/annehmen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ id: bewerbungsNr }), + }); + ok(selbst.status === 403, `und der Bewerber selbst schon gar nicht (${selbst.status})`); + + /* Die rechte Hand sieht sie -- mit Namen, denn sie entscheidet ueber + einen Menschen und nicht ueber einen Listeneintrag. */ + const alsHand = await hole(handAn.keks, "/workspace/api/vorlagen"); + const sieht = (alsHand.daten?.bewerbungen || []).find((x) => x.id === bewerbungsNr); + ok(alsHand.daten?.darf_entscheiden === true, "die rechte Hand entscheidet"); + ok(!!sieht && sieht.person_name === "Marina", + ` und sieht, wer sich beworben hat (${sieht?.person_name})`); + ok(sieht?.selbst === false, " als fremde Bewerbung, nicht als eigene"); +} + +/* ---------- 3c. Die Zusage macht daraus Arbeit ------------------------ */ +console.log("\n=== 3c. Annehmen und ablehnen — mit Notiz ===\n"); +{ + const a = await hole(handAn.keks, "/workspace/api/vorlagen/bewerbung/annehmen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ id: bewerbungsNr, kommentar: "danke dir, mach das gern" }), + }); + ok(a.status === 200 && a.daten?.zustand === "angenommen", + `die rechte Hand sagt zu (${a.status})`); + ok(Number.isInteger(a.daten?.aufgabe_id), + ` und JETZT entsteht die Aufgabe (#${a.daten?.aufgabe_id})`); + + const brett = await hole(modiAn.keks, "/workspace/api/aufgaben"); + const neu = (brett.daten?.aufgaben || []) + .find((x) => x.vorlage === bewerbungsVorlage.schluessel); + ok(!!neu, "sie liegt auf seinem Brett"); + /* SIE MUSS AUSSEHEN WIE EINE VERTEILTE. Ein zweiter Weg zum Anlegen + waere ein zweiter Satz Regeln -- deshalb geht die Zusage durch + dieselbe Funktion wie das Uebernehmen. */ + ok(neu?.kategorie === bewerbungsVorlage.gruppe, + `mit ihrer Kategorie (${neu?.kategorie} / erwartet ${bewerbungsVorlage.gruppe})`); + ok(!!neu?.frist, `und mit Frist (${neu?.frist})`); + ok(neu?.verantwortlich_id === idModi, "und der Bewerber ist verantwortlich"); + + /* Die Bewerbung ist damit vom Tisch -- sonst entschiede gleich noch + jemand ein zweites Mal darueber. */ + const danach = await hole(handAn.keks, "/workspace/api/vorlagen"); + ok(!(danach.daten?.bewerbungen || []).some((x) => x.id === bewerbungsNr), + "und sie wartet nicht mehr auf eine Antwort"); + const nochmal = await hole(handAn.keks, "/workspace/api/vorlagen/bewerbung/annehmen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ id: bewerbungsNr }), + }); + ok(nochmal.status === 409 && nochmal.daten?.fehler === "keine_bewerbung", + `zweimal entscheiden geht nicht (${nochmal.status})`); + + /* ---- UND DIE ABSAGE, mit Notiz ------------------------------------ */ + const nr2 = katalog.aufgaben.findIndex((v) => v.gruppe === "events"); + const v2 = katalog.aufgaben[nr2]; + await hole(modiAn.keks, "/workspace/api/vorlagen/bewerben", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ vorlage: v2.schluessel }), + }); + const offen2 = (await hole(handAn.keks, "/workspace/api/vorlagen")) + .daten?.bewerbungen?.find((x) => x.vorlage === v2.schluessel); + ok(!!offen2, "eine zweite Bewerbung liegt vor"); + ok(offen2?.text === null, + " auch ohne Wort dazu — der Text ist freiwillig, wie bei den Aufgaben"); + + const ab = await hole(handAn.keks, "/workspace/api/vorlagen/bewerbung/ablehnen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ id: offen2.id, kommentar: "das macht diesmal jemand anders" }), + }); + ok(ab.status === 200 && ab.daten?.zustand === "abgelehnt", + `und wird abgelehnt (${ab.status})`); + ok(ab.daten?.aufgabe_id === null, + " ohne dass Arbeit entsteht, die niemand bestellt hat"); + + /* DIE NOTIZ BLEIBT STEHEN -- das ist der Teil, den Filipe + ausdruecklich genannt hat ("mit einem text als notiz"). Gelesen + wird direkt aus der Datenbank: Ein Feld, das der Server zwar + annimmt, aber nirgends speichert, saehe von aussen genauso aus. */ + const { DatabaseSync: DB2 } = await import("node:sqlite"); + const d2 = new DB2(process.env.WORKSPACE_DB, { readOnly: true }); + const zeilen = d2.prepare(`SELECT zustand, entscheid_text, entschieden_von, aufgabe_id + FROM vorlagen_bewerbungen ORDER BY id`).all(); + d2.close(); + const zu = zeilen.find((z) => z.zustand === "angenommen"); + const weg = zeilen.find((z) => z.zustand === "abgelehnt"); + ok(zu?.entscheid_text === "danke dir, mach das gern", + `die Notiz der Zusage steht in der Datenbank ("${zu?.entscheid_text}")`); + ok(weg?.entscheid_text === "das macht diesmal jemand anders", + `und die der Absage auch ("${weg?.entscheid_text}")`); + ok(zu?.entschieden_von === idHand && weg?.entschieden_von === idHand, + "bei beiden steht, WER entschieden hat"); + ok(Number.isInteger(zu?.aufgabe_id) && weg?.aufgabe_id === null, + "und nur an der Zusage haengt eine Aufgabe"); + + /* ---- Nach einer Absage darf man es wieder versuchen ---------------- */ + const nochmalBewerben = await hole(modiAn.keks, "/workspace/api/vorlagen/bewerben", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ vorlage: v2.schluessel, text: "doch noch Zeit" }), + }); + ok(nochmalBewerben.status === 201, + `nach einer Absage kann man sich wieder bewerben (${nochmalBewerben.status})`); + /* GEGENPROBE zur Sperre oben: Waere der Index ueber ALLE Zustaende + eindeutig, kaeme hier 409 -- und eine Absage waere ein Bann. */ + const wiederOffen = (await hole(handAn.keks, "/workspace/api/vorlagen")) + .daten?.bewerbungen?.filter((x) => x.vorlage === v2.schluessel).length; + ok(wiederOffen === 1, `und sie wartet wieder auf Antwort (${wiederOffen})`); + + /* Und auf etwas, das man schon hat, bewirbt man sich nicht. */ + const doppelt = await hole(modiAn.keks, "/workspace/api/vorlagen/bewerben", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ vorlage: bewerbungsVorlage.schluessel }), + }); + ok(doppelt.status === 409 && doppelt.daten?.fehler === "liegt_schon_bei_dir", + `auf etwas, das schon auf dem Brett liegt, bewirbt man sich nicht (${doppelt.status})`); } /* ---------- 4. DogFather traegt fuer jemanden ein -------------------- */ @@ -406,6 +599,60 @@ console.log("\n=== 6. Das Brett im echten Browser ===\n"); gesucht wurde -- etwa wenn `innerText` leer bliebe. */ ok(ersatzschreibungen(`${bild.text} waehrend`).length === 1, "und ein eingeschmuggeltes waehrend wuerde auffallen"); + /* ==== AM BILDSCHIRM: BEWERBEN STATT UEBERNEHMEN (23.09.2026) === + + Filipe: „die modis sollen bei all diesen voschlaegen auch nur + bewerben koennen." + + ALLES BISHERIGE HAT DEN SERVER GEPRUEFT. Hier geht es um die + andere Haelfte: Steht der richtige Knopf da? Ein Modi, der + „Uebernehmen" liest und dann eine Absage bekommt, haelt die + Seite fuer kaputt. */ + const knoepfe = await seite.evaluate(() => + [...document.querySelectorAll(".vorlagenbrett__karte .knopf")] + .map((k) => k.textContent.trim())); + /* DIE ZAHL GEHOERT IN DIE BEDINGUNG. Bei null Karten waere + `every()` wahr, ohne einen Knopf angesehen zu haben -- genau der + Fehler, der am 01.09. dreimal gruen dastand. */ + ok(knoepfe.length >= 10 && knoepfe.every((t) => t === "Bewerben"), + `alle ${knoepfe.length} Knoepfe heissen "Bewerben" (${[...new Set(knoepfe)].join(", ")})`); + /* GEGENPROBE: „Uebernehmen" kommt nirgends mehr vor -- weder als + Knopf noch als „Alle 12 uebernehmen" darunter. */ + const uebernahme = await seite.evaluate(() => + [...document.querySelectorAll("#vorlagenbrett button")] + .filter((k) => /übernehmen/i.test(k.textContent)).length); + ok(uebernahme === 0, + `Gegenprobe: kein einziger Uebernehmen-Knopf mehr (${uebernahme})`); + + /* Die schon beantragte Karte sagt, dass sie wartet -- und WER + antwortet. Sonst wartet man auf niemanden. + + ERST ZUR RICHTIGEN KATEGORIE. Die offene Bewerbung liegt bei + „Events", das Brett startet bei der ersten Kategorie. Ohne + diesen Klick maesse die Pruefung eine Karte, die gar nicht auf + dem Bildschirm steht -- und waere rot, ohne dass etwas kaputt + ist. (Beim Bauen genau so passiert.) */ + await seite.evaluate(() => { + const reihe = document.querySelector('.vorlagenbrett .stufenleiter'); + const k = [...(reihe?.querySelectorAll('button') || [])] + .find((b) => /Events/i.test(b.textContent)); + if (k) k.click(); + }); + await seite.waitForTimeout(700); + const wartet = await seite.evaluate(() => { + const w = document.querySelector(".vorlagenbrett__warte"); + const karte = w?.closest(".vorlagenbrett__karte"); + return w ? { + satz: w.textContent.trim(), + zurueck: !![...karte.querySelectorAll("button")] + .find((b) => /zurückziehen/i.test(b.textContent)), + } : null; + }); + ok(!!wartet, "die Karte, auf die er sich beworben hat, sagt es"); + ok(/rechte hand/i.test(wartet?.satz || ""), + ` und nennt, wer antwortet ("${wartet?.satz}")`); + ok(wartet?.zurueck === true, " mit einem Weg zurueck"); + ok(konsole.length === 0, konsole.length ? `Fehler in der Konsole: ${konsole[0]}` : "keine Fehler in der Konsole"); /* DER PFAD GEHOERT AN DIESE DATEI, nicht ans Arbeitsverzeichnis. @@ -528,6 +775,85 @@ console.log(""); ok(danach.karten === vorher + 1, "die Aufgabe liegt auf dem Brett (" + vorher + " -> " + danach.karten + ")"); ok(danach.fuerMarina > 0, "und zwar bei Marina (" + danach.fuerMarina + ")"); + + /* ==== DIE BEWERBUNG AM BILDSCHIRM ENTSCHEIDEN (23.09.2026) ====== + + Filipe: „nur dogfather und die rechte hand sollen annehmen oder + ablehnen koennen, mit einem text als notiz." + + DER SERVERTEIL IST OBEN GEPRUEFT. Hier geht es um das, was ein + Mensch tut: Sieht er die Bewerbung, steht der Name dabei, und + bekommt er ein Feld fuer seine Notiz? + + ZUR RICHTIGEN KATEGORIE -- die offene Bewerbung liegt bei + „Events". Ohne den Klick maesse die Pruefung eine Karte, die gar + nicht auf dem Bildschirm steht. */ + await seite.evaluate(() => { + const reihe = document.querySelector(".vorlagenbrett .stufenleiter"); + const k = [...(reihe?.querySelectorAll("button") || [])] + .find((b) => /Events/i.test(b.textContent)); + if (k) k.click(); + }); + await seite.waitForTimeout(800); + + const bw = await seite.evaluate(() => { + const reihe = document.querySelector(".vorlagenbrett__bewerbung"); + if (!reihe) return null; + const karte = reihe.closest(".vorlagenbrett__karte"); + return { + name: reihe.querySelector(".vorlagenbrett__bname")?.textContent.trim() || "", + wort: reihe.querySelector(".vorlagenbrett__bwort")?.textContent.trim() || "", + schild: karte.querySelector(".vorlagenbrett__bschild")?.textContent.trim() || "", + knoepfe: [...reihe.querySelectorAll("button")].map((b) => b.textContent.trim()), + auffaellig: karte.dataset.beworben === "ja", + titel: karte.querySelector(".vorlagenbrett__titel")?.textContent.trim() || "", + }; + }); + ok(!!bw, "DogFather sieht die Bewerbung an der Karte"); + ok(bw?.name === "Marina", `mit dem Namen dessen, der sie geschrieben hat (${bw?.name})`); + /* MAN ENTSCHEIDET UEBER EINEN MENSCHEN, nicht ueber einen + Listeneintrag -- deshalb stehen beide Knoepfe an SEINER Zeile. */ + ok(JSON.stringify(bw?.knoepfe) === JSON.stringify(["Annehmen", "Ablehnen"]), + `und beiden Wegen daneben (${(bw?.knoepfe || []).join(", ")})`); + ok(bw?.wort.includes("doch noch Zeit"), + `sein Wort dazu steht dabei (${bw?.wort})`); + /* Eine Karte mit Bewerbung ist der einzige Grund, dieses Brett + gerade zu oeffnen -- sie darf auffallen. */ + ok(bw?.auffaellig === true, "und die Karte faellt auf"); + + /* ---- Jetzt wirklich druecken, samt Notiz ---------------------- */ + const vorherK = await seite.evaluate(() => document.querySelectorAll(".karte").length); + await seite.evaluate(() => { + [...document.querySelectorAll(".vorlagenbrett__bewerbung button")] + .find((b) => b.textContent.trim() === "Annehmen")?.click(); + }); + await seite.waitForTimeout(600); + /* DER KASTEN MUSS BEANTWORTET WERDEN. Beim ersten Anlauf einer + aehnlichen Pruefung habe ich ihn stehen lassen und dann gemeldet, + der Weg sei kaputt -- er war es nie, ich hatte nur nicht geklickt. */ + const kastenDa = await seite.evaluate(() => + !!document.querySelector("dialog.nachfrage[open]")); + ok(kastenDa, "es kommt ein Kasten für die Notiz — kein stilles Durchdrücken"); + const feldDa = await seite.evaluate(() => { + const f = document.getElementById("nachfrage-grund"); + return !!f && !f.closest("[hidden]"); + }); + ok(feldDa, " mit einem Feld für den Text — „mit einem text als notiz“"); + await seite.fill("#nachfrage-grund", "danke dir, mach das gern"); + await seite.click('dialog.nachfrage [data-teil="ja"]'); + await seite.waitForTimeout(2200); + + const nachher = await seite.evaluate(() => ({ + karten: document.querySelectorAll(".karte").length, + nochBeworben: document.querySelectorAll(".vorlagenbrett__bewerbung").length, + })); + ok(nachher.karten === vorherK + 1, + `die Aufgabe entsteht mit der Zusage (${vorherK} -> ${nachher.karten})`); + /* UND DIE BEWERBUNG IST VOM TISCH. Bliebe sie stehen, entschiede + gleich noch jemand ein zweites Mal ueber dieselbe Bitte. */ + ok(nachher.nochBeworben === 0, + `und die Bewerbung wartet nicht mehr (${nachher.nochBeworben})`); + ok(konsole.length === 0, konsole.length ? "Fehler in der Konsole: " + konsole[0].slice(0, 80) : "keine Fehler in der Konsole"); @@ -540,16 +866,32 @@ console.log("\n=== 7. Gegenprobe: kann das hier ueberhaupt scheitern? ===\n"); /* Eine Vorlagennummer, die es nicht gibt. Kaeme dafuer ein 200, wuerden die Haken oben nichts beweisen -- dann sagt der Server zu allem ja. */ - const a = await hole(modiAn.keks, "/workspace/api/vorlagen/uebernehmen", { + /* MIT DEM ZUGANG VON DOGFATHER, nicht mit dem des Modis: Seit dem + 23.09. bekommt ein Modi auf dieser Route ohnehin 403 -- die + Gegenprobe haette also ab da nur noch bewiesen, dass die + Rechtepruefung greift, und nichts mehr ueber erfundene Nummern + gesagt. Genau so verliert eine Pruefung still ihren Sinn. */ + const a = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", { method: "POST", headers: jsonKopf, - body: JSON.stringify({ art: "katalog", nr: 9999, gruppe: "aufbau" }), + body: JSON.stringify({ art: "katalog", nr: 9999, gruppe: "aufbau", person_id: idModi }), }); ok(a.status === 400, `eine erfundene Vorlagennummer wird abgelehnt (${a.status})`); - const b = await hole(modiAn.keks, "/workspace/api/vorlagen/uebernehmen", { + const b = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", { method: "POST", headers: jsonKopf, - body: JSON.stringify({ art: "katalog_alle", gruppe: "gibtesnicht" }), + body: JSON.stringify({ art: "katalog_alle", gruppe: "gibtesnicht", person_id: idModi }), }); ok(b.status === 400, `und eine erfundene Etappe ebenso (${b.status})`); + /* Und eine erfundene Bewerbungsnummer. */ + const c = await hole(handAn.keks, "/workspace/api/vorlagen/bewerbung/annehmen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ id: 999999 }), + }); + ok(c.status === 409, `eine erfundene Bewerbung ebenso (${c.status})`); + const e = await hole(modiAn.keks, "/workspace/api/vorlagen/bewerben", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ vorlage: "gibt-es-nicht" }), + }); + ok(e.status === 400, `und eine erfundene Vorlage in der Bewerbung (${e.status})`); } console.log(`\n${fehler ? "FEHLGESCHLAGEN" : "BESTANDEN"} -- ` diff --git a/server/workspace-vorlagen.js b/server/workspace-vorlagen.js index d85a998b..a5bc3b27 100644 --- a/server/workspace-vorlagen.js +++ b/server/workspace-vorlagen.js @@ -36,8 +36,18 @@ import express from "express"; import { - db, sitzungLesen, istLeitung, darfCreator, protokolliere, echteIp, kategorienFuer, - termineSichtbar, MODI_KATEGORIEN, MODI_STUFEN, + db, + sitzungLesen, + istLeitung, + darfCreator, + protokolliere, + echteIp, + kategorienFuer, + termineSichtbar, + MODI_KATEGORIEN, + MODI_STUFEN, + entscheidetUeberAufgaben, + darfAufgabenVerteilen, } from "./workspace.js"; export const vorlagenRouter = express.Router(); @@ -1431,6 +1441,26 @@ vorlagenRouter.get("/workspace/api/vorlagen", (req, res) => { aufbauplan: kategorienFuer(req.person).length ? { gruppen: MODI_GRUPPEN, aufgaben: MODI_KATALOG } : null, + /* ==== DIE BEWERBUNGEN KOMMEN MIT (23.09.2026) ================= + + Sie stehen in DERSELBEN Antwort wie der Katalog und nicht in + einer zweiten Abfrage. Das Brett zeichnet sich bei jedem + Kategoriewechsel neu; eine eigene Abfrage daneben waere ein + zweiter Weg, auf dem die beiden auseinanderlaufen koennen -- + und man saehe eine Karte als "beworben", die es nicht mehr ist. + + WER WAS SIEHT, entscheidet vorlagenBewerbungenFuer(): Wer + entscheidet, bekommt alle offenen; alle anderen nur ihre + eigenen. Im Browser zu filtern hiesse, die fremden Namen + trotzdem auszuliefern. + + UND OB ICH ENTSCHEIDEN DARF, sagt ebenfalls der Server. Die + Oberflaeche vergleicht keine Rollennamen -- assets/js/aufgaben.js + bekommt jeder, der die Seite oeffnet. */ + bewerbungen: kategorienFuer(req.person).length + ? vorlagenBewerbungenFuer(req.person) : null, + darf_entscheiden: entscheidetUeberAufgaben(req.person), + darf_verteilen: darfAufgabenVerteilen(req.person), }); }); @@ -1454,6 +1484,253 @@ function gleicheHerkunft(req, res, next) { next(); } +/* ===================================================================== + AUS EINER VORLAGE WIRD EINE AUFGABE -- an EINER Stelle (23.09.2026) + + Dieselben acht Zeilen standen im Zweig "katalog" der Uebernehmen- + Route. Seit eine Zusage auf eine Bewerbung dasselbe tut, braucht es + sie zweimal -- und zwei Fassungen desselben INSERT sind zwei + Gelegenheiten, dass eine davon beim naechsten Feld nicht mitgeht. + + DIE FRIST WIRD IN ORTSZEIT GERECHNET. Ueber toISOString kaeme + zwischen Mitternacht und zwei Uhr der Vortag heraus -- derselbe + Fehler, der im Haus schon einmal eine Pruefung falsch rot gemacht + hat. + + DIE KENNUNG DER VORLAGE WANDERT MIT. Nur so kann das Brett spaeter + zeigen, was schon uebernommen ist -- der Titel taugt dafuer nicht, + er darf geaendert werden. */ +function katalogAufgabeAnlegen(v, zielId, vonId) { + const d = new Date(Date.now() + (v.tage || 7) * 86400_000); + const frist = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}` + + `-${String(d.getDate()).padStart(2, "0")}`; + db().prepare(`INSERT INTO aufgaben + (titel, beschreibung, status, prioritaet, verantwortlich_id, frist, + kategorie, vorlage, erstellt, erstellt_von) + VALUES (?,?,'offen',?,?,?,?,?,?,?)`) + .run(v.titel, v.text, v.tage <= 1 ? "hoch" : "mittel", + zielId, frist, v.kategorie, v.schluessel, jetzt(), vonId); + return db().prepare("SELECT last_insert_rowid() AS id").get().id; +} + +/* Wie lang darf ein Wort zu einer Bewerbung sein? Dieselbe Groesse wie + drueben bei den Aufgaben -- ein Satz, kein Aufsatz. */ +const BEWERBUNG_TEXT_MAX = 600; + +/** Die offenen Bewerbungen, so wie sie die Seite braucht. + * + * WER WAS SIEHT, ENTSCHEIDET HIER DER SERVER: Wer entscheidet, sieht + * alle offenen; alle anderen sehen nur die eigenen. Im Browser zu + * filtern hiesse, die fremden Namen trotzdem auszuliefern -- und + * assets/js/aufgaben.js bekommt jeder, der die Seite oeffnet. + */ +function vorlagenBewerbungenFuer(person) { + const darf = entscheidetUeberAufgaben(person); + const zeilen = db().prepare(` + SELECT b.id, b.vorlage, b.kategorie, b.person_id, b.text, b.zustand, + b.entscheid_text, b.entschieden_am, b.erstellt, p.name AS person_name + FROM vorlagen_bewerbungen b + LEFT JOIN personen p ON p.id = b.person_id + WHERE b.zustand = 'beworben' ${darf ? "" : "AND b.person_id = ?"} + ORDER BY b.id`).all(...(darf ? [] : [person.id])); + return zeilen.map((z) => ({ + id: z.id, vorlage: z.vorlage, kategorie: z.kategorie, + person_id: z.person_id, person_name: z.person_name || "Jemand", + text: z.text || null, erstellt: z.erstellt, + selbst: z.person_id === person.id, + })); +} + +/* ===================================================================== + SICH AUF EINE VORLAGE BEWERBEN (23.09.2026) + + Filipe: "die modis sollen bei all diesen voschlaegen auch nur + bewerben koennen. die aufgaben aus der vorlage, da sollen die modis + sich nur bewerben koennen und nur dogfather und die rechte hand + sollen annehmen oder ablehnen koennen, mit einem text als notiz." + + WER DARF WAS -- und die zweite Zeile ist die, die er ausdruecklich + gesagt hat: + + bewerben wer NICHT verteilen darf (die Modis) + entscheiden DogFather und die rechte Hand + uebernehmen wer verteilen darf, wie bisher + + DIE LINKE HAND STEHT ABSICHTLICH NICHT BEI DEN ENTSCHEIDERN. Sie + gehoert sonst ueberall dazu (Auftrag vom 22.09.: "ich will dass die + linke hand auch ueberall zu sehen ist"), hier hat Filipe genau zwei + genannt. Das ist keine Vergesslichkeit von mir, sondern seine + Aufzaehlung -- und dieselbe Grenze zieht das Haus schon bei den + Aufgaben-Bewerbungen (entscheidetUeberAufgaben). + + DER TEXT IST FREIWILLIG BEIM BEWERBEN und freiwillig beim + Entscheiden. Beim ABLEHNEN einer zugeteilten Aufgabe ist eine + Begruendung Pflicht -- dort sagt jemand ab, der gefragt wurde. Hier + bittet jemand; ein "bitte" braucht keine Begruendung. Filipes Wort + ist "mit einem text als notiz", also eine Moeglichkeit, keine Huerde. + ===================================================================== */ + +/** Die Vorlage zu einer Kennung -- oder null. + * Gesucht wird ueber den SCHLUESSEL und nicht ueber die Nummer in der + * Liste: Die Nummer verschiebt sich, sobald jemand eine Vorlage + * einfuegt, und dann bewirbt man sich auf etwas anderes, als man + * gesehen hat. Genau dieser Fehler ist am 16.09. schon einmal + * passiert. */ +function vorlageZuSchluessel(schluessel) { + return MODI_AUFGABEN_FLACH.find((v) => v.schluessel === schluessel) || null; +} + +vorlagenRouter.post("/workspace/api/vorlagen/bewerben", gleicheHerkunft, + express.json({ limit: "4kb" }), (req, res) => { + try { + if (!kategorienFuer(req.person).length) { + /* Wortgleich mit einer erfundenen Vorlage -- wer den Katalog + nicht bekommt, soll nicht erfahren, dass es ihn gibt. */ + return res.status(400).json({ fehler: "Unbekannte Vorlage." }); + } + /* WER VERTEILEN DARF, BEWIRBT SICH NICHT. Er traegt sich ein -- + eine Bitte an sich selbst waere ein Vorgang ohne Gegenueber, + und im schlimmsten Fall entscheidet jemand ueber die eigene + Bewerbung. */ + if (darfAufgabenVerteilen(req.person)) { + return res.status(403).json({ fehler: "du_entscheidest_selbst" }); + } + const v = vorlageZuSchluessel(String(req.body?.vorlage || "")); + if (!v) return res.status(400).json({ fehler: "Unbekannte Vorlage." }); + + const text = String(req.body?.text ?? "").trim().slice(0, BEWERBUNG_TEXT_MAX) || null; + const d = db(); + + /* SCHON BEWORBEN? Die Datenbank sagt es ohnehin (der Index ist + eindeutig) -- aber eine saubere Antwort ist besser als ein + Fehler aus der Tiefe. */ + const schon = d.prepare(`SELECT id FROM vorlagen_bewerbungen + WHERE vorlage = ? AND person_id = ? AND zustand = 'beworben'`) + .get(v.schluessel, req.person.id); + if (schon) return res.status(409).json({ fehler: "schon_beworben" }); + + /* UND LIEGT SIE SCHON AUF DEM BRETT? Sich auf etwas zu bewerben, + das man bereits hat, ist eine Bitte ins Leere. */ + const liegtSchon = d.prepare(`SELECT id FROM aufgaben + WHERE vorlage = ? AND verantwortlich_id = ? AND status <> 'erledigt'`) + .get(v.schluessel, req.person.id); + if (liegtSchon) return res.status(409).json({ fehler: "liegt_schon_bei_dir" }); + + d.prepare(`INSERT INTO vorlagen_bewerbungen + (vorlage, kategorie, person_id, text, zustand, erstellt) + VALUES (?,?,?,?,'beworben',?)`) + .run(v.schluessel, v.kategorie, req.person.id, text, jetzt()); + + protokolliere("vorlage_beworben", { + personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), + detail: `${v.schluessel} ${v.titel}`.slice(0, 120), + }); + res.status(201).json({ ok: true, bewerbungen: vorlagenBewerbungenFuer(req.person) }); + } catch (f) { + console.error("[vorlagen] bewerben:", f?.message); + res.status(503).json({ fehler: "nicht_verfuegbar" }); + } + }); + +/* ---- Die eigene Bewerbung zuruecknehmen ------------------------------ + Sie wird GELOESCHT und nicht auf "zurueckgezogen" gesetzt: Wer sich + nicht mehr bewirbt, hat nichts getan, was jemand nachlesen muesste -- + und eine Zeile, die nur noch sagt "hier stand mal eine Bewerbung", + waere in der Liste der Leitung Laerm. Dieselbe Entscheidung wie bei + den Aufgaben-Bewerbungen. */ +vorlagenRouter.post("/workspace/api/vorlagen/bewerbung-zurueck", gleicheHerkunft, + express.json({ limit: "2kb" }), (req, res) => { + try { + 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. Ein 403 verriete, dass es sie + gibt und wem sie gehoert. */ + if (!b || b.person_id !== req.person.id || b.zustand !== "beworben") { + return res.status(404).json({ fehler: "nicht_gefunden" }); + } + db().prepare("DELETE FROM vorlagen_bewerbungen WHERE id = ?").run(id); + protokolliere("vorlage_bewerbung_zurueck", { + personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), + detail: String(b.vorlage).slice(0, 120), + }); + res.json({ ok: true, bewerbungen: vorlagenBewerbungenFuer(req.person) }); + } catch (f) { + console.error("[vorlagen] bewerbung-zurueck:", f?.message); + res.status(503).json({ fehler: "nicht_verfuegbar" }); + } + }); + +/* ---- Entscheiden: annehmen oder ablehnen ------------------------------ + + EINE SCHLEIFE FUER BEIDE WEGE. Zwei fast gleiche Routen waeren zwei + Gelegenheiten, dass eine die Rechtepruefung oder das Protokoll + vergisst -- und es ist immer die zweite. + + BEI DER ZUSAGE ENTSTEHT DIE AUFGABE, und zwar ueber dieselbe + Funktion wie beim Uebernehmen (katalogAufgabeAnlegen). Damit sieht + eine erbetene Aufgabe genauso aus wie eine verteilte: gleiche Frist, + gleiche Kategorie, gleiche Kennung. Ein zweiter Weg waere ein + zweiter Satz Regeln. */ +for (const [weg, ja] of [["annehmen", true], ["ablehnen", false]]) { + vorlagenRouter.post(`/workspace/api/vorlagen/bewerbung/${weg}`, gleicheHerkunft, + express.json({ limit: "4kb" }), (req, res) => { + try { + /* NUR DOGFATHER UND DIE RECHTE HAND -- woertlich nach Filipes + Aufzaehlung, und dieselbe Schranke wie bei den + Aufgaben-Bewerbungen. */ + if (!entscheidetUeberAufgaben(req.person)) { + return res.status(403).json({ fehler: "nicht_erlaubt" }); + } + const id = Number(req.body?.id); + if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" }); + + const d = db(); + const b = d.prepare("SELECT * FROM vorlagen_bewerbungen WHERE id = ?").get(id); + if (!b || b.zustand !== "beworben") { + return res.status(409).json({ fehler: "keine_bewerbung" }); + } + const v = vorlageZuSchluessel(b.vorlage); + if (!v) return res.status(409).json({ fehler: "vorlage_weg" }); + + /* GIBT ES DIE PERSON NOCH? Zwischen Bewerbung und Antwort + koennen Tage liegen. Eine Aufgabe an einen abgeschalteten + Zugang waere Arbeit, die nie jemand sieht. */ + const wer = d.prepare( + "SELECT id, name FROM personen WHERE id = ? AND aktiv = 1").get(b.person_id); + if (!wer) return res.status(409).json({ fehler: "person_weg" }); + + const kommentar = String(req.body?.kommentar ?? "").trim() + .slice(0, BEWERBUNG_TEXT_MAX) || null; + const nun = jetzt(); + let aufgabeId = null; + + if (ja) { + aufgabeId = katalogAufgabeAnlegen(v, wer.id, req.person.id); + } + d.prepare(`UPDATE vorlagen_bewerbungen + SET zustand = ?, entscheid_text = ?, entschieden_von = ?, + entschieden_am = ?, aufgabe_id = ? + WHERE id = ?`) + .run(ja ? "angenommen" : "abgelehnt", kommentar, req.person.id, + nun, aufgabeId, id); + + 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), + }); + res.json({ + ok: true, zustand: ja ? "angenommen" : "abgelehnt", + aufgabe_id: aufgabeId, + bewerbungen: vorlagenBewerbungenFuer(req.person), + }); + } catch (f) { + console.error(`[vorlagen] bewerbung/${weg}:`, f?.message); + res.status(503).json({ fehler: "nicht_verfuegbar" }); + } + }); +} + vorlagenRouter.post("/workspace/api/vorlagen/uebernehmen", gleicheHerkunft, express.json({ limit: "16kb" }), (req, res) => { try { @@ -1572,9 +1849,26 @@ vorlagenRouter.post("/workspace/api/vorlagen/uebernehmen", gleicheHerkunft, bekommt, soll nicht erfahren, dass es ihn gibt. */ return res.status(400).json({ fehler: "Unbekannte Art." }); } - const zielId = req.person.rolle === "modi" - ? req.person.id - : Number(req.body.person_id) || null; + /* ==== WER SICH HIER SELBST ETWAS NIMMT (23.09.2026) ====== + + Filipe: "die modis sollen bei all diesen voschlaegen auch + nur bewerben koennen." + + Bis heute stand hier: Ein Modi nimmt sich die Aufgabe + selbst. Jetzt geht das ueber eine Bewerbung -- und zwar + HIER abgelehnt und nicht nur im Browser ausgeblendet. Ein + ausgeblendeter Knopf ist eine Bitte; abgelehnt wird an + dieser Stelle. + + GEPRUEFT WIRD "DARF VERTEILEN" UND NICHT "IST MODI": Die + Frage ist nicht, wer jemand IST, sondern was er darf. So + gilt die Regel auch fuer jede Rolle, die morgen dazukommt -- + und in dieser Datei steht kein Rollenname, der nicht + hineingehoert. */ + if (!darfAufgabenVerteilen(req.person)) { + return res.status(403).json({ fehler: "erst_bewerben" }); + } + const zielId = Number(req.body.person_id) || null; if (!zielId) return res.status(400).json({ fehler: "Bitte zuerst eine Person wählen." }); /* Nur an jemanden, fuer den die Kategorien ueberhaupt gelten -- sonst landete eine Modi-Aufgabe samt Kategorie an einem @@ -1607,17 +1901,7 @@ vorlagenRouter.post("/workspace/api/vorlagen/uebernehmen", gleicheHerkunft, return res.status(400).json({ fehler: "Unbekannte Vorlage." }); } for (const v of auswahl) { - /* Frist in ORTSZEIT gerechnet. Ueber toISOString kaeme - zwischen Mitternacht und zwei Uhr der Vortag heraus. */ - const d = new Date(Date.now() + (v.tage || 7) * 86400_000); - const frist = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}` - + `-${String(d.getDate()).padStart(2, "0")}`; - db().prepare(`INSERT INTO aufgaben - (titel, beschreibung, status, prioritaet, verantwortlich_id, frist, - kategorie, vorlage, erstellt, erstellt_von) - VALUES (?,?,'offen',?,?,?,?,?,?,?)`) - .run(v.titel, v.text, v.tage <= 1 ? "hoch" : "mittel", - zielPerson.id, frist, v.kategorie, v.schluessel, jetzt(), req.person.id); + katalogAufgabeAnlegen(v, zielPerson.id, req.person.id); angelegt.push(v.titel); } diff --git a/server/workspace.js b/server/workspace.js index a02dfb98..a0a81c19 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -4542,6 +4542,69 @@ export function db() { KEIN ON DELETE CASCADE AUF personen: Wer das Haus verlaesst, nimmt die GIFs des Teams nicht mit. "von_id" wird null, die Kachel bleibt. */ + /* BEWERBUNGEN AUF EINE VORLAGE (23.09.2026). + + Filipe: "die modis sollen bei all diesen voschlaegen auch nur + bewerben koennen. die aufgaben aus der vorlage, da sollen die + modis sich nur bewerben koennen und nur dogfather und die + rechte hand sollen annehmen oder ablehnen koennen, mit einem + text als notiz." + + WARUM NICHT DIE VORHANDENE BEWERBUNG AUS aufgaben_zuteilung: + Die beantwortet "wie steht DIESE Aufgabe bei DIESEM Menschen" + -- sie braucht also eine Aufgabe, die es gibt. Hier gibt es + noch keine. Es ist eine Bitte um etwas, das erst entstehen + soll. + + Der naheliegende Weg waere gewesen, beim Bewerben gleich die + Aufgabe anzulegen und die vorhandene Bewerbung daranzuhaengen. + Dann steht nach zwoelf Absagen zwoelfmal Arbeit auf dem Brett, + die niemand bestellt hat -- und um das zu vermeiden, muesste + das Ablehnen Aufgaben wieder LOESCHEN. Loeschen als Nebenwirkung + einer Absage ist genau die Sorte Regel, die irgendwann das + Falsche trifft. + + Also: Die Aufgabe entsteht erst mit der Zusage. Bis dahin gibt + es nur diese Zeile. + + DIE WORTE SIND DIESELBEN WIE DRUEBEN (zustand, entscheid_text, + entschieden_von, entschieden_am). Zwei Namen fuer dieselbe + Sache waeren zwei Sprachen im selben Haus. + + "aufgabe_id" steht nach der Zusage darin -- damit laesst sich + von der Bewerbung aus zeigen, was aus ihr geworden ist. Kein + Fremdschluessel: Wird die Aufgabe spaeter geloescht, bleibt die + Bewerbung als Vorgang stehen; sie hat stattgefunden. + + ON DELETE CASCADE auf personen: Wer das Haus verlaesst, + hinterlaesst keine Bewerbung, auf die jemand noch antworten + soll. */ + CREATE TABLE IF NOT EXISTS vorlagen_bewerbungen ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + vorlage TEXT NOT NULL, + kategorie TEXT, + person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE, + text TEXT, + zustand TEXT NOT NULL DEFAULT 'beworben', + entscheid_text TEXT, + entschieden_von INTEGER REFERENCES personen(id) ON DELETE SET NULL, + entschieden_am TEXT, + aufgabe_id INTEGER, + erstellt TEXT NOT NULL + ); + /* EINMAL BEWERBEN, NICHT DREIMAL -- und zwar von der Datenbank + durchgesetzt, nicht von einer Abfrage davor, die man vergessen + kann. + + NUR AUF OFFENE: Wer abgelehnt wurde, darf sich spaeter wieder + bewerben (Lage aendert sich, Woche aendert sich). Ein Index + ueber ALLE Zustaende haette genau das verboten. */ + CREATE UNIQUE INDEX IF NOT EXISTS idx_vorlagen_bewerbung_offen + ON vorlagen_bewerbungen (vorlage, person_id) WHERE zustand = 'beworben'; + /* Die Frage im Betrieb lautet "was wartet gerade auf Antwort?" */ + CREATE INDEX IF NOT EXISTS idx_vorlagen_bewerbung_zustand + ON vorlagen_bewerbungen (zustand, id); + CREATE TABLE IF NOT EXISTS chat_gifs ( id INTEGER PRIMARY KEY AUTOINCREMENT, datei TEXT NOT NULL, diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index f3869d62..d7310796 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,15 +16,15 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +