diff --git a/server/pruef-womit.mjs b/server/pruef-womit.mjs new file mode 100644 index 00000000..636b20a3 --- /dev/null +++ b/server/pruef-womit.mjs @@ -0,0 +1,316 @@ +/* ===================================================================== + Womit anfangen? (15.09.2026) + + Letztes Stück von Stufe 9: „Wichtig-vs-Aufwand im Report". + + --------------------------------------------------------------------- + DIE FRAGE, DIE EINE AUFGABENLISTE NICHT BEANTWORTET + + Ein Brett mit dreißig offenen Karten sagt, WAS zu tun ist. Es sagt + nicht, WOMIT man anfängt. Sind acht Karten „wichtig", ist keine davon + die erste. + + --------------------------------------------------------------------- + DIE DREI FRAGEN + + 1. Landet jede Aufgabe im richtigen Feld — alle vier Ecken? + 2. Verschwindet etwas? ← die wichtigste + 3. Ist der Aufwand wirklich freiwillig — und lässt er sich + zurücknehmen? + + Die zweite ist der Grund für die fünfte Gruppe: Eine Übersicht, die + einen Teil der Arbeit unsichtbar macht, ist schlimmer als keine. Man + verlässt sich darauf und übersieht genau das, was fehlt. Deshalb wird + hier gezählt, ob die Summe der Felder die Summe der Aufgaben ist. + + Aufrufen mit: node server/pruef-womit.mjs + ===================================================================== */ + +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { request as httpAnfrage } from "node:http"; +import { portMussFreiSein } from "./helfer-port.mjs"; + +const PORT = await portMussFreiSein(4418, "pruef-womit"); + +const ordner = mkdtempSync(join(tmpdir(), "ws-womit-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = String(PORT); +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; + +const express = (await import("express")).default; +const ec = express.response.cookie; +express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; +import { notbremse } from "./helfer-notbremse.mjs"; +await import("./index.js"); +notbremse(180_000, "pruef-womit"); +await new Promise((r) => setTimeout(r, 700)); + +process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); }); +process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); }); + +let fehler = 0, geprueft = 0; +const melde = (t) => console.log(t); +const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +const HAUS = "workspace.dogfather-universe.com"; + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); +const womit = await import("./workspace-womit.js"); + +const oeffne = () => new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); + +let idDogi; +{ + const d = oeffne(); + const schluessel = randomBytes(32).toString("hex"); + d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") + .run("code_kennung_schluessel", schluessel, jetzt); + const salt = randomBytes(16).toString("hex"); + const hash = scryptSync("CODE-DOGI-0001", salt, 64, + { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); + d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)" + + " VALUES ('Filipe','admin',?,?,?,?,1,?)") + .run(hash, salt, 32768, createHmac("sha256", schluessel).update("CODE-DOGI-0001").digest("hex"), jetzt); + idDogi = d.prepare("SELECT last_insert_rowid() AS id").get().id; + + /* ALLE VIER ECKEN, plus eine ohne Schaetzung, plus zwei, die nicht + mitzaehlen duerfen. */ + const rein = d.prepare(`INSERT INTO aufgaben + (titel, status, prioritaet, aufwand, erstellt, erstellt_von) + VALUES (?,?,?,?,?,?)`); + rein.run("Wichtig und klein", "offen", "hoch", "klein", jetzt, idDogi); + rein.run("Wichtig und gross", "offen", "hoch", "gross", jetzt, idDogi); + rein.run("Wichtig und mittel", "offen", "hoch", "mittel", jetzt, idDogi); + rein.run("Normal und klein", "offen", "mittel", "klein", jetzt, idDogi); + rein.run("Normal und gross", "arbeit", "niedrig", "gross", jetzt, idDogi); + rein.run("Ohne Schaetzung", "offen", "hoch", null, jetzt, idDogi); + rein.run("Schon erledigt", "erledigt", "hoch", "klein", jetzt, idDogi); + rein.run("Abgebrochen", "abgebrochen", "hoch", "klein", jetzt, idDogi); + d.close(); +} + +function ruf(pfad, methode, kopf = {}, koerper = null) { + return new Promise((fertig, schief) => { + const a = httpAnfrage({ host: "127.0.0.1", port: PORT, path: pfad, method: methode, + headers: { Host: HAUS, ...(koerper ? { "Content-Type": "application/json" } : {}), ...kopf } }, + (antwort) => { + let text = ""; + antwort.on("data", (s) => { text += s; }); + antwort.on("end", () => fertig({ code: antwort.statusCode, text, kopf: antwort.headers })); + }); + a.on("error", schief); + if (koerper) a.write(JSON.stringify(koerper)); + a.end(); + }); +} +const json = (a) => { try { return JSON.parse(a.text); } catch { return {}; } }; +const anm = await ruf("/workspace/api/anmelden", "POST", {}, + { rolle: "admin", code: "CODE-DOGI-0001" }); +const kDogi = [].concat(anm.kopf?.["set-cookie"] || []).map((c) => c.split(";")[0]).join("; "); +ok(!!kDogi, "DogFather ist angemeldet"); + +/* ======================================================================= + 1. ALLE VIER ECKEN + ======================================================================= */ +melde(""); +melde("=== 1. Die vier Ecken ==="); +{ + const f = (p, a) => womit.feldVon({ status: "offen", prioritaet: p, aufwand: a }); + ok(f("hoch", "klein") === "sofort", `wichtig + klein → ${f("hoch", "klein")}`); + ok(f("hoch", "gross") === "einplanen", `wichtig + groß → ${f("hoch", "gross")}`); + ok(f("mittel", "klein") === "nebenbei", `normal + klein → ${f("mittel", "klein")}`); + ok(f("niedrig", "gross") === "spaeter", `normal + groß → ${f("niedrig", "gross")}`); + + /* "mittel" zaehlt als NICHT klein -- die Trennlinie liegt dort, wo sie + im Alltag liegt: zwischendurch oder Zeit nehmen. */ + ok(f("hoch", "mittel") === "einplanen", + `wichtig + mittel gehört zu „einplanen“ (${f("hoch", "mittel")})`); + + ok(f("hoch", null) === "offen", "ohne Schätzung: eigene Gruppe"); + ok(f("hoch", "riesig") === "offen", + "und ein unbekannter Wert landet dort ebenfalls, statt irgendwo"); + + ok(womit.FELDER.length === 5, `${womit.FELDER.length} Felder (vier plus die offene Gruppe)`); + ok(womit.FELDER.every((x) => x.name && x.satz), + "jedes Feld sagt, was es heißt und was zu tun ist"); + /* Die Namen sind Handlungsanweisungen, keine Etiketten. */ + ok(womit.FELDER.some((x) => /Sofort/i.test(x.name)) && womit.FELDER.some((x) => /Später/i.test(x.name)), + "und die Namen sagen, was zu tun ist"); +} + +/* ======================================================================= + 2. ES VERSCHWINDET NICHTS + + Die wichtigste Zeile: Eine Uebersicht, die einen Teil der Arbeit + unsichtbar macht, ist schlimmer als keine. + ======================================================================= */ +melde(""); +melde("=== 2. Nichts verschwindet ==="); +{ + const alle = [ + { id: 1, status: "offen", prioritaet: "hoch", aufwand: "klein" }, + { id: 2, status: "offen", prioritaet: "hoch", aufwand: "gross" }, + { id: 3, status: "arbeit", prioritaet: "niedrig", aufwand: null }, + { id: 4, status: "review", prioritaet: "mittel", aufwand: "mittel" }, + { id: 5, status: "erledigt", prioritaet: "hoch", aufwand: "klein" }, + { id: 6, status: "abgebrochen", prioritaet: "hoch", aufwand: "klein" }, + ]; + const felder = womit.verteilen(alle); + const summe = felder.reduce((n, f) => n + f.anzahl, 0); + const offen = alle.filter((a) => a.status !== "erledigt" && a.status !== "abgebrochen").length; + ok(summe === offen, + `die Felder zusammen ergeben genau die offenen Aufgaben (${summe} von ${offen})`); + + ok(felder.find((f) => f.schluessel === "offen").anzahl === 1, + "die Aufgabe ohne Schätzung steht in ihrer eigenen Gruppe"); + + /* GEGENPROBE -- ohne sie beweist die Summe nichts: Erledigtes zaehlt + wirklich NICHT mit. */ + ok(summe === 4, `erledigte und abgebrochene sind draußen (${summe} statt 6)`); + + /* Und die Felder kommen IMMER alle fuenf zurueck, auch leere -- sonst + koennte die Oberflaeche nicht zwischen "keine" und "vergessen" + unterscheiden. */ + ok(felder.length === womit.FELDER.length, + `alle ${felder.length} Felder kommen zurück, auch die leeren`); +} + +/* ======================================================================= + 3. DER AUFWAND IST FREIWILLIG — UND ZURÜCKNEHMBAR + ======================================================================= */ +melde(""); +melde("=== 3. Freiwillig ==="); +{ + const liste = json(await ruf("/workspace/api/aufgaben", "GET", { cookie: kDogi })).aufgaben || []; + const ohne = liste.find((a) => a.titel === "Ohne Schaetzung"); + ok(!!ohne && ohne.aufwand === null, "eine Aufgabe darf ohne Aufwand bestehen"); + + const setzen = await ruf(`/workspace/api/aufgaben/${ohne.id}`, "PATCH", + { cookie: kDogi }, { aufwand: "klein" }); + ok(setzen.code === 200, `der Aufwand lässt sich setzen (${setzen.code})`); + + const zurueck = await ruf(`/workspace/api/aufgaben/${ohne.id}`, "PATCH", + { cookie: kDogi }, { aufwand: "" }); + ok(zurueck.code === 200, `und wieder zurücknehmen (${zurueck.code})`); + const nach = (json(await ruf("/workspace/api/aufgaben", "GET", { cookie: kDogi })).aufgaben || []) + .find((a) => a.id === ohne.id); + ok(nach?.aufwand === null, "danach steht sie wieder ohne Schätzung da"); + + const bloedsinn = await ruf(`/workspace/api/aufgaben/${ohne.id}`, "PATCH", + { cookie: kDogi }, { aufwand: "riesig" }); + ok(bloedsinn.code === 400, `ein erfundener Wert wird abgelehnt (${bloedsinn.code})`); + ok(/Aufwand/i.test(json(bloedsinn).fehler || ""), + `und die Absage sagt, woran es liegt (${json(bloedsinn).fehler})`); + + /* Die Stufen kommen mit der Liste -- sonst fuehrt der Browser eine + zweite, die beim naechsten Wert veraltet. */ + const daten = json(await ruf("/workspace/api/aufgaben", "GET", { cookie: kDogi })); + ok(Array.isArray(daten.aufwand) && daten.aufwand.length === womit.AUFWAND.length, + `die ${daten.aufwand?.length} Aufwandsstufen kommen mit der Liste`); + ok(daten.aufwand.every((s) => s.schluessel && s.name), + "jede mit Schlüssel und Namen"); +} + +/* ======================================================================= + 4. IM REPORT + ======================================================================= */ +melde(""); +melde("=== 4. Im Report ==="); +{ + const d = json(await ruf("/workspace/api/report?tage=30", "GET", { cookie: kDogi })); + ok(Array.isArray(d.womit), "der Report bringt die Verteilung mit"); + const nach = (s) => d.womit.find((f) => f.schluessel === s); + ok(nach("sofort")?.anzahl === 1, `„Sofort“: ${nach("sofort")?.anzahl}`); + ok(nach("einplanen")?.anzahl === 2, + `„Einplanen“: ${nach("einplanen")?.anzahl} (groß und mittel)`); + ok(nach("nebenbei")?.anzahl === 1, `„Nebenbei“: ${nach("nebenbei")?.anzahl}`); + ok(nach("spaeter")?.anzahl === 1, `„Später“: ${nach("spaeter")?.anzahl}`); + ok(nach("offen")?.anzahl === 1, `„Noch nicht eingeschätzt“: ${nach("offen")?.anzahl}`); + + const summe = d.womit.reduce((n, f) => n + f.anzahl, 0); + ok(summe === 6, `zusammen ${summe} offene Aufgaben — die beiden fertigen fehlen zu Recht`); + + ok((nach("sofort")?.aufgaben || [])[0]?.titel === "Wichtig und klein", + "und die Titel stehen dabei"); +} + +/* ======================================================================= + 5. IM BROWSER + ======================================================================= */ +melde(""); +melde("=== 5. Im Browser ==="); +{ + const { chromium } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); + const browser = await chromium.launch(); + const kontext = await browser.newContext({ viewport: { width: 1280, height: 1000 } }); + const seite = await kontext.newPage(); + const konsole = []; + seite.on("console", (m) => { if (m.type() === "error" && !/favicon|ERR_/i.test(m.text())) konsole.push(m.text()); }); + seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message)); + + await seite.goto(`http://127.0.0.1:${PORT}/workspace/`, { waitUntil: "domcontentloaded" }); + await seite.click('.rolle[data-rolle="admin"]'); + await seite.fill("#code", "CODE-DOGI-0001"); + await seite.click("button[type=submit]"); + await seite.waitForURL("**/start.html", { timeout: 20000 }); + + /* ---- Der Abschnitt im Report ---- */ + await seite.goto(`http://127.0.0.1:${PORT}/workspace/report.html`, { waitUntil: "networkidle" }); + await seite.waitForTimeout(1200); + const r = await seite.evaluate(() => ({ + felder: document.querySelectorAll(".womit__feld").length, + namen: [...document.querySelectorAll(".womit__name")].map((x) => x.textContent.trim()), + zahlen: [...document.querySelectorAll(".womit__zahl")].map((x) => x.textContent.trim()), + eintraege: document.querySelectorAll(".womit__eintrag").length, + })); + ok(r.felder === 5, `${r.felder} Felder stehen im Report`); + ok(r.namen.includes("Sofort") && r.namen.includes("Noch nicht eingeschätzt"), + `mit sprechenden Namen: ${r.namen.join(", ")}`); + ok(r.zahlen.length === r.felder, "jedes mit seiner Zahl"); + ok(r.eintraege >= 5, `${r.eintraege} Aufgaben sind namentlich aufgeführt`); + + /* ---- Die Auswahl im Aufgaben-Dialog ---- */ + await seite.goto(`http://127.0.0.1:${PORT}/workspace/aufgaben.html`, { waitUntil: "networkidle" }); + await seite.waitForTimeout(900); + await seite.evaluate(() => { + const k = [...document.querySelectorAll(".karte")] + .find((x) => x.textContent.includes("Wichtig und klein")); + k?.querySelector("button")?.click(); + }); + await seite.waitForTimeout(600); + const a = await seite.evaluate(() => { + const f = document.getElementById("b-aufwand"); + if (!f) return null; + return { + werte: [...f.options].map((o) => o.value), + texte: [...f.options].map((o) => o.textContent.trim()), + gewaehlt: f.value, + }; + }); + ok(a !== null, "die Auswahl steht im Dialog"); + ok(a.werte.length === womit.AUFWAND.length + 1, + `mit ${a.werte.length} Möglichkeiten (drei Stufen plus „noch nicht eingeschätzt“)`); + ok(a.werte[0] === "", "und die erste ist „noch nicht eingeschätzt“ — keine erfundene Vorgabe"); + ok(a.gewaehlt === "klein", + `der gespeicherte Wert steht drin (${a.gewaehlt}) — die Liste vom Server überschreibt ihn nicht`); + ok(a.texte.some((t) => /Stunde|Tag/.test(t)), + `und jede Stufe sagt, was sie ungefähr heißt: ${a.texte.join(" | ").slice(0, 80)}`); + + ok(konsole.length === 0, + konsole.length ? "Fehler im Browser: " + konsole[0].slice(0, 90) : "keine Fehler im Browser"); + await browser.close(); +} + +/* ------------------------------------------------------------------- */ +melde(""); +melde(`${geprueft} Pruefungen, ${fehler} Fehler`); +melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG"); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ } +process.exit(fehler ? 1 : 0); diff --git a/server/workspace-aufgaben.js b/server/workspace-aufgaben.js index 50ae08bf..0609ec9b 100644 --- a/server/workspace-aufgaben.js +++ b/server/workspace-aufgaben.js @@ -16,6 +16,7 @@ import { ohneTeamDogi, ohneAgentur, siehtModis, TEAM_DOGI_ROLLEN, } from "./workspace.js"; import { eskalationSetzen } from "./workspace-eskalation.js"; +import { AUFWAND, AUFWAND_SCHLUESSEL } from "./workspace-womit.js"; export const aufgabenRouter = express.Router(); @@ -208,7 +209,7 @@ function darfAendern(person, aufgabe) { } const SPALTEN = ` - a.id, a.titel, a.beschreibung, a.status, a.prioritaet, a.frist, + a.id, a.titel, a.beschreibung, a.status, a.prioritaet, a.aufwand, a.frist, a.creator_id, a.verantwortlich_id, a.erstellt, a.geaendert, a.erledigt_am, a.creator_extern, a.verantwortlich_extern, ${externSql("pc.name", "a.creator_extern")} AS creator_name, @@ -276,6 +277,9 @@ aufgabenRouter.get("/workspace/api/aufgaben", (req, res) => { res.json({ aufgaben: reihen, + /* Die Aufwandsstufen kommen mit: Eine zweite Liste im Browser + waere die, die beim naechsten Wert veraltet. */ + aufwand: AUFWAND, /* Die Kategorien kommen mit der Liste, nicht ueber einen eigenen Aufruf: Das Brett braucht beides im selben Augenblick, und ein zweiter Aufruf waere ein zweiter Weg, der einzeln scheitern @@ -447,6 +451,16 @@ function pruefeFelder(körper, { neu, person }) { if (!PRIORITAETEN.includes(körper.prioritaet)) fehler.push("Unbekannte Priorität."); else aus.prioritaet = körper.prioritaet; } + /* DER AUFWAND IST FREIWILLIG -- ein leerer Wert nimmt die Schaetzung + zurueck, statt sie zu erzwingen. Wer nicht weiss, wie gross etwas + ist, soll nicht raten muessen; die Uebersicht sagt dann eben + "noch nicht eingeschaetzt", und das ist eine ehrliche Auskunft. */ + if (körper.aufwand !== undefined) { + const w = String(körper.aufwand ?? "").trim(); + if (!w) aus.aufwand = null; + else if (!AUFWAND_SCHLUESSEL.includes(w)) fehler.push("Unbekannter Aufwand."); + else aus.aufwand = w; + } if (körper.frist !== undefined) { const f = String(körper.frist ?? "").trim(); if (!f) aus.frist = null; diff --git a/server/workspace-reports.js b/server/workspace-reports.js index 218f8da3..966f4161 100644 --- a/server/workspace-reports.js +++ b/server/workspace-reports.js @@ -22,6 +22,7 @@ import express from "express"; import { db, protokolliere, echteIp, sitzungLesen, betreuteIds, darfCreator, istLeitung, istDogFather, siehtAlles, sichtbareCreatorIds, } from "./workspace.js"; +import { verteilen } from "./workspace-womit.js"; import { BEREICHE } from "./workspace-bereiche.js"; export const reportRouter = express.Router(); @@ -210,8 +211,20 @@ reportRouter.get("/workspace/api/report", (req, res) => { } } + /* --- Womit anfangen? (15.09.2026) -------------------------------- + Die offenen Aufgaben nach wichtig und Aufwand verteilt. Gerechnet + am Server: Der Report, die Startseite und das Brett lesen + dieselben Aufgaben, und drei Rechnungen fuer dieselbe Frage geben + irgendwann drei Antworten. */ + const womitAufgaben = db().prepare( + `SELECT a.id, a.titel, a.status, a.prioritaet, a.aufwand, a.frist + FROM aufgaben a + WHERE a.status NOT IN ('erledigt','abgebrochen')${nurA}`).all(...wertA); + const womit = verteilen(womitAufgaben); + res.json({ zeitraum: { tage, ab, bis: heute }, + womit, creator, creator_name: creator ? (db().prepare("SELECT name FROM personen WHERE id = ?").get(creator)?.name ?? null) diff --git a/server/workspace-womit.js b/server/workspace-womit.js new file mode 100644 index 00000000..cf0fdbcc --- /dev/null +++ b/server/workspace-womit.js @@ -0,0 +1,130 @@ +/* ===================================================================== + Womit anfangen? (15.09.2026) + + Letztes Stück von Stufe 9: „Wichtig-vs-Aufwand im Report". + + --------------------------------------------------------------------- + DIE FRAGE, DIE EINE AUFGABENLISTE NICHT BEANTWORTET + + Ein Brett mit dreißig offenen Karten sagt, WAS zu tun ist. Es sagt + nicht, WOMIT man anfängt. „Wichtig" allein hilft dabei nicht: Wenn + acht Karten wichtig sind, ist keine davon die erste. + + Die zweite Angabe, die dafür fehlt, ist der AUFWAND. Wichtig und + klein ist etwas anderes als wichtig und groß — das eine macht man + jetzt, das andere plant man ein. Mit beiden Angaben zusammen fällt + die Entscheidung von selbst. + + --------------------------------------------------------------------- + VIER FELDER, UND JEDES SAGT, WAS ZU TUN IST + + wichtig + klein SOFORT in einer halben Stunde erledigt, + und es zählt + wichtig + groß EINPLANEN braucht einen Termin, keinen + guten Willen + normal + klein NEBENBEI wenn zwischendurch Luft ist + normal + groß SPÄTER ehrlich: das wird gerade nichts + + Die Namen sind Handlungsanweisungen, keine Etiketten. „Quadrant 2" + sagt niemandem, was er tun soll. + + --------------------------------------------------------------------- + OHNE SCHÄTZUNG VERSCHWINDET NICHTS + + Der Aufwand ist freiwillig. Aufgaben ohne Schätzung landen deshalb + NICHT stillschweigend in einer Ecke, sondern in einer eigenen, klar + benannten Gruppe: „noch nicht eingeschätzt". Eine Übersicht, die + einen Teil der Arbeit unsichtbar macht, ist schlimmer als keine -- + man verlässt sich darauf und übersieht genau das, was fehlt. + ===================================================================== */ + +/** Die drei Aufwandsstufen. Bewusst nur drei. + * + * Fünf Stufen klingen genauer und sind es nicht: Niemand unterscheidet + * verlässlich zwischen "eher mittel" und "eher groß". Drei kann man + * ohne Nachdenken vergeben, und nur was ohne Nachdenken geht, wird + * auch gepflegt. */ +export const AUFWAND = [ + { schluessel: "klein", name: "klein", etwa: "unter einer Stunde" }, + { schluessel: "mittel", name: "mittel", etwa: "ein halber Tag" }, + { schluessel: "gross", name: "groß", etwa: "mehr als ein Tag" }, +]; + +export const AUFWAND_SCHLUESSEL = AUFWAND.map((a) => a.schluessel); + +/** Die vier Felder plus das fünfte für alles ohne Schätzung. */ +export const FELDER = [ + { + schluessel: "sofort", name: "Sofort", + satz: "Wichtig und schnell erledigt. Damit anfangen.", + wichtig: true, klein: true, + }, + { + schluessel: "einplanen", name: "Einplanen", + satz: "Wichtig, aber größer. Braucht einen Termin, keinen guten Willen.", + wichtig: true, klein: false, + }, + { + schluessel: "nebenbei", name: "Nebenbei", + satz: "Schnell gemacht, wenn zwischendurch Luft ist.", + wichtig: false, klein: true, + }, + { + schluessel: "spaeter", name: "Später", + satz: "Weder dringend noch schnell. Ehrlich: Das wird gerade nichts.", + wichtig: false, klein: false, + }, + { + schluessel: "offen", name: "Noch nicht eingeschätzt", + satz: "Ohne Aufwand lässt sich nicht sagen, wohin sie gehören.", + wichtig: null, klein: null, + }, +]; + +/** In welches Feld gehört diese Aufgabe? + * + * „Wichtig" ist die vorhandene Priorität `hoch` -- es wird kein + * zweites Feld dafür erfunden. Wer eine Aufgabe als wichtig markiert, + * hat das schon getan. + * + * „Klein" ist `klein`; `mittel` und `gross` zaehlen beide als nicht + * klein. Die Trennlinie liegt dort, wo sie im Alltag liegt: Schaffe + * ich das zwischendurch, oder muss ich mir Zeit nehmen? */ +export function feldVon(aufgabe) { + if (!aufgabe) return null; + if (!AUFWAND_SCHLUESSEL.includes(aufgabe.aufwand)) return "offen"; + const wichtig = aufgabe.prioritaet === "hoch"; + const klein = aufgabe.aufwand === "klein"; + return FELDER.find((f) => f.wichtig === wichtig && f.klein === klein)?.schluessel ?? "offen"; +} + +/** Die offenen Aufgaben auf die Felder verteilen. + * + * ERLEDIGTES UND ABGEBROCHENES ZAEHLT NICHT MIT. Die Frage lautet + * "womit fange ich an?" -- fertige Arbeit beantwortet sie nicht, und + * sie wuerde die Felder so voll machen, dass man nichts mehr sieht. + * + * @returns {Array} die Felder in ihrer Reihenfolge, jedes mit seinen + * Aufgaben und ihrer Anzahl + */ +export function verteilen(aufgaben) { + const nach = new Map(FELDER.map((f) => [f.schluessel, []])); + for (const a of aufgaben || []) { + if (a.status === "erledigt" || a.status === "abgebrochen") continue; + const feld = feldVon(a); + nach.get(feld)?.push(a); + } + return FELDER.map((f) => ({ + schluessel: f.schluessel, + name: f.name, + satz: f.satz, + anzahl: nach.get(f.schluessel).length, + /* Nur Titel und Nummer -- der Report ist eine Uebersicht, keine + zweite Aufgabenliste. Wer eine Karte oeffnen will, geht ins + Brett; die Nummer reicht dafuer. */ + aufgaben: nach.get(f.schluessel).slice(0, 12).map((a) => ({ + id: a.id, titel: a.titel, frist: a.frist ?? null, + })), + gekuerzt: nach.get(f.schluessel).length > 12, + })); +} diff --git a/server/workspace.js b/server/workspace.js index 06c2d900..c5efe3bb 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -2097,6 +2097,20 @@ function umstellungen(d) { loeschen, nicht den Vertrag, der daran hing. Die Datei verliert nur ihren Bezug und steht danach wieder in der Ablage. */ ["dateien", "aufgabe_id", "INTEGER REFERENCES aufgaben(id) ON DELETE SET NULL"], + + /* DER AUFWAND EINER AUFGABE (15.09.2026, Stufe 9). + + "Wichtig" gibt es schon -- das ist die Prioritaet. Was fehlte, ist + die zweite Angabe, ohne die man nicht entscheiden kann: Schaffe + ich das zwischendurch, oder muss ich mir Zeit nehmen? + + OHNE CHECK-LISTE in der Datenbank, und das ist Absicht: Die + erlaubten Werte stehen in workspace-womit.js, und eine CHECK-Liste + daneben waere eine zweite Wahrheit, die beim naechsten Wert + nachgezogen werden muesste -- ueber einen Tabellenneubau, an dem + im Projekt schon dreimal Spalten verlorengegangen sind. Geprueft + wird beim Schreiben, an der Stelle, die die Liste kennt. */ + ["aufgaben", "aufwand", "TEXT"], ]) { try { const vorhanden = d.prepare(`PRAGMA table_info(${tabelle})`).all().map((s) => s.name); diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index cdd3e288..ba8491e8 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609151321", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609151328", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609151321", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609151328", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609151321", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609151328", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609151321", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609151328", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/report.css b/workspace/assets/css/report.css index e1c1deb8..ce94bc8b 100644 --- a/workspace/assets/css/report.css +++ b/workspace/assets/css/report.css @@ -284,3 +284,47 @@ } /* Ohne Trend soll die zweite Zeile keinen Platz belegen. */ .block .kachel:not(:has(.trend)) { row-gap: 0; } + +/* ===================================================================== + WOMIT ANFANGEN? (15.09.2026) + + Vier Felder plus das fuenfte fuer alles ohne Schaetzung. Die + Reihenfolge ist die Aussage: "Sofort" steht oben links, "Spaeter" + unten. Wer von links oben nach rechts unten liest, liest die + Rangfolge. + + Die Kante links sagt die Dringlichkeit -- aber der NAME sagt sie + auch. Farbe allein waere hier besonders schlecht: Es geht um eine + Entscheidung, nicht um eine Stimmung. + ===================================================================== */ + +.womit { + display: grid; gap: 12px; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); +} +.womit__feld { + padding: 14px 16px; + background: color-mix(in srgb, var(--tinte) 94%, #ffffff 6%); + border: 1px solid var(--rand); + border-left: 3px solid var(--rand-hell); +} +.womit__feld[data-feld="sofort"] { border-left-color: #6f9e77; } +.womit__feld[data-feld="einplanen"] { border-left-color: #8ec9ff; } +.womit__feld[data-feld="nebenbei"] { border-left-color: var(--rand-hell); } +.womit__feld[data-feld="spaeter"] { border-left-color: var(--rand); } +.womit__feld[data-feld="offen"] { border-left-style: dashed; } + +.womit__kopf { display: flex; align-items: baseline; gap: 10px; } +.womit__name { margin: 0; font-size: 1rem; font-weight: 660; color: var(--text); } +.womit__zahl { + margin-left: auto; font-size: 1.15rem; font-weight: 700; + color: var(--text-leise); font-variant-numeric: tabular-nums; +} +.womit__satz { + margin: 5px 0 10px; font-size: .82rem; line-height: 1.55; color: var(--text-still); +} +.womit__liste { margin: 0; padding: 0 0 0 16px; display: grid; gap: 4px; } +.womit__eintrag { font-size: .84rem; } +.womit__eintrag a { color: var(--text-leise); text-decoration: none; } +.womit__eintrag a:hover { color: var(--text); text-decoration: underline; } +.womit__mehr { margin: 8px 0 0; font-size: .78rem; color: var(--text-still); } diff --git a/workspace/assets/js/aufgaben.js b/workspace/assets/js/aufgaben.js index 82e817ea..41ab215f 100644 --- a/workspace/assets/js/aufgaben.js +++ b/workspace/assets/js/aufgaben.js @@ -773,12 +773,20 @@ if (!zeigen && $(`${praefix}-kategorie`)) $(`${praefix}-kategorie`).value = ''; } + /* Die Aufwandsstufen kommen mit der Aufgabenliste. Hier + weitergereicht, damit die Auswahl im Dialog dieselbe Liste zeigt, + die der Server annimmt. */ + function aufwandUebernehmen(daten) { + window.aufwandStufenSetzen?.(daten?.aufwand); + } + async function laden() { try { const antwort = await hole('/workspace/api/aufgaben'); if (!antwort.ok) { melde('Aufgaben konnten nicht geladen werden.'); return; } const daten = await antwort.json(); alleAufgaben = daten.aufgaben || []; + aufwandUebernehmen(daten); kategorien = daten.kategorien || []; /* `null` muss `null` BLEIBEN -- ein `|| []` daraus zu machen waere der stille Fehler: Aus "gilt immer" wuerde "gilt nie", und das @@ -865,6 +873,7 @@ } kategorieFeldZeigen('b'); if ($('b-kategorie')) $('b-kategorie').value = a.kategorie || ''; + if ($('b-aufwand')) $('b-aufwand').value = a.aufwand || ''; window.anhaengeLaden?.(a.id); dialog.showModal(); $('b-titel').focus(); @@ -880,6 +889,7 @@ $('b-speichern').addEventListener('click', async () => { const daten = { + aufwand: $('b-aufwand') ? $('b-aufwand').value : undefined, titel: $('b-titel').value, beschreibung: $('b-text').value, prioritaet: $('b-prio').value, @@ -1510,3 +1520,35 @@ Das laesst sich nicht rueckgaengig machen. Erledigte Aufgaben sollten normalerwe } }); })(); + +/* ===================================================================== + DIE AUFWANDSSTUFEN KOMMEN VOM SERVER (15.09.2026) + + Die Auswahl wird aus der Liste gebaut, die mit den Aufgaben kommt -- + nicht aus drei