Drei Wuensche aus zwei Bildschirmfotos und einem Satz. === 1. SIE DARF SAGEN, WAS PASST UND WAS NICHT === Filipe: "die rechte hand soll bei diesen aufgaben auch wie die modis sagen koennen ob es passt oder nicht ... die rechte hand ist da um mir zu helfen aber auch wie die modis um mir zu sagen was passt und nicht wo die denken bedarf oder nicht. perfektionier das alles." SIE KONNTE ES NICHT, UND ZWAR AN DREI STELLEN GLEICHZEITIG -- die zweite und dritte kamen erst zum Vorschein, nachdem die erste behoben war. Genau deshalb steht "perfektionier das alles" ueber diesem Commit und nicht "eine Zeile geaendert": ERSTENS: Die Frage "an wessen Liste arbeitest du?" kannte nur Creator und Modis (dann die eigene). Sie fiel durch beide Zweige und konnte ueberall antworten, nirgends urteilen. ZWEITENS: Nach der Reparatur durfte sie -- und bekam den KATALOG DER CREATOR. Ihre Meldung trug einen Schluessel, den der Eingang gar nicht kennt, und wurde dort stillschweigend uebersprungen. Die Pruefung meldete "sie darf urteilen (200)" und eine Zeile spaeter "ihre Meldung steht NICHT in DogFathers Eingang". Beides stimmte. DRITTENS: Der Eingang sammelt aus der Liste der Teammitglieder, und die hiess `rolle = 'modi'`. Sie war die Empfaengerin dieser Seite und kam auf ihr selbst nicht vor. Wer nicht in der Liste steht, kann melden, so viel er will -- es erreicht niemanden. Jetzt steht sie in derselben Liste (aber `id <> ich`: eine Karte ueber sich selbst ist kein Ueberblick, sondern ein Spiegel), bekommt denselben Beobachtungskatalog wie die Modis und meldet in denselben Eingang. Alle Rollenvergleiche kommen dabei aus der vorhandenen Menge TEAM_DOGI_ROLLEN statt als zwei Vergleiche danebengeschrieben -- sonst steht dort beim naechsten Mal einer zu wenig. === 2. DIE LEISTE === Filipe: "der hintergrund dieser leiste soll extrem speziell aussehen wie ein universum und soll von lila auf babyblau wechseln, von links nach rechts ... der husky links soll auch babyblau strahlen und nicht rot und der rechts lila. dan brauch ich auch noch einen teilen button." DIE RICHTUNG IST DIE EIGENTLICHE AENDERUNG: Der Schein lief von UNTEN nach oben (die Glut der Agenturseite, nur in Lila). Jetzt von LINKS nach rechts, mit zwei Nebeln und sieben Sternen -- als Verlaufslagen und nicht als Elemente: Sieben Punkte waeren sieben Knoten im Baum auf jeder der zwanzig Seiten, nur fuer Zierde. DIE ZEICHEN TAUSCHEN DIE SEITEN. Links stand ein warmes Rot, fest hingeschrieben in start.css; rechts strahlte der zweite Husky babyblau. Jetzt umgekehrt -- so hat jede Seite der Leiste beide Farben, statt dass jede nur eine hat. Beim rechten wird die FUELLUNG mitgetauscht, nicht nur der Schein: Ein lila Schein um ein blaues Zeichen waere ein Rand, keine Farbe. "UEBERTRIEBEN GEIL" HAT EINE GRENZE, und sie ist gemessen, nicht geschaetzt. Erster Anlauf: .30 -- der Verlauf war zu ahnen, nicht zu sehen, Kontrast 5,26:1. Zweiter: .40 -- sichtbar, aber 4,79:1 in der Mitte, bei einer Grenze von 4,5 zu knapp. Jetzt kraeftige Enden und eine zurueckhaltende Mitte: 5,92 / 5,26 / 7,06:1 an drei Stellen des Verlaufs, gemessen an echten Bildpunkten. Die Farbe liest das Auge an den Enden; in der Mitte gewinnt die Lesbarkeit. DER TEILEN-KNOPF fehlte, weil `DARF_TEILEN` Leitung und Scouts kennt -- die rechte Hand ist keins von beidem. Gefragt wird jetzt nicht nach der Rolle, sondern nach `ich.marke`: Die setzt der Server genau dann, wenn jemand zu Team Dogi gehoert. Der Rollenname bleibt damit aus einer Datei heraus, die jeder herunterlaedt -- und die Frage lautet ohnehin "gehoert diese Person hierher?". === 3. SEIN KALENDER BLEIBT GANZ === Filipe: "mein kalender (dogfather) auf dieser seite hier soll komplett verbunden sein mit dem kalender in der workspace seite ABER NUR IN DER DOGFATHER ROLLE BEI DOGFATHER: NICHT BEI VANVAN." Bei Aufgaben, Bereichen und Dateien ist die Haustrennung eine Hilfe -- man will das andere Haus dort gerade nicht sehen. Beim Kalender waere sie eine Falle: Wer auf der Team-Seite einen Termin eintraegt und die Haelfte seines Tages nicht sieht, legt ihn auf eine Zeit, in der er schon woanders sitzt. Ein Kalender, der nur die halbe Wahrheit zeigt, ist schlimmer als keiner. Geprueft wird die ROLLE und nicht der Name -- ein Name waere die Stelle, an der es beim naechsten Menschen bricht. === WAS DIE PRUEFUNGEN GEZEIGT HABEN === Eine Erwartung war seit Stunden stumm rot: pruef-modi-checkliste verlangte GENAU EINE Zusatzkachel, und seit dem Ideen-Board sind es drei. Ich hatte sie nach der Aenderung nicht noch einmal laufen lassen. Sie zaehlt jetzt nicht mehr auf eine feste Zahl, sondern prueft die AUSSAGE: Wer welche bekommen soll, bekommt welche -- und jede gehoert in die Gruppe "Team Dogi". Eine Pruefung, die bei jeder neuen Kachel rot wird, erzieht dazu, ihr Rotwerden zu ignorieren. Zwei weitere Erwartungen haben sich gedreht und sind im Quelltext begruendet: Der Kalender auf crew. zeigt DogFather jetzt alles (die Pruefung unterscheidet dafuer ER-ja/SIE-nein statt einer einzelnen Antwort -- das ist mehr, nicht weniger), und "aus dem Eingang verschwunden" fragt jetzt nach Schluessel UND Person: Seit die rechte Hand denselben Katalog benutzt, koennen zwei Menschen denselben Punkt melden, und die Pruefung meldete einen Fehler, den es nicht gab. Und eine war wertlos: "die rechte Hand sieht den Agenturtermin nicht" lief gegen einen leeren Kalender -- sie sah ueberhaupt nichts. Sie hat jetzt zwei eigene Termine, einen aus jedem Haus; erst damit sagt die Messung etwas. pruef-modi-checkliste 59 -> 70 · pruef-team-stufen 24 -> 26 · pruef-team-ampel 32 · pruef-haus-trennung 61 -> 62 · pruef-rollen 278 · pruef-css-klassen gruen · pruef-modi-wortleck 5. Co-Authored-By: Claude Opus 5 <[email protected]>
643 lines
29 KiB
JavaScript
643 lines
29 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,
|
||
} 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";
|
||
}
|
||
|
||
/* 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);
|
||
}
|
||
|
||
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. */
|
||
const modis = db().prepare(
|
||
`SELECT id, name, bild, letzter_login, aktiv, erstellt, stufe FROM personen
|
||
WHERE rolle IN ('hand','modi') AND id <> ?
|
||
ORDER BY aktiv DESC, name`).all(person.id);
|
||
|
||
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();
|
||
|
||
/* =================================================================
|
||
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] })),
|
||
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),
|
||
/* 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 || 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" });
|
||
}
|
||
});
|