/* ===================================================================== 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 } 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; if (istLeitung(ich)) { /* ---------- DAS TEAM ---------- Ein Segment je aktiver Person. 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 */ const leute = d.prepare( `SELECT id, name, rolle FROM personen WHERE aktiv = 1 ORDER BY rolle, name`).all(); 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; return res.json({ art: "team", titel: "Team versorgt", prozent: segmente.length ? Math.round((versorgt / segmente.length) * 100) : 100, segmente, zahlen: [ { wert: segmente.length, schild: "im Team" }, { wert: segmente.filter((s) => s.stufe === "dran").length, schild: "heute dran" }, { wert: segmente.filter((s) => s.stufe === "offen").length, schild: "liegt liegen" }, ], }); } /* ---------- 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" }, { wert: betreuteIds(zeigt).length, schild: "betreut" }, ], }); } catch (fehler) { console.error("[workspace] Zentrale:", fehler?.message); res.status(503).json({ fehler: "nicht_verfuegbar" }); } });