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]>
677 lines
30 KiB
JavaScript
677 lines
30 KiB
JavaScript
/* =====================================================================
|
||
workspace-bereiche.js — LIVE, Content, Technik, Community und Schutz.
|
||
|
||
Diese fuenf Bereiche aus Phase 2 haben im Konzept dieselbe Grundform:
|
||
Eintraege zu einem Creator, mit Art, Datum, Titel, Text und Status.
|
||
Sie unterscheiden sich nur darin, WELCHE Arten es gibt und ob eine
|
||
Bewertung oder eine Dringlichkeit dazugehoert.
|
||
|
||
Deshalb ein gemeinsamer Unterbau statt fuenf fast gleicher Module:
|
||
eine Tabelle, eine Sichtbarkeitsregel, eine Pruefung. Ein Fehler laesst
|
||
sich damit an einer Stelle beheben statt an fuenf, und ein neuer
|
||
Bereich ist ein Eintrag in BEREICHE -- kein neues Modul.
|
||
|
||
Die Feldnamen und Arten stammen woertlich aus dem Deck (Seiten 7-12).
|
||
===================================================================== */
|
||
|
||
import express from "express";
|
||
import {
|
||
db, protokolliere, echteIp, sitzungLesen, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather,
|
||
externPruefen, externSql,
|
||
} from "./workspace.js";
|
||
|
||
export const bereicheRouter = express.Router();
|
||
|
||
/* Die Bereiche und ihre Arten. Bewusst hier und nicht in der Datenbank:
|
||
Es sind Festlegungen aus dem Konzept, keine Nutzdaten. */
|
||
export const BEREICHE = {
|
||
live: {
|
||
name: "LIVE-Analyse",
|
||
arten: { vorbereitung: "Vorbereitung", mitschrift: "Während LIVE", auswertung: "Auswertung" },
|
||
bewertung: true, // Review-Score aus dem Konzept, Seite 7
|
||
dringlichkeit: false,
|
||
},
|
||
/* Content ist der einzige Bereich mit einer STRECKE statt einer Liste:
|
||
Die Arten sind hier Stufen, die ein Video der Reihe nach durchlaeuft.
|
||
Grundlage ist der Satz aus dem Konzept ("Hook bis Upload") und die
|
||
Recherche vom 31.08.2026: Eine Redaktionsplanung ist eine Strecke,
|
||
kein Kalender -- ein Kalender voller Termine verbirgt genau die drei
|
||
Stellen, an denen es klemmt.
|
||
|
||
Fuenf Stufen, nicht drei: Zwischen "Idee" und "Veroeffentlicht" lag
|
||
vorher alles, was den Unterschied macht. Und nicht mehr als fuenf --
|
||
jede weitere Spalte ist eine, die auf dem Handy nicht mehr passt. */
|
||
content: {
|
||
/* Umbenannt am 01.09.2026: "Planung" klang nach Terminen und
|
||
Tabellen. Was hier wirklich passiert, ist das Sammeln und
|
||
Weiterentwickeln von IDEEN -- der Kalender daneben plant. */
|
||
name: "Content-Ideen",
|
||
arten: {
|
||
idee: "Idee",
|
||
skript: "Hook & Skript",
|
||
gedreht: "Gedreht",
|
||
fertig: "Fertig & geplant",
|
||
veroeffentlicht: "Veröffentlicht",
|
||
},
|
||
bewertung: false,
|
||
dringlichkeit: false,
|
||
strecke: true, // hat eine Reihenfolge, nicht nur Kategorien
|
||
},
|
||
technik: {
|
||
name: "Technik",
|
||
arten: { setup: "Setup", problem: "Problem", loesung: "Lösung", anleitung: "Anleitung" },
|
||
bewertung: false,
|
||
dringlichkeit: true,
|
||
},
|
||
community: {
|
||
name: "Community",
|
||
arten: { moderation: "Moderation", aktion: "Aktion", konflikt: "Konflikt" },
|
||
bewertung: false,
|
||
dringlichkeit: true,
|
||
},
|
||
schutz: {
|
||
name: "Schutz & Regeln",
|
||
arten: { richtlinie: "Richtlinie", vorfall: "Vorfall", eskalation: "Eskalation", gelernt: "Gelernt" },
|
||
bewertung: false,
|
||
dringlichkeit: true,
|
||
},
|
||
/* DER SECHSTE BEREICH (06.09.2026).
|
||
|
||
Kernprinzip 04 des Konzepts: "Die Agentur bleibt angebunden." Fuenf
|
||
Bereiche standen, dieser fehlte -- und damit fehlte der einzige
|
||
Ort, an dem steht, welche Kampagne laeuft, welche Schulung ansteht
|
||
und wie ein Anliegen an die Agentur ausgegangen ist. Das lief
|
||
ueber private Nachrichten: nicht auffindbar, nicht nachvollziehbar,
|
||
und beim naechsten Mal fing man wieder von vorn an.
|
||
|
||
VIER ARTEN, und die vierte ist der Punkt:
|
||
kampagne was laeuft, bis wann, unter welcher Bedingung
|
||
schulung Termine und Inhalte, verweist auf die Bibliothek
|
||
anliegen der OFFIZIELLE Weg hin -- mit Stand
|
||
zustaendig wer wofuer zustaendig ist: wir oder die Agentur
|
||
|
||
Die letzte Art ist keine Verlegenheit, sondern das, was das Konzept
|
||
ausdruecklich fordert ("Betreuung & Umsetzung ist unsere Seite,
|
||
offizielle Wege und Plattformthemen sind Agenturseite"). Solange
|
||
das nur im Kopf steht, wird bei jedem Streitfall neu verhandelt,
|
||
wer eigentlich haette handeln muessen.
|
||
|
||
Dringlichkeit: ja. Ein Anliegen an die Agentur, das drei Wochen
|
||
liegt, ist etwas anderes als eines von gestern -- und ohne Stufe
|
||
sieht man den Unterschied in einer Liste nicht.
|
||
Bewertung: nein. Hier wird nichts benotet; es ist ein
|
||
Zusammenarbeits-Bereich, kein Analysebereich. */
|
||
/* FUER ALLE, NICHT FUER EINEN (07.09.2026).
|
||
|
||
Die anderen fuenf Bereiche sind Betreuungsakten: Dort steht etwas
|
||
UEBER einen Creator, und nur er und seine Betreuung sehen es. Die
|
||
Agentur ist das Gegenteil -- ein Event, eine Schulung, eine
|
||
Zustaendigkeit gilt fuer alle gleichzeitig.
|
||
|
||
Bis heute lief sie trotzdem unter der Akten-Regel, und das hatte
|
||
eine Folge, die niemandem auffiel: Ein Event ohne Creator-Zuordnung
|
||
traf weder `creator_id = ich` noch `erstellt_von = ich` -- fuer
|
||
jeden Creator war die Agentur-Seite schlicht LEER. Nicht "kaputt",
|
||
nicht "Fehler": leer, so wie eine Seite aussieht, auf der noch
|
||
nichts steht. Gemessen am 07.09.2026: Von drei Eintraegen sah die
|
||
Creatorin genau den einen, der ihr zugeordnet war.
|
||
|
||
Zwei Schalter, weil es zwei verschiedene Fragen sind:
|
||
fuerAlle -- jeder sieht jeden Eintrag dieses Bereichs
|
||
ohneCreatorBezug -- ein Eintrag gehoert hier NIEMANDEM einzeln */
|
||
agentur: {
|
||
name: "Agentur",
|
||
arten: {
|
||
kampagne: "Agentur-Events",
|
||
schulung: "Schulung",
|
||
anliegen: "Anliegen an die Agentur",
|
||
zustaendig: "Zuständigkeit",
|
||
},
|
||
bewertung: false,
|
||
dringlichkeit: true,
|
||
fuerAlle: true,
|
||
ohneCreatorBezug: true,
|
||
},
|
||
};
|
||
|
||
/* Aus den Bereichseinstellungen abgeleitet, nicht danebengeschrieben.
|
||
Eine zweite Liste waere beim naechsten Bereich auseinandergelaufen --
|
||
und zwar still. */
|
||
const BEREICHE_FUER_ALLE = Object.entries(BEREICHE)
|
||
.filter(([, e]) => e.fuerAlle).map(([k]) => k);
|
||
|
||
const DRINGLICHKEITEN = ["hoch", "mittel", "niedrig"];
|
||
const STATUS = ["offen", "erledigt"];
|
||
const TITEL_MAX = 160;
|
||
const TEXT_MAX = 6000;
|
||
|
||
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();
|
||
}
|
||
|
||
bereicheRouter.use("/workspace/api/bereich", angemeldet);
|
||
|
||
/* ---------- Die Bereiche sind fuer Creator zum LESEN da ----------------
|
||
|
||
Wunsch Filipe, 31.08.2026: "die creator sollen da nur die sehen die wir
|
||
ihnen eintragen, also dogfather manager und scout."
|
||
|
||
Vorher durfte ein Creator hier selbst Eintraege anlegen, aendern und
|
||
die eigenen wieder loeschen. Das klingt harmlos, hebelt aber den Zweck
|
||
dieser Bereiche aus: Sie sind die Betreuungsakte -- LIVE-Auswertungen,
|
||
Technikbefunde, Konfliktverlaeufe, Schutzvorfaelle. Was dort steht,
|
||
muss der Stand der Betreuung sein und nicht das, was jemand ueber sich
|
||
selbst notiert hat. Ein Creator, der eine Auswertung umschreiben oder
|
||
eine unbequeme Notiz loeschen kann, macht die Akte wertlos.
|
||
|
||
Der Creator SIEHT weiterhin alles, was zu ihm gehoert -- vollstaendig,
|
||
nichts wird ihm vorenthalten. Er kann es nur nicht veraendern.
|
||
|
||
Bewusst 403 mit klarem Text und nicht 404: Der Bereich existiert fuer
|
||
ihn ja, er steht sogar davor. Ein 404 wuerde hier luegen. (Bei etwas,
|
||
das er gar nicht sehen darf, bleibt es weiterhin 404.)
|
||
|
||
Steht als EINE Schranke vor allen schreibenden Wegen. Die Alternative
|
||
-- in jeder der drei Stellen einzeln pruefen -- ist genau die Bauweise,
|
||
durch die am 31.08.2026 schon einmal ein Loch entstanden ist: Zwei von
|
||
drei Stellen waren abgesichert, die dritte vergessen. */
|
||
/* Ein Creator schreibt hier nicht -- MIT EINER AUSNAHME, die am
|
||
01.09.2026 dazukam: Was er SELBST angelegt hat, darf er auch
|
||
aendern und loeschen.
|
||
|
||
Die urspruengliche Regel ("hier tragen deine Betreuer ein") bleibt
|
||
damit unangetastet: Er kann weiterhin nichts an dem aendern, was
|
||
ueber ihn geschrieben wurde -- keine Bewertung, keine Notiz, keinen
|
||
Eintrag seines Managers.
|
||
|
||
Noetig wurde die Ausnahme durch die fertigen Vorlagen: Wenn ein
|
||
Creator sich eine Content-Idee uebernimmt, ist das SEINE Idee. Sie
|
||
danach nicht umbenennen zu duerfen waere absurd -- er haette einen
|
||
Eintrag erzeugt, den nur ein anderer bearbeiten kann.
|
||
|
||
Entscheidend ist erstellt_von, nicht creator_id: Ein Eintrag, den
|
||
sein Betreuer ueber ihn angelegt hat, traegt zwar seine creator_id,
|
||
gehoert ihm aber nicht. */
|
||
function nichtSchreibendFuerCreator(req, res, next) {
|
||
if (req.person?.rolle !== "creator") return next();
|
||
|
||
/* Neu anlegen bleibt gesperrt -- der Weg dafuer ist "Vorlage
|
||
uebernehmen", und der prueft eigene Regeln. */
|
||
/* NUR im Bereich content. Das ist der Bereich, in dem der Creator
|
||
selbst arbeitet -- seine Ideen, sein Material. LIVE, Technik,
|
||
Community und Schutz sind die BETREUUNGSAKTE: Dort schreiben die
|
||
Betreuer ueber ihn, und daran aendert er nichts, auch nicht an
|
||
etwas, das frueher einmal von ihm stammte. Das war am 31.08.2026
|
||
ausdruecklich so entschieden und bleibt so.
|
||
|
||
Ein erster Anlauf hatte die Ausnahme fuer ALLE Bereiche erlaubt --
|
||
die Pruefung pruef-bereiche-lesend hat das sofort gemeldet. */
|
||
const bereich = String(req.path).split("/")[4] || "";
|
||
if (bereich !== "content") {
|
||
return res.status(403).json({
|
||
fehler: "Hier tragen deine Betreuer ein. Du siehst alles, was zu dir gehört.",
|
||
});
|
||
}
|
||
|
||
const id = Number(String(req.path).split("/").pop());
|
||
if (Number.isInteger(id) && id > 0) {
|
||
try {
|
||
const e = db().prepare("SELECT erstellt_von FROM eintraege WHERE id = ?").get(id);
|
||
if (e && e.erstellt_von === req.person.id) return next();
|
||
} catch { /* faellt unten auf die Sperre zurueck */ }
|
||
}
|
||
|
||
return res.status(403).json({
|
||
fehler: "Das hat deine Betreuung eingetragen – ändern können sie es. "
|
||
+ "Was du selbst angelegt hast, kannst du bearbeiten.",
|
||
});
|
||
}
|
||
|
||
/* Express 4 -- dort ist der Platzhalter `*`, nicht `*name` wie in
|
||
Fassung 5. Mit der Fassung-5-Schreibweise haette der Pfad wortwoertlich
|
||
gepasst werden muessen und die Schranke waere NIE gegriffen: kein
|
||
Fehler, keine Warnung, nur eine Sperre, die stumm nichts tut. */
|
||
for (const weg of ["post", "patch", "delete", "put"]) {
|
||
bereicheRouter[weg]("/workspace/api/bereich/*", nichtSchreibendFuerCreator);
|
||
}
|
||
|
||
/* Scouts haben mit der Creator-Betreuung nichts zu tun -- sie sehen hier
|
||
nichts. Creator sehen ihren eigenen Bereich, das Management alles. */
|
||
export function sichtbar(person) {
|
||
/* NUR DogFather sieht alles (01.09.2026). Vorher stand hier
|
||
istLeitung() -- damit sah auch jeder Manager jeden Creator. Ein
|
||
Manager faellt jetzt in dieselbe Regel wie ein Scout: nur die
|
||
Creator, die ihm zugeteilt sind. Ausdruecklicher Wunsch:
|
||
"NUR DIE ROLLE DOGFATHER SOLL WIRKLICH ALLEINE ALLES SEHEN."
|
||
|
||
Geaendert wird ausschliesslich, wer was SIEHT. Was ein Manager
|
||
darf (freigeben, aendern, Personen verwalten), haengt weiterhin an
|
||
istLeitung und bleibt unveraendert -- sonst haette dieser eine
|
||
Wunsch stillschweigend seine halben Rechte mitgenommen. */
|
||
if (istDogFather(person)) return { wo: "1=1", werte: [] };
|
||
|
||
/* "ODER ich habe ihn selbst angelegt" kam am 02.09.2026 dazu -- aus
|
||
demselben Grund wie bei den Aufgaben: Ein Eintrag ohne creator_id
|
||
(Feld auf "—" gelassen oder seit heute ein freier Name) traf keine
|
||
der Bedingungen und war fuer den, der ihn gerade geschrieben hatte,
|
||
sofort unsichtbar. Nur DogFather sah ihn noch.
|
||
|
||
Es sieht weiterhin niemand etwas Fremdes -- nur das Eigene. */
|
||
if (person.rolle === "creator") {
|
||
return { wo: "(e.creator_id = ? OR e.erstellt_von = ?)", werte: [person.id, person.id] };
|
||
}
|
||
/* Ein Scout sieht die Bereiche der Creator, die er betreut -- dazu,
|
||
was er selbst eingetragen hat. */
|
||
const b = betreutWo(person, "e.creator_id");
|
||
return b
|
||
? { wo: `(e.erstellt_von = ? OR ${b.wo})`, werte: [person.id, ...b.werte] }
|
||
: { wo: "e.erstellt_von = ?", werte: [person.id] };
|
||
}
|
||
|
||
/* Dieselbe Regel, aber mit der Ausnahme fuer Bereiche, die allen
|
||
gehoeren.
|
||
|
||
BEWUSST EINE EIGENE FUNKTION, NICHT EINE AENDERUNG AN sichtbar():
|
||
sichtbar() beantwortet auch Fragen zu Aufgaben, Dateien, Terminen und
|
||
Calls (siehe die Aufrufe in workspace-aufgaben.js,
|
||
workspace-dateien.js, workspace-kalender.js, workspace-calls.js). Wer
|
||
dort "1=1" einschleust, weil er an Agentur-Eintraege dachte, oeffnet
|
||
nebenbei fremde Dateien und fremde Termine -- ein Loch, das man dem
|
||
Code nicht ansieht, weil an der geaenderten Stelle nichts davon
|
||
steht.
|
||
|
||
Nur Eintraege gehen hier durch, und nur die aus einem Bereich mit
|
||
`fuerAlle`. Der Rest faellt woertlich auf die alte Regel zurueck.
|
||
|
||
`praefix` ist da, weil die Suche dieselbe Tabelle unter demselben
|
||
Kuerzel "e" fuehrt -- geht das eines Tages auseinander, faellt es
|
||
beim Aufruf auf, nicht erst im Betrieb. */
|
||
export function sichtbarEintrag(person, praefix = "e") {
|
||
const regel = sichtbar(person);
|
||
if (!regel) return regel;
|
||
if (regel.wo === "1=1") return regel; // DogFather sieht ohnehin alles
|
||
if (!BEREICHE_FUER_ALLE.length) return regel;
|
||
const liste = BEREICHE_FUER_ALLE.map(() => "?").join(", ");
|
||
return {
|
||
wo: `(${praefix}.bereich IN (${liste}) OR ${regel.wo})`,
|
||
werte: [...BEREICHE_FUER_ALLE, ...regel.werte],
|
||
};
|
||
}
|
||
|
||
const SPALTEN = `
|
||
e.id, e.bereich, e.art, e.titel, e.text, e.datum, e.bewertung,
|
||
e.dringlichkeit, e.status, e.creator_id, e.erstellt, e.erstellt_von, e.geaendert,
|
||
e.hook, e.format, e.saeule_id, e.geplant,
|
||
e.event_ende, e.event_aufgaben, e.event_regeln,
|
||
e.creator_extern,
|
||
${externSql("pc.name", "e.creator_extern")} AS creator_name,
|
||
pe.name AS erstellt_name,
|
||
(SELECT name FROM content_saeulen s WHERE s.id = e.saeule_id) AS saeule_name,
|
||
(SELECT slot FROM content_saeulen s WHERE s.id = e.saeule_id) AS saeule_slot`;
|
||
|
||
/* Passt die gewaehlte Saeule zu dem Creator, um den es geht? Gibt einen
|
||
Fehlertext zurueck oder null, wenn alles stimmt. */
|
||
function saeulePasst(saeuleId, creatorId) {
|
||
if (!saeuleId) return null;
|
||
const s = db().prepare("SELECT creator_id FROM content_saeulen WHERE id = ?").get(saeuleId);
|
||
if (!s) return "Diese Säule gibt es nicht.";
|
||
if (creatorId && s.creator_id !== creatorId) return "Diese Säule gehört zu einem anderen Creator.";
|
||
return null;
|
||
}
|
||
|
||
const VERBUND = `
|
||
FROM eintraege e
|
||
LEFT JOIN personen pc ON pc.id = e.creator_id
|
||
LEFT JOIN personen pe ON pe.id = e.erstellt_von`;
|
||
|
||
/* ---------- Lesen ------------------------------------------------------- */
|
||
|
||
bereicheRouter.get("/workspace/api/bereich/:bereich", (req, res) => {
|
||
try {
|
||
const bereich = String(req.params.bereich);
|
||
const einstellung = BEREICHE[bereich];
|
||
if (!einstellung) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
|
||
const regel = sichtbarEintrag(req.sicht || req.person);
|
||
if (!regel) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
|
||
const eintraege = db().prepare(`
|
||
SELECT ${SPALTEN} ${VERBUND}
|
||
WHERE ${regel.wo} AND e.bereich = ?
|
||
ORDER BY
|
||
CASE e.status WHEN 'offen' THEN 0 ELSE 1 END,
|
||
CASE e.dringlichkeit WHEN 'hoch' THEN 0 WHEN 'mittel' THEN 1 ELSE 2 END,
|
||
e.datum DESC, e.id DESC`).all(...regel.werte, bereich);
|
||
|
||
res.json({ bereich, einstellung, eintraege });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Bereich lesen:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
/* ---------- Prüfen ------------------------------------------------------- */
|
||
|
||
function pruefe(bereich, körper, { neu }) {
|
||
const einstellung = BEREICHE[bereich];
|
||
const fehler = [];
|
||
const aus = {};
|
||
|
||
if (neu || körper.titel !== undefined) {
|
||
const t = String(körper.titel ?? "").trim();
|
||
if (t.length < 2) fehler.push("Titel fehlt.");
|
||
else if (t.length > TITEL_MAX) fehler.push("Titel ist zu lang.");
|
||
else aus.titel = t;
|
||
}
|
||
if (neu || körper.art !== undefined) {
|
||
const a = String(körper.art ?? "");
|
||
if (!Object.hasOwn(einstellung.arten, a)) fehler.push("Unbekannte Art.");
|
||
else aus.art = a;
|
||
}
|
||
if (körper.text !== undefined) {
|
||
const t = String(körper.text ?? "").trim();
|
||
if (t.length > TEXT_MAX) fehler.push("Text ist zu lang.");
|
||
else aus.text = t || null;
|
||
}
|
||
if (neu || körper.datum !== undefined) {
|
||
const d = String(körper.datum ?? "").trim();
|
||
if (!d) aus.datum = jetzt().slice(0, 10);
|
||
else if (!/^\d{4}-\d{2}-\d{2}$/.test(d) || Number.isNaN(Date.parse(d))) {
|
||
fehler.push("Datum ist ungültig.");
|
||
} else aus.datum = d;
|
||
}
|
||
if (körper.status !== undefined) {
|
||
if (!STATUS.includes(körper.status)) fehler.push("Unbekannter Status.");
|
||
else aus.status = körper.status;
|
||
}
|
||
/* Bewertung und Dringlichkeit gibt es nur dort, wo der Bereich sie
|
||
vorsieht -- sonst werden sie stillschweigend verworfen. */
|
||
if (einstellung.bewertung && körper.bewertung !== undefined) {
|
||
if (körper.bewertung === null || körper.bewertung === "") aus.bewertung = null;
|
||
else {
|
||
const b = Number(körper.bewertung);
|
||
if (!Number.isInteger(b) || b < 1 || b > 5) fehler.push("Bewertung muss 1 bis 5 sein.");
|
||
else aus.bewertung = b;
|
||
}
|
||
}
|
||
if (einstellung.dringlichkeit && körper.dringlichkeit !== undefined) {
|
||
if (!DRINGLICHKEITEN.includes(körper.dringlichkeit)) fehler.push("Unbekannte Dringlichkeit.");
|
||
else aus.dringlichkeit = körper.dringlichkeit;
|
||
}
|
||
|
||
/* Die vier Content-Felder gibt es nur in der Content-Planung. In jedem
|
||
anderen Bereich werden sie stillschweigend verworfen -- wie Bewertung
|
||
und Dringlichkeit auch. Sonst stuende irgendwann ein Aufhaenger an
|
||
einem Schutzvorfall. */
|
||
if (bereich === "content") {
|
||
if (körper.hook !== undefined) {
|
||
const h = String(körper.hook ?? "").trim();
|
||
if (h.length > 300) fehler.push("Der Hook ist zu lang.");
|
||
else aus.hook = h || null;
|
||
}
|
||
if (körper.format !== undefined) {
|
||
const f = String(körper.format ?? "").trim();
|
||
if (f.length > 60) fehler.push("Das Format ist zu lang.");
|
||
else aus.format = f || null;
|
||
}
|
||
if (körper.saeule_id !== undefined) {
|
||
if (körper.saeule_id === null || körper.saeule_id === "") aus.saeule_id = null;
|
||
else {
|
||
const s = Number(körper.saeule_id);
|
||
if (!Number.isInteger(s) || s < 1) fehler.push("Unbekannte Säule.");
|
||
else aus.saeule_id = s;
|
||
}
|
||
}
|
||
if (körper.geplant !== undefined) {
|
||
const g = String(körper.geplant ?? "").trim();
|
||
if (!g) aus.geplant = null;
|
||
/* Datum ODER Datum mit Uhrzeit -- wer nur den Tag weiss, soll nicht
|
||
zu einer erfundenen Uhrzeit gezwungen werden. */
|
||
else if (!/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2})?$/.test(g) || Number.isNaN(Date.parse(g))) {
|
||
fehler.push("Der geplante Zeitpunkt ist ungültig.");
|
||
} else aus.geplant = g;
|
||
}
|
||
}
|
||
/* ---- Agentur-Events: Zeitraum, Aufgaben, Regeln -------------------
|
||
Nur im Agentur-Bereich, nach demselben Muster wie die vier
|
||
Content-Felder darueber: anderswo werden sie stillschweigend
|
||
verworfen. Sonst haenge eines Tages ein Preisausschreiben an einem
|
||
Schutzvorfall. */
|
||
if (bereich === "agentur") {
|
||
if (körper.event_ende !== undefined) {
|
||
const e = String(körper.event_ende ?? "").trim();
|
||
if (!e) aus.event_ende = null;
|
||
else if (!/^\d{4}-\d{2}-\d{2}$/.test(e) || Number.isNaN(Date.parse(e))) {
|
||
fehler.push("Das Ende des Zeitraums ist ungültig.");
|
||
} else aus.event_ende = e;
|
||
}
|
||
for (const [feld, name] of [["event_aufgaben", "Die Aufgaben"], ["event_regeln", "Die Regeln"]]) {
|
||
if (körper[feld] === undefined) continue;
|
||
const t = String(körper[feld] ?? "").trim();
|
||
if (t.length > TEXT_MAX) fehler.push(`${name} sind zu lang.`);
|
||
else aus[feld] = t || null;
|
||
}
|
||
/* "Bis" vor "von" ist kein Zeitraum, sondern ein Tippfehler -- und
|
||
einer, den man einer Liste nicht ansieht: Der Eintrag stuende da,
|
||
liefe aber schon abgelaufen ein. Geprueft wird gegen das Datum,
|
||
das GERADE gesetzt wird; beim Aendern nur einer der beiden Seiten
|
||
ergaenzt der Aufrufer die andere mit (siehe PATCH). */
|
||
if (aus.event_ende && aus.datum && aus.event_ende < aus.datum) {
|
||
fehler.push("Das Ende liegt vor dem Beginn.");
|
||
}
|
||
}
|
||
|
||
if (körper.creator_id !== undefined) {
|
||
const w = körper.creator_id;
|
||
if (w === null || w === "") aus.creator_id = null;
|
||
else {
|
||
const z = Number(w);
|
||
if (!Number.isInteger(z) || z < 1) fehler.push("Ungültige Zuordnung.");
|
||
else aus.creator_id = z;
|
||
}
|
||
}
|
||
|
||
/* Ein frei eingetragener Name statt eines Creators -- eine Marke, ein
|
||
Partner, jemand ohne Konto im Workspace. Er schlaegt die Auswahl:
|
||
Wer tippt, meint das Getippte. Der Eintrag gehoert dann zu keinem
|
||
Konto; sichtbar bleibt er ueber erstellt_von (siehe sichtbar()). */
|
||
externPruefen(körper, aus, "creator", fehler);
|
||
|
||
/* Ein Agentur-Eintrag gehoert NIEMANDEM einzeln (07.09.2026).
|
||
|
||
Er gilt fuer alle, und genau deshalb sieht ihn auch jeder (siehe
|
||
`fuerAlle` oben). Eine Zuordnung waere hier nicht bloss unnoetig,
|
||
sie waere irrefuehrend: Ein Event mit dem Namen einer Creatorin
|
||
daneben liest sich, als sei es ihres. Die Oberflaeche bietet
|
||
deshalb nur noch "Agentur" an -- und diese Zeilen sorgen dafuer,
|
||
dass ein Aufruf an der Oberflaeche vorbei zum selben Ergebnis
|
||
kommt. Eine Regel, die nur im Formular steht, ist keine Regel.
|
||
|
||
GANZ ZUM SCHLUSS, und das ist der Punkt: Der freie Name wird eine
|
||
Zeile darueber gesetzt. Stuende dieser Riegel vorher -- der
|
||
naheliegende Ort, gleich bei der Creator-Zuordnung --, haette
|
||
externPruefen ihn danach wieder aufgemacht, und ein getippter Name
|
||
stuende trotz allem am Event. Zwei richtige Regeln, falsche
|
||
Reihenfolge, kein Fehler zu sehen. */
|
||
if (einstellung.ohneCreatorBezug) {
|
||
aus.creator_id = null;
|
||
aus.creator_extern = null;
|
||
}
|
||
|
||
return { aus, fehler };
|
||
}
|
||
|
||
/* ---------- Anlegen ------------------------------------------------------ */
|
||
|
||
bereicheRouter.post("/workspace/api/bereich/:bereich", gleicheHerkunft, (req, res) => {
|
||
try {
|
||
const bereich = String(req.params.bereich);
|
||
if (!BEREICHE[bereich]) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
const regel = sichtbar(req.person);
|
||
if (!regel) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
|
||
const { aus, fehler } = pruefe(bereich, req.body || {}, { neu: true });
|
||
if (fehler.length) return res.status(400).json({ fehler: fehler.join(" ") });
|
||
|
||
/* Ein Creator schreibt immer in den eigenen Bereich, egal was im
|
||
Aufruf steht. Ein Scout in den eines Creators, den er betreut --
|
||
niemals in einen fremden und niemals "auf sich selbst", denn ein
|
||
Scout hat gar keinen eigenen Betreuungsbereich. */
|
||
if (req.person.rolle === "creator") {
|
||
aus.creator_id = req.person.id;
|
||
aus.creator_extern = null; // der eigene Bereich gehört ihm, nicht einem Namen
|
||
} else if (req.person.rolle === "scout" && !aus.creator_extern) {
|
||
/* Der freie Name nimmt diesen Zweig ausdrücklich aus: Wer "Marke
|
||
XY" eintippt, will keinen seiner Creator zugeordnet bekommen.
|
||
Ohne diese Ausnahme hätte die Notlösung darunter den getippten
|
||
Namen stillschweigend durch den erstbesten Creator ersetzt --
|
||
der Eintrag stünde dann bei jemandem, der nichts damit zu tun
|
||
hat. Sichtbar bleibt er über erstellt_von. */
|
||
const w = Number(aus.creator_id);
|
||
if (!darfCreator(req.person, w)) {
|
||
const erster = betreuteIds(req.person)[0];
|
||
if (!erster) return res.status(403).json({ fehler: "Dir ist kein Creator zugeteilt." });
|
||
aus.creator_id = erster;
|
||
}
|
||
}
|
||
if (aus.creator_id
|
||
&& !db().prepare("SELECT 1 FROM personen WHERE id = ? AND rolle = 'creator'").get(aus.creator_id)) {
|
||
return res.status(400).json({ fehler: "Zugeordneter Creator existiert nicht." });
|
||
}
|
||
|
||
/* Eine Saeule gehoert immer GENAU EINEM Creator. Ohne diese Pruefung
|
||
koennte man ein Video an die Saeule eines fremden Creators haengen
|
||
-- und darueber in der Balance-Auswertung Namen sehen, die einen
|
||
nichts angehen. Die Sichtbarkeitsregel oben schuetzt die Eintraege,
|
||
nicht die Verweise zwischen ihnen. */
|
||
const saeuleFehler = saeulePasst(aus.saeule_id, aus.creator_id);
|
||
if (saeuleFehler) return res.status(400).json({ fehler: saeuleFehler });
|
||
|
||
const { lastInsertRowid } = db().prepare(`
|
||
INSERT INTO eintraege
|
||
(bereich, art, titel, text, datum, bewertung, dringlichkeit, status,
|
||
creator_id, creator_extern, erstellt, erstellt_von, hook, format, saeule_id, geplant,
|
||
event_ende, event_aufgaben, event_regeln)
|
||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(
|
||
bereich, aus.art, aus.titel, aus.text ?? null, aus.datum,
|
||
aus.bewertung ?? null, aus.dringlichkeit ?? "mittel", aus.status ?? "offen",
|
||
aus.creator_id ?? null, aus.creator_extern ?? null, jetzt(), req.person.id,
|
||
aus.hook ?? null, aus.format ?? null, aus.saeule_id ?? null, aus.geplant ?? null,
|
||
aus.event_ende ?? null, aus.event_aufgaben ?? null, aus.event_regeln ?? null);
|
||
|
||
protokolliere("eintrag_angelegt", {
|
||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||
detail: `${bereich} #${lastInsertRowid} ${aus.titel}`.slice(0, 120),
|
||
});
|
||
res.status(201).json({ id: Number(lastInsertRowid) });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Eintrag anlegen:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
/* ---------- Ändern und Löschen ------------------------------------------- */
|
||
|
||
function holen(req, id) {
|
||
const regel = sichtbarEintrag(req.person);
|
||
if (!regel) return null;
|
||
return db().prepare(`SELECT e.* ${VERBUND} WHERE ${regel.wo} AND e.id = ?`)
|
||
.get(...regel.werte, id);
|
||
}
|
||
|
||
bereicheRouter.patch("/workspace/api/bereich/:bereich/:id", gleicheHerkunft, (req, res) => {
|
||
try {
|
||
const bereich = String(req.params.bereich);
|
||
const id = Number(req.params.id);
|
||
if (!BEREICHE[bereich] || !Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
|
||
|
||
const eintrag = holen(req, id);
|
||
if (!eintrag || eintrag.bereich !== bereich) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
|
||
const { aus, fehler } = pruefe(bereich, req.body || {}, { neu: false });
|
||
if (fehler.length) return res.status(400).json({ fehler: fehler.join(" ") });
|
||
if (!istLeitung(req.person)) delete aus.creator_id;
|
||
|
||
/* Zeitraum beim AENDERN -- genauso wichtig wie beim Anlegen und
|
||
leichter zu uebersehen: pruefe() sieht nur die Felder, die im
|
||
Aufruf stehen. Wer nur das Ende verschiebt, schickt kein `datum`
|
||
mit; die Pruefung dort haette nichts zu vergleichen und liesse
|
||
jedes Ende durch. Deshalb hier gegen den GESPEICHERTEN Wert, und
|
||
zwar in beide Richtungen -- man kann den Zeitraum auch dadurch
|
||
verdrehen, dass man den Beginn nach hinten schiebt. */
|
||
if (bereich === "agentur" && (aus.event_ende !== undefined || aus.datum !== undefined)) {
|
||
const von = aus.datum !== undefined ? aus.datum : eintrag.datum;
|
||
const bis = aus.event_ende !== undefined ? aus.event_ende : eintrag.event_ende;
|
||
if (von && bis && bis < von) {
|
||
return res.status(400).json({ fehler: "Das Ende liegt vor dem Beginn." });
|
||
}
|
||
}
|
||
|
||
/* Beim Aendern gilt dieselbe Regel -- sonst waere die Pruefung beim
|
||
Anlegen wertlos: Man legt ohne Saeule an und haengt sie danach an. */
|
||
if (aus.saeule_id !== undefined) {
|
||
const zuWem = aus.creator_id !== undefined ? aus.creator_id : eintrag.creator_id;
|
||
const saeuleFehler = saeulePasst(aus.saeule_id, zuWem);
|
||
if (saeuleFehler) return res.status(400).json({ fehler: saeuleFehler });
|
||
}
|
||
|
||
const felder = Object.keys(aus);
|
||
if (!felder.length) return res.status(400).json({ fehler: "nichts_zu_aendern" });
|
||
|
||
db().prepare(
|
||
`UPDATE eintraege SET ${felder.map((f) => `${f} = ?`).join(", ")}, geaendert = ? WHERE id = ?`
|
||
).run(...felder.map((f) => aus[f]), jetzt(), id);
|
||
|
||
protokolliere("eintrag_geaendert", {
|
||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||
detail: `${bereich} #${id} ${felder.join(",")}`.slice(0, 120),
|
||
});
|
||
res.json({ ok: true });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Eintrag ändern:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
bereicheRouter.delete("/workspace/api/bereich/:bereich/:id", gleicheHerkunft, (req, res) => {
|
||
try {
|
||
const bereich = String(req.params.bereich);
|
||
const id = Number(req.params.id);
|
||
if (!BEREICHE[bereich] || !Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
|
||
|
||
const eintrag = holen(req, id);
|
||
if (!eintrag || eintrag.bereich !== bereich) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
|
||
/* Löschen darf das Management und wer den Eintrag selbst geschrieben
|
||
hat -- sonst könnte ein Creator eine Notiz des Managements über
|
||
seinen eigenen Bereich verschwinden lassen. */
|
||
if (!istLeitung(req.person) && eintrag.erstellt_von !== req.person.id) {
|
||
return res.status(403).json({ fehler: "nicht_erlaubt" });
|
||
}
|
||
|
||
db().prepare("DELETE FROM eintraege WHERE id = ?").run(id);
|
||
protokolliere("eintrag_geloescht", {
|
||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||
detail: `${bereich} #${id} ${eintrag.titel}`.slice(0, 120),
|
||
});
|
||
res.json({ ok: true });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Eintrag löschen:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|