Letztes unumgesetztes Stueck aus dem Konzept, Seite 5, Block 03. Profil,
Ziele und 90-Tage-Plan gab es schon -- die Erstanalyse dazwischen fehlte.
"Die Betreuung startet strukturiert: Ausgangslage verstehen, Ziele
festlegen und daraus konkrete Arbeitspakete bauen."
Der letzte Halbsatz ist der entscheidende. Ein Check, der mit einer Note
endet, ist eine Beurteilung. Ein Check, der mit Aufgaben endet, ist
Betreuung. Jeder Punkt mit Handlungsbedarf bietet deshalb direkt an,
daraus eine Aufgabe zu machen -- mit dem Befund als Herkunft und
automatisch hoher Prioritaet. Dieselbe Linie wie beim Review ("endet mit
einer Entscheidung") und beim Call ("endet mit To-dos").
Die vier Felder aus dem Deck, mit je vier Punkten: Profil & Auftritt,
LIVE-Struktur, Content-Muster, Community & Modis. Drei Stufen: Passt,
Ausbaufaehig, Handlungsbedarf.
Die Punkte stehen fest im Code, NICHT in der Datenbank. Eine
Erstanalyse, bei der jeder eigene Punkte anlegt, ist keine Erstanalyse
mehr -- man koennte zwei Creator nicht mehr vergleichen, und genau das
ist ihr Zweck.
Sichtbarkeit, bewusst anders als beim Profil:
- LESEN darf auch der Creator selbst. Der Check ist Teil SEINES
Entwicklungsplans ("jeder Creator bekommt einen eigenen, lebenden
Entwicklungsplan"), kein Urteil hinter seinem Ruecken. Wer etwas
festhalten will, das er nicht sehen soll, hat dafuer die interne
Notiz im Profil -- die bleibt beim Management.
- AENDERN duerfen nur Management und zustaendiger Scout. Es ist eine
Fremdeinschaetzung; koennte der Creator sie selbst setzen, waere es
eine Selbsteinschaetzung. Geprueft: Luna bekommt 403 beim Bewerten und
beim Anlegen einer Aufgabe, sieht aber ihren Check inklusive Notizen.
- Ein nicht zustaendiger Scout bekommt 404, nicht 403.
Kein Speichern-Knopf: Jede Bewertung und jede Notiz wird sofort
gesichert. Eine Erstanalyse geht man im Gespraech durch -- da will
niemand am Ende noch an einen Knopf denken.
Ein leergeraeumter Punkt (keine Bewertung, keine Notiz) wird geloescht
statt als leere Zeile stehen zu bleiben. So sagt der Bestand direkt, wie
weit die Analyse ist.
Zwei neue Hinweise auf dem Dashboard:
- "Creator ohne Start-Check" -- die Sorte Luecke, die sonst niemandem
auffaellt, weil ja nichts fehlt: es fing nur nie an.
- "Punkt im Start-Check braucht Handlung", sichtbar fuer alle, die den
Check auch sehen duerfen.
Aus dem Test: Sechzehn dauerhaft offene Notizfelder machten die Seite
7026 px lang. Das Feld erscheint jetzt erst mit der Bewertung -- vorher
hat man ohnehin nichts zu notieren. 2460 px.
292 lines
12 KiB
JavaScript
292 lines
12 KiB
JavaScript
/* =====================================================================
|
||
workspace-startcheck.js — Erstanalyse (Konzept, Seite 5, Block 03).
|
||
|
||
"Die Betreuung startet strukturiert: Ausgangslage verstehen, Ziele
|
||
festlegen und daraus konkrete Arbeitspakete bauen."
|
||
|
||
Der letzte Satz ist der entscheidende. Ein Check, der mit einer Note
|
||
endet, ist eine Beurteilung. Ein Check, der mit Aufgaben endet, ist
|
||
Betreuung. Deshalb laesst sich jeder Punkt mit Handlungsbedarf direkt
|
||
in eine Aufgabe verwandeln -- dieselbe Linie wie beim Review
|
||
("endet mit einer Entscheidung") und beim Call ("endet mit To-dos").
|
||
|
||
Die Punkte stehen fest im Code, nicht in der Datenbank. Eine
|
||
Erstanalyse, bei der jeder seine eigenen Punkte anlegt, ist keine
|
||
Erstanalyse mehr -- man koennte zwei Creator nicht mehr vergleichen,
|
||
und genau das ist ihr Zweck.
|
||
===================================================================== */
|
||
|
||
import express from "express";
|
||
import { db, protokolliere, echteIp, sitzungLesen, darfCreator, betreuteIds } from "./workspace.js";
|
||
|
||
export const startcheckRouter = express.Router();
|
||
|
||
/* Die vier Felder aus dem Deck, mit je vier Punkten. Bewusst knapp
|
||
gehalten: Eine Liste, die man in einem Gespraech durchgehen kann. */
|
||
export const FELDER = [
|
||
{
|
||
schluessel: "profil",
|
||
name: "Profil & Auftritt",
|
||
punkte: [
|
||
{ schluessel: "bild", name: "Profilbild und Name", hinweis: "Erkennt man auf Anhieb, wer das ist?" },
|
||
{ schluessel: "bio", name: "Beschreibung / Bio", hinweis: "Steht dort, was einen erwartet?" },
|
||
{ schluessel: "kanaele", name: "Verlinkte Kanäle", hinweis: "Führen die Verweise irgendwohin – und stimmen sie noch?" },
|
||
{ schluessel: "wiedererkennung", name: "Wiedererkennbarkeit", hinweis: "Farben, Bilder, Sprache – passt das zusammen?" },
|
||
],
|
||
},
|
||
{
|
||
schluessel: "live",
|
||
name: "LIVE-Struktur",
|
||
punkte: [
|
||
{ schluessel: "zeiten", name: "Feste Sendezeiten", hinweis: "Weiß die Community, wann sie kommen kann?" },
|
||
{ schluessel: "ablauf", name: "Roter Faden im Stream", hinweis: "Gibt es einen erkennbaren Ablauf, oder passiert es einfach?" },
|
||
{ schluessel: "begruessung", name: "Umgang mit neuen Zuschauern", hinweis: "Werden Neue angesprochen oder gehen sie unter?" },
|
||
{ schluessel: "laenge", name: "Länge und Rhythmus", hinweis: "Passt die Dauer zur Energie – und wird sie durchgehalten?" },
|
||
],
|
||
},
|
||
{
|
||
schluessel: "content",
|
||
name: "Content-Muster",
|
||
punkte: [
|
||
{ schluessel: "formate", name: "Wiederkehrende Formate", hinweis: "Gibt es etwas, worauf man sich freuen kann?" },
|
||
{ schluessel: "rhythmus", name: "Veröffentlichungsrhythmus", hinweis: "Regelmäßig oder in Schüben?" },
|
||
{ schluessel: "einstiege", name: "Einstiege der ersten Sekunden", hinweis: "Bleibt man hängen oder scrollt man weiter?" },
|
||
{ schluessel: "verwertung", name: "LIVE wird zu Content", hinweis: "Werden gute Momente aus dem Stream weiterverwendet?" },
|
||
],
|
||
},
|
||
{
|
||
schluessel: "community",
|
||
name: "Community & Modis",
|
||
punkte: [
|
||
{ schluessel: "stamm", name: "Stammzuschauer", hinweis: "Gibt es Namen, die immer wieder auftauchen?" },
|
||
{ schluessel: "moderation", name: "Moderation besetzt", hinweis: "Ist jemand da, wenn es voll oder ungemütlich wird?" },
|
||
{ schluessel: "regeln", name: "Regeln sichtbar", hinweis: "Weiß man, was gilt, bevor etwas passiert?" },
|
||
{ schluessel: "stoerer", name: "Umgang mit Störern", hinweis: "Gibt es ein geübtes Vorgehen – oder wird improvisiert?" },
|
||
],
|
||
},
|
||
];
|
||
|
||
const BEWERTUNGEN = new Set(["ok", "mittel", "handlung"]);
|
||
const NOTIZ_MAX = 2000;
|
||
|
||
/* Nachschlagewerk, damit ein Aufruf nicht irgendeinen Punkt erfinden kann. */
|
||
const GUELTIG = new Map();
|
||
for (const f of FELDER) {
|
||
for (const p of f.punkte) GUELTIG.set(`${f.schluessel}:${p.schluessel}`, { feld: f, punkt: p });
|
||
}
|
||
const PUNKTE_GESAMT = GUELTIG.size;
|
||
|
||
const jetzt = () => new Date().toISOString();
|
||
|
||
function angemeldet(req, res, next) {
|
||
const person = sitzungLesen(req);
|
||
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
|
||
req.person = person;
|
||
next();
|
||
}
|
||
|
||
function gleicheHerkunft(req, res, next) {
|
||
const herkunft = req.get("origin");
|
||
if (!herkunft) return next();
|
||
let erlaubt;
|
||
try { erlaubt = new URL(herkunft).host === req.get("host"); } catch { erlaubt = false; }
|
||
if (!erlaubt) return res.status(403).json({ fehler: "fremde_herkunft" });
|
||
next();
|
||
}
|
||
|
||
startcheckRouter.use("/workspace/api/startcheck", angemeldet);
|
||
|
||
/* Lesen darf: das Management, der zustaendige Scout -- und der Creator
|
||
selbst. Der Check ist Teil SEINES Entwicklungsplans ("jeder Creator
|
||
bekommt einen eigenen, lebenden Entwicklungsplan"), nicht ein Urteil
|
||
hinter seinem Ruecken. Wer etwas festhalten will, das der Creator
|
||
nicht sehen soll, hat dafuer die interne Notiz im Profil.
|
||
Aendern duerfen ihn nur Management und Scout: Es ist eine
|
||
Fremdeinschaetzung, sonst waere es eine Selbsteinschaetzung. */
|
||
function darfLesen(person, creatorId) {
|
||
if (person.rolle === "creator") return person.id === creatorId;
|
||
return darfCreator(person, creatorId);
|
||
}
|
||
const darfAendern = (person, creatorId) =>
|
||
person.rolle !== "creator" && darfCreator(person, creatorId);
|
||
|
||
/* ---------- Auswahl der Creator ----------------------------------------- */
|
||
|
||
startcheckRouter.get("/workspace/api/startcheck/creator", (req, res) => {
|
||
try {
|
||
const person = req.person;
|
||
if (person.rolle === "creator") {
|
||
return res.json({ creator: [{ id: person.id, name: person.name }], eigen: true });
|
||
}
|
||
let nur = "";
|
||
let werte = [];
|
||
if (person.rolle === "scout") {
|
||
const ids = betreuteIds(person);
|
||
if (!ids.length) return res.json({ creator: [], eigen: false });
|
||
nur = ` AND id IN (${ids.map(() => "?").join(",")})`;
|
||
werte = ids;
|
||
}
|
||
res.json({
|
||
creator: db().prepare(
|
||
`SELECT id, name FROM personen WHERE rolle = 'creator' AND aktiv = 1${nur}
|
||
ORDER BY name`).all(...werte),
|
||
eigen: false,
|
||
});
|
||
} catch (fehler) {
|
||
console.error("[workspace] Start-Check Creator:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
/* ---------- Lesen -------------------------------------------------------- */
|
||
|
||
startcheckRouter.get("/workspace/api/startcheck/:id", (req, res) => {
|
||
try {
|
||
const id = Number(req.params.id);
|
||
if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
|
||
if (!darfLesen(req.person, id)) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
|
||
const person = db().prepare(
|
||
"SELECT id, name, rolle FROM personen WHERE id = ?").get(id);
|
||
if (!person || person.rolle !== "creator") {
|
||
return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
}
|
||
|
||
const zeilen = db().prepare(`
|
||
SELECT s.feld, s.punkt, s.bewertung, s.notiz, s.geaendert, p.name AS von
|
||
FROM startcheck s LEFT JOIN personen p ON p.id = s.geaendert_von
|
||
WHERE s.creator_id = ?`).all(id);
|
||
const karte = new Map(zeilen.map((z) => [`${z.feld}:${z.punkt}`, z]));
|
||
|
||
const felder = FELDER.map((f) => ({
|
||
schluessel: f.schluessel,
|
||
name: f.name,
|
||
punkte: f.punkte.map((p) => {
|
||
const z = karte.get(`${f.schluessel}:${p.schluessel}`);
|
||
return {
|
||
schluessel: p.schluessel, name: p.name, hinweis: p.hinweis,
|
||
bewertung: z?.bewertung ?? null, notiz: z?.notiz ?? null,
|
||
geaendert: z?.geaendert ?? null, von: z?.von ?? null,
|
||
};
|
||
}),
|
||
}));
|
||
|
||
const geprueft = zeilen.filter((z) => BEWERTUNGEN.has(z.bewertung)).length;
|
||
res.json({
|
||
creator: { id: person.id, name: person.name },
|
||
felder,
|
||
geprueft,
|
||
gesamt: PUNKTE_GESAMT,
|
||
handlungsbedarf: zeilen.filter((z) => z.bewertung === "handlung").length,
|
||
darf_aendern: darfAendern(req.person, id),
|
||
});
|
||
} catch (fehler) {
|
||
console.error("[workspace] Start-Check lesen:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
/* ---------- Einen Punkt setzen ------------------------------------------- */
|
||
|
||
startcheckRouter.put("/workspace/api/startcheck/:id", gleicheHerkunft, (req, res) => {
|
||
try {
|
||
const id = Number(req.params.id);
|
||
if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
|
||
/* 404 statt 403 auch hier: Wer nicht lesen darf, soll nicht ueber
|
||
die Fehlermeldung erfahren, dass es die Person gibt. */
|
||
if (!darfLesen(req.person, id)) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
if (!darfAendern(req.person, id)) {
|
||
return res.status(403).json({ fehler: "Den Start-Check füllt die Betreuung aus." });
|
||
}
|
||
|
||
const feld = String(req.body?.feld ?? "");
|
||
const punkt = String(req.body?.punkt ?? "");
|
||
if (!GUELTIG.has(`${feld}:${punkt}`)) {
|
||
return res.status(400).json({ fehler: "Diesen Punkt gibt es nicht." });
|
||
}
|
||
|
||
const b = req.body?.bewertung;
|
||
const bewertung = b === null || b === "" ? null : String(b);
|
||
if (bewertung !== null && !BEWERTUNGEN.has(bewertung)) {
|
||
return res.status(400).json({ fehler: "Unbekannte Bewertung." });
|
||
}
|
||
const notiz = String(req.body?.notiz ?? "").trim().slice(0, NOTIZ_MAX) || null;
|
||
|
||
/* Ein Punkt ohne Bewertung und ohne Notiz ist kein Eintrag, sondern
|
||
ein zurueckgenommener -- der verschwindet, statt als leere Zeile
|
||
stehen zu bleiben. */
|
||
if (bewertung === null && !notiz) {
|
||
db().prepare("DELETE FROM startcheck WHERE creator_id = ? AND feld = ? AND punkt = ?")
|
||
.run(id, feld, punkt);
|
||
} else {
|
||
db().prepare(`
|
||
INSERT INTO startcheck (creator_id, feld, punkt, bewertung, notiz, geaendert, geaendert_von)
|
||
VALUES (?,?,?,?,?,?,?)
|
||
ON CONFLICT(creator_id, feld, punkt) DO UPDATE SET
|
||
bewertung = excluded.bewertung, notiz = excluded.notiz,
|
||
geaendert = excluded.geaendert, geaendert_von = excluded.geaendert_von`).run(
|
||
id, feld, punkt, bewertung, notiz, jetzt(), req.person.id);
|
||
}
|
||
|
||
protokolliere("startcheck_gesetzt", {
|
||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||
detail: `Creator #${id} ${feld}/${punkt} = ${bewertung ?? "offen"}`.slice(0, 120),
|
||
});
|
||
res.json({ ok: true });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Start-Check setzen:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
/* ---------- Aus einem Befund eine Aufgabe machen -------------------------
|
||
"... und daraus konkrete Arbeitspakete bauen." Ohne diesen Schritt
|
||
waere der Check eine Beurteilung statt Betreuung. */
|
||
|
||
startcheckRouter.post("/workspace/api/startcheck/:id/aufgabe", gleicheHerkunft, (req, res) => {
|
||
try {
|
||
const id = Number(req.params.id);
|
||
if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
|
||
if (!darfLesen(req.person, id)) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
if (!darfAendern(req.person, id)) {
|
||
return res.status(403).json({ fehler: "nicht_erlaubt" });
|
||
}
|
||
|
||
const feld = String(req.body?.feld ?? "");
|
||
const punkt = String(req.body?.punkt ?? "");
|
||
const eintrag = GUELTIG.get(`${feld}:${punkt}`);
|
||
if (!eintrag) return res.status(400).json({ fehler: "Diesen Punkt gibt es nicht." });
|
||
|
||
const titel = String(req.body?.titel ?? "").trim().slice(0, 160);
|
||
if (titel.length < 3) return res.status(400).json({ fehler: "Was genau ist zu tun?" });
|
||
|
||
const frist = String(req.body?.frist ?? "").trim();
|
||
if (frist && (!/^\d{4}-\d{2}-\d{2}$/.test(frist) || Number.isNaN(Date.parse(frist)))) {
|
||
return res.status(400).json({ fehler: "Frist ist kein gültiges Datum." });
|
||
}
|
||
|
||
const gespeichert = db().prepare(
|
||
"SELECT bewertung FROM startcheck WHERE creator_id = ? AND feld = ? AND punkt = ?")
|
||
.get(id, feld, punkt);
|
||
const prio = gespeichert?.bewertung === "handlung" ? "hoch" : "mittel";
|
||
|
||
const { lastInsertRowid } = db().prepare(`
|
||
INSERT INTO aufgaben
|
||
(titel, beschreibung, status, prioritaet, creator_id, verantwortlich_id,
|
||
frist, erstellt, erstellt_von)
|
||
VALUES (?,?,'offen',?,?,?,?,?,?)`).run(
|
||
titel,
|
||
`Aus dem Start-Check: ${eintrag.feld.name} – ${eintrag.punkt.name}`,
|
||
prio, id, id, frist || null, jetzt(), req.person.id);
|
||
|
||
protokolliere("startcheck_aufgabe", {
|
||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||
detail: `#${lastInsertRowid} ${titel}`.slice(0, 120),
|
||
});
|
||
res.status(201).json({ id: Number(lastInsertRowid), prioritaet: prio });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Start-Check Aufgabe:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|