Zwei Auftraege in einem Zug: "mach weiter" (Blueprint Kapitel 4/4.1)
und, zum Bildschirmfoto der Seite, "das muss viel krasser sein".
KAPITEL 4 -- DIE MITGLIEDER-KACHEL
Der Blueprint nennt: "Name & Foto · Rolle(n)/Kategorie(n) · Status
(aktiv/pausiert) · Anzahl offener Aufgaben · Datum 'Modi seit' ·
Schnellaktionen". Name, Foto und Zahlen standen schon; Status, "dabei
seit", Stufe und ein Knopf zum Schreiben kommen dazu.
KAPITEL 4.1 -- DIE DREI STUFEN
Probe, Standard, Senior. Sie sind eine ARBEITSEINTEILUNG, keine
Rechtegrenze -- was ein Modi darf, haengt an der Rolle; die Stufe sagt,
wo er im Team steht. Gesetzt werden sie nur von DogFather: Der
Blueprint gibt der rechten Hand den gleichen UEBERBLICK, aber
"Verwaltungsrechte optional durch Owner freischaltbar", also aus.
NULL heisst "Probe" und nicht "unbekannt" -- ein dritter Zustand waere
eine Frage, die niemand beantworten kann.
ZWEI ECHTE FEHLER, BEIDE VON DER NEUEN PRUEFUNG GEFUNDEN
1. PAUSIERTE VERSCHWANDEN KOMPLETT. In der Abfrage stand `AND aktiv =
1`. Wer jemanden pausierte, bei dem verschwand er samt seiner
OFFENEN RUECKMELDUNGEN aus dem Eingang -- die warteten weiter auf
eine Antwort, nur sah sie niemand mehr.
2. DER EINGANG HAETTE AUF FRISCHER ANLAGE 503 GELIEFERT. Die
Checklisten-Tabellen entstehen beim ersten Aufruf einer Checkliste;
diese Seite liest sie aber auch. Wer sie oeffnete, bevor je jemand
eine Checkliste angesehen hatte, bekam einen Fehler ohne Erklaerung
-- auf einer neuen Anlage also beim allerersten Blick. Das Schema
hat jetzt einen Besitzer, der es herausgibt; ein zweites CREATE
TABLE waere der Anfang von zwei Schemata gewesen.
"VIEL KRASSER" -- UND ZWAR MIT INFORMATION, NICHT MIT LAERM
* ZWEI REIHEN STATT EINER. Alle fuenf Kaesten lagen in EINEM Raster
mit 150 px Mindestbreite: Im Bildschirmfoto stand "Community 12
von" -- abgeschnitten mitten in der Zahl -- und die laengste Liste
machte die ganze Reihe so hoch wie sich selbst. Jetzt oben, was
eine Antwort braucht, darunter das Team; 300 px Mindestbreite.
* DIE KARTE WAR FAHL, und das war ein Fehler: `--r` fiel auf ein
helles Grau zurueck, aus dem das Kantenlicht einen Nebel ueber die
ganze Karte legte. Sie traegt jetzt die Farbe der Stufe -- kein
Nebel, und man sieht am Rand, wer wo steht.
* SECHS ZAHLENKAESTEN WURDEN DREI BALKEN. "40 offen" beantwortet
nicht, wie weit man ist: 40 von 40 ist etwas anderes als 40 von
100. Gruen (in Ordnung) und Bernstein (zu besprechen) fuellen den
Balken; die ganze Zeile ist der Weg dorthin.
* DIE DREI ZAHLEN DER SEITE stehen oben als Zahlen statt in einem
Satz. Die Warnfarbe erscheint NUR, wenn wirklich etwas wartet --
eine Null in Bernstein waere ein Alarm ohne Anlass, und ab dem
dritten Mal sieht man ihn nicht mehr.
GEMESSEN
pruef-team-stufen (neu) 24, mit Gegenprobe: hoch- und zurueckstufen,
und dieselbe Stufe zweimal zu setzen darf
keine zweite Protokollzeile erzeugen
pruef-rollen 274 pruef-modi-checkliste 59
pruef-zwischenspeicher 21
Ansicht: Rechner 1440 und Handy 412 -- keine Skriptfehler, nichts
ragt seitlich heraus (0 px).
Co-Authored-By: Claude Opus 5 <[email protected]>
534 lines
24 KiB
JavaScript
534 lines
24 KiB
JavaScript
/* =====================================================================
|
||
DIE LAGE DES MODI-TEAMS -- an einer Stelle (10.09.2026)
|
||
|
||
Wunsch Filipe: *"dan will ich auch dass vanvan und ich diese sachen
|
||
von den modis auch in der dogfather rolle sehen. da soll es eine
|
||
kategorie geben für uns beide nur sonst keinen wo wir dass alles
|
||
sehen und behandeln können. so wie mit dem ideen-board."*
|
||
|
||
DIE DOGFATHER-ROLLE UND DIE RECHTE HAND. Das sind Filipe und VanVan
|
||
(beide `admin`) sowie seit dem 10.09.2026 die Stellvertretung. Der
|
||
Blueprint V3.0 sagt in Kapitel 3.1 ueber sie: "Kann im Alltag
|
||
vertretend Entscheidungen treffen." Genau das passiert hier -- aus
|
||
einer Rueckmeldung wird eine Aufgabe oder eine begruendete Absage.
|
||
Ihr diese Seite zu verwehren hiesse, ihr die Vertretung zu geben und
|
||
den Ort dafuer wegzunehmen.
|
||
|
||
Fuer alle anderen gibt es diese Seite nicht: 404 wie bei einer
|
||
Adresse, die niemand kennt. Ein "kein Zugriff" waere die Auskunft,
|
||
dass es sie gibt.
|
||
|
||
SEHEN UND BEHANDELN, in dieser Reihenfolge: Die Seite zaehlt nur --
|
||
bewertet wird dort, wo die Punkte stehen. Jede Zahl ist deshalb ein
|
||
Verweis in den jeweiligen Bereich, mit der Person schon
|
||
vorausgewaehlt. Eine zweite Stelle zum Bewerten waere eine zweite
|
||
Stelle, an der es auseinanderlaeuft.
|
||
|
||
UND SIE IST KEINE UEBERWACHUNG. Dieselbe Ueberlegung steht schon
|
||
ueber team.html: Eine Seite mit Zahlen ueber Kollegen wird als
|
||
Kontrolle gelesen, und dann arbeitet niemand mehr offen damit.
|
||
Deshalb zaehlt sie, was NOCH NICHT BESPROCHEN ist -- nicht, wer wie
|
||
fleissig war. "Offen" heisst hier: Darueber habt ihr noch nicht
|
||
geredet. Es ist eine Liste fuer euch, keine Note fuer ihn.
|
||
===================================================================== */
|
||
import express from "express";
|
||
import {
|
||
db, sitzungLesen, istDogFather, heuteLokal, protokolliere, echteIp,
|
||
} from "./workspace.js";
|
||
import { MODI_PUNKTE } from "./workspace-modi-punkte.js";
|
||
import { checklistenTabellen } from "./workspace-checkliste.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 --
|
||
die Saetze aendern sich nicht zur Laufzeit. */
|
||
const GESAMT = Object.fromEntries(
|
||
Object.entries(MODI_PUNKTE).map(([b, g]) => [b, Object.values(g).flat().length]));
|
||
|
||
/** Wer den Eingang oeffnen darf.
|
||
*
|
||
* An EINER Stelle und nicht dreimal `istDogFather(person) ||
|
||
* person.rolle === "hand"`: Diese Datei hat drei Schranken, und die
|
||
* vierte kommt mit der naechsten Route. Wer eine davon vergisst,
|
||
* merkt es nicht -- eine vergessene Schranke faellt nur dem auf, der
|
||
* hindurchgeht. */
|
||
/* =====================================================================
|
||
DIE DREI STUFEN (Blueprint V3.0, Kapitel 4.1)
|
||
|
||
Probe -- in der Einarbeitung, wird eingeschaetzt.
|
||
Standard -- der Normalfall: moderiert, arbeitet eigene Aufgaben ab.
|
||
Senior -- kann zusaetzlich neue Probe-Modis einschaetzen, mehrere
|
||
Kategorien gleichzeitig tragen und die rechte Hand bei
|
||
Abwesenheit vertreten.
|
||
|
||
SIE SIND EINE ARBEITSEINTEILUNG, KEINE RECHTEGRENZE -- jedenfalls
|
||
heute noch nicht. Das gehoert ausgesprochen, damit niemand sie
|
||
spaeter fuer eine haelt: Was ein Modi in dieser App darf, haengt an
|
||
der ROLLE. Die Stufe sagt, wo er im Team steht, und macht den
|
||
naechsten Schritt sichtbar. Die Rechte, die der Blueprint dort
|
||
nennt, liegen ausserdem groesstenteils gar nicht hier, sondern im
|
||
Stream (Timeout, Bann).
|
||
|
||
Die Reihenfolge ist die Steigerung -- sie steht hier einmal, damit
|
||
keine Liste im Browser eine eigene erfindet. */
|
||
export const STUFEN = ["probe", "standard", "senior"];
|
||
|
||
export const STUFEN_NAME = {
|
||
probe: "Probe",
|
||
standard: "Standard",
|
||
senior: "Senior",
|
||
};
|
||
|
||
/** NULL heisst "Probe", nicht "unbekannt".
|
||
*
|
||
* Ein dritter Zustand waere eine Frage, die niemand beantworten kann,
|
||
* und ein neuer Mensch faengt ohnehin in der Einarbeitung an. Die
|
||
* Abbildung steht an EINER Stelle -- stuende sie in jeder Abfrage,
|
||
* waere sie beim naechsten Mal in einer davon anders. */
|
||
function stufeVon(wert) {
|
||
return STUFEN.includes(String(wert || "")) ? String(wert) : "probe";
|
||
}
|
||
|
||
function darfEingang(person) {
|
||
return istDogFather(person) || person?.rolle === "hand";
|
||
}
|
||
|
||
teamlageRouter.get("/workspace/api/teamlage", (req, res) => {
|
||
try {
|
||
const person = sitzungLesen(req);
|
||
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
|
||
/* 404 und nicht 403 -- siehe Kopf. */
|
||
if (!darfEingang(person)) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
|
||
/* DIE CHECKLISTEN-TABELLEN ENTSTEHEN BEIM ERSTEN AUFRUF EINER
|
||
CHECKLISTE -- diese Seite liest sie aber auch.
|
||
|
||
Wer den Eingang oeffnete, BEVOR je jemand eine Checkliste
|
||
angesehen hatte, bekam hier eine 503 ohne Erklaerung. Auf einer
|
||
frischen Anlage also beim allerersten Blick. Gefunden hat das
|
||
nicht der Betrieb, sondern pruef-team-stufen.mjs, die genau
|
||
diesen Zustand herstellt. */
|
||
checklistenTabellen();
|
||
|
||
/* AUCH DIE PAUSIERTEN (10.09.2026, Blueprint Kapitel 4).
|
||
|
||
Die Mitglieder-Kachel dort nennt ausdruecklich "Status
|
||
(aktiv/pausiert)" -- also muss man Pausierte auch SEHEN. Vorher
|
||
stand hier `AND aktiv = 1`: Wer jemanden pausierte, bei dem
|
||
verschwand er samt seiner offenen Rueckmeldungen aus dem
|
||
Eingang. Die warteten weiter auf eine Antwort, nur sah sie
|
||
niemand mehr.
|
||
|
||
Aktive zuerst, dann nach Namen -- wer pausiert ist, steht unten
|
||
und nicht mittendrin. */
|
||
const modis = db().prepare(
|
||
`SELECT id, name, bild, letzter_login, aktiv, erstellt, stufe FROM personen
|
||
WHERE rolle = 'modi' ORDER BY aktiv DESC, name`).all();
|
||
|
||
if (!modis.length) {
|
||
/* `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);
|
||
const heute = heuteLokal();
|
||
|
||
/* ALLES IN VIER ABFRAGEN, nicht vier je Person. Bei zehn Modis
|
||
waeren das vierzig -- den Fehler hat das Haus bei den Terminen
|
||
schon einmal gemacht und ihn dort ausdruecklich vermerkt. */
|
||
const stand = new Map(); // person -> bereich -> {gut, verbessern}
|
||
for (const z of db().prepare(
|
||
`SELECT creator_id, bereich, stufe, COUNT(*) AS n FROM punkt_stand
|
||
WHERE creator_id IN (${platz}) GROUP BY creator_id, bereich, stufe`).all(...nummern)) {
|
||
if (!stand.has(z.creator_id)) stand.set(z.creator_id, {});
|
||
const b = stand.get(z.creator_id);
|
||
b[z.bereich] = b[z.bereich] || { gut: 0, verbessern: 0 };
|
||
if (z.stufe === "gut" || z.stufe === "verbessern") b[z.bereich][z.stufe] = z.n;
|
||
}
|
||
|
||
const aufgaben = new Map();
|
||
for (const z of db().prepare(
|
||
`SELECT verantwortlich_id AS p,
|
||
COUNT(*) AS offen,
|
||
SUM(CASE WHEN frist IS NOT NULL AND frist < ? THEN 1 ELSE 0 END) AS ueberfaellig
|
||
FROM aufgaben
|
||
WHERE verantwortlich_id IN (${platz})
|
||
AND status NOT IN ('erledigt', 'abgebrochen')
|
||
GROUP BY verantwortlich_id`).all(heute, ...nummern)) {
|
||
aufgaben.set(z.p, { offen: z.offen, ueberfaellig: z.ueberfaellig || 0 });
|
||
}
|
||
|
||
const ideen = new Map();
|
||
for (const z of db().prepare(
|
||
`SELECT erstellt_von AS p, COUNT(*) AS n FROM eintraege
|
||
WHERE bereich = 'ideen' AND erstellt_von IN (${platz}) GROUP BY erstellt_von`)
|
||
.all(...nummern)) {
|
||
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();
|
||
for (const z of db().prepare(
|
||
`SELECT creator_id AS p, COUNT(*) AS n FROM punkt_nachricht
|
||
WHERE creator_id IN (${platz}) GROUP BY creator_id`).all(...nummern)) {
|
||
nachrichten.set(z.p, z.n);
|
||
}
|
||
|
||
res.json({
|
||
heute,
|
||
bereiche: GESAMT,
|
||
eingang,
|
||
angebote,
|
||
/* DIE STUFEN KOMMEN VOM SERVER, nicht aus einer Liste im Browser.
|
||
|
||
Eine zweite Aufzaehlung dort waere die Stelle, an der beim
|
||
naechsten Mal eine Stufe fehlt oder anders heisst -- genau so
|
||
stand am 07.09. unter dem Codefeld "PERSOENLICHER ZUGANGSCODE ·
|
||
undefined", weil eine Rolle in der Browser-Liste vergessen
|
||
worden war. Einmal geschrieben, einmal geaendert. */
|
||
stufen: STUFEN.map((wert) => ({ wert, name: STUFEN_NAME[wert] })),
|
||
modis: modis.map((m) => {
|
||
const s = stand.get(m.id) || {};
|
||
const je = {};
|
||
for (const [b, gesamt] of Object.entries(GESAMT)) {
|
||
const gut = s[b]?.gut || 0;
|
||
const verbessern = s[b]?.verbessern || 0;
|
||
je[b] = { gesamt, gut, verbessern, offen: Math.max(gesamt - gut - verbessern, 0) };
|
||
}
|
||
return {
|
||
id: m.id,
|
||
name: m.name,
|
||
bild: m.bild ? `/workspace/api/steckbrief/bild/${m.bild}` : null,
|
||
letzter_login: m.letzter_login,
|
||
/* Die drei Felder aus Kapitel 4: Status, "dabei seit", Stufe. */
|
||
aktiv: !!m.aktiv,
|
||
seit: m.erstellt || null,
|
||
stufe: stufeVon(m.stufe),
|
||
aufgaben: aufgaben.get(m.id) || { offen: 0, ueberfaellig: 0 },
|
||
ideen: ideen.get(m.id) || 0,
|
||
nachrichten: nachrichten.get(m.id) || 0,
|
||
bereiche: je,
|
||
};
|
||
}),
|
||
});
|
||
} catch (fehler) {
|
||
console.error("[workspace] Team-Lage:", fehler?.message);
|
||
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.
|
||
===================================================================== */
|
||
/* =====================================================================
|
||
DIE STUFE SETZEN (Blueprint Kapitel 4.1, Punkt 3: "Rechte anpassen")
|
||
|
||
NUR DOGFATHER, nicht die rechte Hand. Der Blueprint trennt das
|
||
ausdruecklich: Sie hat den gleichen UEBERBLICK, aber
|
||
"Verwaltungsrechte optional durch Owner freischaltbar" -- also
|
||
standardmaessig aus. Wer im Team auf welcher Stufe steht, ist eine
|
||
Personalentscheidung; sie gehoert dem, der die Leute holt.
|
||
|
||
404 statt 403, wie ueberall auf dieser Seite: Wer nicht darf, muss
|
||
nicht erfahren, dass es den Weg gibt.
|
||
===================================================================== */
|
||
teamlageRouter.put("/workspace/api/teamlage/stufe", gleicheHerkunft,
|
||
express.json({ limit: "2kb" }), (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 personId = Number(req.body?.person_id) || 0;
|
||
const stufe = String(req.body?.stufe || "");
|
||
if (!STUFEN.includes(stufe)) {
|
||
return res.status(400).json({ fehler: "Unbekannte Stufe." });
|
||
}
|
||
|
||
/* NUR AN EINEM MODI. Ein Manager oder Creator hat keine Stufe --
|
||
sie stuende in der Datenbank und wuerde nirgends angezeigt,
|
||
also waere sie ein stiller Datensatz, den niemand mehr
|
||
zuordnen kann. Und die rechte Hand hat auch keine: Sie ist
|
||
eine Rolle, keine Stufe darin. */
|
||
const ziel = db().prepare(
|
||
"SELECT id, name, rolle, stufe FROM personen WHERE id = ?").get(personId);
|
||
if (!ziel || ziel.rolle !== "modi") {
|
||
return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
}
|
||
const vorher = STUFEN.includes(String(ziel.stufe || "")) ? String(ziel.stufe) : "probe";
|
||
if (vorher === stufe) return res.json({ ok: true, stufe, geaendert: false });
|
||
|
||
db().prepare("UPDATE personen SET stufe = ? WHERE id = ?").run(stufe, ziel.id);
|
||
/* Ins Protokoll, weil es eine Personalentscheidung ist: Wer sie
|
||
wann getroffen hat, muss spaeter nachvollziehbar sein -- gerade
|
||
wenn jemand zurueckgestuft wird. */
|
||
protokolliere("stufe_gesetzt", {
|
||
personId: ziel.id, rolle: ziel.rolle, ip: echteIp(req),
|
||
detail: `${ziel.name}: ${vorher} -> ${stufe}`,
|
||
});
|
||
return res.json({ ok: true, stufe, geaendert: true });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Stufe setzen:", fehler?.message);
|
||
return res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
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 (!darfEingang(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 (!darfEingang(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" });
|
||
}
|
||
});
|