diff --git a/server/pruef-agentur.mjs b/server/pruef-agentur.mjs index 9a518ad1..0784f804 100644 --- a/server/pruef-agentur.mjs +++ b/server/pruef-agentur.mjs @@ -122,11 +122,16 @@ function anlegen(d, name, rolle, code) { return d.prepare("SELECT last_insert_rowid() AS id").get().id; } -let idDogi, idLuna; +/* Mia kam am 07.09.2026 dazu, und sie ist kein Beiwerk: Sie legt nichts + an, ihr gehoert nichts, sie betreut niemanden. Genau deshalb beweist + erst SIE, dass ein Agentur-Eintrag wirklich fuer alle sichtbar ist -- + bei Luna allein bliebe offen, ob es an ihr oder an der Regel lag. */ +let idDogi, idLuna, idMia; { const d = new DatabaseSync(DB); idDogi = anlegen(d, "Filipe", "admin", "CODE-DOGI-0001"); idLuna = anlegen(d, "Luna", "creator", "CODE-CREA-0001"); + idMia = anlegen(d, "Mia", "creator", "CODE-CREA-0002"); d.close(); } @@ -262,6 +267,24 @@ ok(/Bereich 'agentur' freigeschaltet/.test(ausgabe), ok(mitExtern?.creator_extern === "Gastauftritt Mia", `die Spalte "creator_extern" ebenfalls ("${mitExtern?.creator_extern}")`); + /* DIE DREI EVENT-SPALTEN MUESSEN DEN TAUSCH UEBERLEBEN (07.09.2026). + + Sie entstehen frueher als diese Umstellung (Spalten-Nachtrag in + workspace.js), und der Neubau hier schreibt eine feste Spaltenliste + ab. Wer sie dort vergisst, verliert sie samt Inhalt -- ohne + Fehlermeldung, und die Zeilenzahl stimmte weiterhin. Genau dieser + Weg wird hier wirklich gegangen: Die Tabelle stand vorher im ALTEN + Bauplan, ohne diese Spalten. + + `PRAGMA table_info` und nicht "SELECT laeuft durch": Eine fehlende + Spalte faellt beim SELECT zwar auch auf -- aber erst irgendwann, + beim naechsten Event, in einem 503. */ + const spalten = d.prepare("PRAGMA table_info(eintraege)").all().map((s) => s.name); + const fehlend = ["event_ende", "event_aufgaben", "event_regeln"].filter((s) => !spalten.includes(s)); + ok(fehlend.length === 0, + fehlend.length ? `nach der Umstellung fehlen: ${fehlend.join(", ")}` + : "die drei Event-Spalten haben die Umstellung ueberlebt"); + /* Der Index muss nach dem Tausch wieder da sein -- er wird beim DROP mit entfernt. Ohne ihn laeuft alles weiter, nur langsam, und niemand merkt es je. */ @@ -312,6 +335,7 @@ async function anmelden(rolle, code) { } const keksDogi = await anmelden("admin", "CODE-DOGI-0001"); const keksLuna = await anmelden("creator", "CODE-CREA-0001"); +const keksMia = await anmelden("creator", "CODE-CREA-0002"); async function ruf(art, weg, keks, koerper) { const a = await fetch(BASIS + weg, { @@ -340,11 +364,29 @@ const ARTEN = ["kampagne", "schulung", "anliegen", "zustaendig"]; "Bewertung ist aus — hier wird zusammengearbeitet, nicht benotet"); } +/* ANGELEGT WIE IM ECHTEN BETRIEB: OHNE ZUORDNUNG (07.09.2026). + + Hier stand `creator_id: idLuna` -- und damit prueften die Zeilen + weiter unten einen Fall, den es im Alltag nicht gibt. Ein Agentur- + Event gehoert niemandem einzeln; Filipe legt es fuer alle an, das + Feld bleibt leer. Weil die Pruefung es aber jedes Mal Luna zuwies, + sah Luna es auch -- und die Zeile "sie sieht ihre 4 Eintraege" war + grün. + + In Wirklichkeit war die Agentur-Seite fuer JEDEN Creator leer: Ein + Eintrag ohne creator_id traf weder `creator_id = ich` noch + `erstellt_von = ich`. Gemessen am 07.09.2026, bevor es repariert + wurde: von drei Eintraegen sah die Creatorin genau den einen, den + die Pruefung ihr zugeschoben hatte. + + Das ist die dritte Sorte Fehler aus CLAUDE.md -- nicht uebersprungen, + nicht kaputt: gelaufen, gruen, und am Thema vorbei. Deshalb wird ab + jetzt der Weg gegangen, den es wirklich gibt. */ let angelegt = 0; for (const art of ARTEN) { const { status } = await ruf("POST", "/workspace/api/bereich/agentur", keksDogi, { art, titel: `Test ${art}`, text: "Inhalt", datum: heute, - dringlichkeit: "mittel", creator_id: idLuna, + dringlichkeit: "mittel", }); if (status === 200 || status === 201) angelegt++; else ok(false, `Art "${art}" liess sich nicht anlegen (HTTP ${status})`); @@ -360,13 +402,141 @@ ok(angelegt === ARTEN.length, `alle ${angelegt} Arten lassen sich anlegen`); ok(status >= 400, `eine erfundene Art wird abgelehnt (HTTP ${status})`); } -/* Der Creator SIEHT seinen Agentur-Bereich -- er ist der Grund, warum - es ihn gibt. */ +/* ======================================================================= + 4b. FUER ALLE, NICHT FUER EINEN + + Die Agentur ist der einzige Bereich, in dem ein Eintrag ALLEN gilt. + Geprueft wird deshalb beides: dass jeder Creator alles sieht -- und + dass diese Ausnahme NICHT auf die Betreuungsbereiche abfaerbt. Der + zweite Teil ist der wichtigere: Eine Sichtbarkeitsregel, die man an + der falschen Stelle aufmacht, gibt fremde Akten frei, und man sieht + es der Zeile nicht an. + ======================================================================= */ +melde("\n=== Fuer alle, nicht fuer einen ==="); { const { status, daten } = await ruf("GET", "/workspace/api/bereich/agentur", keksLuna); ok(status === 200, `Luna darf den Bereich oeffnen (HTTP ${status})`); ok((daten?.eintraege || []).length === ARTEN.length, - `sie sieht ihre ${(daten?.eintraege || []).length} Eintraege`); + `sie sieht alle ${(daten?.eintraege || []).length} Eintraege — obwohl keiner ihr zugeordnet ist`); + ok((daten?.eintraege || []).every((e) => e.creator_id === null), + "und keiner davon traegt eine Creator-Zuordnung"); +} +{ + /* Mia ist eine ZWEITE Creatorin, die nichts angelegt hat und der + nichts gehoert. Wenn die Regel nur ueber `erstellt_von` gestolpert + waere, saehe Luna vielleicht etwas und Mia nichts. */ + const { daten } = await ruf("GET", "/workspace/api/bereich/agentur", keksMia); + ok((daten?.eintraege || []).length === ARTEN.length, + `auch Mia sieht alle ${(daten?.eintraege || []).length} — sie hat mit keinem davon etwas zu tun`); +} +{ + /* Eine ausdrueckliche Zuordnung muss der Server verwerfen. Sonst + genuegt ein Aufruf an der Oberflaeche vorbei -- oder ein spaeteres + Formular, das das Feld wieder mitschickt --, und das Event ist + fuer alle anderen wieder unsichtbar. */ + await ruf("POST", "/workspace/api/bereich/agentur", keksDogi, { + art: "kampagne", titel: "Versuch mit Zuordnung", datum: heute, creator_id: idLuna, + }); + const { daten } = await ruf("GET", "/workspace/api/bereich/agentur", keksMia); + const e = (daten?.eintraege || []).find((x) => x.titel === "Versuch mit Zuordnung"); + ok(!!e, "ein Eintrag, der ausdruecklich Luna zugewiesen wurde, ist trotzdem fuer Mia da"); + ok(e?.creator_id === null, `und seine Zuordnung wurde verworfen (creator_id = ${e?.creator_id})`); + /* Und derselbe Riegel fuer den freien Namen -- er wird an einer + ANDEREN Stelle gesetzt (externPruefen) und waere sonst das Loch + neben der geschlossenen Tuer. */ + await ruf("POST", "/workspace/api/bereich/agentur", keksDogi, { + art: "kampagne", titel: "Versuch mit Namen", datum: heute, creator_extern: "Marke XY", + }); + const { daten: d2 } = await ruf("GET", "/workspace/api/bereich/agentur", keksMia); + const n = (d2?.eintraege || []).find((x) => x.titel === "Versuch mit Namen"); + ok(n?.creator_extern === null || n?.creator_extern === undefined, + `auch ein frei getippter Name wird verworfen (creator_extern = ${JSON.stringify(n?.creator_extern)})`); +} +{ + /* DIE ENTSCHEIDENDE GEGENPROBE. + + Die Ausnahme haette man auch in sichtbar() einbauen koennen -- eine + Zeile weniger, und dieselbe Wirkung fuer die Agentur. Nur haengen + an derselben Funktion auch Aufgaben, Dateien, Termine und Calls. + Diese Pruefung ist der Grund, warum das auffiele. */ + await ruf("POST", "/workspace/api/bereich/technik", keksDogi, { + art: "problem", titel: "Nur fuer Mia", datum: heute, creator_id: idMia, + }); + const { daten } = await ruf("GET", "/workspace/api/bereich/technik", keksLuna); + const fremd = (daten?.eintraege || []).filter((e) => e.titel === "Nur fuer Mia"); + ok(fremd.length === 0, + `Luna sieht Mias Technik-Eintrag NICHT — die Agentur-Ausnahme faerbt nicht ab (${fremd.length} gefunden)`); +} +{ + /* Sehen ja, schreiben nein -- ausdruecklicher Wunsch: "die sollen + alles sehen was wir eintragen aber nicht eintragen koennen". */ + const { status } = await ruf("POST", "/workspace/api/bereich/agentur", keksLuna, { + art: "kampagne", titel: "Von Luna", datum: heute, + }); + ok(status === 403, `Luna darf hier NICHTS eintragen (HTTP ${status})`); +} + +/* ======================================================================= + 4c. AGENTUR-EVENTS: Zeitraum, Aufgaben, Regeln + ======================================================================= */ +melde("\n=== Agentur-Events ==="); +{ + /* ORTSZEIT, nicht UTC. `new Date(...).toISOString().slice(0,10)` waere + zwischen Mitternacht und zwei Uhr einen Tag daneben -- und weil der + Server den Zeitraum gegen `heute` (Ortszeit) prueft, waere diese + Pruefung nachts rot, ohne dass sich etwas geaendert haette. + Gefunden von pruef-struktur, das genau danach sucht. Mittags + gerechnet, damit auch eine Zeitumstellung im Zeitraum nichts + verschiebt. */ + const ende = (() => { + const d = new Date(`${heute}T12:00:00`); + d.setDate(d.getDate() + 14); + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; + })(); + const { status } = await ruf("POST", "/workspace/api/bereich/agentur", keksDogi, { + art: "kampagne", titel: "Sommer-Aktion", text: "Beschreibung", datum: heute, + event_ende: ende, event_aufgaben: "1 Punkt je Live-Stunde", event_regeln: "Nur eigenes Konto", + }); + ok(status === 201, `ein Event mit Zeitraum, Aufgaben und Regeln laesst sich anlegen (HTTP ${status})`); + + const { daten } = await ruf("GET", "/workspace/api/bereich/agentur", keksLuna); + const e = (daten?.eintraege || []).find((x) => x.titel === "Sommer-Aktion"); + ok(e?.event_ende === ende, `das Ende kommt beim Creator an (${e?.event_ende})`); + ok(e?.event_aufgaben === "1 Punkt je Live-Stunde", "die Aufgaben kommen an"); + ok(e?.event_regeln === "Nur eigenes Konto", "die Regeln kommen an"); + + /* GEGENPROBEN. Ohne sie hiesse "es laesst sich anlegen" nur, dass der + Server irgendetwas annimmt. */ + const verdreht = await ruf("POST", "/workspace/api/bereich/agentur", keksDogi, { + art: "kampagne", titel: "Verdreht", datum: ende, event_ende: heute }); + ok(verdreht.status === 400, `ein Ende VOR dem Beginn wird abgelehnt (HTTP ${verdreht.status})`); + + const unsinn = await ruf("POST", "/workspace/api/bereich/agentur", keksDogi, { + art: "kampagne", titel: "Unsinn", datum: heute, event_ende: "31.12.2026" }); + ok(unsinn.status === 400, `ein Datum in falscher Schreibweise wird abgelehnt (HTTP ${unsinn.status})`); + + /* Beim AENDERN gilt dasselbe -- und zwar auch dann, wenn nur EINE + der beiden Seiten mitgeschickt wird. Genau hier waere die Pruefung + sonst blind: pruefe() sieht nur, was im Aufruf steht. */ + const { daten: d3 } = await ruf("GET", "/workspace/api/bereich/agentur", keksDogi); + const id = (d3?.eintraege || []).find((x) => x.titel === "Sommer-Aktion")?.id; + const nurEnde = await ruf("PATCH", `/workspace/api/bereich/agentur/${id}`, keksDogi, + { event_ende: "2020-01-01" }); + ok(nurEnde.status === 400, + `auch beim Aendern nur des Endes schlaegt der verdrehte Zeitraum an (HTTP ${nurEnde.status})`); + const nurBeginn = await ruf("PATCH", `/workspace/api/bereich/agentur/${id}`, keksDogi, + { datum: "2099-01-01" }); + ok(nurBeginn.status === 400, + `und ebenso, wenn nur der Beginn nach hinten geschoben wird (HTTP ${nurBeginn.status})`); + + /* Die Event-Felder gehoeren der Agentur. In einem anderen Bereich + muessen sie stillschweigend verfallen -- sonst haengt eines Tages + ein Preisausschreiben an einem Schutzvorfall. */ + await ruf("POST", "/workspace/api/bereich/schutz", keksDogi, { + art: "vorfall", titel: "Kein Event", datum: heute, event_regeln: "darf nicht ankommen" }); + const { daten: d4 } = await ruf("GET", "/workspace/api/bereich/schutz", keksDogi); + const v = (d4?.eintraege || []).find((x) => x.titel === "Kein Event"); + ok(!v?.event_regeln, `im Schutz-Bereich verfallen die Event-Felder (${JSON.stringify(v?.event_regeln)})`); } /* ======================================================================= @@ -451,6 +621,75 @@ melde("\n=== Die Kachel ==="); fehlend.length ? `diese Eintraege fehlen auf der Seite: ${fehlend.join(", ")}` : `alle vier Titel stehen da: ${kartenTitel.join(" · ")}`); + /* ---- Das Event auf dem Bildschirm -------------------------------- + Bis hierher ist bewiesen, dass die Daten stimmen. Das sagt noch + nichts darueber, ob jemand sie SIEHT: Zeitraum, Aufgaben und + Regeln koennen vollstaendig in der Antwort stehen und trotzdem + nirgends gezeichnet werden -- ein vergessener Aufruf in karte() + reicht, und niemand merkt es, weil die Karte ja da ist. */ + const event = await seite.$$eval(".eintrag-karte", (karten) => { + const k = karten.find((x) => x.querySelector(".eintrag-titel")?.textContent.trim() === "Sommer-Aktion"); + if (!k) return null; + return { + stand: k.dataset.event, + marke: k.querySelector(".event-zeitraum__marke")?.textContent.trim(), + spanne: k.querySelector(".event-zeitraum__spanne")?.textContent.trim(), + schilder: [...k.querySelectorAll(".event-block__schild")].map((s) => s.textContent.trim()), + texte: [...k.querySelectorAll(".event-block__text")].map((s) => s.textContent.trim()), + }; + }); + ok(!!event, "die Event-Karte Sommer-Aktion steht auf der Seite"); + ok(event?.stand === "laeuft", `sie ist als laufend gekennzeichnet ("${event?.stand}")`); + ok(/läuft bis/.test(event?.marke || ""), + `der Zustand steht als WORT da, nicht nur als Farbe ("${event?.marke}")`); + ok(/–/.test(event?.spanne || ""), `und der Zeitraum als Spanne ("${event?.spanne}")`); + ok(event?.schilder.join(",") === "Aufgaben,Regeln", + `beide Bloecke sind beschriftet (${event?.schilder.join(", ")})`); + ok(event?.texte.includes("1 Punkt je Live-Stunde") && event?.texte.includes("Nur eigenes Konto"), + "und tragen wirklich ihren Inhalt"); + + /* GEGENPROBE dazu: Eine Karte, die KEIN Event ist, darf nichts davon + haben. Sonst bewiese der Block oben nur, dass die Elemente + existieren -- nicht, dass sie zur richtigen Art gehoeren. */ + const schulung = await seite.$$eval(".eintrag-karte", (karten) => { + const k = karten.find((x) => x.querySelector(".eintrag-titel")?.textContent.trim() === "Test schulung"); + return k ? { event: k.dataset.event ?? null, bloecke: k.querySelectorAll(".event-block").length } : null; + }); + ok(schulung && !schulung.event && schulung.bloecke === 0, + `eine Schulung bekommt weder Zustand noch Event-Bloecke (${JSON.stringify(schulung)})`); + + /* ---- Die Zuordnung bietet nur "Agentur" an ------------------------ + Der Ausgangspunkt des ganzen Umbaus: Hier stand die Creator-Liste, + und ein versehentlicher Klick darauf machte das Event fuer alle + anderen unsichtbar. */ + await seite.click("#neu-oeffnen"); + await seite.waitForTimeout(400); + const wahl = await seite.$$eval("#f-creator option", (o) => o.map((x) => x.textContent.trim())); + ok(wahl.length === 1 && wahl[0] === "Agentur", + `die Zuordnung bietet genau eine Wahl an: ${JSON.stringify(wahl)}`); + const hinweis = await seite.$eval("#hinweis-creator", (e) => e.textContent.trim()).catch(() => ""); + ok(/allen/.test(hinweis), `und sagt, was das bedeutet ("${hinweis}")`); + + /* ---- Das Formular wechselt mit der Art --------------------------- + Beim Oeffnen steht die erste Art (Agentur-Events) drin, also + muessen die Event-Felder sichtbar sein. Wechselt man auf Schulung, + muessen sie verschwinden -- und geleert werden, sonst faehrt ein + unsichtbares Enddatum an einer Schulung mit. */ + const sichtbar = (id) => seite.$eval(id, (e) => !e.hidden); + ok(await sichtbar("#feld-ende") && await sichtbar("#feld-aufgaben") && await sichtbar("#feld-regeln"), + "bei Agentur-Events stehen Zeitraum, Aufgaben und Regeln im Formular"); + const schild = await seite.$eval("#schild-titel", (e) => e.textContent.trim()); + ok(schild === "Titel des Events", `und die Beschriftung passt zur Art ("${schild}")`); + + await seite.fill("#f-ende", "2030-01-01"); + await seite.selectOption("#f-art", "schulung"); + await seite.waitForTimeout(300); + ok(!(await sichtbar("#feld-ende")) && !(await sichtbar("#feld-aufgaben")), + "bei Schulung verschwinden sie wieder"); + const restEnde = await seite.$eval("#f-ende", (e) => e.value); + ok(restEnde === "", + `und das Enddatum faehrt nicht unsichtbar mit (Wert: "${restEnde}")`); + /* GEGENPROBE zum Selektor: Ein erfundener Bereich darf NICHT wie ein echter aussehen -- weder in der Ueberschrift noch mit Eintraegen. Die Anwendung schickt einen dorthin zurueck, wo er hingehoert. */ diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js index 640b6e9a..7f5a3625 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -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) { diff --git a/server/workspace-hinweise.js b/server/workspace-hinweise.js index 370a945a..2d0da0a4 100644 --- a/server/workspace-hinweise.js +++ b/server/workspace-hinweise.js @@ -136,7 +136,20 @@ hinweisRouter.get("/workspace/api/hinweise", (req, res) => { WHERE ${d.wo} AND d.status = 'review'`, d.werte)); } - /* ---------- Betreuungsbereiche ---------- */ + /* ---------- 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", diff --git a/server/workspace-suche.js b/server/workspace-suche.js index d7dbd0cd..72395b5f 100644 --- a/server/workspace-suche.js +++ b/server/workspace-suche.js @@ -30,7 +30,11 @@ import { 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, BEREICHE } from "./workspace-bereiche.js"; +/* sichtbarEintrag, NICHT sichtbar: Sonst faende ein Creator ueber die + Suche kein einziges Agentur-Event -- obwohl es auf der Agentur-Seite + vor ihm steht. Eine Suche, die weniger findet als die Seite zeigt, + liest sich wie "gibt es nicht". */ +import { sichtbarEintrag as sichtbarEintraege, BEREICHE } from "./workspace-bereiche.js"; export const sucheRouter = express.Router(); diff --git a/server/workspace.js b/server/workspace.js index a5a5c606..1e49d30e 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -246,6 +246,28 @@ function umstellungen(d) { ["aufgaben", "abgebrochen_am", "TEXT"], ["aufgaben", "abbruch_von", "INTEGER REFERENCES personen(id) ON DELETE SET NULL"], ["aufgaben", "status_vorher", "TEXT"], + + /* AGENTUR-EVENTS (07.09.2026). + + Ein Event ist kein Notizzettel mit Ueberschrift und Text. Wer + mitmachen soll, muss vier Dinge finden, ohne zu fragen: bis wann + es laeuft, was zu tun ist, was es zu gewinnen gibt und was + verboten ist. Genau daran scheitern Aktionen -- nicht am Aufruf, + sondern an den Rueckfragen danach. + + Der BEGINN ist `datum` (gibt es schon, ist NOT NULL). Neu ist + das Ende und die beiden Textfelder. `event_`-Vorsatz, weil es im + Workspace bereits eine Tabelle `aufgaben` gibt -- eine Spalte + `aufgaben` auf `eintraege` waere beim Lesen von Code jedes Mal + eine Verwechslung. + + ADD COLUMN und kein Tabellenneubau: Es aendert sich kein CHECK, + also darf die Tabelle stehen bleiben. Der Neubau vom 06.09. war + noetig, weil dort der erlaubte Wertebereich von `bereich` wuchs + -- und er ist der Weg, bei dem man Inhalte verlieren kann. */ + ["eintraege", "event_ende", "TEXT"], + ["eintraege", "event_aufgaben", "TEXT"], + ["eintraege", "event_regeln", "TEXT"], ]) { try { const vorhanden = d.prepare(`PRAGMA table_info(${tabelle})`).all().map((s) => s.name); @@ -501,15 +523,31 @@ function umstellungen(d) { format TEXT, saeule_id INTEGER, geplant TEXT, - creator_extern TEXT + creator_extern TEXT, + /* Die drei Event-Spalten MUESSEN hier mit stehen (07.09.2026). + + Der Spalten-Nachtrag weiter oben laeuft frueher als dieser + Neubau. Auf einer Datenbank, die noch den alten CHECK hat + -- eine Sicherung von vor dem 06.09., in einen heutigen + Stand eingespielt --, waeren die drei Spalten also erst + angelegt und hier sofort wieder weggeworfen worden: mit + allem, was drinsteht, ohne Fehlermeldung, und die + Zeilenzahl haette weiterhin gestimmt. Genau die + Verlustart, vor der der Kommentar zu dieser Umstellung + warnt. */ + event_ende TEXT, + event_aufgaben TEXT, + event_regeln TEXT ); INSERT INTO eintraege_neu (id, bereich, art, titel, text, datum, bewertung, dringlichkeit, status, creator_id, erstellt, erstellt_von, geaendert, - hook, format, saeule_id, geplant, creator_extern) + hook, format, saeule_id, geplant, creator_extern, + event_ende, event_aufgaben, event_regeln) SELECT id, bereich, art, titel, text, datum, bewertung, dringlichkeit, status, creator_id, erstellt, erstellt_von, geaendert, - hook, format, saeule_id, geplant, creator_extern + hook, format, saeule_id, geplant, creator_extern, + event_ende, event_aufgaben, event_regeln FROM eintraege; DROP TABLE eintraege; ALTER TABLE eintraege_neu RENAME TO eintraege; diff --git a/workspace/assets/css/bereich.css b/workspace/assets/css/bereich.css index 343a3588..9fb3fe82 100644 --- a/workspace/assets/css/bereich.css +++ b/workspace/assets/css/bereich.css @@ -298,3 +298,102 @@ .ampel .schritt { margin-left: 0; } .ampel__knopf { flex: 1 1 auto; text-align: center; } } + +/* ===================================================================== + AGENTUR-EVENTS (07.09.2026) + + Ein Event ist die einzige Eintragsart, die eine LAUFZEIT hat. Alles + andere in diesen Bereichen ist ein Vermerk zu einem Tag; ein Event + ist eine Einladung, an der man teilnehmen kann -- oder eben nicht + mehr. Deshalb ist der Zustand (kommt / läuft / vorbei) das Erste auf + der Karte und nicht eine Fussnote. + + Farbe wird hier als AUSKUNFT eingesetzt, nicht als Schmuck: gedecktes + Grün heisst "du kannst mitmachen", Bernstein "noch nicht", Grau + "vorbei". Die Farben stammen aus derselben Familie wie die Ampel + (--k-gut / --k-mittel), damit nicht zwei Bedeutungssysteme + nebeneinander laufen. Und weil Farbe allein nie ausreicht, steht der + Zustand zusaetzlich als WORT da ("läuft bis 30.09.2026"). + ===================================================================== */ + +.event-zeitraum { + display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; + margin: 2px 0 10px; + font-size: .82rem; +} +.event-zeitraum__marke { + --f: var(--k-neutral); + display: inline-flex; align-items: center; gap: 6px; + padding: 3px 10px; + border: 1px solid color-mix(in srgb, var(--f) 34%, transparent); + border-radius: 999px; + background: color-mix(in srgb, var(--f) 12%, transparent); + color: color-mix(in srgb, var(--f) 82%, #fff); + font-weight: 600; letter-spacing: .01em; + font-variant-numeric: tabular-nums; +} +/* Der Punkt davor ist reine Zierde und darf deshalb NICHT vorgelesen + werden -- ein Screenreader saehe sonst ein Aufzaehlungszeichen mitten + im Satz. */ +.event-zeitraum__marke::before { + content: ""; width: 7px; height: 7px; border-radius: 50%; + background: var(--f); flex: none; +} +.event-zeitraum[data-stand="laeuft"] .event-zeitraum__marke { --f: var(--k-gut); } +.event-zeitraum[data-stand="kommt"] .event-zeitraum__marke { --f: var(--k-mittel); } +.event-zeitraum[data-stand="vorbei"] .event-zeitraum__marke { --f: var(--k-neutral); } +.event-zeitraum__spanne { + color: var(--text-leise); + font-variant-numeric: tabular-nums; +} + +/* Ein vorbeigelaufenes Event bleibt lesbar, tritt aber zurueck. Kein + display:none -- was gelaufen ist, will man nachschlagen koennen. + Gedaempft wird nur die Flaeche, nicht die Schrift: Text unter 4.5:1 + waere nicht mehr lesbar, und "alt" ist kein Grund, jemanden + auszuschliessen. */ +.eintrag-karte[data-event="vorbei"] { opacity: .82; } + +.event-block { + margin: 10px 0 0; + padding: 10px 12px; + border: 1px solid var(--rand); + border-radius: var(--radius-klein); + background: rgba(255, 255, 255, .022); +} +.event-block__schild { + margin: 0 0 4px; + /* .74rem = 11.84 px, nicht .7rem. Die Schriftgroessen-Pruefung zieht + die Grenze bei 11.5 px, und sie hat recht: In Grossbuchstaben mit + Sperrung wirkt eine Schrift kleiner, als die Zahl sagt -- und das + hier ist die Beschriftung, an der man die Regeln vom Aufgabenteil + unterscheidet. */ + font-size: .74rem; font-weight: 700; + letter-spacing: .09em; text-transform: uppercase; + color: var(--text-still); +} +.event-block__text { + margin: 0; + font-size: .87rem; line-height: 1.6; color: var(--text); + /* pre-line, nicht pre-wrap: Zeilenumbrueche bleiben (die Aufzaehlung + ist der Sinn dieser Felder), aber eine versehentlich eingefuegte + Einrueckung aus einer Chatnachricht reisst die Karte nicht auf. */ + white-space: pre-line; + overflow-wrap: anywhere; +} + +/* Hinweis unter einem Formularfeld -- erklaert das Feld, statt es + kommentarlos wegzunehmen. */ +.feld-hinweis { + margin: 5px 0 0; + font-size: .76rem; line-height: 1.45; + color: var(--text-still); +} + +@media (max-width: 560px) { + /* Auf dem Handy untereinander: Marke und Spanne nebeneinander + ergaeben bei "vorbei seit 30.09.2026" einen Umbruch mitten im + Datum. */ + .event-zeitraum { gap: 6px; } + .event-zeitraum__spanne { flex-basis: 100%; } +} diff --git a/workspace/assets/js/bereich.js b/workspace/assets/js/bereich.js index 896ed85e..045246f6 100644 --- a/workspace/assets/js/bereich.js +++ b/workspace/assets/js/bereich.js @@ -51,6 +51,32 @@ const melde = (t) => { $('fehler').textContent = t || ''; }; const datum = (iso) => (iso ? iso.slice(0, 10).split('-').reverse().join('.') : '—'); + /* ---------- Agentur-Events --------------------------------------------- + Ein Event ist kein Eintrag mit Ueberschrift und Text, sondern eine + Aktion mit Zeitraum, Aufgaben und Regeln. Welche Art das ist, sagt + der Server (die Arten kommen aus workspace-bereiche.js) -- die + Kennung steht hier trotzdem woertlich, weil das Formular je Art + anders aussieht und es dafuer keine allgemeine Regel gibt. Sie + steht an EINER Stelle, damit sie sich nicht verteilt. */ + const EVENT_BEREICH = 'agentur'; + const EVENT_ART = 'kampagne'; + const istEventArt = (art) => bereich === EVENT_BEREICH && art === EVENT_ART; + + /* Laeuft das Event noch, kommt es erst, ist es vorbei? + Der Vergleich laeuft auf den ISO-Zeichenketten, nicht auf Date -- + "2026-09-12" < "2026-09-30" ist als Text schon richtig, und ein + Date() haette hier eine Zeitzone erfunden, die niemand gemeint hat. + Ohne Ende gilt der Beginntag als der ganze Zeitraum. */ + function eventStand(e) { + const heute = window.heuteLokal ? window.heuteLokal() : new Date().toISOString().slice(0, 10); + const von = (e.datum || '').slice(0, 10); + const bis = (e.event_ende || von).slice(0, 10); + if (!von) return null; + if (heute < von) return { schluessel: 'kommt', text: 'startet ' + datum(von) }; + if (heute > bis) return { schluessel: 'vorbei', text: 'vorbei seit ' + datum(bis) }; + return { schluessel: 'laeuft', text: von === bis ? 'heute' : 'läuft bis ' + datum(bis) }; + } + async function hole(pfad, optionen = {}) { const a = await fetch(pfad, { credentials: 'same-origin', ...optionen }); if (a.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); } @@ -91,10 +117,45 @@ } k.append(kopf); + /* ---- Agentur-Event: Zeitraum, Aufgaben, Regeln ------------------- + Die Reihenfolge ist nicht beliebig. Wer auf ein Event schaut, + fragt in genau dieser Folge: Laeuft das ueberhaupt noch? Was + muss ich tun? Was gilt dabei? Steht der Zeitraum unten im Fuss + -- da, wo bei allen anderen Karten das Datum steht --, liest man + erst die Aufgaben und danach, dass es seit einer Woche vorbei + ist. */ + const stand = istEventArt(e.art) ? eventStand(e) : null; + if (stand) { + k.dataset.event = stand.schluessel; + const zeile = el('p', 'event-zeitraum'); + zeile.dataset.stand = stand.schluessel; + zeile.append(el('span', 'event-zeitraum__marke', stand.text)); + zeile.append(el('span', 'event-zeitraum__spanne', + e.event_ende && e.event_ende !== e.datum + ? `${datum(e.datum)} – ${datum(e.event_ende)}` + : datum(e.datum))); + k.append(zeile); + } + if (e.text) k.append(el('p', 'eintrag-text', e.text)); + if (stand) { + for (const [feld, name] of [['event_aufgaben', 'Aufgaben'], ['event_regeln', 'Regeln']]) { + if (!e[feld]) continue; + const block = el('div', 'event-block'); + block.append(el('h4', 'event-block__schild', name)); + /* Zeilenumbrueche bleiben stehen (white-space: pre-line im CSS). + Eine Aufzaehlung, die zu einem Absatz zusammenlaeuft, ist + genau das, was diese beiden Felder verhindern sollen. */ + block.append(el('p', 'event-block__text', e[feld])); + k.append(block); + } + } + const fuss = el('div', 'eintrag-fuss'); - fuss.append(el('span', null, datum(e.datum))); + /* Beim Event steht der Zeitraum schon oben -- das Datum hier waere + dieselbe Zahl ein zweites Mal. */ + if (!stand) fuss.append(el('span', null, datum(e.datum))); if (LEITUNG.has(ich.rolle) && e.creator_name) fuss.append(el('span', null, '· ' + e.creator_name)); if (e.erstellt_name) fuss.append(el('span', null, '· von ' + e.erstellt_name)); if (e.status === 'erledigt') fuss.append(el('span', null, '· erledigt')); @@ -276,6 +337,44 @@ /* ---------- Neuer Eintrag ---------------------------------------------- */ + /* Das Formular sieht bei einem Agentur-Event anders aus als bei allem + anderen -- nicht "zusaetzlich", sondern anders: Aus "Überschrift" + wird der Titel des Events, aus "Text" die Beschreibung, aus dem + Datum ein Zeitraum, und Dringlichkeit hat hier nichts zu suchen + (ein Event ist nicht dringend, es hat einen Termin). + + Umgeschaltet wird bei JEDEM Artwechsel und beim Oeffnen. Nur beim + Oeffnen zu schalten waere der bequeme Fehler: Man oeffnet mit + "Schulung", waehlt "Agentur-Events" und fuellt ein Formular aus, + das noch nach der alten Art fragt. */ + function formularAnpassen() { + const event = istEventArt($('f-art').value); + + $('feld-ende').hidden = !event; + $('feld-aufgaben').hidden = !event; + $('feld-regeln').hidden = !event; + if (einstellung?.dringlichkeit) $('feld-dringlich').hidden = event; + + $('schild-datum').textContent = event ? 'Von' : 'Datum'; + $('schild-titel').textContent = event ? 'Titel des Events' : 'Überschrift'; + /* textContent wuerde das mit erschlagen -- + deshalb nur der erste Textknoten, das kleine "(optional)" bleibt + stehen. */ + const textSchild = $('schild-text').firstChild; + if (textSchild) textSchild.nodeValue = event ? 'Beschreibung ' : 'Text '; + + /* Leeren, was gerade unsichtbar wird. Sonst reicht ein Wechsel von + "Agentur-Events" auf "Schulung", und das Ende des Zeitraums + faehrt unsichtbar mit -- der Server nimmt es an, weil es zum + Bereich gehoert, und an der Schulung stuende ein Enddatum, das + niemand eingetragen hat und niemand sieht. */ + if (!event) { + $('f-ende').value = ''; + $('f-aufgaben').value = ''; + $('f-regeln').value = ''; + } + } + const umschalten = (auf) => { /* Auch hier die Regel, nicht nur einmal beim Aufbau: Diese Funktion ist die einzige Stelle, die das Formular je wieder sichtbar macht. @@ -288,11 +387,17 @@ }; $('neu-oeffnen').addEventListener('click', () => { $('f-datum').value = window.heuteLokal(); + formularAnpassen(); umschalten(true); }); $('neu-abbrechen').addEventListener('click', () => { - $('neu').reset(); $('neu-fehler').textContent = ''; umschalten(false); + $('neu').reset(); $('neu-fehler').textContent = ''; formularAnpassen(); umschalten(false); }); + /* "change" und nicht "input": Die Auswahl wird ueber wahl.js zu einer + Knopfliste umgebaut, und die meldet ihre Wahl als change-Ereignis + am urspruenglichen
- +
+ +
- +
- +
+ + +
@@ -117,14 +143,14 @@ - - - - - - - - - + + + + + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index 49db669a..27e19eff 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -76,11 +76,11 @@ - - - - - - + + + + + + diff --git a/workspace/chat.html b/workspace/chat.html index 6e841428..485c49b5 100644 --- a/workspace/chat.html +++ b/workspace/chat.html @@ -9,16 +9,16 @@ - - + + - - + + @@ -154,9 +154,9 @@ - - - - + + + + diff --git a/workspace/content.html b/workspace/content.html index 2f92be6e..04772e71 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -166,12 +166,12 @@ - - - - - - - + + + + + + + diff --git a/workspace/dateien.html b/workspace/dateien.html index 7f4cff8d..8556ceb4 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -95,10 +95,10 @@ - - - - - + + + + + diff --git a/workspace/index.html b/workspace/index.html index a8c4b38c..e26d59fb 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -27,7 +27,7 @@ - + @@ -173,6 +173,6 @@
- + diff --git a/workspace/kalender.html b/workspace/kalender.html index cd4d5271..e3feb78a 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -257,11 +257,11 @@ - - - - - - + + + + + + diff --git a/workspace/leistung.html b/workspace/leistung.html index 48c03eec..7debec84 100644 --- a/workspace/leistung.html +++ b/workspace/leistung.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -199,10 +199,10 @@ - - - - - + + + + + diff --git a/workspace/personen.html b/workspace/personen.html index cb887e40..91f762a8 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -123,10 +123,10 @@ - - - - - + + + + + diff --git a/workspace/profil.html b/workspace/profil.html index c680e023..20f5bd0e 100644 --- a/workspace/profil.html +++ b/workspace/profil.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -201,11 +201,11 @@ - - - - - - + + + + + + diff --git a/workspace/report.html b/workspace/report.html index 339cbca3..80c56c44 100644 --- a/workspace/report.html +++ b/workspace/report.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -93,11 +93,11 @@ - - - - - - + + + + + + diff --git a/workspace/scouting.html b/workspace/scouting.html index 6b01f284..b3e907b3 100644 --- a/workspace/scouting.html +++ b/workspace/scouting.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -126,10 +126,10 @@ - - - - - + + + + + diff --git a/workspace/start.html b/workspace/start.html index 6591cfad..242d4c3c 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -9,8 +9,8 @@ - - + + @@ -160,10 +160,10 @@

- - - - - + + + + + diff --git a/workspace/startcheck.html b/workspace/startcheck.html index 5f5f88a3..246fb58e 100644 --- a/workspace/startcheck.html +++ b/workspace/startcheck.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -72,11 +72,11 @@ - - - - - - + + + + + + diff --git a/workspace/steckbrief.html b/workspace/steckbrief.html index 3fbe0570..db31c2ae 100644 --- a/workspace/steckbrief.html +++ b/workspace/steckbrief.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -113,10 +113,10 @@ - - - - - + + + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index dae9f08b..163c0dfd 100644 --- a/workspace/uebersicht.html +++ b/workspace/uebersicht.html @@ -9,9 +9,9 @@ - - - + + + @@ -82,10 +82,10 @@ - + - - - + + + diff --git a/workspace/wissen.html b/workspace/wissen.html index cabb918a..1ce5b700 100644 --- a/workspace/wissen.html +++ b/workspace/wissen.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -177,10 +177,10 @@ - - - - - + + + + +