Filipe hat den Bauplan "Willkommen & Anleitung -- Einweisung je Rolle"
(27 Seiten, Stand 06.10.2026) geschickt: pruefen, perfektionieren und
auf der Crew-Seite umsetzen.
AUSGANGSMESSUNG, bevor etwas angefasst wurde:
agentur creator 18 · manager 22 · scout 21 · spicy 23 Karten
crew 0 Karten -- die Schnittstelle antwortete 404 "nicht_hier"
Die Seite lud also, war aber leer. Baulich vorbereitet war sie schon:
Alle drei Inhaltstabellen tragen `haus` mit UNIQUE (haus, rolle,
kachel), und die Aussaat nimmt das Haus als Parameter.
WAS JETZT DA IST
gast 14 Karten (5 Muss, 4 Regelmaessig, 5 Bedarf)
modi 29 Karten (7 / 7 / 15)
linke 29 Karten (7 / 7 / 15)
hand 34 Karten (8 / 7 / 19)
Jede Zahl entspricht Kapitel 13 des Bauplans. Dazu je Rolle
Leitgedanke, drei Saetze, fuenf Erste Schritte, "Nicht deins",
Rhythmus und Fragen -- woertlich aus Kapitel 06 bis 09.
VOR DEM SCHREIBEN ABGEGLICHEN, nicht danach: Ein Skript prueft jede
Karte gegen die echte Kachelliste der Rolle. Ergebnis fuer alle vier
Fassungen: Kartenzahl wie im Plan, jede Karte trifft eine Kachel,
jede Kachel hat eine Karte, keine doppelt.
ZWEI KACHELN AUF EINER SEITE. Auf der Crew-Adresse fuehren "Chat"
(das Team) und "Rudel-Chat" (`?raum=treff`) beide auf chat.html.
`schluesselVonZiel` behielt nur `b` -- beide ergaben denselben
Schluessel, und damit konnten sie keine eigenen Karten haben, obwohl
der Bauplan ihnen verschiedene Einordnungen gibt (fuer die rechte
Hand: Team-Chat Muss, Rudel-Chat nur bei Bedarf). Jetzt wird auch
`raum` unterschieden. Auf der Agenturadresse gibt es nur eine
Chat-Kachel; dort aendert sich nichts.
DAS HAUS KOMMT AUS DER ADRESSE, nicht aus der angesehenen Rolle.
`const HAUS = "agentur"` ist einer Funktion gewichen. Nimmt DogFather
die Sicht einer anderen Rolle ein, wechselt er die ROLLE, nicht die
ADRESSE -- wer hier `req.sicht` naehme, bekaeme auf der Crew-Adresse
Agenturinhalte, sobald eine Sicht gesetzt ist.
`FASSUNGEN` stand an zehn Stellen fest und haette die Crew ueberall
abgewiesen. Jetzt `fassungenFuer(haus)` -- eine Funktion statt zweier
Listen an zehn Stellen.
Neue Zeilenart `vorweg` fuer "Drei Dinge vorweg" (nur die Community,
Bauplan Kapitel 04). Fehlt die Liste bei einer Rolle, wird nichts
eingetragen -- kein Sonderfall noetig.
Eigene Datei fuer die Crew-Inhalte: anleitung-tabellen.js traegt
schon die Agenturfassungen und ist 856 Zeilen lang. Getrennte Dateien
sind hier dasselbe Prinzip wie getrennte Haeuser.
pruef-anleitung 125 -> 127. Fuenf Zeilen darin verteidigten den alten
Zustand ("ein Modi bekommt die Anleitung nicht, erwartet 404") -- sie
sind mitgewandert statt stehenzubleiben. Dazu eine neue Gegenprobe
zur Haustrennung: In der Crew-Fassung darf keine Agenturkachel
vorkommen.
Datenbank vorher gesichert und zurueckgelesen (integrity_check,
84 Karten).
Co-Authored-By: Claude Opus 5 <[email protected]>
593 lines
26 KiB
JavaScript
593 lines
26 KiB
JavaScript
/* =====================================================================
|
||
workspace-anleitung.js — die Einweisung je Rolle (Etappe 1)
|
||
|
||
Bauplan: „Kachel Anleitung", Stand 06.10.2026, geprueft in
|
||
`02 Projekte/Anleitung-Kachel – Bauplan.md`.
|
||
|
||
---------------------------------------------------------------------
|
||
WAS DIESE SCHNITTSTELLE LIEFERT UND WAS NICHT
|
||
|
||
Sie liefert die TEXTE und die STUFE je Kachel. Sie liefert NICHT den
|
||
Namen, das Symbol, die Farbe oder die Adresse einer Kachel — die
|
||
stehen in `workspace/assets/js/bereiche.js`, und nur dort. Die Seite
|
||
verbindet beides ueber den Kachel-Schluessel
|
||
(`window.Bereiche.schluesselVonZiel`).
|
||
|
||
Das ist Absicht und der Grund, warum diese Datei so kurz ist: Der
|
||
Server kennt die Kacheln des Agenturhauses gar nicht. `bereicheFuer`
|
||
gibt dort `null` zurueck — „nimm die Liste aus dem Browser". Haette
|
||
ich sie hier noch einmal hingeschrieben, um „alles an einer Stelle"
|
||
zu haben, waere genau das die zweite Liste, die beim naechsten Umbau
|
||
veraltet und dann eine Kachel erklaert, die es nicht mehr gibt.
|
||
|
||
---------------------------------------------------------------------
|
||
DREI SCHRANKEN, NICHT EINE
|
||
|
||
1. `rechte.js` sperrt die Seite fuer die Crew.
|
||
2. Diese Datei prueft Haus und Rolle noch einmal selbst.
|
||
3. Die Hinweise (ab Etappe 2) pruefen es ein drittes Mal.
|
||
|
||
Am 15.09.2026 ist ein Weg fuer die Community genau durch Luecke 3
|
||
gerutscht, obwohl Tafel und Seite sauber gesperrt waren. Die
|
||
Hinweise sind eine eigene Tuer.
|
||
===================================================================== */
|
||
|
||
import express from "express";
|
||
import { db, sitzungLesen } from "./workspace.js";
|
||
import {
|
||
STUFEN, PFLEGE_ROLLEN, fassungenFuer,
|
||
} from "./anleitung-tabellen.js";
|
||
|
||
export const anleitungRouter = express.Router();
|
||
|
||
/** In welchem Haus sind wir?
|
||
*
|
||
* DIE ADRESSE ENTSCHEIDET, NICHT DIE ANGESEHENE ROLLE (06.10.2026).
|
||
* Hier stand `const HAUS = "agentur"` -- die Anleitung gab es nur
|
||
* im Agenturhaus, und die Crew bekam an fuenf Stellen ein 404
|
||
* ("nicht_hier"). Seit heute hat auch Team Dogi ihre vier
|
||
* Fassungen; das Haus kommt deshalb aus der Anmeldung.
|
||
*
|
||
* GEFRAGT WIRD IMMER `req.person`, NIE `req.sicht`: Nimmt DogFather
|
||
* die Sicht einer anderen Rolle ein, wechselt er damit die ROLLE,
|
||
* nicht die ADRESSE. Wer hier die angesehene Person naehme, bekaeme
|
||
* auf der Crew-Adresse Agenturinhalte, sobald eine Sicht gesetzt
|
||
* ist -- und genau das ist die Trennung, die seit dem 24.09. gilt. */
|
||
const hausVon = (p) => (p?.haus === "crew" ? "crew" : "agentur");
|
||
|
||
function angemeldet(req, res, next) {
|
||
const person = sitzungLesen(req);
|
||
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
|
||
req.person = person;
|
||
next();
|
||
}
|
||
|
||
anleitungRouter.use("/workspace/api/anleitung", angemeldet);
|
||
|
||
/** Wessen Fassung wird gezeigt?
|
||
*
|
||
* `req.sicht` ist der HAUSEIGENE Umschalter: DogFather kann die Sicht
|
||
* eines anderen einnehmen, und dann zeigt jede Seite das, was dieser
|
||
* andere sieht. Hier gilt dasselbe — und genau deshalb braucht die
|
||
* Anleitung keine eigene „Vorschau": Die gibt es schon, sie heisst
|
||
* „Meine Sicht" und steht in der Kopfleiste.
|
||
*
|
||
* Ohne gewaehlte Sicht ist DogFather `admin`, und fuer `admin` gibt es
|
||
* keine Fassung (Bauplan, Kapitel 11). Dann sagt die Seite das, statt
|
||
* ihm irgendeine fremde Rolle unterzuschieben. */
|
||
function fassungFuer(req) {
|
||
const person = req.sicht || req.person;
|
||
return { person, rolle: String(person?.rolle || "") };
|
||
}
|
||
|
||
/** WESSEN Haken werden gezeigt?
|
||
*
|
||
* Die der angesehenen Person. DogFather, der die Sicht eines Creators
|
||
* einnimmt, soll sehen, wie weit DIESER Creator ist — das ist der
|
||
* Sinn der Sicht. Setzen darf er sie nicht (`darf_abhaken`).
|
||
*
|
||
* Ohne gewaehlte Sicht ist es die eigene Person, und das ist der
|
||
* Normalfall. */
|
||
function wessenStand(req) {
|
||
return (req.sicht || req.person)?.id ?? 0;
|
||
}
|
||
|
||
anleitungRouter.get("/workspace/api/anleitung", (req, res) => {
|
||
try {
|
||
const { person, rolle } = fassungFuer(req);
|
||
|
||
/* DIE CREW HAT HIER NICHTS ZU SUCHEN. 404 und nicht 403: Auf der
|
||
Crew-Adresse gibt es diese Seite nicht, und „gibt es nicht" ist
|
||
die richtige Auskunft — 403 waere der Hinweis, dass es sie
|
||
woanders doch gibt. */
|
||
|
||
const d = db();
|
||
const darfPflegen = PFLEGE_ROLLEN.includes(String(req.person?.rolle || ""));
|
||
|
||
if (!fassungenFuer(hausVon(req.person)).includes(rolle)) {
|
||
/* DogFather ohne gewaehlte Sicht. Kein Fehler, sondern eine
|
||
Auskunft: Er bekommt keine Einweisung, kann aber jede ansehen.
|
||
Die Rollen stehen MIT Namen dabei, damit die Seite keine
|
||
zweite Rollenliste braucht. */
|
||
return res.json({
|
||
rolle, hat_fassung: false, darf_pflegen: darfPflegen,
|
||
fassungen: fassungenFuer(hausVon(req.person)),
|
||
satz: "Für dich gibt es keine Einweisung – du kennst den Laden. "
|
||
+ "Wähle oben unter „Meine Sicht“ eine Rolle, dann siehst du genau das, "
|
||
+ "was diese Person zu lesen bekommt.",
|
||
});
|
||
}
|
||
|
||
const kopf = d.prepare(
|
||
"SELECT leitgedanke FROM anl_rolle WHERE haus = ? AND rolle = ?").get(hausVon(req.person), rolle);
|
||
|
||
const karten = d.prepare(
|
||
`SELECT id, kachel, stufe, wozu, tust, merke, reihe, geaendert
|
||
FROM anl_karte WHERE haus = ? AND rolle = ?
|
||
ORDER BY reihe, id`).all(hausVon(req.person), rolle);
|
||
|
||
/* `erledigt` kommt aus derselben Abfrage und nicht aus einer
|
||
zweiten: Zwei Abfragen koennten zwischen den Zeilen
|
||
auseinanderlaufen, und dann zeigte der Fortschritt eine andere
|
||
Zahl als die Haken darunter. */
|
||
const zeilen = d.prepare(
|
||
`SELECT z.id, z.art, z.titel, z.text, z.ziel, z.reihe,
|
||
(s.person_id IS NOT NULL) AS erledigt
|
||
FROM anl_zeile z
|
||
LEFT JOIN anl_stand s ON s.zeile_id = z.id AND s.person_id = ?
|
||
WHERE z.haus = ? AND z.rolle = ?
|
||
ORDER BY z.art, z.reihe, z.id`).all(wessenStand(req), hausVon(req.person), rolle);
|
||
|
||
const nachArt = (art) => zeilen.filter((z) => z.art === art);
|
||
const schritte = nachArt("schritt");
|
||
const fertig = schritte.filter((z) => z.erledigt).length;
|
||
|
||
/* ==================================================================
|
||
DIE MARKE "NEU" -- UND WARUM SIE EIN ENDE BRAUCHT
|
||
|
||
Der Bauplan sagt: "Aendert DogFather einen Eintrag, traegt er
|
||
fuer alle die Marke Neu." Er sagt nicht, wann sie verschwindet.
|
||
Ohne Ende ist nach einem halben Jahr alles neu, und die Marke
|
||
sagt nichts mehr -- dieselbe Krankheit wie eine Warnung, die
|
||
immer kommt.
|
||
|
||
Sie gilt deshalb, solange BEIDES zutrifft:
|
||
* die Aenderung ist hoechstens 14 Tage her, UND
|
||
* die Person hat die Seite seit der Aenderung nicht geoeffnet.
|
||
|
||
Der zweite Teil ist der wichtigere: Wer nachgesehen hat, hat es
|
||
gesehen. Der erste faengt den ab, der monatelang nicht
|
||
hereinschaut -- fuer den waere nach der Rueckkehr sonst die
|
||
halbe Seite "neu", und das hilft niemandem.
|
||
|
||
`zuletzt` wird NACH dem Rechnen fortgeschrieben, nicht davor --
|
||
sonst waere die Marke schon beim ersten Laden weg. */
|
||
const besuch = d.prepare(
|
||
"SELECT gesehen_am FROM anl_besuch WHERE person_id = ?").get(person?.id ?? 0);
|
||
const zuletzt = besuch?.gesehen_am || "";
|
||
const FRIST = new Date(Date.now() - 14 * 86400_000).toISOString();
|
||
const istNeu = (geaendert) => !!geaendert && geaendert > FRIST && geaendert > zuletzt;
|
||
|
||
/* DIE ZAHL GEHOERT IN DIE ANTWORT, nicht nur in die Liste. Die
|
||
Seite kann damit sagen „18 Karten" — und eine Pruefung kann
|
||
nachrechnen, ohne die Liste selbst zu zaehlen. */
|
||
res.json({
|
||
rolle,
|
||
hat_fassung: true,
|
||
darf_pflegen: darfPflegen,
|
||
leitgedanke: kopf?.leitgedanke || "",
|
||
stufen: STUFEN,
|
||
saetze: nachArt("satz").map((z) => z.text),
|
||
schritte: schritte.map((z) => ({
|
||
id: z.id, titel: z.titel, text: z.text, ziel: z.ziel, erledigt: !!z.erledigt,
|
||
})),
|
||
/* DER FORTSCHRITT ZAEHLT NUR DIE ERSTEN SCHRITTE -- so steht es
|
||
im Bauplan, und das ist der Grund, warum es ihn gibt: "Niemand
|
||
muss 18 Karten abhaken. Fertig ist, wer die Ersten Schritte
|
||
erledigt hat." */
|
||
fortschritt: { erledigt: fertig, gesamt: schritte.length },
|
||
/* Abhaken darf nur, wer diese Fassung auch IST. DogFather, der
|
||
sich die Sicht eines Creators ansieht, bekommt die Haken zu
|
||
sehen, kann sie aber nicht setzen -- es waere sonst sein
|
||
eigener Fortschritt an fremden Schritten. */
|
||
darf_abhaken: String(req.person?.rolle || "") === rolle,
|
||
karten: karten.map((k) => ({ ...k, neu: istNeu(k.geaendert) })),
|
||
nicht: nachArt("nicht").map((z) => ({ id: z.id, titel: z.titel, text: z.text })),
|
||
takt: nachArt("takt").map((z) => ({ id: z.id, titel: z.titel, text: z.text })),
|
||
fragen: nachArt("frage").map((z) => ({ id: z.id, titel: z.titel, text: z.text })),
|
||
anzahl: karten.length,
|
||
});
|
||
|
||
/* Erst antworten, dann den Besuch vermerken. Haette die Zeile
|
||
davor gestanden, waere jede Aenderung schon beim ersten Laden
|
||
nicht mehr "neu" gewesen -- die Marke haette nie jemand gesehen. */
|
||
if (person?.id) {
|
||
d.prepare(
|
||
`INSERT INTO anl_besuch (person_id, gesehen_am) VALUES (?,?)
|
||
ON CONFLICT(person_id) DO UPDATE SET gesehen_am = excluded.gesehen_am`)
|
||
.run(person.id, new Date().toISOString());
|
||
}
|
||
} catch (fehler) {
|
||
console.error("[workspace] Anleitung:", fehler?.message);
|
||
res.status(500).json({ fehler: "serverfehler" });
|
||
}
|
||
});
|
||
|
||
/* =====================================================================
|
||
EINEN ERSTEN SCHRITT ABHAKEN — ODER DEN HAKEN WEGNEHMEN
|
||
=====================================================================
|
||
|
||
PUT und nicht POST: Der Aufruf setzt einen Zustand auf einen Wert.
|
||
Zweimal „erledigt" nacheinander ist derselbe Zustand wie einmal —
|
||
bei einem wackeligen Mobilfunknetz ist das der Unterschied zwischen
|
||
„nochmal tippen hilft" und „jetzt ist es doppelt".
|
||
|
||
WER DARF: nur die Person selbst, und nur an Schritten IHRER Rolle.
|
||
Beides wird hier geprueft und nicht nur in der Oberflaeche — eine
|
||
ausgeblendete Schaltflaeche hat noch nie etwas geschuetzt.
|
||
===================================================================== */
|
||
anleitungRouter.put("/workspace/api/anleitung/schritt/:id", (req, res) => {
|
||
try {
|
||
const person = req.person;
|
||
|
||
const id = Number(req.params.id);
|
||
if (!Number.isInteger(id) || id <= 0) {
|
||
return res.status(400).json({ fehler: "keine_nummer" });
|
||
}
|
||
const erledigt = req.body?.erledigt === true;
|
||
|
||
const d = db();
|
||
/* DIE ZEILE MUSS EIN SCHRITT SEIN UND DER EIGENEN ROLLE GEHOEREN.
|
||
Ohne `art = 'schritt'` liesse sich jede Zeile abhaken — auch eine
|
||
Frage oder eine Rhythmus-Zeile. Das faellt niemandem auf, bis
|
||
der Fortschritt eines Tages 7 von 5 meldet. */
|
||
const zeile = d.prepare(
|
||
`SELECT id FROM anl_zeile
|
||
WHERE id = ? AND art = 'schritt' AND haus = ? AND rolle = ?`)
|
||
.get(id, hausVon(req.person), String(person?.rolle || ""));
|
||
if (!zeile) return res.status(404).json({ fehler: "kein_schritt" });
|
||
|
||
if (erledigt) {
|
||
d.prepare(
|
||
`INSERT INTO anl_stand (person_id, zeile_id, erledigt_am) VALUES (?,?,?)
|
||
ON CONFLICT(person_id, zeile_id) DO NOTHING`)
|
||
.run(person.id, id, new Date().toISOString());
|
||
} else {
|
||
d.prepare("DELETE FROM anl_stand WHERE person_id = ? AND zeile_id = ?")
|
||
.run(person.id, id);
|
||
}
|
||
|
||
/* Der neue Stand kommt zurueck, damit die Seite ihn nicht selbst
|
||
hochrechnen muss. Eine Seite, die mitzaehlt, zaehlt irgendwann
|
||
anders als der Server. */
|
||
const stand = d.prepare(
|
||
`SELECT COUNT(*) AS gesamt,
|
||
SUM(CASE WHEN s.person_id IS NOT NULL THEN 1 ELSE 0 END) AS erledigt
|
||
FROM anl_zeile z
|
||
LEFT JOIN anl_stand s ON s.zeile_id = z.id AND s.person_id = ?
|
||
WHERE z.haus = ? AND z.rolle = ? AND z.art = 'schritt'`)
|
||
.get(person.id, hausVon(req.person), String(person.rolle));
|
||
|
||
res.json({
|
||
erledigt,
|
||
fortschritt: { erledigt: Number(stand?.erledigt || 0), gesamt: Number(stand?.gesamt || 0) },
|
||
});
|
||
} catch (fehler) {
|
||
console.error("[workspace] Anleitung abhaken:", fehler?.message);
|
||
res.status(500).json({ fehler: "serverfehler" });
|
||
}
|
||
});
|
||
|
||
/* =====================================================================
|
||
DER HINWEIS: „Erste Schritte noch offen"
|
||
=====================================================================
|
||
|
||
EINE QUELLE, ZWEI ANZEIGEN. Diese Funktion speist sowohl die Zeile
|
||
unter „Was ist dran" als auch die Zahl an der Kachel — und ab dieser
|
||
Etappe zusaetzlich die breite Kachel oben auf der Startseite.
|
||
|
||
Das ist kein Nebeneffekt, sondern die Regel des Hauses: Der Bauplan
|
||
wollte einen Punkt bei „Was ist dran" UND einen eigenen Zaehler an
|
||
der Kachel. Zwei getrennt gebaute Zaehler zeigen irgendwann zwei
|
||
verschiedene Zahlen uebereinander auf einem Bildschirm, und dann
|
||
glaubt man keiner mehr.
|
||
|
||
`try` um alles: Ein Fehler hier wuerde sonst die GANZE Hinweisliste
|
||
mitnehmen — Aufgaben, Termine, Reviews. Ein kaputter Hinweis darf
|
||
nicht die zwoelf gesunden daneben loeschen.
|
||
===================================================================== */
|
||
export function offeneHinweise(person) {
|
||
try {
|
||
if (!person) return [];
|
||
const rolle = String(person.rolle || "");
|
||
if (!fassungenFuer(hausVon(person)).includes(rolle)) return [];
|
||
|
||
const stand = db().prepare(
|
||
`SELECT COUNT(*) AS gesamt,
|
||
SUM(CASE WHEN s.person_id IS NOT NULL THEN 1 ELSE 0 END) AS erledigt
|
||
FROM anl_zeile z
|
||
LEFT JOIN anl_stand s ON s.zeile_id = z.id AND s.person_id = ?
|
||
WHERE z.haus = ? AND z.rolle = ? AND z.art = 'schritt'`)
|
||
.get(person.id, hausVon(person), rolle);
|
||
|
||
const gesamt = Number(stand?.gesamt || 0);
|
||
const offen = gesamt - Number(stand?.erledigt || 0);
|
||
/* Gibt es fuer diese Rolle noch gar keine Schritte (Etappe 3 steht
|
||
aus), ist `gesamt` 0 und `offen` 0 — kein Hinweis. Richtig: Ein
|
||
„0 Schritte offen" waere eine Meldung ueber nichts. */
|
||
if (offen <= 0) return [];
|
||
|
||
return [{
|
||
art: "anl_schritte", stufe: "offen",
|
||
text: "Erste Schritte noch offen", ziel: "anleitung.html", anzahl: offen,
|
||
}];
|
||
} catch (fehler) {
|
||
console.error("[workspace] Anleitung-Hinweise:", fehler?.message);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
|
||
/* =====================================================================
|
||
DIE PFLEGE — NUR DOGFATHER UND SPICY (Etappe 4)
|
||
=====================================================================
|
||
|
||
Bauplan Kapitel 11: Rolle waehlen und Vorschau, Stufe aendern, Texte
|
||
bearbeiten, Stand je Person, Hinweis bei fehlendem Eintrag.
|
||
|
||
DIE VORSCHAU IST NICHT GEBAUT WORDEN -- es gibt sie schon. Der
|
||
Sicht-Umschalter in der Kopfleiste zeigt jede Seite mit den Augen
|
||
einer anderen Person, auch diese. Eine zweite Rollenauswahl waere
|
||
dieselbe Funktion an einer Stelle, an der sie niemand sucht.
|
||
|
||
WER DARF: `PFLEGE_ROLLEN` = admin und spicy. Entscheidung Filipe am
|
||
06.10.2026, dieselbe Regel wie bei den Manager-Zielen. Geprueft wird
|
||
`req.person`, NICHT `req.sicht`: Wer die Sicht eines Creators
|
||
einnimmt, soll sehen, was dieser sieht -- und in dem Moment nichts
|
||
aendern koennen, ohne es zu merken.
|
||
===================================================================== */
|
||
|
||
function nurPflege(req, res, next) {
|
||
if (!PFLEGE_ROLLEN.includes(String(req.person?.rolle || ""))) {
|
||
return res.status(403).json({ fehler: "nicht_erlaubt" });
|
||
}
|
||
next();
|
||
}
|
||
|
||
const text = (v, max) => String(v ?? "").trim().slice(0, max);
|
||
|
||
/** Eine Karte aendern: Stufe und die drei Saetze. */
|
||
anleitungRouter.put("/workspace/api/anleitung/karte/:id", nurPflege, (req, res) => {
|
||
try {
|
||
const id = Number(req.params.id);
|
||
if (!Number.isInteger(id) || id <= 0) return res.status(400).json({ fehler: "keine_nummer" });
|
||
const stufe = String(req.body?.stufe ?? "");
|
||
if (!STUFEN.some((s) => s.schluessel === stufe)) {
|
||
return res.status(400).json({ fehler: "unbekannte_stufe" });
|
||
}
|
||
const d = db();
|
||
const n = d.prepare(
|
||
`UPDATE anl_karte SET stufe = ?, wozu = ?, tust = ?, merke = ?,
|
||
geaendert = ?, geaendert_von = ?
|
||
WHERE id = ? AND haus = ?`)
|
||
.run(stufe, text(req.body?.wozu, 300), text(req.body?.tust, 400),
|
||
text(req.body?.merke, 400), new Date().toISOString(), req.person.id, id, hausVon(req.person));
|
||
if (!n.changes) return res.status(404).json({ fehler: "keine_karte" });
|
||
res.json({ ok: true });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Anleitung Karte:", fehler?.message);
|
||
res.status(500).json({ fehler: "serverfehler" });
|
||
}
|
||
});
|
||
|
||
/** Eine fehlende Karte anlegen.
|
||
*
|
||
* Gebraucht fuer genau den Fall, den der Bauplan "Fuer diese Kachel
|
||
* fehlt ein Eintrag" nennt: Eine neue Kachel kommt ins Haus, und die
|
||
* Anleitung kennt sie noch nicht. Ohne diesen Weg muesste jemand in
|
||
* die Datenbank. */
|
||
anleitungRouter.post("/workspace/api/anleitung/karte", nurPflege, (req, res) => {
|
||
try {
|
||
const rolle = String(req.body?.rolle ?? "");
|
||
const kachel = text(req.body?.kachel, 80);
|
||
const stufe = String(req.body?.stufe ?? "bedarf");
|
||
if (!fassungenFuer(hausVon(req.person)).includes(rolle)) return res.status(400).json({ fehler: "unbekannte_rolle" });
|
||
if (!kachel) return res.status(400).json({ fehler: "keine_kachel" });
|
||
if (!STUFEN.some((s) => s.schluessel === stufe)) {
|
||
return res.status(400).json({ fehler: "unbekannte_stufe" });
|
||
}
|
||
const d = db();
|
||
const platz = d.prepare(
|
||
"SELECT COALESCE(MAX(reihe), -1) + 1 AS n FROM anl_karte WHERE haus = ? AND rolle = ?")
|
||
.get(hausVon(req.person), rolle).n;
|
||
try {
|
||
d.prepare(
|
||
`INSERT INTO anl_karte (haus, rolle, kachel, stufe, wozu, tust, merke,
|
||
reihe, geaendert, geaendert_von)
|
||
VALUES (?,?,?,?,?,?,?,?,?,?)`)
|
||
.run(hausVon(req.person), rolle, kachel, stufe, text(req.body?.wozu, 300),
|
||
text(req.body?.tust, 400), text(req.body?.merke, 400), platz,
|
||
new Date().toISOString(), req.person.id);
|
||
} catch {
|
||
/* Die Tabelle hat UNIQUE(haus, rolle, kachel) -- eine zweite
|
||
Karte zur selben Kachel kann es nicht geben, und das ist der
|
||
Grund, warum die Zuordnungspruefung "genau eine" sagen darf. */
|
||
return res.status(409).json({ fehler: "gibt_es_schon" });
|
||
}
|
||
res.json({ ok: true });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Anleitung Karte anlegen:", fehler?.message);
|
||
res.status(500).json({ fehler: "serverfehler" });
|
||
}
|
||
});
|
||
|
||
/** Eine Karte entfernen -- fuer Kacheln, die es nicht mehr gibt. */
|
||
anleitungRouter.delete("/workspace/api/anleitung/karte/:id", nurPflege, (req, res) => {
|
||
try {
|
||
const id = Number(req.params.id);
|
||
if (!Number.isInteger(id) || id <= 0) return res.status(400).json({ fehler: "keine_nummer" });
|
||
const n = db().prepare("DELETE FROM anl_karte WHERE id = ? AND haus = ?").run(id, hausVon(req.person));
|
||
if (!n.changes) return res.status(404).json({ fehler: "keine_karte" });
|
||
res.json({ ok: true });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Anleitung Karte loeschen:", fehler?.message);
|
||
res.status(500).json({ fehler: "serverfehler" });
|
||
}
|
||
});
|
||
|
||
/** Eine Listenzeile aendern (Satz, Schritt, Nicht deins, Takt, Frage). */
|
||
anleitungRouter.put("/workspace/api/anleitung/zeile/:id", nurPflege, (req, res) => {
|
||
try {
|
||
const id = Number(req.params.id);
|
||
if (!Number.isInteger(id) || id <= 0) return res.status(400).json({ fehler: "keine_nummer" });
|
||
const d = db();
|
||
const alt = d.prepare("SELECT art FROM anl_zeile WHERE id = ? AND haus = ?").get(id, hausVon(req.person));
|
||
if (!alt) return res.status(404).json({ fehler: "keine_zeile" });
|
||
/* Ein Ziel gibt es nur beim Schritt. Bei einer Frage waere es ein
|
||
Feld, das nirgends angezeigt wird -- und beim naechsten Umbau
|
||
haelt es jemand fuer eine Zusage. */
|
||
const ziel = alt.art === "schritt" ? (text(req.body?.ziel, 200) || null) : null;
|
||
d.prepare(
|
||
`UPDATE anl_zeile SET titel = ?, text = ?, ziel = ?, geaendert = ?, geaendert_von = ?
|
||
WHERE id = ? AND haus = ?`)
|
||
.run(text(req.body?.titel, 200), text(req.body?.text, 600), ziel,
|
||
new Date().toISOString(), req.person.id, id, hausVon(req.person));
|
||
res.json({ ok: true });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Anleitung Zeile:", fehler?.message);
|
||
res.status(500).json({ fehler: "serverfehler" });
|
||
}
|
||
});
|
||
|
||
/** Den Leitgedanken einer Rolle aendern. */
|
||
anleitungRouter.put("/workspace/api/anleitung/rolle/:rolle", nurPflege, (req, res) => {
|
||
try {
|
||
const rolle = String(req.params.rolle || "");
|
||
if (!fassungenFuer(hausVon(req.person)).includes(rolle)) return res.status(404).json({ fehler: "unbekannte_rolle" });
|
||
db().prepare(
|
||
`INSERT INTO anl_rolle (haus, rolle, leitgedanke, geaendert, geaendert_von)
|
||
VALUES (?,?,?,?,?)
|
||
ON CONFLICT(haus, rolle) DO UPDATE SET leitgedanke = excluded.leitgedanke,
|
||
geaendert = excluded.geaendert, geaendert_von = excluded.geaendert_von`)
|
||
.run(hausVon(req.person), rolle, text(req.body?.leitgedanke, 600),
|
||
new Date().toISOString(), req.person.id);
|
||
res.json({ ok: true });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Anleitung Rolle:", fehler?.message);
|
||
res.status(500).json({ fehler: "serverfehler" });
|
||
}
|
||
});
|
||
|
||
/** Wer ist wie weit? (Bauplan: "Stand je Person: fertig/offen mit Datum")
|
||
*
|
||
* Es ist eine ARBEITSLAGE, keine Ueberwachung -- dieselbe
|
||
* Unterscheidung wie bei der Team-Lage. Hier steht, wer seine
|
||
* Einweisung noch offen hat, damit jemand nachfragen kann. Nicht,
|
||
* was er gelesen hat oder wie lange er gebraucht hat. */
|
||
anleitungRouter.get("/workspace/api/anleitung/stand", nurPflege, (req, res) => {
|
||
try {
|
||
const leute = db().prepare(
|
||
`SELECT p.id, p.name, p.rolle,
|
||
(SELECT COUNT(*) FROM anl_zeile z
|
||
WHERE z.haus = ? AND z.rolle = p.rolle AND z.art = 'schritt') AS gesamt,
|
||
(SELECT COUNT(*) FROM anl_stand s JOIN anl_zeile z2 ON z2.id = s.zeile_id
|
||
WHERE s.person_id = p.id AND z2.art = 'schritt') AS erledigt,
|
||
(SELECT MAX(s2.erledigt_am) FROM anl_stand s2 WHERE s2.person_id = p.id) AS zuletzt
|
||
FROM personen p
|
||
WHERE p.aktiv = 1 AND p.rolle IN (${fassungenFuer(hausVon(req.person)).map(() => "?").join(",")})
|
||
ORDER BY p.rolle, p.name`).all(hausVon(req.person), ...fassungenFuer(hausVon(req.person)));
|
||
res.json({
|
||
leute: leute.map((p) => ({
|
||
name: p.name, rolle: p.rolle,
|
||
erledigt: Number(p.erledigt || 0), gesamt: Number(p.gesamt || 0),
|
||
fertig: Number(p.gesamt || 0) > 0 && Number(p.erledigt || 0) >= Number(p.gesamt || 0),
|
||
zuletzt: p.zuletzt || null,
|
||
})),
|
||
});
|
||
} catch (fehler) {
|
||
console.error("[workspace] Anleitung Stand:", fehler?.message);
|
||
res.status(500).json({ fehler: "serverfehler" });
|
||
}
|
||
});
|
||
|
||
|
||
/* =====================================================================
|
||
DIE EINWEISUNG BEIM ERSTEN ANMELDEN (Etappe 6)
|
||
=====================================================================
|
||
|
||
Zwei Wege, und beide sind klein: "Soll ich?" und "Ich habe sie
|
||
gezeigt." Alles andere -- die fuenf Schritte, das Hervorheben, der
|
||
Text -- steht im Browser, weil nur er weiss, wo die Dinge auf dem
|
||
Bildschirm liegen.
|
||
|
||
DIE ANTWORT IST IMMER 200, auch fuer die Crew und fuer DogFather --
|
||
dann eben mit `zeigen: false`. Ein 404 waere hier das Falsche: Das
|
||
Skript laeuft auf JEDER Startseite, und ein Fehlschlag in der
|
||
Konsole bei jedem Aufruf ist genau die Meldung, die die naechste
|
||
echte verdeckt.
|
||
===================================================================== */
|
||
|
||
anleitungRouter.get("/workspace/api/anleitung/einweisung", (req, res) => {
|
||
try {
|
||
/* NICHT `req.sicht`: Die Einweisung gehoert der Person, die
|
||
angemeldet ist. DogFather, der die Sicht eines Creators
|
||
einnimmt, soll nicht dessen Begruessung bekommen -- und erst
|
||
recht nicht fuer ihn abhaken, dass sie gezeigt wurde. */
|
||
const person = req.person;
|
||
const rolle = String(person?.rolle || "");
|
||
if (!fassungenFuer(hausVon(person)).includes(rolle)) {
|
||
return res.json({ zeigen: false, rolle });
|
||
}
|
||
const d = db();
|
||
const schon = d.prepare(
|
||
"SELECT 1 AS da FROM anl_einweisung WHERE person_id = ? AND rolle = ?")
|
||
.get(person.id, rolle);
|
||
|
||
/* Die Muss-Kacheln als SCHLUESSEL. Die Adressen kennt nur der
|
||
Browser (siehe oben); er rechnet sie mit derselben Funktion aus,
|
||
mit der auch die Karten verbunden werden. */
|
||
const muss = d.prepare(
|
||
`SELECT kachel FROM anl_karte
|
||
WHERE haus = ? AND rolle = ? AND stufe = 'muss' ORDER BY reihe`)
|
||
.all(hausVon(req.person), rolle).map((z) => z.kachel);
|
||
|
||
res.json({
|
||
zeigen: !schon,
|
||
rolle,
|
||
name: person.name || "",
|
||
muss,
|
||
});
|
||
} catch (fehler) {
|
||
console.error("[workspace] Einweisung:", fehler?.message);
|
||
/* Auch im Fehlerfall kein 500: Eine kaputte Begruessung darf die
|
||
Startseite nicht mit einer roten Zeile begruessen. */
|
||
res.json({ zeigen: false });
|
||
}
|
||
});
|
||
|
||
/** Gezeigt -- egal ob durchgeklickt oder "Spaeter".
|
||
*
|
||
* BEIDES ZAEHLT, und das ist Absicht. Der Bauplan sagt: "Spaeter ist
|
||
* erlaubt. Dann steht unter 'Was ist dran': Erste Schritte noch
|
||
* offen." Wer "Spaeter" waehlt, hat die Begruessung gesehen und soll
|
||
* sie nicht bei jedem Aufruf wieder bekommen -- der Hinweis
|
||
* uebernimmt. Ein Fenster, das so lange wiederkommt, bis man das
|
||
* Richtige anklickt, ist keine Einladung, sondern eine Sperre. */
|
||
anleitungRouter.post("/workspace/api/anleitung/einweisung/gesehen", (req, res) => {
|
||
try {
|
||
const person = req.person;
|
||
const rolle = String(person?.rolle || "");
|
||
if (!fassungenFuer(hausVon(person)).includes(rolle)) {
|
||
return res.json({ ok: true });
|
||
}
|
||
db().prepare(
|
||
`INSERT INTO anl_einweisung (person_id, rolle, gezeigt_am) VALUES (?,?,?)
|
||
ON CONFLICT(person_id, rolle) DO NOTHING`)
|
||
.run(person.id, rolle, new Date().toISOString());
|
||
res.json({ ok: true });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Einweisung merken:", fehler?.message);
|
||
res.status(500).json({ fehler: "serverfehler" });
|
||
}
|
||
});
|