ZWEI SACHEN, beide auf Wunsch vom 31.08.2026.
1) "die creator sollen da nur die sehen die wir ihnen eintragen, also
dogfather manager und scout."
Vorher durfte ein Creator in LIVE, Content, Technik, Community und Schutz
selbst anlegen, aendern und eigene Eintraege loeschen. Das hebelt den
Zweck dieser Bereiche aus: Sie sind die Betreuungsakte. Wer eine
LIVE-Auswertung umschreiben oder eine unbequeme Notiz verschwinden lassen
kann, macht die Akte wertlos.
Der Creator SIEHT weiterhin alles, was zu ihm gehoert -- vollstaendig.
Er kann es nur nicht veraendern. Umgesetzt als EINE Schranke vor allen
schreibenden Wegen, nicht als Pruefung an drei Stellen: Genau so ist hier
schon einmal ein Loch entstanden (zwei von drei Stellen abgesichert, die
dritte vergessen). 403 mit klarem Text statt 404 -- der Bereich existiert
fuer ihn ja, er steht davor.
Vorsicht Express 4: Der Platzhalter heisst dort `*`, nicht `*name` wie in
Fassung 5. Mit der falschen Schreibweise haette die Sperre stumm nichts
getan -- kein Fehler, keine Warnung.
37 Pruefungen, Schwerpunkt auf den verneinenden Faellen: alle fuenf
Bereiche, alle drei Wege, dazu der eigene Alteintrag (der frueher
ausdruecklich erlaubt war) und der Nachweis, dass hinterher wirklich
nichts veraendert wurde.
2) Content-Planung: aus der Liste wird eine Strecke.
Recherchiert statt geraten. Die Quellen sind an drei Punkten eindeutig:
* "A content calendar for creators is a pipeline, not a datebook" --
eine Wand aus Terminen verbirgt genau die Stellen, an denen es hakt.
* Die ersten ein bis drei Sekunden entscheiden ueber die Verbreitung.
Der Hook ist deshalb ein eigenes Feld auf jeder Karte, kein Fliesstext.
* Regelmaessigkeit schlaegt Menge. Wer regelmaessig senden will, braucht
VORRAT -- und Vorrat ist die eine Zahl, die eine Ideenliste
verschweigt.
Daraus:
* Fuenf Stufen statt drei (Idee, Hook & Skript, Gedreht, Fertig &
geplant, Veroeffentlicht). Alte 'produktion'-Eintraege werden beim
naechsten Start auf 'gedreht' umgestellt -- ohne das fielen sie aus
jeder Spalte heraus.
* Neue Felder hook, format, saeule_id, geplant (ALTER TABLE, gefahrlos).
* Content-Saeulen je Creator, drei bis fuenf, nach der 70/20/10-Regel.
Mit Balance-Balken ueber 90 Tage, nur Veroeffentlichtes -- was auf dem
Kanal steht, ist die Wahrheit ueber seine Ausrichtung.
* Die Kachel VORRAT rechnet in TAGE um ("reicht noch etwa 7 Tage").
"4 Videos uebrig" sagt nichts; die Tage sagen, ob man am Wochenende
drehen muss. Gezaehlt wird nur Gedrehtes und Fertiges -- eine Idee ist
kein Video, und wer Ideen mitzaehlt, steht am Donnerstag ohne Material.
* "Ideen lassen sich direkt in Aufgabe/Termin/LIVE-Thema ueberfuehren"
-- woertlich aus dem Konzept, das letzte unumgesetzte Stueck. Die Idee
bleibt bestehen und bekommt einen Vermerk, damit niemand dieselbe
Aufgabe zweimal anlegt.
Farben: Die fuenf Saeulenfarben sind Daten, keine Dekoration. Geprueft
gegen genau diesen Hintergrund (#0a0d13) auf Helligkeitsband,
Farbstaerke, Farbfehlsichtigkeit (schlechtestes Paar dE 8.4),
Normalsicht-Abstand (19.3) und Kontrast -- alle Pruefungen bestanden.
Immer mit Legende UND Beschriftung; Farbe allein traegt nie die
Information.
Beim Ansehen des ersten Bildes fiel der klassische Brett-Fehler auf: Die
Spalte "Veroeffentlicht" waechst ewig und schiebt alles andere aus dem
Bild. Sie zeigt jetzt die juengsten sechs, der Rest ist einen Klick
entfernt. Die vorderen Spalten bleiben ungekuerzt -- was dort liegt, ist
Arbeit.
Nur EIN Schreiber fuer die Tabelle eintraege: workspace-content.js
enthaelt ausschliesslich, was es woanders nicht gibt (Saeulen,
Kennzahlen, Ueberfuehren). Zwei Module auf denselben Zeilen waeren der
sichere Weg zu zwei Wahrheiten.
45 + 40 Pruefungen, Computer und Handy, dazu die Gegenprobe: Ein Creator
sieht die ganze Strecke, die Kennzahlen und die Verteilung -- und keinen
einzigen Knopf, auch keinen versteckten. Ausgeblendet ist nicht dasselbe
wie nicht vorhanden.
Co-Authored-By: Claude Opus 5 <[email protected]>
270 lines
10 KiB
JavaScript
270 lines
10 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 */ }
|
||
}
|
||
|
||
/* ---------- 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; }
|
||
$('wer').textContent = ich.name + ' · ' + (ich.rolle_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] || '';
|
||
|
||
/* 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();
|
||
})();
|
||
})();
|