Der Wunsch stand an fuenf Stellen gleichlautend: "ich will dass da schon fertige Sachen stehen". Eine leere Seite mit einem Knopf "Neuer Eintrag" verlangt vom Creator genau das, was er noch nicht kann -- zu wissen, was ueberhaupt hineingehoert. WARUM VORLAGEN UND KEINE VORAUSGEFUELLTEN DATEN. Man koennte beim Anlegen eines Creators dreissig Eintraege in seine Datenbank schreiben. Das waere falsch: Sie waeren ab dem ersten Tag "seine" Eintraege und damit Altlast; aendert Filipe spaeter eine Formulierung, gilt sie nur fuer neue Creator; und die Liste saehe voll aus, obwohl noch nichts geschehen ist -- das Gegenteil einer ehrlichen Uebersicht. Vorlagen bleiben deshalb VORSCHLAEGE, bis jemand sie uebernimmt. Sie stehen im Code, gelten fuer alle sofort, und werden erst dann zu Daten, wenn sie gebraucht werden. Was uebernommen ist, ist ein ganz normaler Eintrag -- aenderbar, loeschbar, und von spaeteren Aenderungen an der Vorlage unberuehrt. INHALTE, fachlich begruendet: 13 Content-Ideen, jede mit AUSFORMULIERTEM Aufhaenger und Format. Die ersten ein bis drei Sekunden entscheiden ueber die Verbreitung -- "mach was Persoenliches" hilft niemandem, "Das haette ich am Anfang gern gewusst" kann man sagen. Verteilt nach 70/20/10 (Wert, Community, Eigenwerbung). 21 LIVE-Punkte in drei Abschnitten. Der mittlere ist der wichtigste und gibt es sonst nirgends: Was WAEHREND der Sendung auffaellt, ist am naechsten Tag weg. Diese Punkte sind so formuliert, dass man sie in einem Moment anklicken kann, in dem man eigentlich keine Zeit hat. Die Vorbereitung ist die laengste Liste, weil ein LIVE dort steht und faellt. Ton zuerst -- der Grund Nummer eins, warum Leute wieder gehen. "ICH BRAUCHE HILFE" wird eine AUFGABE, keine Nachricht. Eine Nachricht ist gelesen und dann weg; eine Aufgabe bleibt stehen, bis sie jemand erledigt, geht an den zustaendigen Betreuer (ohne Betreuer an die Leitung -- eine Bitte um Hilfe darf nicht ins Leere laufen), traegt hohe Prioritaet und eine Frist von drei Tagen. Ohne Frist bleibt sie liegen; das ist der Unterschied zwischen einer Aufgabe und einem Zettel. "CONTENT-PLANUNG" HEISST JETZT "CONTENT-IDEEN". "Planung" klang nach Terminen und Tabellen; was dort wirklich passiert, ist das Sammeln und Weiterentwickeln von Ideen. Der Kalender daneben plant. EIN CREATOR DARF SEINE EIGENEN EINTRAEGE AENDERN -- aber nur im Bereich Content. Wenn er sich eine Idee uebernimmt, ist das SEINE Idee; sie danach nicht umbenennen zu duerfen waere absurd. LIVE, Technik, Community und Schutz bleiben die Betreuungsakte, dort aendert er nichts. Ein erster Anlauf hatte die Ausnahme fuer alle Bereiche erlaubt -- die Pruefung pruef-bereiche-lesend hat das sofort gemeldet. DREI EIGENE FEHLER, VON DEN PRUEFUNGEN GEFUNDEN: - Der Vorlagenblock vergass nach dem Uebernehmen, was schon geholt war: Die Liste wird neu geladen, der Block neu gebaut, und die Markierung am Element war jedes Mal weg. Jetzt merkt sich das Modul die Auswahl. - Die Regel fuer eigene Eintraege war zu breit (siehe oben). - Die Marken im Vorlagenblock waren auf dem Handy 10,2 px klein. Co-Authored-By: Claude Opus 5 <[email protected]>
298 lines
11 KiB
JavaScript
298 lines
11 KiB
JavaScript
/* ===================================================================
|
||
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';
|
||
|
||
/* DogFather und Manager haben dieselben Rechte -- die Unterscheidung
|
||
macht nur die Personenverwaltung, und die prueft der Server. */
|
||
const LEITUNG = new Set(['admin', 'manager']);
|
||
|
||
/* Wer hier eintraegt: DogFather, Manager und Scout. Der Creator liest
|
||
nur mit -- diese Bereiche sind die Betreuungsakte, nicht sein
|
||
Notizbuch. Der Server setzt das ebenfalls durch (siehe
|
||
workspace-bereiche.js); hier geht es nur darum, ihm keine Knoepfe
|
||
hinzustellen, die ohnehin nur eine Absage bringen. */
|
||
const darfEintragen = () => ich && ich.rolle !== 'creator';
|
||
|
||
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 (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'));
|
||
|
||
if (!darfEintragen()) { k.append(fuss); return k; }
|
||
|
||
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 (LEITUNG.has(ich.rolle) || 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 */ }
|
||
await vorlagenZeigen();
|
||
}
|
||
|
||
/* Fertige Punkte zum Uebernehmen -- derzeit nur fuer die LIVE-Analyse.
|
||
Sie stehen ueber der Liste: Wer noch nichts hat, soll nicht erst an
|
||
einem leeren Kasten vorbeiscrollen.
|
||
|
||
Welcher Abschnitt gezeigt wird, richtet sich nach dem gewaehlten
|
||
Filter. Ohne Filter die Vorbereitung -- das ist der Teil, ohne den
|
||
die anderen beiden gar nicht erst sinnvoll sind. */
|
||
async function vorlagenZeigen() {
|
||
const behaelter = $('vorlagen');
|
||
if (!behaelter || bereich !== 'live' || !window.vorlagenBlock) return;
|
||
const abschnitt = ['vorbereitung', 'mitschrift', 'auswertung'].includes(filter)
|
||
? filter : 'vorbereitung';
|
||
await window.vorlagenBlock(behaelter, {
|
||
art: 'live',
|
||
unterart: abschnitt,
|
||
creatorId: creatorWahl(),
|
||
danach: laden,
|
||
});
|
||
}
|
||
|
||
/* Fuer wen wird uebernommen? Ein Creator immer fuer sich (der Server
|
||
setzt das ohnehin), ein Betreuer fuer den gewaehlten Creator. */
|
||
function creatorWahl() {
|
||
const w = document.getElementById('f-creator');
|
||
return w && w.value ? Number(w.value) : null;
|
||
}
|
||
|
||
/* ---------- Neuer Eintrag ---------------------------------------------- */
|
||
|
||
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.
|
||
Sperrt man nur beim Laden, holt der naechste Aufruf den Knopf
|
||
zurueck -- und niemand faellt es auf, weil es der Alltagsweg nicht
|
||
ist. */
|
||
if (!darfEintragen()) { $('neu').hidden = true; $('neu-oeffnen').hidden = true; return; }
|
||
$('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 (LEITUNG.has(ich.rolle)) 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; }
|
||
window.werZeigen?.(ich);
|
||
|
||
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] || '';
|
||
|
||
/* Fuer den Creator verschwindet alles Schreibende -- und die
|
||
Unterzeile sagt, warum. Ein leerer Bereich ohne Erklaerung wirkt
|
||
kaputt; einer mit Erklaerung wirkt gedacht. */
|
||
if (!darfEintragen()) {
|
||
$('neu-oeffnen').hidden = true;
|
||
$('neu').hidden = true;
|
||
$('unterzeile').textContent =
|
||
(UNTERZEILE[bereich] || '') + ' Hier trägt deine Betreuung ein – du siehst alles, was zu dir gehört.';
|
||
}
|
||
|
||
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 (LEITUNG.has(ich.rolle)) {
|
||
$('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();
|
||
})();
|
||
})();
|