Files
dogfather-universe/workspace/assets/js/bereich.js
T
DogFatherGit df9f87af0a Workspace: Phase 2 -- LIVE, Content, Technik, Community, Schutz
Fuenf Bereiche auf einmal, aber NICHT fuenf Module. Im Deck haben sie
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: eine Tabelle, eine Sichtbarkeitsregel,
eine Pruefung, eine Ansichtsseite (bereich.html?b=live). Ein Fehler
laesst sich damit an EINER Stelle beheben statt an fuenf, und ein
weiterer Bereich waere ein Eintrag in BEREICHE -- kein neues Modul.

Arten woertlich aus dem Deck (Seiten 7-12):
  live       Vorbereitung / Waehrend LIVE / Auswertung   + Bewertung 1-5
  content    Idee / Produktion / Veroeffentlicht
  technik    Setup / Problem / Loesung / Anleitung       + Dringlichkeit
  community  Moderation / Aktion / Konflikt              + Dringlichkeit
  schutz     Richtlinie / Vorfall / Eskalation / Gelernt + Dringlichkeit

Die Oberflaeche kennt die Bereiche nicht auswendig -- welche Arten und
Zusatzfelder es gibt, sagt der Server in der Antwort.

Geprueft, und zwar fuer alle fuenf gleichzeitig:
- Sichtbarkeit: Chef sieht alles, Luna nur ihren Bereich, Mika nur
  seinen, Sam (Scout) gar nichts (404 -- Scouts haben mit der
  Creator-Betreuung nichts zu tun)
- Luna legt Eintrag mit creator_id=Mika an: landet still in ihrem
  eigenen Bereich, Mika sieht ihn nicht
- Luna auf Mikas Eintrag: 404; Luna loescht Chefs Eintrag ueber ihren
  Bereich: 403 (loeschen darf das Management und wer ihn schrieb --
  sonst koennte ein Creator eine Notiz ueber sich verschwinden lassen)
- Eintrag ueber den falschen Bereich in der Adresse ansprechen: 404
- unbekannte Art 400, Bewertung 9 (erlaubt 1-5) 400, unbekannter
  Bereich 404, fremde Herkunft 403

Beim Testen sahen Umlaute zunaechst zerstoert aus (efbfbd, das
Unicode-Ersatzzeichen). Ursache war der curl-Aufruf: Git Bash kodiert
$'\xc3\xbc' nach Locale um. Ueber den echten Weg (Browser) kommen
Umlaute, ss und Gedankenstrich unveraendert an und liegen sauber in der
Datenbank -- gegengeprueft auf Byte-Ebene.
2026-08-28 09:19:39 +02:00

241 lines
8.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ===================================================================
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();
})();
})();