DREI TEILE. 1) ROLLE "MANAGER" Ein Manager darf alles, was DogFather darf -- mit genau zwei Vorbehalten: Er kann keine Leitung ANLEGEN und an keiner Leitung etwas AENDERN. Sonst koennte er sich einen zweiten Vollzugang schaffen oder DogFather aussperren. "Nur DogFather hat alle endgueltigen Rechte" heisst genau das. Umgesetzt ueber istLeitung() an EINER Stelle statt 44 einzelner Vergleiche auf "admin" im Server und 26 im Browser. DATENBANK-UMSTELLUNG: CREATE TABLE IF NOT EXISTS fasst eine vorhandene Tabelle nicht an -- die CHECK-Regel stand also weiter auf den alten drei Rollen, und ein Manager waere daran gescheitert, obwohl der Code stimmt. SQLite kann eine CHECK-Regel nicht aendern, also: neue Tabelle, Daten hinueber, alte weg, umbenennen. Davor schreibt der Server eine vollstaendige Sicherung (VACUUM INTO, in sich konsistent). Ohne Sicherung wird NICHT umgestellt. Geprueft nach der Umstellung: alle 13 Tabellen mit gleicher Zeilenzahl, PRAGMA integrity_check ok, keine verwaisten Verweise. Die einzige Abweichung war eine Sitzung mehr -- die eigene Anmeldung, die die Umstellung ausgeloest hat. 2) EIN SICHERHEITSLOCH, DAS DER TEST GEFUNDEN HAT Der erste Entwurf sicherte "Person anlegen" und "Person sperren" ab -- und liess "neuer Zugangscode" offen. Ein Manager konnte DogFather einen neuen Code ausstellen, bekam ihn angezeigt und haette ihn damit aus seinem eigenen Konto ausgesperrt. Im Test aufgefallen, weil ich den negativen Fall durchgespielt habe. Behoben nicht durch eine dritte Einzelpruefung, sondern durch eine Schranke an JEDEM Weg mit einer :id. Der naechste Weg, der dazukommt, ist damit automatisch mitgeschuetzt. Nachgeprueft: Manager bekommt 403 beim Code-Erneuern und Sperren von DogFather UND von sich selbst, darf aber Creator und Scouts verwalten. 3) FOLGEFEHLER DER MASSENERSETZUNG Die Regel "niemals den letzten aktiven DogFather sperren" hatte durch die Umstellung auf istLeitung() ploetzlich auch Manager blockiert -- gezaehlt werden aber nur DogFather-Zugaenge. Jetzt istDogFather(). Geprueft: DogFather kann einen Manager sperren, sich selbst nicht. 4) REIHENFOLGE UND ROLLENWAHL Ueberall DogFather, Manager, Scout, Creator. "ORDER BY rolle" waere alphabetisch gewesen (admin, creator, manager, scout) -- also fast genau falsch herum. Jetzt ein gemeinsamer Sortierausdruck aus workspace.js. Das Auswahlmenue fuer die Rolle ist weg. Es kam als weisses Windows-Menue mitten in einer dunklen Oberflaeche und schnitt "Creator" zu "Crea" ab -- gestalten laesst sich ein aufgeklapptes Systemmenue nicht. Ersetzt durch vier sichtbare Schalter mit Symbol, Farbe je Rolle und einer Zeile, was die Rolle bedeutet. Bei "Manager" gegen "DogFather" ist das der Unterschied zwischen Raten und Wissen. DogFather und Manager stehen dort nur zur Wahl, wenn DogFather selbst davorsitzt -- ein Knopf, der immer scheitert, gehoert nicht hin. Nebenbei: Das Namensfeld war auf eine von zwoelf Spalten gequetscht, weil seine Umgebung keine .feld-Klasse trug. Alle Formulare daraufhin durchsucht, keine weiteren Faelle.
233 lines
9.3 KiB
JavaScript
233 lines
9.3 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,
|
|
} 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 {
|
|
const person = 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 ---------- */
|
|
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));
|
|
}
|
|
}
|
|
|
|
/* ---------- Scout-Pipeline ---------- */
|
|
if (istLeitung(person) || person.rolle === "scout") {
|
|
const nur = istLeitung(person) ? "1=1" : "l.scout_id = ?";
|
|
const werte = istLeitung(person) ? [] : [person.id];
|
|
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" });
|
|
}
|
|
});
|