Agentur: Eintraege gehoeren allen -- und Events bekommen ein eigenes Formular

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]>
This commit is contained in:
2026-09-07 02:31:47 +02:00
co-authored by Claude Opus 5
parent 3914adf46c
commit 947c9d8a26
26 changed files with 898 additions and 204 deletions
+130 -6
View File
@@ -101,19 +101,45 @@ export const BEREICHE = {
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: "Event & Kampagne",
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;
@@ -256,10 +282,41 @@ export function sichtbar(person) {
: { 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,
@@ -289,7 +346,7 @@ bereicheRouter.get("/workspace/api/bereich/:bereich", (req, res) => {
const einstellung = BEREICHE[bereich];
if (!einstellung) return res.status(404).json({ fehler: "nicht_gefunden" });
const regel = sichtbar(req.sicht || req.person);
const regel = sichtbarEintrag(req.sicht || req.person);
if (!regel) return res.status(404).json({ fehler: "nicht_gefunden" });
const eintraege = db().prepare(`
@@ -389,6 +446,35 @@ function pruefe(bereich, körper, { neu }) {
} 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;
@@ -405,6 +491,27 @@ function pruefe(bereich, körper, { neu }) {
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 };
}
@@ -457,12 +564,14 @@ bereicheRouter.post("/workspace/api/bereich/:bereich", gleicheHerkunft, (req, re
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)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(
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.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),
@@ -478,7 +587,7 @@ bereicheRouter.post("/workspace/api/bereich/:bereich", gleicheHerkunft, (req, re
/* ---------- Ändern und Löschen ------------------------------------------- */
function holen(req, id) {
const regel = sichtbar(req.person);
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);
@@ -497,6 +606,21 @@ bereicheRouter.patch("/workspace/api/bereich/:bereich/:id", gleicheHerkunft, (re
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) {