Gemessen am 07.09.2026, bevor irgendetwas geaendert wurde: Von drei Agentur-Eintraegen sah eine Creatorin genau EINEN -- den, der ihr zugeordnet war. Ein Event fuer alle traf weder `creator_id = ich` noch `erstellt_von = ich`; die Agentur-Seite war fuer jeden Creator leer. Nicht kaputt, nicht fehlerhaft: leer, so wie eine Seite aussieht, auf der noch nichts steht. Die Pruefung dazu war gruen. Sie legte ihre Testeintraege mit `creator_id: idLuna` an und pruefte damit einen Fall, den es im Alltag nicht gibt. - Bereichseinstellung `fuerAlle` + `ohneCreatorBezug`, daraus abgeleitet sichtbarEintrag(). BEWUSST neben sichtbar() statt darin: an derselben Funktion haengen Aufgaben, Dateien, Termine und Calls -- wer dort "1=1" einschleust, gibt nebenbei fremde Akten frei. Eine Gegenprobe mit einer zweiten Creatorin haelt das fest. - Die Zuordnung bietet nur noch "Agentur" an. Der Server verwirft eine Zuordnung ausserdem selbst -- inklusive des frei getippten Namens, und zwar NACH externPruefen: davor haette der Name den Riegel wieder aufgemacht. - "Event & Kampagne" heisst jetzt "Agentur-Events" und hat ein eigenes Formular: Von/Bis, Titel, Beschreibung, Aufgaben (Punkte und Preise), Regeln. Die Karte zeigt den Zustand als WORT (laeuft bis / startet / vorbei seit), nicht nur als Farbe. - Drei neue Spalten -- und sie stehen auch im Tabellenneubau vom 06.09. Der laeuft NACH dem Spaltennachtrag und haette sie samt Inhalt weggeworfen, ohne Fehler und mit stimmender Zeilenzahl. - Creator sehen weiterhin alles und tragen weiterhin nichts ein (403). Die Unterzeile sagt jetzt "alles, was hier steht" statt "alles, was zu dir gehoert" -- eine vollstaendige Liste soll sich nicht wie ein Ausschnitt lesen. pruef-agentur: 61 Pruefungen (vorher 40), alle gruen. Zwei eigene Fehler nebenbei gefunden und behoben: eine Beschriftung mit 11,2 px (Grenze 11,5) und ein Testdatum aus UTC statt Ortszeit. Zusaetzlich gruen: css-klassen, struktur, formulare, barrierefrei-workspace, handy. Co-Authored-By: Claude Opus 5 <[email protected]>
291 lines
12 KiB
JavaScript
291 lines
12 KiB
JavaScript
/* =====================================================================
|
|
workspace-hinweise.js — "Was ist dran?" (Konzept, Phase 4).
|
|
|
|
Der erste Baustein der Automationen. Bis hierher musste man selbst
|
|
daran denken, in den richtigen Bereich zu schauen. Diese Datei dreht
|
|
das um: Das System sagt, was liegen bleibt.
|
|
|
|
Drei Regeln, an die sich das hier haelt:
|
|
|
|
1. KEINE eigenen Daten. Ein Hinweis ist immer nur eine Sicht auf
|
|
etwas, das ohnehin schon existiert. Wird die Aufgabe erledigt,
|
|
verschwindet der Hinweis von selbst -- es gibt nichts zu quittieren
|
|
und nichts, was veralten kann.
|
|
|
|
2. JEDER HINWEIS FUEHRT IRGENDWOHIN. Ein Hinweis ohne Ziel ist nur
|
|
ein schlechtes Gewissen. Deshalb traegt jeder einen Link zu der
|
|
Stelle, an der man das Problem tatsaechlich loesen kann.
|
|
|
|
3. DIESELBE SICHTBARKEIT WIE UEBERALL. Die Regeln werden aus den
|
|
Fachmodulen importiert, nicht abgeschrieben. Ein Hinweis darf
|
|
niemals etwas verraten, das die zugehoerige Seite verbergen wuerde
|
|
-- sonst waere die Uebersicht ein Leck.
|
|
===================================================================== */
|
|
|
|
import express from "express";
|
|
import {
|
|
db, sitzungLesen, betreuteIds, istLeitung, istDogFather, pipelineIds,
|
|
} from "./workspace.js";
|
|
import { sichtbar as sichtbarAufgaben } from "./workspace-aufgaben.js";
|
|
import { sichtbar as sichtbarTermine } from "./workspace-kalender.js";
|
|
import { sichtbar as sichtbarDateien } from "./workspace-dateien.js";
|
|
import { sichtbar as sichtbarEintraege } from "./workspace-bereiche.js";
|
|
|
|
export const hinweisRouter = express.Router();
|
|
|
|
/* Reihenfolge = Dringlichkeit. "warnung" steht immer oben. */
|
|
const RANG = { warnung: 0, offen: 1, ruhig: 2 };
|
|
|
|
function angemeldet(req, res, next) {
|
|
const person = sitzungLesen(req);
|
|
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
|
|
req.person = person;
|
|
next();
|
|
}
|
|
|
|
hinweisRouter.use("/workspace/api/hinweise", angemeldet);
|
|
|
|
const p2 = (n) => String(n).padStart(2, "0");
|
|
function heuteLokal() {
|
|
const d = new Date();
|
|
return `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())}`;
|
|
}
|
|
function jetztLokal() {
|
|
const d = new Date();
|
|
return `${heuteLokal()}T${p2(d.getHours())}:${p2(d.getMinutes())}`;
|
|
}
|
|
function inTagen(n) {
|
|
const d = new Date(Date.now() + n * 86400_000);
|
|
return `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())}`;
|
|
}
|
|
|
|
/* Kleine Hilfe: eine Zahl abfragen, Fehler eines einzelnen Hinweises
|
|
duerfen nie die ganze Uebersicht kippen. */
|
|
function zaehle(sql, werte) {
|
|
try {
|
|
return db().prepare(sql).get(...werte)?.n ?? 0;
|
|
} catch {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
hinweisRouter.get("/workspace/api/hinweise", (req, res) => {
|
|
try {
|
|
/* Die gewaehlte Sicht (nur DogFather, sonst er selbst). Waeren die
|
|
Hinweise davon ausgenommen, staende auf der Startseite die eigene
|
|
Lage, waehrend die Kacheln darunter die eines Scouts zeigen --
|
|
zwei Wahrheiten auf einem Bildschirm. */
|
|
const person = req.sicht || req.person;
|
|
const heute = heuteLokal();
|
|
const jetzt = jetztLokal();
|
|
const hinweise = [];
|
|
|
|
const dazu = (art, stufe, text, ziel, anzahl) => {
|
|
if (!anzahl) return;
|
|
hinweise.push({ art, stufe, text, ziel, anzahl });
|
|
};
|
|
|
|
/* ---------- Aufgaben ---------- */
|
|
const a = sichtbarAufgaben(person);
|
|
if (a) {
|
|
dazu("aufgaben_ueberfaellig", "warnung",
|
|
"überfällig", "aufgaben.html",
|
|
zaehle(`SELECT COUNT(*) n FROM aufgaben a
|
|
WHERE ${a.wo} AND a.status <> 'erledigt'
|
|
AND a.frist IS NOT NULL AND a.frist < ?`, [...a.werte, heute]));
|
|
|
|
dazu("aufgaben_heute", "offen",
|
|
"heute fällig", "aufgaben.html",
|
|
zaehle(`SELECT COUNT(*) n FROM aufgaben a
|
|
WHERE ${a.wo} AND a.status <> 'erledigt' AND a.frist = ?`,
|
|
[...a.werte, heute]));
|
|
|
|
/* Aufgaben, die im Review haengen. Sie warten auf jemanden --
|
|
das ist genau die Sorte Stillstand, die niemandem auffaellt. */
|
|
dazu("aufgaben_review", "offen",
|
|
"wartet auf Freigabe", "aufgaben.html",
|
|
zaehle(`SELECT COUNT(*) n FROM aufgaben a
|
|
WHERE ${a.wo} AND a.status = 'review'`, a.werte));
|
|
}
|
|
|
|
/* ---------- Termine und Calls ---------- */
|
|
const t = sichtbarTermine(person);
|
|
if (t) {
|
|
dazu("termine_heute", "offen",
|
|
"heute im Kalender", "kalender.html",
|
|
zaehle(`SELECT COUNT(*) n FROM termine t
|
|
WHERE ${t.wo} AND t.beginn >= ? AND t.beginn < ?`,
|
|
[...t.werte, heute + "T00:00", inTagen(1) + "T00:00"]));
|
|
|
|
/* Vergangene Gespraeche ohne Protokoll -- der einzige Zustand im
|
|
Call-Bereich, der aktiv etwas verlangt. */
|
|
dazu("protokoll_fehlt", "warnung",
|
|
"Gespräch ohne Protokoll", "calls.html",
|
|
zaehle(`SELECT COUNT(*) n FROM termine t
|
|
LEFT JOIN protokolle pr ON pr.termin_id = t.id
|
|
WHERE ${t.wo} AND t.art IN ('call','review')
|
|
AND t.beginn <= ? AND pr.id IS NULL`, [...t.werte, jetzt]));
|
|
}
|
|
|
|
/* ---------- Dateien ---------- */
|
|
const d = sichtbarDateien(person);
|
|
if (d) {
|
|
dazu("dateien_review", istLeitung(person) ? "warnung" : "offen",
|
|
"Datei im Review", "dateien.html",
|
|
zaehle(`SELECT COUNT(*) n FROM dateien d
|
|
WHERE ${d.wo} AND d.status = 'review'`, d.werte));
|
|
}
|
|
|
|
/* ---------- Betreuungsbereiche ----------
|
|
HIER BLEIBT ES ABSICHTLICH BEI sichtbar() (07.09.2026).
|
|
|
|
Seit heute gibt es daneben sichtbarEintrag(), das Agentur-
|
|
Eintraege fuer alle sichtbar macht. Hier waere das falsch, und
|
|
zwar aus zwei Gruenden: Der Hinweis fuehrt fest nach
|
|
`bereich.html?b=live` -- ein Creator bekaeme also eine Zahl aus
|
|
der Agentur und landete beim Klick in einem Bereich, in dem
|
|
nichts davon steht. Und ein dringender Agentur-Punkt ist die
|
|
Aufgabe der Agentur, nicht seine; ein Hinweis, den man nicht
|
|
abarbeiten kann, ist Laerm.
|
|
|
|
Wer das eines Tages umstellen will, muss zuerst das Sprungziel
|
|
mitwandern lassen. */
|
|
const e = sichtbarEintraege(person);
|
|
if (e) {
|
|
dazu("bereiche_dringend", "warnung",
|
|
"dringender Punkt in den Bereichen", "bereich.html?b=live",
|
|
zaehle(`SELECT COUNT(*) n FROM eintraege e
|
|
WHERE ${e.wo} AND e.status = 'offen' AND e.dringlichkeit = 'hoch'`,
|
|
e.werte));
|
|
}
|
|
|
|
/* Offener Handlungsbedarf aus der Erstanalyse. Sichtbar fuer
|
|
Management, zustaendigen Scout und den Creator selbst -- genau
|
|
wie der Start-Check. */
|
|
{
|
|
let woS = null;
|
|
let werteS = [];
|
|
if (istLeitung(person)) woS = "1=1";
|
|
else if (person.rolle === "creator") { woS = "s.creator_id = ?"; werteS = [person.id]; }
|
|
else {
|
|
const ids = betreuteIds(person);
|
|
if (ids.length) {
|
|
woS = `s.creator_id IN (${ids.map(() => "?").join(",")})`;
|
|
werteS = ids;
|
|
}
|
|
}
|
|
if (woS) {
|
|
dazu("startcheck_handlung", "offen",
|
|
"Punkt im Start-Check braucht Handlung", "startcheck.html",
|
|
zaehle(`SELECT COUNT(*) n FROM startcheck s
|
|
WHERE ${woS} AND s.bewertung = 'handlung'`, werteS));
|
|
}
|
|
}
|
|
|
|
/* ---------- Markierte Punkte aus den festen Checklisten ----------
|
|
|
|
Wunsch vom 01.09.2026: "so dass die sehen, da ist was -- und es
|
|
soll auch oben in der Kachel angezeigt werden."
|
|
|
|
NUR FUER CREATOR, und das ist eine bewusste Entscheidung. Fuer
|
|
einen Betreuer waere die Zahl eine Liste dessen, was er selbst
|
|
angehakt hat: Sie waechst mit seiner Arbeit, und er kann sie nicht
|
|
abbauen -- nur der Creator kann. Ein Zaehler, den man nicht auf
|
|
null bringen kann, wird nach zwei Wochen ignoriert, und dann sind
|
|
auch die daneben nichts mehr wert.
|
|
|
|
Vier Bereiche, vier Hinweise, weil vier verschiedene Kacheln
|
|
gemeint sind. Ein Sammelhinweis "6 markierte Punkte" liesse offen,
|
|
wo man nachsehen soll. */
|
|
if (person.rolle === "creator") {
|
|
const BEREICHE = [
|
|
["live", "LIVE-Analyse", "bereich.html?b=live"],
|
|
["content", "Content-Ideen", "content.html"],
|
|
["community", "Community", "bereich.html?b=community"],
|
|
["technik", "Technik", "bereich.html?b=technik"],
|
|
];
|
|
for (const [schluessel, name, ziel] of BEREICHE) {
|
|
dazu(`markiert_${schluessel}`, "warnung",
|
|
`markierter Punkt in ${name}`, ziel,
|
|
/* zaehle() faengt Fehler ab -- gibt es die Tabelle noch nicht
|
|
(niemand hat je eine Checkliste geoeffnet), steht hier 0
|
|
statt eines Ausfalls der ganzen Uebersicht. */
|
|
zaehle(`SELECT COUNT(*) n FROM punkt_stand
|
|
WHERE bereich = ? AND creator_id = ? AND stufe = 'verbessern'`,
|
|
[schluessel, person.id]));
|
|
}
|
|
}
|
|
|
|
/* ---------- Scout-Pipeline ---------- */
|
|
/* NUR DogFather sieht alle Leads (01.09.2026). Ein Scout seine
|
|
eigenen, ein Manager seine eigenen und die seiner zugeteilten
|
|
Scouts -- dieselbe Regel wie in der Pipeline selbst, aus
|
|
derselben Quelle (pipelineIds in workspace.js). Ein Hinweis, der
|
|
mehr verraet als die Seite dahinter, waere ein Leck. */
|
|
if (istDogFather(person) || person.rolle === "scout" || person.rolle === "manager") {
|
|
const ids = istDogFather(person) ? null : pipelineIds(person);
|
|
const nur = ids === null ? "1=1"
|
|
: ids.length ? `l.scout_id IN (${ids.map(() => "?").join(",")})` : "0=1";
|
|
const werte = ids === null ? [] : ids;
|
|
dazu("followup_faellig", "warnung",
|
|
"Follow-up fällig", "scouting.html",
|
|
zaehle(`SELECT COUNT(*) n FROM leads l
|
|
WHERE ${nur} AND l.status NOT IN ('uebergeben','abgelehnt')
|
|
AND l.naechster_followup IS NOT NULL AND l.naechster_followup <= ?`,
|
|
[...werte, heute]));
|
|
|
|
/* Uebergebene Leads, aus denen nie ein Creator wurde. Die
|
|
Uebergabe ist sonst eine Sackgasse, die niemand bemerkt. */
|
|
dazu("uebergabe_offen", "offen",
|
|
"übergeben, aber noch kein Creator angelegt", "scouting.html",
|
|
zaehle(`SELECT COUNT(*) n FROM leads l
|
|
WHERE ${nur} AND l.status = 'uebergeben' AND l.creator_id IS NULL`,
|
|
werte));
|
|
}
|
|
|
|
/* ---------- Nur Management ----------
|
|
Review-Termine und fehlende Zustaendigkeit sind Steuerungswissen.
|
|
Sie erscheinen deshalb weder bei Creator noch bei Scouts -- so wie
|
|
die zugehoerigen Felder im Profil auch. */
|
|
if (istLeitung(person)) {
|
|
dazu("review_faellig", "warnung",
|
|
"Review-Termin überfällig", "profil.html",
|
|
zaehle(`SELECT COUNT(*) n FROM profile f
|
|
JOIN personen p ON p.id = f.person_id AND p.aktiv = 1
|
|
WHERE f.naechster_review IS NOT NULL AND f.naechster_review < ?`, [heute]));
|
|
|
|
dazu("review_bald", "offen",
|
|
"Review steht in den nächsten 7 Tagen an", "profil.html",
|
|
zaehle(`SELECT COUNT(*) n FROM profile f
|
|
JOIN personen p ON p.id = f.person_id AND p.aktiv = 1
|
|
WHERE f.naechster_review BETWEEN ? AND ?`, [heute, inTagen(7)]));
|
|
|
|
dazu("ohne_betreuung", "ruhig",
|
|
"Creator ohne zuständige Person", "personen.html",
|
|
zaehle(`SELECT COUNT(*) n FROM personen p
|
|
LEFT JOIN betreuung b ON b.creator_id = p.id
|
|
WHERE p.rolle = 'creator' AND p.aktiv = 1 AND b.creator_id IS NULL`, []));
|
|
|
|
/* Ein Creator ohne Erstanalyse ist die Sorte Luecke, die sonst
|
|
niemandem auffaellt -- es fehlt ja nichts, es fing nur nie an. */
|
|
dazu("ohne_startcheck", "ruhig",
|
|
"Creator ohne Start-Check", "startcheck.html",
|
|
zaehle(`SELECT COUNT(*) n FROM personen p
|
|
WHERE p.rolle = 'creator' AND p.aktiv = 1
|
|
AND NOT EXISTS (SELECT 1 FROM startcheck s WHERE s.creator_id = p.id)`, []));
|
|
|
|
dazu("ohne_profil", "ruhig",
|
|
"Creator ohne ausgefülltes Profil", "profil.html",
|
|
zaehle(`SELECT COUNT(*) n FROM personen p
|
|
LEFT JOIN profile f ON f.person_id = p.id
|
|
WHERE p.rolle = 'creator' AND p.aktiv = 1 AND f.person_id IS NULL`, []));
|
|
}
|
|
|
|
hinweise.sort((x, y) => (RANG[x.stufe] - RANG[y.stufe]) || (y.anzahl - x.anzahl));
|
|
res.json({ hinweise, stand: new Date().toISOString() });
|
|
} catch (fehler) {
|
|
console.error("[workspace] Hinweise:", fehler?.message);
|
|
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
|
}
|
|
});
|