Betreuung
+…
+ +Einträge werden geladen …
diff --git a/server/index.js b/server/index.js index 06740e60..4b5cf466 100644 --- a/server/index.js +++ b/server/index.js @@ -19,6 +19,7 @@ import { personenRouter } from "./workspace-personen.js"; import { profilRouter } from "./workspace-profil.js"; import { kalenderRouter } from "./workspace-kalender.js"; import { dateienRouter } from "./workspace-dateien.js"; +import { bereicheRouter } from "./workspace-bereiche.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); const SITE_DIR = join(__dirname, ".."); @@ -78,6 +79,7 @@ app.use(personenRouter); app.use(profilRouter); app.use(kalenderRouter); app.use(dateienRouter); +app.use(bereicheRouter); /* Sicherheits-Fix 20.08.2026 (Audit vor dem geplanten öffentlichen Start morgen). SITE_DIR ist der GESAMTE Repo-Ordner (join(__dirname, "..")) — express.static liefert diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js new file mode 100644 index 00000000..877dda03 --- /dev/null +++ b/server/workspace-bereiche.js @@ -0,0 +1,292 @@ +/* ===================================================================== + 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 } 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: { + name: "Content-Planung", + arten: { idee: "Idee", produktion: "Produktion", veroeffentlicht: "Veröffentlicht" }, + bewertung: false, + dringlichkeit: false, + }, + 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, + }, +}; + +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); + +/* Scouts haben mit der Creator-Betreuung nichts zu tun -- sie sehen hier + nichts. Creator sehen ihren eigenen Bereich, das Management alles. */ +function sichtbar(person) { + if (person.rolle === "admin") return { wo: "1=1", werte: [] }; + if (person.rolle === "creator") return { wo: "e.creator_id = ?", werte: [person.id] }; + return null; +} + +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, + pc.name AS creator_name, pe.name AS erstellt_name`; + +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 = sichtbar(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; + } + 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; + } + } + 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. */ + if (req.person.rolle !== "admin") aus.creator_id = req.person.id; + 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." }); + } + + const { lastInsertRowid } = db().prepare(` + INSERT INTO eintraege + (bereich, art, titel, text, datum, bewertung, dringlichkeit, status, + creator_id, erstellt, erstellt_von) + 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, jetzt(), req.person.id); + + 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 = sichtbar(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 (req.person.rolle !== "admin") delete aus.creator_id; + + 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 (req.person.rolle !== "admin" && 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" }); + } +}); diff --git a/server/workspace.js b/server/workspace.js index 35554b05..0d23c57a 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -197,6 +197,31 @@ export function db() { ); CREATE INDEX IF NOT EXISTS idx_dateien_creator ON dateien (creator_id); + /* Eintraege der Phase-2-Bereiche (LIVE, Content, Technik, + Community, Schutz). Eine gemeinsame Tabelle statt fuenf fast + gleicher: Die Bereiche unterscheiden sich nur in den erlaubten + Arten und darin, ob Bewertung oder Dringlichkeit dazugehoeren -- + das steht in workspace-bereiche.js, nicht in der Datenbank. */ + CREATE TABLE IF NOT EXISTS eintraege ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + bereich TEXT NOT NULL + CHECK (bereich IN ('live','content','technik','community','schutz')), + art TEXT NOT NULL, + titel TEXT NOT NULL, + text TEXT, + datum TEXT NOT NULL, + bewertung INTEGER CHECK (bewertung IS NULL OR (bewertung BETWEEN 1 AND 5)), + dringlichkeit TEXT NOT NULL DEFAULT 'mittel' + CHECK (dringlichkeit IN ('hoch','mittel','niedrig')), + status TEXT NOT NULL DEFAULT 'offen' + CHECK (status IN ('offen','erledigt')), + creator_id INTEGER REFERENCES personen(id) ON DELETE SET NULL, + erstellt TEXT NOT NULL, + erstellt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL, + geaendert TEXT + ); + CREATE INDEX IF NOT EXISTS idx_eintraege_bereich ON eintraege (bereich, creator_id); + CREATE TABLE IF NOT EXISTS versuche ( ip TEXT NOT NULL, zeitpunkt TEXT NOT NULL @@ -332,6 +357,7 @@ const GESCHUETZT = { "/workspace/profil.html": ["admin", "creator"], "/workspace/kalender.html": null, "/workspace/dateien.html": null, + "/workspace/bereich.html": ["admin", "creator"], }; workspaceRouter.use((req, res, next) => { diff --git a/workspace/assets/css/bereich.css b/workspace/assets/css/bereich.css new file mode 100644 index 00000000..a0f4f126 --- /dev/null +++ b/workspace/assets/css/bereich.css @@ -0,0 +1,64 @@ +/* =================================================================== + Gemeinsame Ansicht für LIVE, Content, Technik, Community und Schutz. + =================================================================== */ + +.eintrag-karte { + padding: 15px 17px; margin-bottom: 10px; + background: rgba(255, 255, 255, .03); + border: 1px solid var(--rand); + border-left: 3px solid var(--rand); + border-radius: var(--radius-klein); +} +.eintrag-karte[data-dringlich="hoch"] { border-left-color: #d8656e; } +.eintrag-karte[data-dringlich="mittel"] { border-left-color: #4b6c8a; } +.eintrag-karte[data-dringlich="niedrig"] { border-left-color: #3c4a5a; } +.eintrag-karte[data-status="erledigt"] { opacity: .55; } + +.eintrag-kopf { + display: flex; align-items: baseline; flex-wrap: wrap; gap: 10px; +} +.eintrag-titel { font-weight: 600; font-size: .96rem; flex: 1 1 240px; } + +.marke-art { + padding: 1px 9px; border-radius: 999px; + background: rgba(63, 189, 245, .14); color: #7fd2fb; + font-size: .68rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; + white-space: nowrap; +} + +/* Bewertung als Punktreihe statt als Zahl -- auf einen Blick erfassbar, + und ohne Sterne, die im Dunkeln flimmern. */ +.bewertung { display: inline-flex; gap: 3px; align-items: center; } +.bewertung__punkt { + width: 7px; height: 7px; border-radius: 50%; + background: rgba(150, 186, 220, .22); +} +.bewertung__punkt[data-voll="ja"] { background: var(--akzent); } + +.eintrag-text { + margin: 9px 0 0; font-size: .87rem; color: var(--text-leise); + line-height: 1.55; white-space: pre-wrap; +} + +.eintrag-fuss { + display: flex; align-items: center; flex-wrap: wrap; gap: 8px; + margin-top: 11px; font-size: .77rem; color: var(--text-still); +} +.eintrag-knoepfe { display: flex; gap: 6px; margin-left: auto; flex-wrap: wrap; } + +.filter { display: flex; gap: 8px; margin: 0 0 20px; flex-wrap: wrap; } +.filter .schritt[aria-pressed="true"] { + background: rgba(63, 189, 245, .14); + border-color: rgba(63, 189, 245, .55); + color: var(--text); +} + +.leer-hinweis { + padding: 26px 20px; text-align: center; + color: var(--text-still); font-size: .9rem; + border: 1px dashed var(--rand); border-radius: var(--radius); +} + +@media (max-width: 620px) { + .eintrag-knoepfe { margin-left: 0; width: 100%; } +} diff --git a/workspace/assets/js/bereich.js b/workspace/assets/js/bereich.js new file mode 100644 index 00000000..89b19f85 --- /dev/null +++ b/workspace/assets/js/bereich.js @@ -0,0 +1,240 @@ +/* =================================================================== + Eine Ansicht für fünf Bereiche (LIVE, Content, Technik, Community, + Schutz). Welcher gemeint ist, steht in der Adresse: ?b=live + + Welche Arten es gibt und ob Bewertung oder Dringlichkeit dazugehören, + sagt der Server in der Antwort -- diese Datei kennt die Bereiche + nicht auswendig. Ein neuer Bereich braucht hier also keine Zeile. + =================================================================== */ +(() => { + 'use strict'; + + const $ = (id) => document.getElementById(id); + const UNTERZEILE = { + live: 'Vorbereitung, Mitschrift und Auswertung – LIVE systematisch verbessern.', + content: 'Ideen, Produktion und Veröffentlichtes an einer Stelle statt in Chats.', + technik: 'Setups, Fehlerbilder und Lösungen pro Creator sauber dokumentiert.', + community: 'Moderation, Aktionen und Konflikte – ruhig statt Drama.', + schutz: 'Richtlinien, Vorfälle und was daraus gelernt wurde.', + }; + + let ich = null; + let bereich = null; + let einstellung = null; + let filter = ''; + + const el = (tag, klasse, text) => { + const k = document.createElement(tag); + if (klasse) k.className = klasse; + if (text !== undefined) k.textContent = text; + return k; + }; + const melde = (t) => { $('fehler').textContent = t || ''; }; + const datum = (iso) => (iso ? iso.slice(0, 10).split('-').reverse().join('.') : '—'); + + 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'); } + if (a.status === 404) { location.assign('/workspace/start.html'); throw new Error('unbekannt'); } + return a; + } + + function tuKnopf(text, tu) { + const b = el('button', 'schritt', text); + b.type = 'button'; + b.addEventListener('click', async () => { + b.disabled = true; melde(''); + try { await tu(); } catch { /* umgeleitet */ } finally { b.disabled = false; } + }); + return b; + } + + /* ---------- Zeichnen --------------------------------------------------- */ + + function karte(e) { + const k = el('article', 'eintrag-karte'); + k.dataset.dringlich = e.dringlichkeit; + k.dataset.status = e.status; + + const kopf = el('div', 'eintrag-kopf'); + kopf.append(el('span', 'marke-art', einstellung.arten[e.art] || e.art)); + kopf.append(el('h3', 'eintrag-titel', e.titel)); + + if (einstellung.bewertung && e.bewertung) { + const b = el('span', 'bewertung'); + b.setAttribute('aria-label', `Bewertung ${e.bewertung} von 5`); + for (let i = 1; i <= 5; i++) { + const punkt = el('span', 'bewertung__punkt'); + if (i <= e.bewertung) punkt.dataset.voll = 'ja'; + b.append(punkt); + } + kopf.append(b); + } + k.append(kopf); + + if (e.text) k.append(el('p', 'eintrag-text', e.text)); + + const fuss = el('div', 'eintrag-fuss'); + fuss.append(el('span', null, datum(e.datum))); + if (ich.rolle === 'admin' && 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')); + + const knoepfe = el('div', 'eintrag-knoepfe'); + knoepfe.append(tuKnopf(e.status === 'offen' ? 'erledigt ▶' : '◀ wieder offen', async () => { + const a = await hole(`/workspace/api/bereich/${bereich}/${e.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status: e.status === 'offen' ? 'erledigt' : 'offen' }), + }); + if (!a.ok) { melde('Ändern hat nicht geklappt.'); return; } + await laden(); + })); + if (ich.rolle === 'admin' || e.erstellt_von === ich.id) { + knoepfe.append(tuKnopf('löschen', async () => { + if (!confirm(`„${e.titel}" wirklich löschen?`)) return; + const a = await hole(`/workspace/api/bereich/${bereich}/${e.id}`, { method: 'DELETE' }); + if (!a.ok) { melde('Löschen hat nicht geklappt.'); return; } + await laden(); + })); + } + fuss.append(knoepfe); + k.append(fuss); + return k; + } + + function filterBauen() { + const ziel = $('filter'); + ziel.textContent = ''; + const knopf = (wert, text) => { + const b = el('button', 'schritt', text); + b.type = 'button'; + b.setAttribute('aria-pressed', String(filter === wert)); + b.addEventListener('click', async () => { filter = wert; await laden(); }); + return b; + }; + ziel.append(knopf('', 'Alle')); + ziel.append(knopf('offen', 'Nur offene')); + for (const [wert, name] of Object.entries(einstellung.arten)) ziel.append(knopf(wert, name)); + } + + async function laden() { + try { + const a = await hole(`/workspace/api/bereich/${bereich}`); + if (!a.ok) { melde('Einträge konnten nicht geladen werden.'); return; } + const daten = await a.json(); + einstellung = daten.einstellung; + filterBauen(); + + const ziel = $('liste'); + ziel.textContent = ''; + ziel.setAttribute('aria-busy', 'false'); + + const sichtbar = daten.eintraege.filter((e) => { + if (!filter) return true; + if (filter === 'offen') return e.status === 'offen'; + return e.art === filter; + }); + if (!sichtbar.length) { + ziel.append(el('p', 'leer-hinweis', daten.eintraege.length + ? 'Zu dieser Auswahl gibt es nichts.' + : 'Noch kein Eintrag in diesem Bereich.')); + return; + } + for (const e of sichtbar) ziel.append(karte(e)); + } catch { /* umgeleitet */ } + } + + /* ---------- Neuer Eintrag ---------------------------------------------- */ + + const umschalten = (auf) => { + $('neu').hidden = !auf; $('neu-oeffnen').hidden = auf; + if (auf) $('f-titel').focus(); + }; + $('neu-oeffnen').addEventListener('click', () => { + $('f-datum').value = new Date().toISOString().slice(0, 10); + umschalten(true); + }); + $('neu-abbrechen').addEventListener('click', () => { + $('neu').reset(); $('neu-fehler').textContent = ''; umschalten(false); + }); + + $('neu').addEventListener('submit', async (e) => { + e.preventDefault(); + $('neu-fehler').textContent = ''; + const knopf = $('neu-speichern'); + const daten = { + art: $('f-art').value, + titel: $('f-titel').value, + text: $('f-text').value, + datum: $('f-datum').value, + }; + if (einstellung.dringlichkeit) daten.dringlichkeit = $('f-dringlich').value; + if (einstellung.bewertung) daten.bewertung = $('f-bewertung').value || null; + if (ich.rolle === 'admin') daten.creator_id = $('f-creator').value || null; + + knopf.disabled = true; + try { + const a = await hole(`/workspace/api/bereich/${bereich}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(daten), + }); + if (!a.ok) { + $('neu-fehler').textContent = (await a.json().catch(() => ({}))).fehler || 'Ging nicht.'; + return; + } + $('neu').reset(); umschalten(false); + await laden(); + } catch { /* umgeleitet */ } finally { knopf.disabled = false; } + }); + + /* ---------- Start -------------------------------------------------------- */ + + (async () => { + bereich = new URLSearchParams(location.search).get('b') || ''; + if (!/^[a-z]+$/.test(bereich)) { location.assign('/workspace/start.html'); return; } + + try { + const a = await hole('/workspace/api/ich'); + if (!a.ok) { location.assign('/workspace/'); return; } + ich = await a.json(); + } catch { return; } + $('wer').textContent = ich.name + ' · ' + ich.rolle; + + let daten; + try { + const a = await hole(`/workspace/api/bereich/${bereich}`); + if (!a.ok) { location.assign('/workspace/start.html'); return; } + daten = await a.json(); + } catch { return; } + + einstellung = daten.einstellung; + document.title = einstellung.name + ' · Creator Workspace'; + $('marke-bereich').textContent = einstellung.name; + $('titel').textContent = einstellung.name; + $('unterzeile').textContent = UNTERZEILE[bereich] || ''; + + for (const [wert, name] of Object.entries(einstellung.arten)) { + const o = document.createElement('option'); + o.value = wert; o.textContent = name; + $('f-art').append(o); + } + $('feld-dringlich').hidden = !einstellung.dringlichkeit; + $('feld-bewertung').hidden = !einstellung.bewertung; + + if (ich.rolle === 'admin') { + $('feld-creator').hidden = false; + try { + const { personen } = await (await hole('/workspace/api/personen')).json(); + for (const p of personen.filter((p) => p.rolle === 'creator')) { + const o = document.createElement('option'); + o.value = String(p.id); o.textContent = p.name; + $('f-creator').append(o); + } + } catch { /* Zuordnung bleibt leer */ } + } + + await laden(); + })(); +})(); diff --git a/workspace/assets/js/start.js b/workspace/assets/js/start.js index cf540cc7..5fbe3717 100644 --- a/workspace/assets/js/start.js +++ b/workspace/assets/js/start.js @@ -23,11 +23,11 @@ { name: 'Aufgaben', phase: 1, rollen: ['admin', 'creator', 'scout'], ziel: 'aufgaben.html' }, { name: 'Kalender', phase: 1, rollen: ['admin', 'creator', 'scout'], ziel: 'kalender.html' }, { name: 'Dateien', phase: 1, rollen: ['admin', 'creator', 'scout'], ziel: 'dateien.html' }, - { name: 'LIVE-Analyse', phase: 2, rollen: ['admin', 'creator'] }, - { name: 'Content-Planung', phase: 2, rollen: ['admin', 'creator'] }, - { name: 'Technik', phase: 2, rollen: ['admin', 'creator'] }, - { name: 'Community', phase: 2, rollen: ['admin', 'creator'] }, - { name: 'Schutz & Regeln', phase: 2, rollen: ['admin', 'creator'] }, + { name: 'LIVE-Analyse', phase: 2, rollen: ['admin', 'creator'], ziel: 'bereich.html?b=live' }, + { name: 'Content-Planung', phase: 2, rollen: ['admin', 'creator'], ziel: 'bereich.html?b=content' }, + { name: 'Technik', phase: 2, rollen: ['admin', 'creator'], ziel: 'bereich.html?b=technik' }, + { name: 'Community', phase: 2, rollen: ['admin', 'creator'], ziel: 'bereich.html?b=community' }, + { name: 'Schutz & Regeln', phase: 2, rollen: ['admin', 'creator'], ziel: 'bereich.html?b=schutz' }, { name: 'Reports & Review', phase: 2, rollen: ['admin', 'creator'] }, { name: 'Calls', phase: 3, rollen: ['admin', 'creator', 'scout'] }, { name: 'Scout-Pipeline', phase: 3, rollen: ['admin', 'scout'] }, diff --git a/workspace/bereich.html b/workspace/bereich.html new file mode 100644 index 00000000..01833c68 --- /dev/null +++ b/workspace/bereich.html @@ -0,0 +1,104 @@ + + +
+ + +Creator Workspace · + …
+Betreuung
+Einträge werden geladen …