Modis verteilen Aufgaben statt sie zu bekommen -- und Angebote zur Entscheidung
Die drei Beurteilungs-Bereiche liefen bisher in die falsche Richtung: Sie
bewerteten die Modis. Filipe: "die modis sind ja da um mir zu helfen."
Also umgedreht.
WAS SICH GEDREHT HAT
- Alle 101 Punkte sind jetzt Beobachtungen ueber den Stream, nicht
Pflichten des Modis ("Der Ton blieb verstaendlich" statt "Ton geprueft").
- Nur der Modi selbst drueckt auf seiner Liste. Wer sonst darauf zeigt,
bekommt 403 und den Weg zur Team-Lage -- bewerten wird hier niemand.
- "Verbessern" landet als Eingang bei DogFather. Ein Klick macht daraus
eine Aufgabe mit dem Satz des Modis im Text, oder eine Absage mit Grund.
Beides schreibt eine Nachricht zurueck, damit der Modi sieht: angekommen.
ANGEBOTE
Neuer Bereich, in dem Modis planen und vorschlagen: Nutzen und Aufwand
statt Bewertung und Dringlichkeit, dazu ein Feld "was DogFather danach
tun muss". Wird ein Angebot angenommen, entstehen zwei Aufgaben -- eine
beim Modi zum Umsetzen, eine bei DogFather aus genau diesem Feld.
GEMESSEN
pruef-modi-checkliste 59 Pruefungen, 0 Fehler (neu geschrieben)
pruef-rollen 245 statt 244 -- der Zuwachs ist die neue
Angebote-Kachel, alle 11 Modi-Kacheln kommen an
pruef-modi-ideen 30, pruef-modi-verborgen 75, pruef-bereiche-lesend: gruen
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -26,10 +26,33 @@
|
||||
===================================================================== */
|
||||
import express from "express";
|
||||
import {
|
||||
db, sitzungLesen, istDogFather, heuteLokal,
|
||||
db, sitzungLesen, istDogFather, heuteLokal, protokolliere, echteIp,
|
||||
} from "./workspace.js";
|
||||
import { MODI_PUNKTE } from "./workspace-modi-punkte.js";
|
||||
|
||||
/* Titel und Bereich zu einem Schluessel -- damit im Eingang steht,
|
||||
WORUM es geht, und nicht nur ein Schluessel. */
|
||||
const PUNKT = new Map();
|
||||
for (const [bereich, gruppen] of Object.entries(MODI_PUNKTE)) {
|
||||
for (const punkte of Object.values(gruppen)) {
|
||||
for (const p of punkte) PUNKT.set(`${bereich}|${p.schluessel}`, p);
|
||||
}
|
||||
}
|
||||
|
||||
/* Die Kategorie, unter der eine daraus entstandene Aufgabe einsortiert
|
||||
wird. Der Bereich sagt es schon -- eine zweite Zuordnung waere eine
|
||||
zweite Gelegenheit, dass beide auseinanderlaufen. */
|
||||
const KATEGORIE = { live: "technik", community: "community", technik: "technik" };
|
||||
|
||||
function gleicheHerkunft(req, res, next) {
|
||||
const herkunft = req.get("origin");
|
||||
if (!herkunft) return next();
|
||||
let erlaubt;
|
||||
try { erlaubt = new URL(herkunft).host === req.get("host"); } catch { erlaubt = false; }
|
||||
if (!erlaubt) return res.status(403).json({ fehler: "fremde_herkunft" });
|
||||
next();
|
||||
}
|
||||
|
||||
export const teamlageRouter = express.Router();
|
||||
|
||||
/* Wie viele Punkte es je Bereich ueberhaupt gibt. Einmal gerechnet --
|
||||
@@ -49,7 +72,11 @@ teamlageRouter.get("/workspace/api/teamlage", (req, res) => {
|
||||
WHERE rolle = 'modi' AND aktiv = 1 ORDER BY name`).all();
|
||||
|
||||
if (!modis.length) {
|
||||
return res.json({ modis: [], bereiche: GESAMT, heute: heuteLokal() });
|
||||
/* `eingang` auch hier -- die Oberflaeche darf nicht raten
|
||||
muessen, ob das Feld fehlt oder leer ist. */
|
||||
return res.json({
|
||||
modis: [], eingang: [], angebote: [], bereiche: GESAMT, heute: heuteLokal(),
|
||||
});
|
||||
}
|
||||
const platz = modis.map(() => "?").join(",");
|
||||
const nummern = modis.map((m) => m.id);
|
||||
@@ -88,6 +115,46 @@ teamlageRouter.get("/workspace/api/teamlage", (req, res) => {
|
||||
ideen.set(z.p, z.n);
|
||||
}
|
||||
|
||||
/* =================================================================
|
||||
DER EINGANG: was die Modis gemeldet haben (10.09.2026)
|
||||
|
||||
Filipes Entscheidung: erst ein Vorschlag, dann eine Aufgabe.
|
||||
Hier steht, was noch nicht behandelt ist -- offen heisst: Ihr
|
||||
habt noch nicht darauf geantwortet. Was zur Aufgabe wurde oder
|
||||
begruendet abgelehnt ist, faellt heraus; ein Eingang, der nie
|
||||
leer wird, wird nicht mehr angesehen. */
|
||||
const eingang = [];
|
||||
for (const z of db().prepare(
|
||||
`SELECT s.bereich, s.schluessel, s.creator_id, s.begruendung, s.am, p.name AS von_name
|
||||
FROM punkt_stand s LEFT JOIN personen p ON p.id = s.von
|
||||
WHERE s.creator_id IN (${platz})
|
||||
AND s.stufe = 'verbessern' AND s.behandelt_am IS NULL
|
||||
ORDER BY s.am DESC`).all(...nummern)) {
|
||||
const punkt = PUNKT.get(`${z.bereich}|${z.schluessel}`);
|
||||
/* Ein Punkt, den es nicht mehr gibt, wird uebersprungen -- aber
|
||||
gezaehlt waere er sonst und niemand koennte ihn behandeln. */
|
||||
if (!punkt) continue;
|
||||
eingang.push({
|
||||
bereich: z.bereich, schluessel: z.schluessel, person_id: z.creator_id,
|
||||
titel: punkt.titel, hinweis: punkt.text,
|
||||
begruendung: z.begruendung || "", am: z.am, von: z.von_name || null,
|
||||
});
|
||||
}
|
||||
|
||||
/* =================================================================
|
||||
DIE ANGEBOTE (10.09.2026)
|
||||
|
||||
Vorschlaege der Modis, ueber die hier entschieden wird. Sie
|
||||
stehen im selben Eingang wie die Rueckmeldungen -- EIN Ort, an
|
||||
dem etwas auf eine Antwort wartet, nicht zwei. Wer zwei Eingaenge
|
||||
hat, sieht irgendwann in den falschen. */
|
||||
const angebote = db().prepare(
|
||||
`SELECT e.id, e.art, e.titel, e.text, e.einsatz, e.bewertung, e.dringlichkeit,
|
||||
e.datum, e.erstellt_von, p.name AS von_name
|
||||
FROM eintraege e LEFT JOIN personen p ON p.id = e.erstellt_von
|
||||
WHERE e.bereich = 'angebote' AND e.status = 'offen'
|
||||
ORDER BY e.bewertung DESC, e.datum DESC`).all();
|
||||
|
||||
/* Wie viele Rueckmeldungen an seinen Punkten haengen -- das ist
|
||||
das Gespraech, nicht die Note. */
|
||||
const nachrichten = new Map();
|
||||
@@ -100,6 +167,8 @@ teamlageRouter.get("/workspace/api/teamlage", (req, res) => {
|
||||
res.json({
|
||||
heute,
|
||||
bereiche: GESAMT,
|
||||
eingang,
|
||||
angebote,
|
||||
modis: modis.map((m) => {
|
||||
const s = stand.get(m.id) || {};
|
||||
const je = {};
|
||||
@@ -125,3 +194,198 @@ teamlageRouter.get("/workspace/api/teamlage", (req, res) => {
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
/* =====================================================================
|
||||
EINE MELDUNG BEHANDELN (10.09.2026)
|
||||
|
||||
Filipes Entscheidung: *"Erst ein Vorschlag, den du annimmst."* Zwei
|
||||
Antworten, und beide kommen beim Modi an:
|
||||
|
||||
annehmen -> daraus wird eine Aufgabe (fuer den, der drueckt)
|
||||
ablehnen -> mit Begruendung, ohne dass etwas entsteht
|
||||
|
||||
WARUM BEIDE EINE NACHRICHT SCHREIBEN: Eine Meldung, auf die nichts
|
||||
folgt, sieht fuer den Modi aus wie eine, die niemand gelesen hat --
|
||||
und beim dritten Mal meldet er nichts mehr. Die Nachricht haengt am
|
||||
Punkt (punkt_nachricht), also genau dort, wo er sie beim naechsten
|
||||
Mal wiederfindet.
|
||||
|
||||
DIE AUFGABE GEHOERT DEM, DER DRUECKT. Filipe oder VanVan -- wer
|
||||
annimmt, nimmt sie sich. Sie einem Dritten zuzuweisen waere eine
|
||||
Entscheidung ueber jemand anderen, ohne ihn zu fragen.
|
||||
===================================================================== */
|
||||
teamlageRouter.post("/workspace/api/teamlage/behandeln", gleicheHerkunft,
|
||||
express.json({ limit: "16kb" }), (req, res) => {
|
||||
try {
|
||||
const person = sitzungLesen(req);
|
||||
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
|
||||
if (!istDogFather(person)) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
|
||||
const bereich = String(req.body?.bereich || "");
|
||||
const schluessel = String(req.body?.schluessel || "");
|
||||
const personId = Number(req.body?.person_id) || 0;
|
||||
const art = String(req.body?.art || "");
|
||||
const grund = String(req.body?.grund ?? "").trim().slice(0, 2000);
|
||||
if (!["aufgabe", "abgelehnt"].includes(art)) {
|
||||
return res.status(400).json({ fehler: "Unbekannte Art." });
|
||||
}
|
||||
if (!PUNKT.has(`${bereich}|${schluessel}`) || !personId) {
|
||||
return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
}
|
||||
/* EINE ABSAGE OHNE GRUND IST KEINE ANTWORT. Wer nichts dazu
|
||||
schreibt, laesst den Modi genauso ratlos zurueck wie
|
||||
Schweigen -- nur mit dem Unterschied, dass die Meldung jetzt
|
||||
auch noch aus dem Eingang verschwunden ist. */
|
||||
if (art === "abgelehnt" && grund.length < 3) {
|
||||
return res.status(400).json({ fehler: "Bitte kurz sagen, warum nicht." });
|
||||
}
|
||||
|
||||
const meldung = db().prepare(
|
||||
`SELECT begruendung FROM punkt_stand
|
||||
WHERE bereich = ? AND schluessel = ? AND creator_id = ?
|
||||
AND stufe = 'verbessern' AND behandelt_am IS NULL`)
|
||||
.get(bereich, schluessel, personId);
|
||||
/* Schon behandelt oder gar nicht gemeldet -- beides ist "gibt es
|
||||
hier nicht". Zwei Antworten waeren zwei Faelle, die die
|
||||
Oberflaeche unterscheiden muesste, ohne dass es hilft. */
|
||||
if (!meldung) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
|
||||
const punkt = PUNKT.get(`${bereich}|${schluessel}`);
|
||||
const jetzt = new Date().toISOString();
|
||||
let aufgabeId = null;
|
||||
|
||||
if (art === "aufgabe") {
|
||||
const { lastInsertRowid } = db().prepare(`INSERT INTO aufgaben
|
||||
(titel, beschreibung, status, prioritaet, verantwortlich_id,
|
||||
kategorie, erstellt, erstellt_von)
|
||||
VALUES (?,?,'offen','mittel',?,?,?,?)`).run(
|
||||
punkt.titel,
|
||||
/* Der Satz des Modis steht IN der Aufgabe -- ohne ihn muesste
|
||||
man zurueckblaettern, um zu wissen, worum es ging. */
|
||||
meldung.begruendung
|
||||
? `Gemeldet aus „${punkt.titel}“:
|
||||
${meldung.begruendung}`
|
||||
: `Gemeldet aus „${punkt.titel}“.`,
|
||||
person.id, KATEGORIE[bereich] || "sonstiges", jetzt, person.id);
|
||||
aufgabeId = Number(lastInsertRowid);
|
||||
}
|
||||
|
||||
db().prepare(
|
||||
`UPDATE punkt_stand
|
||||
SET behandelt_am = ?, behandelt_von = ?, behandelt_art = ?, aufgabe_id = ?
|
||||
WHERE bereich = ? AND schluessel = ? AND creator_id = ?`)
|
||||
.run(jetzt, person.id, art, aufgabeId, bereich, schluessel, personId);
|
||||
|
||||
db().prepare(`INSERT INTO punkt_nachricht
|
||||
(bereich, schluessel, creator_id, text, von, am) VALUES (?,?,?,?,?,?)`)
|
||||
.run(bereich, schluessel, personId,
|
||||
art === "aufgabe"
|
||||
? `Danke – daraus ist eine Aufgabe geworden.${grund ? " " + grund : ""}`
|
||||
: `Danke fürs Melden. Das machen wir nicht: ${grund}`,
|
||||
person.id, jetzt);
|
||||
|
||||
protokolliere("meldung_behandelt", {
|
||||
personId: person.id, rolle: person.rolle, ip: echteIp(req),
|
||||
detail: `${bereich}/${schluessel} -> ${art}`.slice(0, 120),
|
||||
});
|
||||
res.json({ ok: true, art, aufgabe_id: aufgabeId });
|
||||
} catch (fehler) {
|
||||
console.error("[workspace] Meldung behandeln:", fehler?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
/* =====================================================================
|
||||
UEBER EIN ANGEBOT ENTSCHEIDEN (10.09.2026)
|
||||
|
||||
Filipes Wahl: *"Angebote an dich zur Entscheidung ... Bei Annahme
|
||||
entstehen die Aufgaben fuer alle Beteiligten auf einmal."*
|
||||
|
||||
BEI ANNAHME ZWEI AUFGABEN, nicht eine:
|
||||
* der Modi, der es vorgeschlagen hat, setzt es um
|
||||
* und wenn im Angebot steht, was DogFather dafuer tun muesste,
|
||||
bekommt der, der annimmt, genau das als eigene Aufgabe
|
||||
|
||||
Die zweite ist der Kern von Filipes Wunsch ("entscheiden was ich
|
||||
machen muss"). Ohne sie waere eine Annahme ein Nicken, aus dem nichts
|
||||
folgt -- und beim dritten Mal schlaegt niemand mehr etwas vor.
|
||||
|
||||
ABGELEHNT BRAUCHT EINEN GRUND, wie bei den Rueckmeldungen: Eine
|
||||
Absage ohne Begruendung ist fuer den, der sie bekommt, dasselbe wie
|
||||
Schweigen -- nur endgueltiger.
|
||||
===================================================================== */
|
||||
teamlageRouter.post("/workspace/api/teamlage/angebot", gleicheHerkunft,
|
||||
express.json({ limit: "16kb" }), (req, res) => {
|
||||
try {
|
||||
const person = sitzungLesen(req);
|
||||
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
|
||||
if (!istDogFather(person)) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
|
||||
const id = Number(req.body?.id) || 0;
|
||||
const art = String(req.body?.art || "");
|
||||
const grund = String(req.body?.grund ?? "").trim().slice(0, 2000);
|
||||
if (!["angenommen", "abgelehnt"].includes(art)) {
|
||||
return res.status(400).json({ fehler: "Unbekannte Art." });
|
||||
}
|
||||
if (art === "abgelehnt" && grund.length < 3) {
|
||||
return res.status(400).json({ fehler: "Bitte kurz sagen, warum nicht." });
|
||||
}
|
||||
|
||||
const angebot = db().prepare(
|
||||
`SELECT id, titel, text, einsatz, erstellt_von FROM eintraege
|
||||
WHERE id = ? AND bereich = 'angebote' AND status = 'offen'`).get(id);
|
||||
/* Schon entschieden oder gar keins -- beides ist "gibt es hier
|
||||
nicht". Zwei Antworten waeren zwei Faelle, die die Oberflaeche
|
||||
unterscheiden muesste, ohne dass es hilft. */
|
||||
if (!angebot) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
|
||||
const jetzt = new Date().toISOString();
|
||||
const angelegt = [];
|
||||
if (art === "angenommen") {
|
||||
if (angebot.erstellt_von) {
|
||||
const { lastInsertRowid } = db().prepare(`INSERT INTO aufgaben
|
||||
(titel, beschreibung, status, prioritaet, verantwortlich_id,
|
||||
kategorie, erstellt, erstellt_von)
|
||||
VALUES (?,?,'offen','mittel',?,'planung',?,?)`).run(
|
||||
`Umsetzen: ${angebot.titel}`, angebot.text || null,
|
||||
angebot.erstellt_von, jetzt, person.id);
|
||||
angelegt.push({ fuer: angebot.erstellt_von, id: Number(lastInsertRowid) });
|
||||
}
|
||||
if (angebot.einsatz) {
|
||||
const { lastInsertRowid } = db().prepare(`INSERT INTO aufgaben
|
||||
(titel, beschreibung, status, prioritaet, verantwortlich_id,
|
||||
kategorie, erstellt, erstellt_von)
|
||||
VALUES (?,?,'offen','mittel',?,'planung',?,?)`).run(
|
||||
angebot.einsatz, `Aus dem Angebot „${angebot.titel}“.`,
|
||||
person.id, jetzt, person.id);
|
||||
angelegt.push({ fuer: person.id, id: Number(lastInsertRowid) });
|
||||
}
|
||||
}
|
||||
|
||||
db().prepare(
|
||||
"UPDATE eintraege SET status = ?, geaendert = ? WHERE id = ?")
|
||||
.run(art, jetzt, angebot.id);
|
||||
|
||||
/* Die Antwort gehoert an das Angebot, damit der Modi sie dort
|
||||
wiederfindet, wo er nachsieht. `text` ist das einzige Feld
|
||||
dafuer -- angehaengt, nicht ersetzt. */
|
||||
const antwort = art === "angenommen"
|
||||
? `
|
||||
|
||||
— Angenommen von ${person.name}${grund ? ": " + grund : "."}`
|
||||
: `
|
||||
|
||||
— Nicht umgesetzt (${person.name}): ${grund}`;
|
||||
db().prepare("UPDATE eintraege SET text = COALESCE(text, '') || ? WHERE id = ?")
|
||||
.run(antwort, angebot.id);
|
||||
|
||||
protokolliere("angebot_entschieden", {
|
||||
personId: person.id, rolle: person.rolle, ip: echteIp(req),
|
||||
detail: `#${angebot.id} ${art}`.slice(0, 120),
|
||||
});
|
||||
res.json({ ok: true, art, aufgaben: angelegt.length });
|
||||
} catch (fehler) {
|
||||
console.error("[workspace] Angebot entscheiden:", fehler?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user