Gefunden beim Bauen des Vorlagenbretts, nicht gesucht:
pruef-modi-wortleck war ROT, und zwar seit einem Tag. Sieben
Fundstellen in zwei Dateien, die JEDER bekommt, der die Seite oeffnet
-- ohne Anmeldung, aus dem offenen Netz:
assets/css/team.css --ton-modi, .tmerkmal--r-modi,
.t-gruppe[data-rolle="modi"]
assets/js/teamlage.js die Woerter-Tabelle und drei Rueckfaelle
DER ZUGANG HAELT GENAU SO LANGE, WIE NIEMAND DEN NAMEN IM QUELLTEXT
FINDET. Genau dafuer gibt es diese Pruefung: Am 10.09. hatte ich
denselben Fehler schon einmal gemacht und ihn durch Nachsehen
gefunden, nicht durch Nachdenken -- Nachdenken hatte ich vorher
getan. Seither ist die Regel ein Werkzeug statt eines Vorsatzes.
Nur: Das Werkzeug hat einen Tag lang rot geleuchtet, und niemand hat
hingesehen. Das ist der eigentliche Befund an diesem Commit, und er
steht auch in der Vault-Notiz.
WAS SICH AENDERT
----------------
Die Woerter kommen jetzt vom Server (`rolle_name` aus ROLLEN_NAME) --
so wie ueberall sonst im Haus. Der Browser fuehrt keine eigene Liste
mehr; zwei Listen fuer dieselbe Sache waeren ohnehin zwei
Gelegenheiten, dass eine veraltet.
DIE DREI RUECKFAELLE SIND WEG (`|| 'modi'`, `|| 'Modi'`). Am 21.09.
wurde genau so einer an einer vierten Stelle entfernt, mit zwei
Gruenden: Er schreibt den Namen in eine ausgelieferte Datei, und er
hat noch nie etwas bewirkt -- der Server liefert die Rolle immer mit.
Beides galt fuer diese drei ebenso; sie waren damals nur uebersehen
worden. Fehlt die Rolle jetzt doch einmal, bleibt das Merkmal weg
statt falsch.
DER DRITTE FARBTON HEISST NACH SEINER AUFGABE, nicht nach seinem
Traeger: aus `--ton-modi` wird `--ton-team`, der Grundton des Teams.
Die beiden Haende weichen davon ab -- damit braucht die dritte Rolle
gar keinen eigenen Wahlausdruck mehr, und ihr Name steht nirgends in
der Datei. Das ist nicht nur unverfaenglich, es ist auch richtiger:
Eine Farbe gehoert einer Rolle nicht, sie steht fuer sie.
GEPRUEFT
--------
pruef-modi-wortleck: BESTANDEN, 8 von 8 (vorher 1 Fehler).
Ihre eigene Gegenprobe laeuft mit: eine eingebaute Fundstelle wird
erkannt, und "modifiziert", "Modul", "motion", "Modus" schlagen nicht an.
pruef-teamlage-karten: 39 von 39 weiter gruen -- darunter die Zeilen,
auf die es hier ankommt: "jede Karte nennt ihre Rolle im Klartext
(keine Luecke)" und "jede Ueberschrift nennt ihre Rolle und ihre
Anzahl richtig (Rechte Hand 1 · Linke Hand 1 · Modi 5)". Die Woerter
kommen also weiterhin an, nur aus einer anderen Quelle.
Co-Authored-By: Claude Opus 5 <[email protected]>
788 lines
36 KiB
JavaScript
788 lines
36 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,
|
||
fuehrtTeamDogi,
|
||
heuteLokal,
|
||
protokolliere,
|
||
echteIp,
|
||
ROLLEN_SORTIERUNG,
|
||
TEAM_DOGI_ROLLEN,
|
||
ROLLEN_NAME,
|
||
} 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 FUENF STUFEN (Blueprint V3.0, Kapitel 4.1 -- erweitert 22.09.2026)
|
||
|
||
Probe -- in der Einarbeitung, wird begleitet.
|
||
Standard -- der Normalfall: moderiert, arbeitet eigene
|
||
Aufgaben ab.
|
||
Fortgeschritten-- kennt die Ablaeufe und braucht keinen Anstoss mehr.
|
||
Senior -- schaetzt neue Probe-Modis ein und traegt mehrere
|
||
Kategorien gleichzeitig.
|
||
Vertretung -- springt fuer die rechte Hand ein, wenn sie nicht
|
||
da ist.
|
||
|
||
ES WAREN DREI, UND ZWEI DAVON WAREN KEINE EIGENE STUFE, SONDERN EIN
|
||
BUENDEL. Filipe am 22.09.2026: "und ich will das es ein zwei
|
||
kategorien mehr gibt und das soll geiler aussehen und jede seine
|
||
eigene farbe, stark erkennbar."
|
||
|
||
Die beiden neuen sind nicht erfunden, sondern herausgeloest:
|
||
|
||
(1) "Fortgeschritten" gibt es im Haus laengst -- der
|
||
Entwicklungs-Katalog nennt seine Erwartungsstufen
|
||
probe/standard/fort/senior (ENTWICKLUNG_ERWARTUNG in
|
||
workspace-entwicklungs-punkte.js). Genau diese Mitte fehlte
|
||
hier, und Filipe hatte sie dort am 17.09. selbst eingefordert
|
||
("wieso ist da immer noch nicht perfektionniert wie bei den
|
||
anderen mit fortgeschritten und so"). Zwei verschiedene
|
||
Leitern fuer dieselben Menschen waren der eigentliche Fehler.
|
||
|
||
(2) "Vertretung" stand bis heute IN der Beschreibung von Senior --
|
||
"und die rechte Hand bei Abwesenheit vertreten". Das ist etwas
|
||
anderes als "kann einschaetzen": Einschaetzen koennen mehrere,
|
||
einspringen tut eine bestimmte Person. In einer Zeile gebuendelt
|
||
war es unsichtbar.
|
||
|
||
SIE SIND WEITERHIN EINE ARBEITSEINTEILUNG, KEINE RECHTEGRENZE. Das
|
||
gehoert ausgesprochen, damit niemand sie 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).
|
||
|
||
KEINE DATENBANKAENDERUNG NOETIG, und das ist kein Zufall: Die Spalte
|
||
`personen.stufe` ist absichtlich ein blankes TEXT ohne CHECK (siehe
|
||
den Kommentar in workspace.js). Dort steht woertlich, warum: "Kaeme
|
||
morgen eine vierte dazu, waere ein Tabellenneubau auf einer
|
||
Live-Datenbank ein hoher Preis fuer eine Beschriftung." Heute kamen
|
||
eine vierte und eine fuenfte, und der Preis war null.
|
||
|
||
Die Reihenfolge ist die Steigerung -- sie steht hier einmal, damit
|
||
keine Liste im Browser eine eigene erfindet. */
|
||
export const STUFEN = ["probe", "standard", "fort", "senior", "vertretung"];
|
||
|
||
export const STUFEN_NAME = {
|
||
probe: "Probe",
|
||
standard: "Standard",
|
||
fort: "Fortgeschritten",
|
||
senior: "Senior",
|
||
vertretung: "Vertretung",
|
||
};
|
||
|
||
/** 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";
|
||
}
|
||
|
||
/* AUS DER EINEN QUELLE (10.09.2026). Hier stand
|
||
`istDogFather(person) || person?.rolle === "hand"` -- dieselbe Regel
|
||
noch einmal. Beim naechsten Mal (den Kategorie-Kanaelen) waere sie
|
||
ein drittes Mal dagestanden. Jetzt fragen alle dieselbe Funktion. */
|
||
function darfEingang(person) {
|
||
return fuehrtTeamDogi(person);
|
||
}
|
||
|
||
/** Wer eine Stufe hat -- und damit eine, die sich setzen laesst.
|
||
*
|
||
* NUR MODIS. Ein Manager oder Creator hat keine; die rechte Hand
|
||
* auch nicht -- sie ist eine Rolle, keine Stufe darin. Steht hier an
|
||
* EINER Stelle, weil zwei Stellen danach fragen: die Liste (damit
|
||
* die Knoepfe gar nicht erst erscheinen) und der Weg zum Setzen
|
||
* (damit niemand daran vorbeikommt). Zwei Fassungen waeren die, bei
|
||
* der die Oberflaeche etwas anbietet, das der Server ablehnt. */
|
||
const hatStufe = (p) => p?.rolle === "modi";
|
||
|
||
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.
|
||
|
||
DIE RECHTE HAND GEHOERT DAZU (11.09.2026). Filipe: "die rechte
|
||
hand ist da um mir zu helfen aber auch wie die modis um mir zu
|
||
sagen was passt und nicht."
|
||
|
||
Bis hierher stand hier `rolle = 'modi'`. Damit war sie zwar die
|
||
Empfaengerin dieser Seite, kam auf ihr aber nicht vor -- und vor
|
||
allem: Ihre Rueckmeldungen landeten nirgends. Der Eingang weiter
|
||
unten sammelt genau aus dieser Liste; wer nicht darin steht, kann
|
||
melden, so viel er will, es kommt nicht an.
|
||
|
||
`id <> ?` LAESST DEN LESER SELBST HERAUS. Ohne das saehe die
|
||
rechte Hand eine Karte ueber sich selbst -- mit ihrer eigenen
|
||
Ampel, ihrer eigenen Stufe und ihren eigenen offenen Punkten.
|
||
Das ist kein Ueberblick ueber das Team, das ist ein Spiegel.
|
||
|
||
UND ZWAR DEN, DURCH DESSEN AUGEN GESEHEN WIRD (11.09.2026).
|
||
Hier stand `person.id`, also immer der Angemeldete. In der Sicht
|
||
auf die rechte Hand stand sie deshalb selbst mit auf ihrer
|
||
eigenen Seite -- genau der Spiegel, den der Absatz darueber
|
||
verhindern soll. Der Riegel eine Zeile hoeher (`darfEingang`)
|
||
bleibt beim Angemeldeten: Das ist ein Recht, keine Anzeige. */
|
||
const zeigt = req.sicht || person;
|
||
/* ==== DIE RECHTE HAND STEHT ZUERST (20.09.2026) ==============
|
||
|
||
Filipe: "ich will dass hier wie ueberall die reihenfolge immer
|
||
rechte hand und dan erst die modis."
|
||
|
||
Hier stand `ORDER BY aktiv DESC, name` -- die Rolle wurde nicht
|
||
einmal MITGELESEN. Die Seite konnte also gar nicht wissen, wer
|
||
rechte Hand ist und wer Modi; sie sortierte alphabetisch, und
|
||
damit stand Diene vor Funny, weil D vor F kommt.
|
||
|
||
Sortiert wird jetzt mit ROLLEN_SORTIERUNG -- derselben Regel, die
|
||
auch die Personenliste, der Chat und die Rechtetafel benutzen.
|
||
Eine eigene Reihenfolge hier waere die zweite Wahrheit, und die
|
||
zweite ist die, die beim naechsten Rollenwechsel abweicht. */
|
||
/* ==== DIE LINKE HAND GEHOERT DAZU (23.09.2026) ================
|
||
|
||
Filipe, mit dem Bildschirmfoto dieser Seite: „ich will dass die
|
||
linke hand auch ueberall zu sehen ist. reihenfolge immer. rechte
|
||
hand, dan linke hand und dan modis."
|
||
|
||
Hier stand `rolle IN ('hand','modi')` -- eine abgeschriebene
|
||
Liste, und die linke Hand kam darin nicht vor. Sie fehlte damit
|
||
nicht nur als Karte: Der Eingang weiter unten sammelt aus
|
||
genau dieser Liste. Ihre Rueckmeldungen waeren nirgends
|
||
angekommen -- dasselbe, was am 11.09. schon der rechten Hand
|
||
passiert ist, an derselben Zeile.
|
||
|
||
JETZT AUS DER EINEN LISTE DES HAUSES. `TEAM_DOGI_ROLLEN` ist
|
||
{hand, linke, modi} und steht in crew-adresse.js. Kaeme morgen
|
||
eine weitere Team-Rolle, waere sie hier von selbst dabei.
|
||
|
||
DIE REIHENFOLGE STIMMTE SCHON: ROLLEN_REIHE nennt hand vor
|
||
linke vor modi, und ROLLEN_SORTIERUNG leitet daraus das CASE
|
||
ab. Sie war nur nie zu sehen, weil eine der drei fehlte. */
|
||
const platzRollen = [...TEAM_DOGI_ROLLEN].map(() => "?").join(", ");
|
||
const modis = db().prepare(
|
||
`SELECT id, name, rolle, bild, letzter_login, aktiv, erstellt, stufe FROM personen
|
||
WHERE rolle IN (${platzRollen}) AND id <> ?
|
||
ORDER BY aktiv DESC, ` + ROLLEN_SORTIERUNG + `, name`)
|
||
.all(...TEAM_DOGI_ROLLEN, zeigt.id);
|
||
|
||
if (!modis.length) {
|
||
/* `eingang` auch hier -- die Oberflaeche darf nicht raten
|
||
muessen, ob das Feld fehlt oder leer ist. */
|
||
return res.json({
|
||
leute: [], 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();
|
||
|
||
/* =================================================================
|
||
DIE VERFUEGBARKEITS-AMPEL (Blueprint Kapitel 7.1, 10.09.2026)
|
||
|
||
"Persoenlicher Kalender (privat): Inhalt ausschliesslich fuer die
|
||
jeweilige Person sichtbar; Owner/rechte Hand sehen nur die
|
||
Verfuegbarkeits-Ampel, nicht den Klartext."
|
||
|
||
DIE ERSTE HAELFTE STAND SCHON: termineSichtbar() gibt jedem nur
|
||
seine eigenen Termine -- ausdruecklich auch DogFather. Es fehlte
|
||
genau die zweite: zu sehen, OB jemand kann, ohne zu sehen, WAS er
|
||
vorhat.
|
||
|
||
DIESE ABFRAGE HOLT NIEMALS titel, beschreibung ODER ort. Nicht
|
||
"sie zeigt sie nicht an" -- sie liest sie gar nicht erst. Was
|
||
nicht abgefragt wird, kann auch nicht versehentlich in einer
|
||
Antwort landen, wenn jemand spaeter ein Feld durchreicht.
|
||
|
||
WARUM DIE ZUGEHOERIGKEIT HIER NOCH EINMAL STEHT statt ueber
|
||
termineSichtbar(): Jene Funktion beantwortet die Frage fuer EINE
|
||
Person. Bei zehn Modis waeren das zehn Abfragen -- der Fehler,
|
||
den diese Datei zwei Bildschirme weiter oben ausdruecklich
|
||
vermeidet. Die Aufzaehlung unten ist dieselbe Regel als
|
||
Mengenvereinigung: angelegt von mir, mir zugeordnet, ich bin das
|
||
Gegenueber, oder ich stehe in der Teilnehmerliste.
|
||
|
||
DASS BEIDE DASSELBE SAGEN, IST GEMESSEN und nicht behauptet:
|
||
pruef-team-ampel.mjs stellt beide Wege nebeneinander. Ohne diese
|
||
Probe waere die Ampel eines Tages leise falsch -- und niemand
|
||
haette einen Anhaltspunkt, denn sie zeigt ja nur eine Farbe. */
|
||
const ampelTage = 7;
|
||
const bisTag = new Date(Date.parse(`${heute}T12:00:00Z`) + (ampelTage - 1) * 86400000)
|
||
.toISOString().slice(0, 10);
|
||
|
||
const belegt = new Map(); // person -> tag -> { termine, minuten }
|
||
{
|
||
const platzhalter = platz;
|
||
const zeilen = db().prepare(`
|
||
WITH meins AS (
|
||
SELECT creator_id AS pid, id FROM termine WHERE creator_id IN (${platzhalter})
|
||
UNION
|
||
SELECT teilnehmer_id AS pid, id FROM termine WHERE teilnehmer_id IN (${platzhalter})
|
||
UNION
|
||
SELECT erstellt_von AS pid, id FROM termine WHERE erstellt_von IN (${platzhalter})
|
||
UNION
|
||
SELECT tn.person_id AS pid, tn.termin_id FROM termin_teilnehmer tn
|
||
WHERE tn.person_id IN (${platzhalter})
|
||
)
|
||
SELECT m.pid AS p, date(t.beginn) AS tag,
|
||
COUNT(*) AS termine, SUM(t.dauer_min) AS minuten
|
||
FROM meins m JOIN termine t ON t.id = m.id
|
||
WHERE date(t.beginn) BETWEEN ? AND ?
|
||
GROUP BY m.pid, date(t.beginn)`)
|
||
.all(...nummern, ...nummern, ...nummern, ...nummern, heute, bisTag);
|
||
for (const z of zeilen) {
|
||
if (!belegt.has(z.p)) belegt.set(z.p, {});
|
||
belegt.get(z.p)[z.tag] = { termine: z.termine, minuten: z.minuten || 0 };
|
||
}
|
||
}
|
||
|
||
/* DIE DREI STUFEN.
|
||
|
||
Drei Stunden sind die Grenze zwischen "geht noch was" und "der
|
||
Tag ist verplant" -- ein Sendeabend dauert etwa so lang. Der Wert
|
||
ist ein Anfangswert und steht hier als Zahl mit Namen, damit man
|
||
ihn an EINER Stelle aendern kann, wenn sich das im Alltag anders
|
||
anfuehlt.
|
||
|
||
NULL MINUTEN HEISST "frei", nicht "unbekannt": Ein leerer
|
||
Kalender ist eine Aussage. Wer nichts eingetragen hat, ist
|
||
ansprechbar -- genau das will man wissen. */
|
||
const VOLL_AB_MIN = 180;
|
||
const ampelFuer = (eintrag) => {
|
||
if (!eintrag || !eintrag.minuten) return "frei";
|
||
return eintrag.minuten >= VOLL_AB_MIN ? "voll" : "teils";
|
||
};
|
||
const tagesliste = [];
|
||
for (let i = 0; i < ampelTage; i++) {
|
||
tagesliste.push(new Date(Date.parse(`${heute}T12:00:00Z`) + i * 86400000)
|
||
.toISOString().slice(0, 10));
|
||
}
|
||
|
||
/* 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] })),
|
||
/* DER SCHLUESSEL HEISST "leute" UND NICHT MEHR NACH DER ROLLE
|
||
(21.09.2026).
|
||
|
||
Die Oberflaeche liest ihn in teamlage.js -- also in einer
|
||
Datei, die jeder im Netz ohne Anmeldung herunterladen kann.
|
||
Damit stand der Rollenname der Community dort im CODE, nicht
|
||
nur in einem Kommentar. Filipes Regel vom selben Tag: in
|
||
Kommentaren ja, in Code und sichtbaren Texten nein.
|
||
|
||
Die oertliche Variable darunter heisst weiter `modis` -- sie
|
||
verlaesst diese Datei nie. Umbenannt wurde genau das, was
|
||
ueber die Leitung geht. */
|
||
leute: 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,
|
||
/* DIE ROLLE GEHT MIT (20.09.2026). Ohne sie kann die Seite
|
||
die rechte Hand nicht von einem Modi unterscheiden -- und
|
||
genau das soll sie: andere Reihenfolge, anderes Aussehen.
|
||
Sie stand bisher nicht einmal in der Abfrage. */
|
||
rolle: m.rolle,
|
||
/* UND WIE SIE HEISST (23.09.2026).
|
||
|
||
Der Browser hatte dafuer eine eigene kleine Tabelle
|
||
({hand: "Rechte Hand", linke: "Linke Hand", modi: "Modi"})
|
||
-- in einer Datei, die JEDER bekommt, der die Seite
|
||
oeffnet. Damit stand der verborgene Rollenname im offenen
|
||
Netz, und pruef-modi-wortleck war seit dem 22.09. rot.
|
||
Gefunden hat es die Pruefung, nicht das Lesen.
|
||
|
||
Jetzt kommt das Wort von hier, aus ROLLEN_NAME -- derselben
|
||
Tabelle, aus der es im ganzen Haus kommt. Zwei Listen fuer
|
||
dieselbe Sache waeren ohnehin zwei Gelegenheiten, dass
|
||
eine davon veraltet. */
|
||
rolle_name: ROLLEN_NAME[m.rolle] || m.rolle,
|
||
/* OB SICH EINE STUFE UEBERHAUPT SETZEN LAESST (22.09.2026).
|
||
|
||
Filipe: „wenn ich auf diese sachen druecke werd ich auf
|
||
die startseite geschickt, scheck das bitte ab."
|
||
|
||
Nachgestellt: Die Oberflaeche zeigte die drei Stufen-
|
||
Knoepfe an JEDER Karte -- auch an der der rechten Hand.
|
||
Die hat keine Stufe (sie ist eine Rolle, keine Stufe
|
||
darin), der Server antwortete mit 404, und der 404 warf
|
||
von der Seite (das steckte in holen.js und ist dort
|
||
behoben).
|
||
|
||
Ein Knopf, der mit 404 antwortet, ist schlimmer als
|
||
keiner. Die Antwort auf „hat diese Person eine Stufe"
|
||
kommt deshalb von der Stelle, die es entscheidet -- und
|
||
aus derselben Funktion, die der Weg weiter unten
|
||
benutzt. */
|
||
stufe_aenderbar: hatStufe(m),
|
||
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),
|
||
/* NUR DIE AMPEL. Kein Titel, kein Ort, keine Beschreibung --
|
||
die stehen nicht einmal in der Abfrage. Die Zahl der
|
||
Termine und die belegten Minuten sagen, wie voll es ist,
|
||
ohne zu sagen, womit. */
|
||
verfuegbar: tagesliste.map((tag) => {
|
||
const e = (belegt.get(m.id) || {})[tag];
|
||
return { tag, ampel: ampelFuer(e), termine: e?.termine || 0, minuten: e?.minuten || 0 };
|
||
}),
|
||
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 || !hatStufe(ziel)) {
|
||
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" });
|
||
}
|
||
});
|