diff --git a/server/pruef-entwicklung.mjs b/server/pruef-entwicklung.mjs index 2120f33e..262ec0d2 100644 --- a/server/pruef-entwicklung.mjs +++ b/server/pruef-entwicklung.mjs @@ -384,6 +384,105 @@ console.log("=== Die Kacheln ==="); ok(new Set(toene).size === toene.length, `kein Farbton doppelt (${toene.length} Kacheln)`); } +/* ===================================================================== + WELCHE PUNKTE GELTEN FUER DIESEN MENSCHEN (25.09.2026) + ===================================================================== + + Filipe: „ich will dass die rechte hand und ich diese aufgaben die es + da gibt, individuel aussuchen koennen wer welche aufgabe bekommt. + bis dahin sollen die keine aufgaben sehen. nur die, die die rechte + hand oder dogfather ihnen zutragen." + + BIS HEUTE GALTEN ALLE 68 FUER JEDEN. Geprueft wird deshalb zuerst + das Wichtigste an seinem Satz -- „bis dahin sollen die keine + aufgaben sehen": Ohne Zuteilung ist die Karte LEER. Ohne diese + Zeile waere alles Folgende auch dann gruen, wenn weiterhin alles + fuer jeden gilt. + ===================================================================== */ +console.log("\n=== Punkte je Person aussuchen ==="); +{ + const karte = async (keks) => (await hole( + `/workspace/api/entwicklung/person/${idModi}`, { host: CREW, keks })).daten; + + const vorher = await karte(dogi.keks); + ok(vorher && vorher.gesamt === 0, + `ohne Zuteilung steht auf der Karte nichts (${vorher?.gesamt} Punkte)`); + ok((vorher?.gesamt_katalog || 0) >= 60, + `der Katalog hat trotzdem alle Punkte (${vorher?.gesamt_katalog})`); + + /* DIE LISTE ZUM AUSSUCHEN zeigt ALLES, mit einem Haken an dem, was + gilt. Zeigte sie nur das Zugeteilte, koennte man nie etwas + hinzufuegen -- ein Auswahlfenster, in dem die Auswahl fehlt. */ + const wahl = (await hole(`/workspace/api/entwicklung/person/${idModi}/punkte`, + { host: CREW, keks: dogi.keks })).daten; + const alle = (wahl?.bloecke || []).flatMap((b) => b.punkte); + ok(alle.length === vorher?.gesamt_katalog, + `die Auswahl zeigt den ganzen Katalog (${alle.length})`); + ok(alle.every((x) => x.dabei === false), + `und nichts davon ist angehakt (${alle.filter((x) => x.dabei).length} angehakt)`); + + /* ---- Zwoelf aussuchen ------------------------------------------- */ + const zwoelf = alle.slice(0, 12).map((x) => x.schluessel); + const gesetzt = await hole(`/workspace/api/entwicklung/person/${idModi}/punkte`, + { host: CREW, keks: dogi.keks, methode: "PUT", + rumpf: JSON.stringify({ punkte: zwoelf }) }); + ok(gesetzt.status === 200 && gesetzt.daten?.zugeteilt === 12, + `DogFather sucht zwoelf aus (HTTP ${gesetzt.status}, ${gesetzt.daten?.zugeteilt})`); + + const nachher = await karte(dogi.keks); + const drin = (nachher?.bloecke || []).flatMap((b) => b.punkte).map((x) => x.schluessel); + ok(nachher?.gesamt === 12 && drin.length === 12, + `und danach stehen genau die zwoelf auf der Karte (${nachher?.gesamt})`); + ok(zwoelf.every((s) => drin.includes(s)), + " und zwar dieselben, nicht irgendwelche zwoelf"); + + /* DIE RECHTE HAND SIEHT DASSELBE -- sie beurteilt denselben + Menschen, und zwei verschiedene Listen fuer dieselbe Person waeren + der Grund fuer jede Abweichung, die keine ist. */ + const beiHand = await karte(hand.keks); + ok(beiHand?.gesamt === 12, + `die rechte Hand sieht dieselben zwoelf (${beiHand?.gesamt})`); + + /* UND SIE DARF SELBST AUSSUCHEN -- Filipes Satz nennt beide. */ + const vonHand = await hole(`/workspace/api/entwicklung/person/${idModi}/punkte`, + { host: CREW, keks: hand.keks, methode: "PUT", + rumpf: JSON.stringify({ punkte: alle.slice(0, 5).map((x) => x.schluessel) }) }); + ok(vonHand.status === 200 && vonHand.daten?.zugeteilt === 5, + `die rechte Hand sucht ebenfalls aus (HTTP ${vonHand.status}, ${vonHand.daten?.zugeteilt})`); + + /* ---- Gegenproben ------------------------------------------------- */ + /* EIN ERFUNDENER SCHLUESSEL WIRD VERWORFEN. Ohne diesen Abgleich + liesse sich beliebiger Text in die Tabelle schreiben -- er stuende + danach in keiner Liste und waere auch nicht wegzubekommen. */ + const mitMuell = await hole(`/workspace/api/entwicklung/person/${idModi}/punkte`, + { host: CREW, keks: dogi.keks, methode: "PUT", + rumpf: JSON.stringify({ punkte: [...zwoelf, "gibt_es_nicht", "auch_nicht"] }) }); + ok(mitMuell.daten?.zugeteilt === 12, + `Gegenprobe: erfundene Schluessel fallen weg (${mitMuell.daten?.zugeteilt} statt 14)`); + + /* EIN MODI DARF NICHT AUSSUCHEN -- weder fuer sich noch fuer + andere. Ohne diese Zeile hiesse „DogFather darf" nur, dass + ueberhaupt jemand darf. */ + const vonModi = await hole(`/workspace/api/entwicklung/person/${idModi}/punkte`, + { host: CREW, keks: ayla.keks, methode: "PUT", + rumpf: JSON.stringify({ punkte: [] }) }); + ok(vonModi.status >= 400, + `Gegenprobe: ein Modi sucht nicht aus (${vonModi.status})`); + const nochDa = await karte(dogi.keks); + ok(nochDa?.gesamt === 12, + ` und nach dem Versuch stehen die zwoelf noch (${nochDa?.gesamt})`); + + /* ALLES WEGNEHMEN GEHT AUCH. Eine Auswahl, die man nur erweitern + kann, waere eine Falle: Wer sich vertut, bekaeme den Punkt nie + wieder los. */ + const leer = await hole(`/workspace/api/entwicklung/person/${idModi}/punkte`, + { host: CREW, keks: dogi.keks, methode: "PUT", + rumpf: JSON.stringify({ punkte: [] }) }); + const danach = await karte(dogi.keks); + ok(leer.status === 200 && danach?.gesamt === 0, + `und alles wieder wegnehmen geht ebenfalls (${danach?.gesamt})`); +} + d.close(); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } console.log(""); diff --git a/server/workspace-entwicklung.js b/server/workspace-entwicklung.js index 2ed87fa5..767f0e49 100644 --- a/server/workspace-entwicklung.js +++ b/server/workspace-entwicklung.js @@ -207,6 +207,76 @@ export function tabellen() { entstand, wie ein Tag aus, an dem 43 Dinge gleichzeitig passiert sind. ============================================================= */ + /* ================================================================= + WELCHE PUNKTE GELTEN FUER DIESEN MENSCHEN? (25.09.2026) + ================================================================= + + Filipe: „ich will dass die rechte hand und ich diese aufgaben die + es da gibt, individuel aussuchen koennen wer welche aufgabe + bekommt. bis dahin sollen die keine aufgaben sehen. nur die, die + die rechte hand oder dogfather ihnen zutragen." + + BIS HEUTE GALTEN ALLE 68 FUER JEDEN. Das war bequem und in der + Sache falsch: „Clips, Schnitt und Kommentare" gehoert nicht zu + jemandem, der nur im Chat moderiert -- und ein Punkt, der nie + zutrifft, steht trotzdem in der Zaehlung („0 von 68"). Wer eine + Liste sieht, in der ein Drittel nie passt, liest die ganze Liste + weniger ernst. + + EINE ZEILE JE PAAR, KEINE LISTE IN EINER SPALTE: Eine + kommagetrennte Liste in personen waere schneller gebaut und + liesse sich nicht abfragen („wer hat diesen Punkt?"), nicht + zaehlen und nicht mit einem Fremdschluessel absichern. + + WER UND WANN stehen mit drin. Nicht fuer die Anzeige -- fuer die + Frage „seit wann gilt das eigentlich fuer ihn", die + erfahrungsgemaess in dem Moment kommt, in dem jemand sie nicht + mehr beantworten kann. */ + CREATE TABLE IF NOT EXISTS entwicklung_zuteilung ( + person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE, + schluessel TEXT NOT NULL, + seit TEXT NOT NULL, + von_id INTEGER REFERENCES personen(id) ON DELETE SET NULL, + PRIMARY KEY (person_id, schluessel) + ); + CREATE INDEX IF NOT EXISTS idx_entw_zuteilung_person + ON entwicklung_zuteilung (person_id); + + /* ================================================================= + EINMALIG: WAS SCHON BEURTEILT WURDE, BLEIBT SICHTBAR + ================================================================= + + Ab heute zeigt die Karte nur noch zugeteilte Punkte. Fuer jemanden, + bei dem noch nichts ausgesucht ist, heisst das „leer" -- und genau + das hat Filipe gewollt („bis dahin sollen die keine aufgaben + sehen"). + + FUER BESTEHENDE BEURTEILUNGEN WAERE ES ABER DATENVERLUST AUF DEM + BILDSCHIRM: Wer zwanzig Punkte gesetzt hat, saehe am naechsten + Morgen eine leere Karte -- die Daten liegen noch da, man kommt nur + nicht mehr hin. Das sieht aus wie ein Fehler und ist einer. + + DESHALB WIRD EINMALIG UEBERNOMMEN, wozu es schon eine + Einschaetzung gibt. Nicht mehr: Ein Punkt, den nie jemand gesetzt + hat, war auch vorher nur Papier. + + Ein INSERT, das Vorhandenes stehen laesst, UND EIN FLAG: Das Flag verhindert, dass die + Uebernahme wiederkommt, nachdem jemand bewusst alles abgewaehlt + hat -- sonst waere „ich will diesen Punkt nicht mehr" nach dem + naechsten Neustart rueckgaengig gemacht, und niemand wuesste, + warum. */ + INSERT INTO entwicklung_zuteilung (person_id, schluessel, seit, von_id) + SELECT DISTINCT s.person_id, s.schluessel, MIN(s.am), NULL + FROM entwicklung_stand s + WHERE NOT EXISTS (SELECT 1 FROM einstellungen + WHERE schluessel = 'entwicklung_zuteilung_uebernommen') + GROUP BY s.person_id, s.schluessel + ON CONFLICT(person_id, schluessel) DO NOTHING; + + INSERT INTO einstellungen (schluessel, wert, geaendert, von) + VALUES ('entwicklung_zuteilung_uebernommen', 'ja', datetime('now'), NULL) + ON CONFLICT(schluessel) DO NOTHING; + CREATE TABLE IF NOT EXISTS entwicklung_lauf ( id INTEGER PRIMARY KEY AUTOINCREMENT, person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE, @@ -488,6 +558,40 @@ function katalogFuer(person, ueberMichSelbst) { .map((b) => ({ ...b, punkte: ENTWICKLUNG_PUNKTE[b.schluessel] || [] })); } +/* ===== WELCHE PUNKTE GELTEN FUER DIESEN MENSCHEN? (25.09.2026) ====== + + Filipe: „ich will dass die rechte hand und ich diese aufgaben die es + da gibt, individuel aussuchen koennen wer welche aufgabe bekommt. + bis dahin sollen die keine aufgaben sehen." + + EINE STELLE FUER DIE ANTWORT, drei Aufrufer: die Karte der Leitung, + die Karte des Menschen selbst und die Uebersicht mit den Zahlen. + Drei Abschriften waeren drei Gelegenheiten, dass eine davon beim + naechsten Umbau nicht mitzieht -- und dann stuende in der Uebersicht + „12 von 68", waehrend in der Karte elf Punkte stehen. + + EIN SET UND KEINE LISTE: Gefragt wird 68-mal je Person („gehoert + dieser Punkt dazu?"). In einer Liste ist das 68 x N Vergleiche, im + Set 68 Nachschlagevorgaenge. */ +function zugeteiltFuer(personId) { + return new Set(db().prepare( + "SELECT schluessel FROM entwicklung_zuteilung WHERE person_id = ?") + .all(personId).map((z) => z.schluessel)); +} + +/* Der Katalog, auf das eingegrenzt, was fuer DIESEN Menschen gilt. + `nurSelbst`-Bloecke bleiben unberuehrt: Das ist der Block, den die + Person selbst beantwortet (warum Moderatoren aufhoeren), er gehoert + niemandem sonst und ist nicht Teil der 68. */ +function katalogFuerPerson(person, personId, ueberMichSelbst) { + const dabei = zugeteiltFuer(personId); + return katalogFuer(person, ueberMichSelbst) + .map((b) => (b.nurSelbst + ? b + : { ...b, punkte: b.punkte.filter((pt) => dabei.has(pt.schluessel)) })) + .filter((b) => b.nurSelbst || b.punkte.length); +} + entwicklungRouter.get("/workspace/api/entwicklung/lage", (req, res) => { try { const leitung = fuehrtTeamDogi(req.person); @@ -514,6 +618,15 @@ entwicklungRouter.get("/workspace/api/entwicklung/lage", (req, res) => { .all(req.person.id, new Date(Date.now() - VERBLASST_NACH_TAGEN * TAG).toISOString()); const alteZahl = new Map(alt.map((z) => [z.person_id, z.n])); + /* WIE VIELE PUNKTE GELTEN JE PERSON (25.09.2026). Ohne diese Zahl + stuende in der Uebersicht weiter „von 68" -- und in der Karte + daneben „von 12". Zwei Zahlen fuer dieselbe Sache, und die + Uebersicht ist die, die man zuerst sieht. */ + const zugeteilt = db().prepare( + "SELECT person_id, COUNT(*) AS n FROM entwicklung_zuteilung GROUP BY person_id") + .all(); + const zuteilZahl = new Map(zugeteilt.map((z) => [z.person_id, z.n])); + const anders = db().prepare( `SELECT a.person_id, COUNT(*) AS n FROM entwicklung_stand a JOIN entwicklung_stand b @@ -544,6 +657,10 @@ entwicklungRouter.get("/workspace/api/entwicklung/lage", (req, res) => { gesetzt: meineZahl.get(p.id) || 0, veraltet: alteZahl.get(p.id) || 0, abweichungen: andersZahl.get(p.id) || 0, + /* Wie viele Punkte fuer sie ausgesucht sind. `0` heisst + ausdruecklich „noch keine" und nicht „keine Angabe" -- die + Oberflaeche sagt dann „Punkte auswaehlen", nicht „0 von 68". */ + zugeteilt: zuteilZahl.get(p.id) || 0, })), /* Die eigene Karte -- fuer jeden, der im Team ist. */ ich: { id: req.person.id, name: req.person.name }, @@ -576,7 +693,9 @@ entwicklungRouter.get("/workspace/api/entwicklung/person/:id", nurLeitung, (req, const grenze = Date.now() - VERBLASST_NACH_TAGEN * TAG; const schritte = schritteVon(p.id); - const bloecke = katalogFuer(req.person, false).map((b) => ({ + /* NUR, WAS FUER IHN GILT (25.09.2026). Vorher stand hier + `katalogFuer` -- alle 68 fuer jeden. */ + const bloecke = katalogFuerPerson(req.person, id, false).map((b) => ({ ...b, punkte: b.punkte.map((pt) => { const m = meine.get(pt.schluessel) || null; @@ -662,8 +781,24 @@ entwicklungRouter.get("/workspace/api/entwicklung/person/:id", nurLeitung, (req, erwartung: ENTWICKLUNG_ERWARTUNG, bloecke, abweichungen, - gesetzt: meine.size, - gesamt: ENTWICKLUNG_FREMD.length, + /* „X VON Y" ZAEHLT AB HEUTE DAS ZUGETEILTE (25.09.2026). + + `ENTWICKLUNG_FREMD.length` sind alle 68 -- die Zahl war damit + fuer jeden gleich, egal wie viele Punkte ihn ueberhaupt + betreffen. „0 von 68" bei jemandem, fuer den zwoelf gelten, + ist keine Auskunft, sondern eine Entmutigung. + + `meine.size` musste ebenfalls nachgezogen werden: Wer frueher + zu einem Punkt gesetzt hat, der ihm inzwischen nicht mehr + zugeteilt ist, haette sonst „13 von 12" bekommen. Gezaehlt + wird deshalb nur, was auch in der Liste steht. */ + gesetzt: bloecke.reduce((n, b) => n + + b.punkte.filter((pt) => pt.meins).length, 0), + gesamt: bloecke.reduce((n, b) => n + b.punkte.length, 0), + /* Wie viele es insgesamt GAEBE -- die Oberflaeche sagt damit + „12 von 68 ausgewaehlt", ohne die Liste selbst zu kennen. */ + gesamt_katalog: ENTWICKLUNG_FREMD.length, + darf_zuteilen: true, }); } catch (f) { console.error("[entwicklung] Person:", f?.message); @@ -729,6 +864,93 @@ function schritteVon(personId) { return nach; } +/* ===================================================================== + WELCHE PUNKTE GELTEN FUER DIESEN MENSCHEN -- LESEN UND SETZEN + ===================================================================== + + Filipe: „ich will dass die rechte hand und ich diese aufgaben die es + da gibt, individuel aussuchen koennen wer welche aufgabe bekommt." + + ZWEI WEGE UND NICHT EINER MIT SCHALTER: Lesen liefert den GANZEN + Katalog mit einem Haken an dem, was gilt -- man waehlt nur aus, was + man sieht. Die Karte selbst liefert weiterhin nur das Zugeteilte; + die zwei Fragen sind verschieden und bleiben getrennt. + + GESETZT WIRD DIE GANZE LISTE AUF EINMAL, nicht Punkt fuer Punkt. Wer + zwoelf Haken setzt und dabei die Verbindung verliert, haette sonst + sieben gesetzte und fuenf verlorene -- und saehe nicht, welche. So + ist es eine Entscheidung, und sie gilt ganz oder gar nicht. + ===================================================================== */ +entwicklungRouter.get("/workspace/api/entwicklung/person/:id/punkte", + nurLeitung, (req, res) => { + try { + const id = nummer(req.params.id); + const p = id && db().prepare("SELECT id, name, rolle FROM personen WHERE id = ?").get(id); + if (!p || !KARTEN_ROLLEN.includes(p.rolle)) { + return res.status(404).json({ fehler: "nicht_gefunden" }); + } + const dabei = zugeteiltFuer(id); + res.json({ + person: { id: p.id, name: p.name, rolle_name: ROLLEN_NAME[p.rolle] ?? p.rolle }, + bloecke: katalogFuer(req.person, false).map((b) => ({ + schluessel: b.schluessel, name: b.name, text: b.text, + punkte: b.punkte.map((pt) => ({ + schluessel: pt.schluessel, titel: pt.titel, text: pt.text || null, + stufe: pt.stufe, dabei: dabei.has(pt.schluessel), + })), + })), + }); + } catch (f) { + console.error("[entwicklung] Punkte lesen:", f?.message); + res.status(503).json({ fehler: "nicht_verfuegbar" }); + } + }); + +entwicklungRouter.put("/workspace/api/entwicklung/person/:id/punkte", + gleicheHerkunft, nurLeitung, express.json({ limit: "32kb" }), (req, res) => { + try { + const id = nummer(req.params.id); + const p = id && db().prepare("SELECT id, name, rolle FROM personen WHERE id = ?").get(id); + if (!p || !KARTEN_ROLLEN.includes(p.rolle)) { + return res.status(404).json({ fehler: "nicht_gefunden" }); + } + /* NUR SCHLUESSEL, DIE ES WIRKLICH GIBT. Ohne diesen Abgleich + liesse sich ueber diesen Weg beliebiger Text in die Tabelle + schreiben -- und er stuende danach in keiner Liste, waere aber + auch nicht wegzubekommen. */ + const erlaubt = new Set(ENTWICKLUNG_FREMD.map((pt) => pt.schluessel)); + const wunsch = Array.isArray(req.body?.punkte) ? req.body.punkte : null; + if (!wunsch) return res.status(400).json({ fehler: "ungueltig" }); + const neu = [...new Set(wunsch.map(String).filter((s) => erlaubt.has(s)))]; + + const nun = jetzt(); + /* EINE TRANSAKTION. Zwischen „alles weg" und „neu eintragen" + darf es keinen Moment geben, in dem die Person gar nichts hat + -- sonst saehe ein gleichzeitiger Abruf eine leere Karte, und + bei einem Fehler dazwischen bliebe sie leer. */ + db().exec("BEGIN"); + try { + db().prepare("DELETE FROM entwicklung_zuteilung WHERE person_id = ?").run(id); + const rein = db().prepare(`INSERT INTO entwicklung_zuteilung + (person_id, schluessel, seit, von_id) VALUES (?,?,?,?)`); + for (const s of neu) rein.run(id, s, nun, req.person.id); + db().exec("COMMIT"); + } catch (f) { + db().exec("ROLLBACK"); + throw f; + } + + protokolliere("entwicklung_punkte_zugeteilt", { + personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), + detail: `${p.name}: ${neu.length} Punkte`.slice(0, 120), + }); + res.json({ ok: true, zugeteilt: neu.length }); + } catch (f) { + console.error("[entwicklung] Punkte setzen:", f?.message); + res.status(503).json({ fehler: "nicht_verfuegbar" }); + } + }); + entwicklungRouter.post("/workspace/api/entwicklung/person/:id/:schluessel/schritt", gleicheHerkunft, nurLeitung, express.json({ limit: "8kb" }), (req, res) => { try { diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index e656858e..47d3f54d 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`. --> - - + + - +