Deutsch oeffnet mit „ und schliesst mit “. An 199 Stellen, die ein
Mensch liest, stand als Schlusszeichen ein GERADES " -- „Nächstes"
statt „Nächstes“. Auf sechzehn oeffentlichen Seiten, in vierzig
Dateien des Workspace und in zwoelf Servermodulen, die Texte
verschicken.
Auf dem Bildschirm sieht man den Unterschied sofort. Beim Schreiben
nicht: Das gerade " liegt auf der Tastatur, die anderen nicht.
WARUM EIN ERSTER ANLAUF ZURUECKGENOMMEN WURDE
Ein gerades " ist an vielen Stellen SYNTAX und kein Schriftzeichen --
Grenze einer Zeichenkette, Grenze eines HTML-Attributs, Zeichen in
einem regulaeren Ausdruck. Wer stumpf ersetzt, macht aus
„<a href="https://… ein kaputtes Attribut
/^["'„»\s]+|["'“«.\s]+$/ einen kaputten Ausdruck
"… nichts „mal " + "eben …" eine kaputte Zeichenkette
DIE UNTERSCHEIDUNG LAEUFT AN MERKMALEN, NICHT AN EINER LISTE
< > = dazwischen -> HTML-Marke oder Attribut
endet auf Leerzeichen -> die Zeichenkette hoert hier auf, der
Satz geht in der naechsten Zeile weiter.
Ein deutsches Schlusszeichen steht NIE
hinter einem Leerzeichen.
Rueckstrich mittendrin -> regulaerer Ausdruck
${ ohne } -> mitten in einem Ausdruck
Eine Liste erlaubter Ausnahmen waere die naechste, die niemand
pflegt. Zwoelf Stellen bleiben dadurch stehen, alle zwoelf einzeln
angesehen und alle zwoelf zu Recht -- dort steht das richtige
Schlusszeichen ohnehin weiter unten im Satz.
Fuenf davon waren allerdings ECHTE Fehler HINTER dem Link
(`…>HasiDog</a>".`) -- die erste Regel hatte nur das Attribut
gesehen, nicht den Satz danach. Gezielt nachgezogen.
Ein maskiertes `\"` in workspace-vorlagen.js (28 Hooks) wird zu “ --
ohne Rueckstrich, denn “ begrenzt nichts.
KOMMENTARE BLEIBEN, WIE SIE SIND. Dort liest es niemand ausser mir;
eine Wache, die auch Kosmetik anmahnt, wird weggeklickt. Beim ersten
Messen fielen ausserdem acht Stellen aus buehne.html faelschlich an,
weil `/* */` in HTML (in <style> und <script>) nicht ausgeblendet
war -- jetzt schon.
DIE WACHE DAZU
pruef-struktur prueft es ab sofort mit derselben Regel: 322 Dateien
mit sichtbarem Text, 0 Funde, und die zwoelf bewussten Ausnahmen
werden GEZAEHLT und genannt (erlaubt: 12). Eine Ausnahme, die niemand
sieht, waechst -- und irgendwann steht der echte Fall darin.
GEPRUEFT
pruef-struktur 59 -> 68 Pruefungen, 0 Fehler
node --check auf allen geaenderten JS-Dateien
nachgemessen: 199 geaendert, 12 mit Grund stehen geblieben
gruen geblieben: bewerbung-aufgaben 163, nachwuchs 262,
reaktion 421, support 63, content 45, terminregel 35, treff 85
Und nachgesehen, ob eine Pruefung noch die alte Schreibweise
ERWARTET: 13 Fundstellen, alle dreizehn nur Text in ihrer eigenen
Ausgabe, keine einzige ein Vergleich mit dem Seitentext.
GEGENPROBE: In reaktion.html ein Schlusszeichen zurueckgedreht ->
„workspace/reaktion.html:546 „Nächstes"", mit Datei und Zeile. Und
die Erkennung einzeln gegen HTML-Attribut, fortgesetzte
Zeichenkette, regulaeren Ausdruck und eingesetzten Wert geprueft.
Stempel gesetzt: workspace 670 Verweise in 45 Dateien, oeffentlich
554 in 48 Seiten.
Co-Authored-By: Claude Opus 5 <[email protected]>
620 lines
25 KiB
JavaScript
620 lines
25 KiB
JavaScript
/* ===================================================================
|
||
Content-Ideen.
|
||
|
||
Bekommt als einziger der sechs Bereiche eine eigene Seite. Grund: Die
|
||
anderen fuenf sind Sammlungen -- Eintraege zu einem Thema. Content ist
|
||
eine STRECKE. Ein Video wandert von der Idee bis zum Upload durch
|
||
feste Stufen, und die Frage ist nie "was gibt es alles", sondern "wo
|
||
klemmt es und wie viel liegt bereit".
|
||
|
||
Aus der Recherche (31.08.2026):
|
||
- "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 steht deshalb auf jeder Karte, nicht im Fliesstext.
|
||
- Regelmaessigkeit schlaegt Menge. Ein Video taeglich zur selben Zeit
|
||
schlaegt sieben an einem Tag. Wer regelmaessig senden will, braucht
|
||
Vorrat -- und Vorrat ist die eine Zahl, die eine Ideenliste
|
||
verschweigt.
|
||
=================================================================== */
|
||
(() => {
|
||
'use strict';
|
||
|
||
/* Die Leitungsliste steht in bereiche.js -- eine Antwort statt neun.
|
||
Sie stand hier frueher selbst, ohne 'spicy', und genau das war der
|
||
Fehler, den Filipe am 07.09.2026 gefunden hat. */
|
||
const LEITUNG = window.Bereiche.LEITUNG;
|
||
const darfEintragen = () => ich && ich.rolle !== 'creator';
|
||
|
||
const $ = (id) => document.getElementById(id);
|
||
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 || ''; };
|
||
|
||
let ich = null;
|
||
let stufen = {}; // schluessel -> Name, vom Server
|
||
let eintraege = [];
|
||
let saeulen = [];
|
||
let zahlen = null;
|
||
let filterCreator = '';
|
||
let filterSaeule = '';
|
||
|
||
/* Wie viele Karten je Spalte hoechstens stehen, bevor gekuerzt wird.
|
||
Sechs, weil das die Hoehe der Nachbarspalten ungefaehr trifft --
|
||
eine Spalte, die dreimal so lang ist wie die anderen, ist kein
|
||
Brett mehr, sondern eine Liste mit vier Randnotizen. */
|
||
const SICHTBAR_MAX = 6;
|
||
const offeneSpalten = new Set();
|
||
|
||
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'); }
|
||
return a;
|
||
}
|
||
|
||
const datum = (iso) => (iso ? iso.slice(0, 10).split('-').reverse().join('.') : '');
|
||
const WOCHENTAG = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
|
||
|
||
function zeitpunkt(iso) {
|
||
if (!iso) return '';
|
||
const [d, z] = String(iso).split('T');
|
||
const [j, m, t] = d.split('-');
|
||
const tag = new Date(Number(j), Number(m) - 1, Number(t));
|
||
const w = Number.isNaN(tag.getTime()) ? '' : WOCHENTAG[tag.getDay()] + ', ';
|
||
return `${w}${t}.${m}.` + (z ? ' · ' + z.slice(0, 5) : '');
|
||
}
|
||
|
||
/* Die Kennzahlen-Reihe (Vorrat, Veroeffentlicht, Ideen, ohne Hook,
|
||
ueberfaellig) ist am 01.09.2026 entfallen -- auf Wunsch: "diese
|
||
Kisten sind jetzt ueberall zu viel, die sollen weg."
|
||
|
||
Sie hatte auch inhaltlich kein Recht mehr: Die Strecke darunter
|
||
zeigt Ideen, Produktion und Veroeffentlichtes ohnehin mit Zahl an
|
||
jeder Spalte. Zwei Zaehler fuer dieselbe Sache auf einem Bildschirm
|
||
sind einer zu viel -- und laufen frueher oder spaeter auseinander.
|
||
|
||
Die ABFRAGE bleibt: Aus derselben Antwort speist sich der
|
||
Saeulen-Balken darunter (70/20/10). Sie heisst jetzt zahlenLaden(),
|
||
weil "kennzahlenLaden" auf etwas zeigte, das es nicht mehr gibt. */
|
||
|
||
/* ---------- Säulen-Balance -----------------------------------------------
|
||
|
||
Ein liegender gestapelter Balken: Teil vom Ganzen ueber wenige
|
||
Kategorien. Farben nach Platz (slot), nicht nach Rang -- sonst
|
||
wechselt eine Saeule die Farbe, sobald eine andere sie ueberholt.
|
||
Bis zu fuenf Farben, geprueft gegen Farbfehlsichtigkeit auf genau
|
||
dieser Hintergrundfarbe. Immer mit Legende UND Beschriftung: Farbe
|
||
allein darf nie die einzige Information sein. */
|
||
|
||
function balanceZeigen() {
|
||
const kasten = $('balance');
|
||
if (!zahlen || zahlen.leer || !zahlen.balance) { kasten.hidden = true; return; }
|
||
|
||
const teile = zahlen.balance.filter((b) => b.n > 0);
|
||
const gesamt = teile.reduce((s, b) => s + b.n, 0) + (zahlen.ohneSaeule || 0);
|
||
|
||
$('balance-zeit').textContent = `· letzte ${zahlen.balanceTage} Tage, nur Veröffentlichtes`;
|
||
|
||
const balken = $('balance-balken');
|
||
const legende = $('balance-legende');
|
||
balken.textContent = '';
|
||
legende.textContent = '';
|
||
|
||
if (!gesamt) {
|
||
kasten.hidden = !saeulen.length;
|
||
$('balance-hinweis').textContent = saeulen.length
|
||
? 'Noch nichts veröffentlicht – sobald etwas draußen ist, steht hier die Verteilung.'
|
||
: '';
|
||
return;
|
||
}
|
||
|
||
const stuecke = [...teile];
|
||
if (zahlen.ohneSaeule) {
|
||
stuecke.push({ id: 0, name: 'ohne Säule', slot: 0, n: zahlen.ohneSaeule, ziel: null });
|
||
}
|
||
|
||
for (const s of stuecke) {
|
||
const anteil = Math.round((s.n / gesamt) * 100);
|
||
const stueck = el('span', 'balance__stueck');
|
||
stueck.dataset.slot = String(s.slot);
|
||
stueck.style.width = (s.n / gesamt) * 100 + '%';
|
||
stueck.title = `${s.name}: ${s.n} von ${gesamt} (${anteil} %)`;
|
||
/* Beschriftung direkt im Balken, sobald Platz ist. Bei schmalen
|
||
Stuecken traegt die Legende die Information. */
|
||
if (anteil >= 12) stueck.append(el('span', 'balance__zahl', anteil + ' %'));
|
||
balken.append(stueck);
|
||
|
||
const li = el('li', 'legende__punkt');
|
||
const punkt = el('span', 'legende__farbe');
|
||
punkt.dataset.slot = String(s.slot);
|
||
li.append(punkt);
|
||
li.append(el('span', 'legende__name', s.name));
|
||
li.append(el('span', 'legende__wert', `${anteil} %`));
|
||
if (s.ziel !== null && s.ziel !== undefined) {
|
||
const ab = anteil - s.ziel;
|
||
const marke = el('span', 'legende__ziel',
|
||
`Ziel ${s.ziel} %${ab === 0 ? '' : (ab > 0 ? ` · ${ab} über` : ` · ${-ab} unter`)}`);
|
||
if (Math.abs(ab) > 15) marke.dataset.warn = 'ja';
|
||
li.append(marke);
|
||
}
|
||
legende.append(li);
|
||
}
|
||
|
||
$('balance-hinweis').textContent = zahlen.ohneSaeule
|
||
? `${zahlen.ohneSaeule} veröffentlichte Videos hängen an keiner Säule – dann sagt die Verteilung nur die halbe Wahrheit.`
|
||
: '';
|
||
kasten.hidden = false;
|
||
}
|
||
|
||
/* ---------- Die Strecke -------------------------------------------------- */
|
||
|
||
function karte(e) {
|
||
const k = el('article', 'video');
|
||
if (e.saeule_slot) k.dataset.slot = String(e.saeule_slot);
|
||
/* Fuer den Sprung vom Bericht: Die Karte traegt ihren Zustand und
|
||
ihre Dringlichkeit, damit "3 Content: offen" genau diese drei
|
||
hervorheben kann statt nur die Seite zu oeffnen. */
|
||
k.dataset.status = e.status;
|
||
if (e.dringlichkeit) k.dataset.dringlich = e.dringlichkeit;
|
||
|
||
k.append(el('h3', 'video__titel', e.titel));
|
||
|
||
/* Der Hook steht direkt unter dem Titel und in Anfuehrungszeichen --
|
||
er ist gesprochener Text, kein Etikett. Fehlt er, steht das da:
|
||
eine Luecke, die man sieht, wird gefuellt. */
|
||
if (e.hook) {
|
||
k.append(el('p', 'video__hook', '„' + e.hook + '“'));
|
||
} else if (e.art !== 'idee' && e.art !== 'veroeffentlicht') {
|
||
k.append(el('p', 'video__hook video__hook--fehlt', 'Noch kein Hook'));
|
||
}
|
||
|
||
const marken = el('div', 'video__marken');
|
||
if (e.saeule_name) {
|
||
const m = el('span', 'marke-saeule', e.saeule_name);
|
||
m.dataset.slot = String(e.saeule_slot || 0);
|
||
marken.append(m);
|
||
}
|
||
if (e.format) marken.append(el('span', 'marke-still', e.format));
|
||
if (LEITUNG.has(ich.rolle) && e.creator_name) marken.append(el('span', 'marke-still', e.creator_name));
|
||
if (marken.children.length) k.append(marken);
|
||
|
||
if (e.geplant) {
|
||
const vorbei = e.art !== 'veroeffentlicht' && e.geplant < new Date().toISOString().slice(0, 16);
|
||
const z = el('p', 'video__termin', (vorbei ? 'war fällig ' : 'geplant ') + zeitpunkt(e.geplant));
|
||
if (vorbei) z.dataset.warn = 'ja';
|
||
k.append(z);
|
||
}
|
||
|
||
if (!darfEintragen()) return k;
|
||
|
||
const knoepfe = el('div', 'video__knoepfe');
|
||
const reihe = Object.keys(stufen);
|
||
const jetztIndex = reihe.indexOf(e.art);
|
||
|
||
if (jetztIndex > 0) {
|
||
knoepfe.append(tuKnopf('◀', 'Eine Stufe zurück', () => stufeSetzen(e, reihe[jetztIndex - 1])));
|
||
}
|
||
if (jetztIndex >= 0 && jetztIndex < reihe.length - 1) {
|
||
const weiter = tuKnopf(stufen[reihe[jetztIndex + 1]] + ' ▶',
|
||
'Auf die nächste Stufe', () => stufeSetzen(e, reihe[jetztIndex + 1]));
|
||
weiter.dataset.haupt = 'ja';
|
||
knoepfe.append(weiter);
|
||
}
|
||
|
||
/* Ueberfuehren -- woertlich aus dem Konzept. Nur solange das Video
|
||
noch nicht draussen ist; danach gibt es nichts mehr zu planen. */
|
||
if (e.art !== 'veroeffentlicht') {
|
||
knoepfe.append(tuKnopf('→ Aufgabe', 'Als Aufgabe ins Brett', () => uebernehmen(e, 'aufgabe')));
|
||
if (e.geplant) knoepfe.append(tuKnopf('→ Termin', 'Als Drehtermin in den Kalender', () => uebernehmen(e, 'termin')));
|
||
knoepfe.append(tuKnopf('→ LIVE', 'Als LIVE-Thema vormerken', () => uebernehmen(e, 'live')));
|
||
}
|
||
knoepfe.append(tuKnopf('löschen', 'Dieses Video entfernen', async () => {
|
||
if (!await frageNach({
|
||
titel: `„${e.titel}“ löschen?`,
|
||
was: 'Die Idee verschwindet aus der Planung.',
|
||
bleibt: 'Ein schon eingetragener Drehtermin im Kalender bleibt stehen.',
|
||
endgueltig: true, ja: 'Löschen',
|
||
})) return;
|
||
const a = await hole(`/workspace/api/bereich/content/${e.id}`, { method: 'DELETE' });
|
||
if (!a.ok) { melde('Löschen hat nicht geklappt.'); return; }
|
||
await laden();
|
||
}));
|
||
k.append(knoepfe);
|
||
return k;
|
||
}
|
||
|
||
function tuKnopf(text, titel, tun) {
|
||
const b = el('button', 'tu-knopf', text);
|
||
b.type = 'button';
|
||
b.title = titel;
|
||
b.addEventListener('click', async () => {
|
||
b.disabled = true;
|
||
try { await tun(); } catch { /* umgeleitet */ } finally { b.disabled = false; }
|
||
});
|
||
return b;
|
||
}
|
||
|
||
async function stufeSetzen(e, neueStufe) {
|
||
const daten = { art: neueStufe };
|
||
/* Was veroeffentlicht wird, bekommt das heutige Datum -- sonst
|
||
stuende die Auswertung der letzten 30 Tage auf dem Tag, an dem die
|
||
Idee entstand, und waere durchgehend falsch. */
|
||
if (neueStufe === 'veroeffentlicht') daten.datum = window.heuteLokal();
|
||
const a = await hole(`/workspace/api/bereich/content/${e.id}`, {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(daten),
|
||
});
|
||
if (!a.ok) { melde(sagWas((await a.json().catch(() => ({}))).fehler, 'Ging nicht.')); return; }
|
||
melde('');
|
||
await laden();
|
||
}
|
||
|
||
async function uebernehmen(e, ziel) {
|
||
const a = await hole(`/workspace/api/content/${e.id}/uebernehmen`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ ziel }),
|
||
});
|
||
const d = await a.json().catch(() => ({}));
|
||
if (!a.ok) { melde(sagWas(d.fehler, 'Ging nicht.')); return; }
|
||
melde('');
|
||
/* Sagen, wohin es gewandert ist -- und den Weg dorthin anbieten.
|
||
Eine stille Uebernahme laesst einen ratlos zurueck. */
|
||
$('fehler').textContent = '';
|
||
const hinweis = $('fehler');
|
||
hinweis.textContent = `Als ${d.art} angelegt. `;
|
||
const link = el('a', null, 'Ansehen →');
|
||
link.href = d.wohin;
|
||
hinweis.append(link);
|
||
await laden();
|
||
}
|
||
|
||
function streckeZeigen() {
|
||
const ziel = $('strecke');
|
||
ziel.textContent = '';
|
||
ziel.setAttribute('aria-busy', 'false');
|
||
|
||
let sichtbar = eintraege;
|
||
if (filterCreator) sichtbar = sichtbar.filter((e) => String(e.creator_id) === filterCreator);
|
||
if (filterSaeule === 'ohne') sichtbar = sichtbar.filter((e) => !e.saeule_id);
|
||
else if (filterSaeule) sichtbar = sichtbar.filter((e) => String(e.saeule_id) === filterSaeule);
|
||
|
||
for (const [schluessel, name] of Object.entries(stufen)) {
|
||
const drin = sichtbar.filter((e) => e.art === schluessel);
|
||
const spalte = el('section', 'spalte');
|
||
spalte.dataset.stufe = schluessel;
|
||
|
||
const kopf = el('div', 'spalte__kopf');
|
||
kopf.append(el('h2', 'spalte__name', name));
|
||
kopf.append(el('span', 'spalte__zahl', String(drin.length)));
|
||
spalte.append(kopf);
|
||
|
||
if (!drin.length) {
|
||
spalte.append(el('p', 'spalte__leer', '–'));
|
||
} else {
|
||
/* Die letzte Spalte waechst ewig -- nach einem halben Jahr
|
||
stehen dort hundert Videos und schieben alles andere aus dem
|
||
Bild. Genau daran scheitert jedes Brett mit einer
|
||
"Fertig"-Spalte. Sichtbar bleibt deshalb nur die jüngste
|
||
Handvoll; der Rest ist einen Klick entfernt.
|
||
|
||
Die vorderen Spalten bleiben ungekürzt: Was dort liegt, ist
|
||
Arbeit, und Arbeit darf man nicht wegklappen. */
|
||
const kuerzen = !offeneSpalten.has(schluessel) && drin.length > SICHTBAR_MAX;
|
||
const zeigen = kuerzen ? drin.slice(0, SICHTBAR_MAX) : drin;
|
||
for (const e of zeigen) spalte.append(karte(e));
|
||
|
||
if (drin.length > SICHTBAR_MAX) {
|
||
const rest = drin.length - SICHTBAR_MAX;
|
||
const b = el('button', 'spalte__mehr',
|
||
kuerzen ? `${rest} weitere zeigen` : `nur die letzten ${SICHTBAR_MAX}`);
|
||
b.type = 'button';
|
||
b.addEventListener('click', () => {
|
||
if (offeneSpalten.has(schluessel)) offeneSpalten.delete(schluessel);
|
||
else offeneSpalten.add(schluessel);
|
||
streckeZeigen();
|
||
});
|
||
spalte.append(b);
|
||
}
|
||
}
|
||
ziel.append(spalte);
|
||
}
|
||
}
|
||
|
||
/* ---------- Filter ------------------------------------------------------- */
|
||
|
||
function filterBauen() {
|
||
const zeile = $('filterzeile');
|
||
zeile.textContent = '';
|
||
|
||
const creator = [...new Map(eintraege.filter((e) => e.creator_id)
|
||
.map((e) => [e.creator_id, e.creator_name])).entries()];
|
||
|
||
if (LEITUNG.has(ich.rolle) && creator.length > 1) {
|
||
zeile.append(knopfGruppe('Creator', [['', 'Alle'], ...creator.map(([id, n]) => [String(id), n])],
|
||
filterCreator, (w) => { filterCreator = w; filterBauen(); streckeZeigen(); zahlenLaden(); }));
|
||
}
|
||
if (saeulen.length) {
|
||
const passend = filterCreator
|
||
? saeulen.filter((s) => String(s.creator_id) === filterCreator) : saeulen;
|
||
zeile.append(knopfGruppe('Säule',
|
||
[['', 'Alle'], ...passend.map((s) => [String(s.id), s.name]), ['ohne', 'ohne Säule']],
|
||
filterSaeule, (w) => { filterSaeule = w; filterBauen(); streckeZeigen(); }));
|
||
}
|
||
}
|
||
|
||
function knopfGruppe(name, werte, aktiv, beiWahl) {
|
||
const g = el('div', 'filtergruppe');
|
||
g.append(el('span', 'filtergruppe__schild', name));
|
||
for (const [wert, text] of werte) {
|
||
const b = el('button', 'filter', text);
|
||
b.type = 'button';
|
||
if (wert === aktiv) b.dataset.aktiv = 'ja';
|
||
b.addEventListener('click', () => beiWahl(wert));
|
||
g.append(b);
|
||
}
|
||
return g;
|
||
}
|
||
|
||
/* ---------- Säulen verwalten --------------------------------------------- */
|
||
|
||
function saeulenZeigen() {
|
||
const ziel = $('saeulen-liste');
|
||
ziel.textContent = '';
|
||
if (!saeulen.length) {
|
||
ziel.append(el('p', 'leise', 'Noch keine Säule. Fang mit drei an.'));
|
||
return;
|
||
}
|
||
for (const s of saeulen) {
|
||
const z = el('div', 'saeule');
|
||
const punkt = el('span', 'legende__farbe');
|
||
punkt.dataset.slot = String(s.slot);
|
||
z.append(punkt);
|
||
z.append(el('span', 'saeule__name', s.name));
|
||
if (LEITUNG.has(ich.rolle) && s.creator_name) {
|
||
z.append(el('span', 'saeule__wer', s.creator_name));
|
||
}
|
||
z.append(el('span', 'saeule__zahl', `${s.videos} Video${s.videos === 1 ? '' : 's'}`));
|
||
z.append(el('span', 'saeule__ziel', s.ziel === null ? 'kein Ziel' : `Ziel ${s.ziel} %`));
|
||
/* Auch hier: Ein Creator bekommt gar keinen Knopf erst gebaut. Der
|
||
Kasten ist fuer ihn zwar ausgeblendet -- aber ein Knopf, der nur
|
||
unsichtbar ist, steht trotzdem im Dokument, wird von Vorlese-
|
||
programmen gefunden und laesst sich mit der Tastatur ansteuern.
|
||
Ausgeblendet ist nicht dasselbe wie nicht vorhanden. */
|
||
if (!darfEintragen()) { ziel.append(z); continue; }
|
||
z.append(tuKnopf('löschen', 'Säule entfernen, Videos bleiben', async () => {
|
||
if (!await frageNach({
|
||
titel: `Säule „${s.name}“ entfernen?`,
|
||
was: 'Die Säule verschwindet aus der Auswahl.',
|
||
bleibt: 'Alle Videos bleiben – sie stehen danach nur ohne Säule da '
|
||
+ 'und lassen sich jederzeit einer anderen zuordnen.',
|
||
ja: 'Säule entfernen',
|
||
})) return;
|
||
const a = await hole(`/workspace/api/content/saeulen/${s.id}`, { method: 'DELETE' });
|
||
if (!a.ok) { melde('Ging nicht.'); return; }
|
||
await laden();
|
||
}));
|
||
ziel.append(z);
|
||
}
|
||
}
|
||
|
||
/* ---------- Laden -------------------------------------------------------- */
|
||
|
||
async function zahlenLaden() {
|
||
try {
|
||
const p = filterCreator ? `?creator=${encodeURIComponent(filterCreator)}` : '';
|
||
const a = await hole('/workspace/api/content/kennzahlen' + p);
|
||
if (!a.ok) return;
|
||
zahlen = await a.json();
|
||
balanceZeigen();
|
||
} catch { /* umgeleitet */ }
|
||
}
|
||
|
||
async function laden() {
|
||
try {
|
||
const [bA, sA] = await Promise.all([
|
||
hole('/workspace/api/bereich/content'),
|
||
hole('/workspace/api/content/saeulen'),
|
||
]);
|
||
if (!bA.ok) { location.assign('/workspace/start.html'); return; }
|
||
const b = await bA.json();
|
||
stufen = b.einstellung.arten;
|
||
eintraege = b.eintraege || [];
|
||
saeulen = sA.ok ? (await sA.json()).saeulen || [] : [];
|
||
} catch { return; }
|
||
|
||
/* Fertige Ideen zum Uebernehmen. Steht ueber der Strecke, damit
|
||
jemand mit leeren Spalten nicht erst daran vorbeiscrollen muss. */
|
||
/* Die feste Liste der Ideen -- nach Saeule gruppiert (70/20/10).
|
||
Sie steht fuer jeden gleich da; was sich unterscheidet, ist,
|
||
was die Betreuung dazu vermerkt hat. */
|
||
const behaelter = document.getElementById('vorlagen');
|
||
if (behaelter && window.checkliste) {
|
||
window.checkliste(behaelter, 'content');
|
||
}
|
||
|
||
filterBauen();
|
||
streckeZeigen();
|
||
saeulenZeigen();
|
||
saeulenAuswahlFuellen();
|
||
/* Sprung vom Bericht -- nach streckeZeigen(), vorher gibt es die
|
||
Karten nicht. Nur beim ersten Laden. */
|
||
if (!gesprungen) { gesprungen = true; sprungZeigen(); }
|
||
await zahlenLaden();
|
||
}
|
||
|
||
/* Sprung vom Bericht (01.09.2026). Hervorheben statt filtern: Die
|
||
Strecke lebt davon, dass man Idee, Produktion und Veroeffentlicht
|
||
nebeneinander sieht -- wer nur die offenen zeigt, nimmt genau den
|
||
Ueberblick weg, wegen dem man hergekommen ist. */
|
||
let gesprungen = false;
|
||
function sprungZeigen() {
|
||
const was = window.zielSchluessel?.();
|
||
if (was === 'offen') {
|
||
window.zielMarkieren?.(document.querySelectorAll('.video[data-status="offen"]'),
|
||
'Content-Einträge, die noch offen sind');
|
||
} else if (was === 'dringend') {
|
||
/* offen UND hoch -- genau so zaehlt der Bericht. */
|
||
window.zielMarkieren?.(
|
||
document.querySelectorAll('.video[data-dringlich="hoch"][data-status="offen"]'),
|
||
'Content-Einträge, die offen und als dringend eingestuft sind');
|
||
}
|
||
}
|
||
|
||
function saeulenAuswahlFuellen() {
|
||
const feld = $('f-saeule');
|
||
const vorher = feld.value;
|
||
feld.textContent = '';
|
||
const leer = document.createElement('option');
|
||
leer.value = ''; leer.textContent = '— keine Säule —';
|
||
feld.append(leer);
|
||
const passend = LEITUNG.has(ich.rolle) && $('f-creator')?.value
|
||
? saeulen.filter((s) => String(s.creator_id) === $('f-creator').value)
|
||
: saeulen;
|
||
for (const s of passend) {
|
||
const o = document.createElement('option');
|
||
o.value = String(s.id); o.textContent = s.name;
|
||
feld.append(o);
|
||
}
|
||
feld.value = vorher;
|
||
feld.dispatchEvent(new Event('wahl:auffrischen'));
|
||
}
|
||
|
||
/* ---------- Formular ----------------------------------------------------- */
|
||
|
||
const umschalten = (auf) => {
|
||
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', () => umschalten(true));
|
||
$('neu-abbrechen').addEventListener('click', () => {
|
||
$('neu').reset(); $('neu-fehler').textContent = ''; umschalten(false);
|
||
});
|
||
|
||
$('neu').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
$('neu-fehler').textContent = '';
|
||
const daten = {
|
||
titel: $('f-titel').value.trim(),
|
||
hook: $('f-hook').value.trim(),
|
||
art: $('f-art').value,
|
||
format: $('f-format').value.trim(),
|
||
saeule_id: $('f-saeule').value || null,
|
||
geplant: $('f-geplant').value || null,
|
||
text: $('f-text').value.trim(),
|
||
datum: window.heuteLokal(),
|
||
};
|
||
if (LEITUNG.has(ich.rolle)) {
|
||
daten.creator_id = $('f-creator').value || null;
|
||
daten.creator_extern = $('f-creator').dataset.extern || null;
|
||
}
|
||
|
||
const a = await hole('/workspace/api/bereich/content', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(daten),
|
||
});
|
||
if (!a.ok) {
|
||
$('neu-fehler').textContent = sagWas((await a.json().catch(() => ({}))).fehler, 'Ging nicht.');
|
||
return;
|
||
}
|
||
$('neu').reset(); umschalten(false);
|
||
await laden();
|
||
});
|
||
|
||
$('saeulen-oeffnen').addEventListener('click', () => { $('saeulen-kasten').hidden = false; });
|
||
$('saeulen-zu').addEventListener('click', () => { $('saeulen-kasten').hidden = true; });
|
||
|
||
$('saeule-neu').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const daten = {
|
||
name: $('s-name').value.trim(),
|
||
ziel: $('s-ziel').value === '' ? null : Number($('s-ziel').value),
|
||
creator_id: LEITUNG.has(ich.rolle) ? $('s-creator').value : undefined,
|
||
};
|
||
const a = await hole('/workspace/api/content/saeulen', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(daten),
|
||
});
|
||
if (!a.ok) { melde(sagWas((await a.json().catch(() => ({}))).fehler, 'Ging nicht.')); return; }
|
||
melde('');
|
||
$('saeule-neu').reset();
|
||
await laden();
|
||
});
|
||
|
||
/* ---------- Start -------------------------------------------------------- */
|
||
|
||
(async () => {
|
||
try {
|
||
const a = await hole('/workspace/api/ich');
|
||
if (!a.ok) { location.assign('/workspace/'); return; }
|
||
ich = await a.json();
|
||
} catch {
|
||
/* NICHT MEHR STUMM (20.09.2026). Hier stand `catch { return; }`
|
||
-- und damit blieb die Seite bei jedem Netzaussetzer fuer
|
||
immer auf "wird geladen …" stehen. Gemessen auf 13 von 21
|
||
Seiten. `ladefehler()` sagt, was los ist, und bietet einen
|
||
Knopf an, der es noch einmal versucht. */
|
||
window.ladefehler?.();
|
||
return;
|
||
}
|
||
window.werZeigen?.(ich);
|
||
|
||
if (!darfEintragen()) {
|
||
$('neu-oeffnen').hidden = true;
|
||
$('neu').hidden = true;
|
||
$('saeulen-oeffnen').hidden = true;
|
||
$('unterzeile').textContent =
|
||
'Von der Idee bis zum Upload. Hier plant deine Betreuung – du siehst alles, was zu dir gehört.';
|
||
}
|
||
|
||
/* Die Stufen kommen vom Server, nicht aus dieser Datei. Stuenden sie
|
||
hier, waeren sie beim naechsten Mal an einer Stelle geaendert und
|
||
an der anderen nicht. */
|
||
try {
|
||
const b = await (await hole('/workspace/api/bereich/content')).json();
|
||
stufen = b.einstellung.arten;
|
||
for (const [wert, name] of Object.entries(stufen)) {
|
||
const o = document.createElement('option');
|
||
o.value = wert; o.textContent = name;
|
||
$('f-art').append(o);
|
||
}
|
||
} catch { /* laden() faengt es gleich noch einmal */ }
|
||
|
||
if (LEITUNG.has(ich.rolle)) {
|
||
$('feld-creator').hidden = false;
|
||
$('feld-saeule-creator').hidden = false;
|
||
try {
|
||
const { personen } = await (await hole('/workspace/api/personen')).json();
|
||
for (const p of personen.filter((p) => p.rolle === 'creator')) {
|
||
for (const feld of ['f-creator', 's-creator']) {
|
||
const o = document.createElement('option');
|
||
o.value = String(p.id); o.textContent = p.name;
|
||
o.dataset.person = 'ja';
|
||
if (p.bild) o.dataset.bild = p.bild;
|
||
$(feld).append(o);
|
||
}
|
||
}
|
||
} catch { /* Auswahl bleibt leer */ }
|
||
window.personenwahl?.('f-creator', { hinweis: "Kein Konto im Workspace – der Eintrag gehört dann zu keiner Person und taucht in personenbezogenen Auswertungen nicht auf. Sichtbar bleibt er für dich." });
|
||
/* Bei den Saeulen KEIN freier Name: Eine Content-Saeule gehoert
|
||
immer genau einem Creator, der Server prueft das. Ein freier
|
||
Name waere dort eine Eingabe, die garantiert abgelehnt wird. */
|
||
window.personenwahl?.('s-creator', { frei: false });
|
||
/* Wechselt der Creator, passen die Säulen dazu -- sonst böte die
|
||
Liste Säulen an, die zu jemand anderem gehören, und der Server
|
||
lehnte beim Speichern ab. */
|
||
$('f-creator').addEventListener('change', saeulenAuswahlFuellen);
|
||
}
|
||
|
||
await laden();
|
||
})();
|
||
})();
|