/* ===================================================================== workspace-zentrale.js — die Zahlen fuer den Ring auf der Startseite. Wunsch Filipe (08.09.2026): die Begruessungskachel nach dem Vorbild von VanVans Business Hub umbauen -- grosser Ring links, Text in der Mitte, Uhr rechts. Dort zeigt der Ring "91 % deiner Produktakten": ein Segment je Produkt, eingefaerbt nach Vollstaendigkeit. --------------------------------------------------------------------- WARUM ES DIESELBE ZAHL HIER NICHT GIBT Nachgemessen an der echten Datenbank am 08.09.2026: Termine 31 · Wissen 7 · Schulungen 4 · Personen 9 · AUFGABEN 1 Leistung, Leads, Profile, Content-Saeulen, Dateien: jeweils 0 VanVans Kranz funktioniert, weil 253 Produkte ein BILD ergeben. Ein Kranz aus einer Aufgabe ergibt keins. Die naheliegende Uebertragung ("dann eben Aufgaben") haette eine leere Scheibe erzeugt, die aussieht wie ein Ladefehler. Filipe hat deshalb DAS TEAM gewaehlt: ein Segment je Person, gefaerbt danach, wie es um sie steht. Neun Segmente sind wenige, aber sie sind ECHT und aendern sich taeglich -- und die Zahl waechst mit dem Team. --------------------------------------------------------------------- UND WARUM DAS NICHT FUER JEDEN GEHT Ein Creator darf die Teamliste nicht sehen. Das ist keine Feinheit, sondern die Regel, die diesen ganzen Arbeitsplatz traegt (Wunsch Filipe, 07.09.2026: "jeder soll und darf im kalender immer nur seine eigenen eintraege nur sehen"). Ein Ring, der neun Namen einfaerbt, waere fuer einen Creator ein Leck -- er koennte daran ablesen, wer heute Termine hat und wer nicht. Deshalb liefert diese Route ZWEI VERSCHIEDENE RINGE, je nach Rolle: Leitung (DogFather, Manager, Scout, Spicy) -> das Team: ein Segment je aktiver Person Creator -> der eigene Tag: ein Segment je Stunde, plus die eigenen Termine als Marken Das ist keine Notloesung, sondern die richtige Antwort auf dieselbe Frage: "Wie weit ist das, wofuer ich zustaendig bin?" Fuer DogFather ist das sein Team, fuer eine Creatorin ihr Tag. KEINE EIGENEN DATEN, wie bei den Hinweisen: Alles hier ist eine SICHT auf Zeilen, die ohnehin existieren. Es gibt nichts zu pflegen und nichts, was veralten kann. ===================================================================== */ import express from "express"; import { db, sitzungLesen, istLeitung, betreuteIds, scoutsVon, verborgeneIds, TEAM_DOGI_ROLLEN } from "./workspace.js"; export const zentraleRouter = express.Router(); function angemeldet(req, res, next) { const person = sitzungLesen(req); if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" }); req.person = person; next(); } zentraleRouter.use("/workspace/api/zentrale", angemeldet); const p2 = (n) => String(n).padStart(2, "0"); /* ORTSZEIT, NICHT UTC. toISOString() liefert zwischen Mitternacht und zwei Uhr noch den Vortag -- der Ring stuende dann auf den Terminen von gestern. Derselbe Fehler ist mir am 08.09. zweimal an einem Abend passiert; pruef-struktur.mjs sucht ihn inzwischen im ganzen Repo. */ function heuteLokal() { const d = new Date(); return `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())}`; } zentraleRouter.get("/workspace/api/zentrale", (req, res) => { try { const d = db(); const heute = heuteLokal(); const ich = req.person; /* Wessen Lage wird gezeigt? Bei aktiver Fremdsicht die der Person, in die man hineinsieht -- sonst zeigte die Kachel die eigene Lage und alles darunter eine fremde. Genau diese Uneinheitlichkeit hat auf der Startseite schon einmal zu "ist das jetzt meins oder ihrs?" gefuehrt. */ const zeigt = req.person.sicht || req.person; /* ================================================================ WESSEN TEAM? JEDE ROLLE SIEHT IHR EIGENES (08.09.2026) Filipe, nachdem Managerin Schulle "2 Creator" angezeigt bekam, obwohl sie einen hat: "die zahl die da angezeigt wird soll bitte immer jedem genau zutreffend sein" -- und dazu, wer was sieht: "dogfather und cigdem haben die zahl vom insgesamten. manager sehen nur die gesamte zahl ihrer scouts und ihren creator die ihnen zugeteilt sind, die scout sehen die zahl nur von ihren creator und die creator da termine vom tag selber" Vorher stand hier `SELECT ... FROM personen WHERE aktiv = 1` -- ALLE, fuer jeden aus der Leitung gleich. Deshalb sah Schulle das ganze Haus statt ihres Teams. Die Zahl war nicht falsch berechnet, sie beantwortete die falsche Frage. Die Zuordnung selbst wird NICHT hier nachgebaut: `betreuteIds` kennt die Kette Manager -> Scouts -> deren Creator bereits, und `scoutsVon` die Scouts. Eine zweite Rechenvorschrift fuer dieselbe Frage waere genau der Weg, auf dem zwei Wahrheiten entstehen. SCOUTS BEKOMMEN JETZT AUCH DIESEN RING. Sie zaehlen nicht zur Leitung und sahen deshalb den Stundenring des eigenen Tages -- aber ein Scout hat ein Team, naemlich seine Creator. Genau danach hat Filipe gefragt. */ const istTeamsicht = istLeitung(ich) || ich.rolle === "scout"; if (istTeamsicht) { /* ---------- DAS TEAM ---------- Ein Segment je Person im eigenen Verantwortungsbereich. Der Zustand einer Person ist bewusst GROB in drei Stufen -- mehr taeuschte Genauigkeit vor, die es nicht gibt (dieselbe Ueberlegung steht in VanVans Kranz). frei - nichts Offenes, keine Termine heute -> ruhig dran - hat heute Termine -> die sind versorgt offen - hat unerledigte Termine aus der VERGANGENHEIT -> das ist das, was liegen bleibt */ let leute; if (ich.rolle === "admin" || ich.rolle === "spicy") { /* Das ganze Haus -- und zwar ohne sich selbst: Wer den Ring ansieht, ist die Person, die ihn liest; sich selbst als Segment im eigenen Team zu zaehlen, verschiebt jede Prozent- angabe um einen Platz. */ /* AUCH HIER GILT DIE VERBERGUNGSREGEL (09.09.2026). Diese Abfrage geht als einzige NICHT durch die zentralen Listenfunktionen -- sie holt sich das Haus selbst. Genau deshalb war sie die eine Stelle, an der Spicy Media VanVan noch gesehen hat, nachdem alle anderen Wege zu waren. `server/pruef-verborgen.mjs` hat es beim ersten Lauf gemeldet; ohne die Pruefung waere es nicht aufgefallen, denn im Ring steht nur ein farbiges Segment mit einem Namen in der Sprechblase. */ const weg = verborgeneIds(ich); const zusatz = weg.length ? ` AND id NOT IN (${weg.map(() => "?").join(",")})` : ""; leute = d.prepare( `SELECT id, name, rolle FROM personen WHERE aktiv = 1 AND id <> ?${zusatz} ORDER BY rolle, name`).all(ich.id, ...weg); } else { /* Manager: seine Scouts UND die Creator (eigene wie die seiner Scouts). Scout: nur seine Creator. */ const ids = [...new Set([ ...(ich.rolle === "manager" ? scoutsVon(ich.id) : []), ...betreuteIds(ich), ])].filter((id) => id !== ich.id); leute = ids.length ? d.prepare( `SELECT id, name, rolle FROM personen WHERE aktiv = 1 AND id IN (${ids.map(() => "?").join(",")}) ORDER BY rolle, name`).all(...ids) : []; } const heuteZaehler = d.prepare( `SELECT COUNT(*) AS n FROM termine t WHERE t.erledigt = 0 AND substr(t.beginn, 1, 10) = ? AND (t.creator_id = ? OR t.erstellt_von = ? OR EXISTS (SELECT 1 FROM termin_teilnehmer x WHERE x.termin_id = t.id AND x.person_id = ?))`); const altZaehler = d.prepare( `SELECT COUNT(*) AS n FROM termine t WHERE t.erledigt = 0 AND substr(t.beginn, 1, 10) < ? AND (t.creator_id = ? OR t.erstellt_von = ? OR EXISTS (SELECT 1 FROM termin_teilnehmer x WHERE x.termin_id = t.id AND x.person_id = ?))`); const segmente = leute.map((p) => { const heuteN = heuteZaehler.get(heute, p.id, p.id, p.id).n; const altN = altZaehler.get(heute, p.id, p.id, p.id).n; const stufe = altN > 0 ? "offen" : (heuteN > 0 ? "dran" : "frei"); /* NUR DER VORNAME nach aussen. Der Ring braucht ein Kuerzel fuer die Sprechblase, nicht den Datensatz einer Person. */ return { name: String(p.name || "").trim().split(/\s+/)[0] || "?", rolle: p.rolle, stufe, heute: heuteN, alt: altN }; }); const versorgt = segmente.filter((s) => s.stufe !== "offen").length; /* Ein Scout fuehrt Creator, kein "Team" im Sinne des Hauses. Das Wort auf dem Ring sagt deshalb, WEN er zaehlt -- sonst liest ein Scout "Team versorgt" und sucht die anderen vier Rollen darin. */ const nurCreator = ich.rolle === "scout"; return res.json({ art: "team", titel: nurCreator ? "Creator versorgt" : "Team versorgt", prozent: segmente.length ? Math.round((versorgt / segmente.length) * 100) : 100, segmente, zahlen: [ { wert: segmente.length, schild: nurCreator ? "meine Creator" : "im Team" }, { wert: segmente.filter((s) => s.stufe === "dran").length, schild: "heute dran" }, /* "liegt liegen" stand hier und war nicht zu verstehen -- Filipe wortwoertlich: "keine ahnung was das bedeuten soll". Gezaehlt werden Personen mit unerledigten Terminen aus der VERGANGENHEIT. "Ueberfaellig" sagt das in einem Wort und ist der Begriff, der auch sonst im Haus dafuer steht. */ { wert: segmente.filter((s) => s.stufe === "offen").length, schild: "überfällig" }, ], }); } /* ---------- DER EIGENE TAG ---------- Fuer Creator. Ein Segment je Stunde von 6 bis 24 -- der Teil des Tages, in dem gearbeitet wird. Nachts einen leeren Halbkreis zu zeigen sagt nichts; 18 Segmente fuellen den Ring ordentlich. Gefuellt sind die vergangenen Stunden, markiert die mit einem eigenen Termin. Der Ring beantwortet damit "wie weit ist mein Tag" -- dieselbe Frage wie bei VanVan, nur auf das bezogen, wofuer eine Creatorin zustaendig ist. */ const VON = 6, BIS = 24; const jetzt = new Date(); const stundeJetzt = jetzt.getHours(); const meine = d.prepare( `SELECT t.beginn FROM termine t WHERE substr(t.beginn, 1, 10) = ? AND (t.creator_id = ? OR t.erstellt_von = ? OR EXISTS (SELECT 1 FROM termin_teilnehmer x WHERE x.termin_id = t.id AND x.person_id = ?))`) .all(heute, zeigt.id, zeigt.id, zeigt.id); const mitTermin = new Set(meine.map((t) => Number(String(t.beginn).slice(11, 13)))); const segmente = []; for (let s = VON; s < BIS; s++) { segmente.push({ name: `${p2(s)} Uhr`, rolle: "stunde", stufe: mitTermin.has(s) ? "dran" : (s < stundeJetzt ? "frei" : "offen") }); } const durch = Math.min(Math.max(stundeJetzt - VON, 0), BIS - VON); return res.json({ art: "tag", titel: "Tag geschafft", prozent: Math.round((durch / (BIS - VON)) * 100), segmente, zahlen: [ { wert: meine.length, schild: "heute" }, { wert: [...mitTermin].filter((s) => s >= stundeJetzt).length, schild: "kommt noch" }, /* "BETREUT" IST FUER EINEN MODI IMMER NULL (10.09.2026). Er betreut keine Creator -- das ist die Arbeit der Scouts und Manager. Auf seiner Startseite stand deshalb dauerhaft "0 betreut": eine Zahl, die nie etwas anderes sagen kann, und damit schlimmer als keine. Wer sie sieht, sucht nach dem Fehler. Stattdessen zaehlt sie, wie viele im Modi-Team sind. Das ist die Zahl, die an dieser Stelle etwas bedeutet -- und sie bekommt nur zu sehen, wer ohnehin weiss, dass es die Runde gibt. */ TEAM_DOGI_ROLLEN.has(zeigt.rolle) ? { wert: db().prepare( `SELECT COUNT(*) AS n FROM personen WHERE rolle IN (${ [...TEAM_DOGI_ROLLEN].map((r) => `'${r}'`).join(", ")}) AND aktiv = 1`).get().n, schild: "im Team" } : { wert: betreuteIds(zeigt).length, schild: "betreut" }, ], }); } catch (fehler) { console.error("[workspace] Zentrale:", fehler?.message); res.status(503).json({ fehler: "nicht_verfuegbar" }); } });