Files
dogfather-universe/workspace/assets/js/schulung.js
T
DogFatherGitandClaude Opus 5 461d41943a Keine Maschinensprache mehr auf dem Bildschirm
Filipe: "keine kryptischen oder technischen fehlermeldungen, sondern
klare aussagen darueber, was passiert ist und wie man das problem
loesen kann."

DAS PROBLEM, NACHGEMESSEN.

Der Server antwortet im Fehlerfall mit { fehler: "..." }. Darin stehen
ZWEI verschiedene Dinge, und von aussen sehen sie gleich aus:
Maschinenkennungen (nicht_verfuegbar, nicht_gefunden, ungueltig -- 43
verschiedene) und fertige deutsche Saetze.

An 84 Stellen stand `textContent = d.fehler` -- ungefiltert. Wer beim
Hochladen einer Datei Pech hatte, las woertlich "nicht_verfuegbar" auf
dem Bildschirm und wusste nicht einmal, ob er selbst schuld war.

Gezaehlt: 474 Antworten mit "nicht_verfuegbar", 392 mit
"nicht_gefunden", 125 mit "ungueltig".

DREI AUSGAENGE, NICHT ZWEI.

`sagWas()` in workspace/assets/js/meldung.js:
  bekannte Kennung   -> ihr Satz
  unbekannte Kennung -> der Ersatzsatz der Stelle, NIE die Kennung
                        (sie geht in die Konsole, wo sie jemandem
                        auffaellt, der sie beheben kann)
  fertiger Satz      -> unveraendert durch

Erkannt wird eine Kennung daran, dass sie nur aus Kleinbuchstaben,
Ziffern und Unterstrichen besteht. Ein deutscher Satz hat immer
Leerzeichen; eine Kennung nie. Die Unterscheidung ist entschieden,
nicht geraten.

UND WARUM DIE TABELLE NICHT ALTERN KANN.

Eine von Hand gepflegte Liste ist hier schon zweimal teuer geworden
(die abgeschriebene Spaltenliste, die feste Umbruchschwelle). Deshalb
rechnet pruef-meldungen.mjs die Kennungen AUS DEM SERVER aus statt sie
zu kennen -- eine neue ohne Satz macht sie rot. Man kann es nicht mehr
vergessen.

Sie prueft drei Dinge und beweist zu jedem, dass sie auch "nicht in
Ordnung" sagen kann:
  1. vollstaendig -- jede Kennung hat einen Satz
  2. angeschlossen -- keine Rohanzeige, und jede Seite, deren Skript
     sagWas benutzt, laedt auch meldung.js
  3. lesbar -- kein Satz enthaelt Kennung, Zahl oder englisches Wort

GEFUNDEN HAT SIE SOFORT EINEN ECHTEN FEHLER: leistung.html laedt seine
Skripte ohne `defer` und fiel damit durch meinen Einbau. Dort waere
sagWas nicht definiert gewesen -- aus einer Fehlermeldung waere ein
Absturz geworden, also schlimmer als vorher.

NEBENBEFUND, DER MICH FAST EINE FALSCHE MELDUNG GEKOSTET HAETTE:
"alter_offen" heisst NICHT "etwas Aelteres ist offen", sondern
"Altersbestaetigung steht noch aus" (workspace.js:4864). Geraten
haette ich es falsch uebersetzt.

GEMESSEN: pruef-meldungen 8/0, alle 46 Skripte syntaktisch in Ordnung.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-19 14:46:13 +02:00

268 lines
9.9 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.
/* ===================================================================
Unterweisungen — durchgehen und beidseitig bestätigen.
Erscheint im Bereich "Schutz & Regeln". Der Ablauf ist bewusst
dreistufig sichtbar, damit niemand raten muss, woran es hängt:
offen niemand hat bestätigt
halb einer von beiden -- und die Seite sagt, wer fehlt
abgeschlossen beide, mit Datum und Namen. Ab hier unveränderlich.
Warum die halbe Stufe sichtbar ist: Ein Nachweis, bei dem nur einer
unterschrieben hat, ist kein Nachweis. Wer das nicht sieht, hält ihn
für fertig -- und merkt es erst, wenn jemand danach fragt.
=================================================================== */
(() => {
'use strict';
const el = (tag, klasse, text) => {
const k = document.createElement(tag);
if (klasse) k.className = klasse;
if (text !== undefined) k.textContent = text;
return k;
};
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
? new Date(iso).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit' })
: null);
let daten = null;
let wahl = null; /* welcher Creator wird gerade angesehen */
window.schulungBlock = async function (behaelter) {
if (!behaelter) return;
try {
const a = await hole('/workspace/api/schulung');
if (!a.ok) return;
daten = await a.json();
} catch { return; }
behaelter.textContent = '';
const block = el('section', 'schulung');
/* ---- Kopf ---- */
const kopf = el('div', 'schulung__kopf');
const t = el('div', null);
t.append(el('p', 'feldschild', 'Unterweisungen'));
t.append(el('p', 'schulung__unter',
'Dokumente, die gemeinsam durchgegangen und von BEIDEN Seiten bestätigt '
+ 'werden. Eine abgeschlossene Bestätigung ist der Nachweis und lässt sich '
+ 'danach nicht mehr ändern.'));
kopf.append(t);
/* Betreuer wählen aus, für wen sie gerade bestätigen. */
if (daten.ich.rolle !== 'creator' && daten.creatoren.length) {
const feld = el('div', 'schulung__wahl');
feld.append(el('label', 'feld-schild', 'Creator'));
const s = el('select');
s.id = 'schulung-creator';
for (const c of daten.creatoren) {
const o = document.createElement('option');
o.value = String(c.id);
o.textContent = c.name;
s.append(o);
}
wahl = wahl && daten.creatoren.some((c) => c.id === wahl) ? wahl : daten.creatoren[0].id;
s.value = String(wahl);
s.addEventListener('change', () => {
wahl = Number(s.value);
window.schulungBlock(behaelter);
});
feld.append(s);
kopf.append(feld);
} else {
wahl = daten.ich.id;
}
block.append(kopf);
if (!daten.schulungen.length) {
const leer = el('div', 'leer-hinweis');
leer.append(el('p', null, daten.darf_anlegen
? 'Noch keine Unterweisung angelegt.'
: 'Es gibt noch keine Unterweisungen.'));
if (daten.darf_anlegen) {
leer.append(el('p', 'leer-hinweis__tipp',
'Wähle unten ein Dokument aus der Wissens-Bibliothek – oder lade dort '
+ 'zuerst eines hoch.'));
}
block.append(leer);
}
/* ---- Die Liste ---- */
const liste = el('div', 'schulung__liste');
for (const s of daten.schulungen) {
const stand = daten.stand.find((b) => b.schulung_id === s.id && b.creator_id === wahl);
const stufe = !stand ? 'offen'
: stand.abgeschlossen ? 'fertig' : 'halb';
const k = el('article', 'unterweisung');
k.dataset.stufe = stufe;
const zeile = el('div', 'unterweisung__kopf');
const links = el('div', null);
links.append(el('h3', 'unterweisung__titel', s.titel));
if (s.datei) links.append(el('p', 'unterweisung__datei', s.datei));
if (s.beschreibung) links.append(el('p', 'unterweisung__text', s.beschreibung));
zeile.append(links);
const marke = el('span', 'unterweisung__marke',
stufe === 'fertig' ? 'Bestätigt'
: stufe === 'halb' ? 'Halb – einer fehlt' : 'Offen');
zeile.append(marke);
k.append(zeile);
/* ---- Die beiden Unterschriften ---- */
const beide = el('div', 'unterschriften');
for (const [wer, wann, name] of [
['Creator', stand?.creator_am, stand?.creator_name],
['Betreuung', stand?.betreuer_am, stand?.betreuer_name],
]) {
const u = el('div', 'unterschrift');
u.dataset.da = wann ? 'ja' : 'nein';
u.append(el('span', 'unterschrift__wer', wer));
u.append(el('span', 'unterschrift__wann',
wann ? `${name || ''} · ${datum(wann)}` : 'steht noch aus'));
beide.append(u);
}
k.append(beide);
/* ---- Was man jetzt tun kann ---- */
const fuss = el('div', 'unterweisung__fuss');
if (s.wissen) {
const auf = el('a', 'schritt', 'Dokument öffnen');
auf.href = `/workspace/api/wissen/${s.wissen}/datei`;
auf.target = '_blank';
auf.rel = 'noopener noreferrer';
fuss.append(auf);
}
const alsCreator = daten.ich.rolle === 'creator';
const schonIch = alsCreator ? stand?.creator_am : stand?.betreuer_am;
if (stufe === 'fertig') {
const fest = el('span', 'unterweisung__fest',
`Abgeschlossen am ${datum(stand.abgeschlossen_am)} – unveränderlich.`);
fuss.append(fest);
/* Stimmt die Datei noch mit der bestätigten überein? Diese Frage
stellt sonst niemand -- und sie entscheidet, ob der Nachweis
noch etwas wert ist. */
if (stand.datei_stimmt === false) {
fuss.append(el('span', 'unterweisung__warnung',
'Achtung: Das Dokument wurde seit der Bestätigung ausgetauscht.'));
}
} else if (schonIch) {
fuss.append(el('span', 'unterweisung__wartet',
alsCreator ? 'Du hast bestätigt. Es fehlt noch deine Betreuung.'
: 'Du hast bestätigt. Es fehlt noch der Creator.'));
} else {
const b = el('button', 'knopf knopf--klein', 'Durchgegangen und bestätigt');
b.type = 'button';
b.addEventListener('click', async () => {
if (!confirm('Bestätigen, dass ihr das Dokument gemeinsam durchgegangen seid?\n\n'
+ 'Sobald BEIDE bestätigt haben, ist der Nachweis endgültig und lässt sich '
+ 'nicht mehr ändern.')) return;
b.disabled = true;
b.textContent = 'wird gespeichert …';
try {
const a = await hole(`/workspace/api/schulung/${s.id}/bestaetigen`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ creator_id: wahl }),
});
if (!a.ok) {
const f = await a.json().catch(() => ({}));
alert(sagWas(f.fehler, 'Ging nicht.'));
b.disabled = false;
b.textContent = 'Durchgegangen und bestätigt';
return;
}
window.schulungBlock(behaelter);
} catch { b.disabled = false; }
});
fuss.append(b);
}
k.append(fuss);
liste.append(k);
}
block.append(liste);
/* ---- Neue Unterweisung (nur Leitung) ---- */
if (daten.darf_anlegen) block.append(await neuFormular(behaelter));
behaelter.append(block);
behaelter.hidden = false;
};
/* Eine neue Unterweisung entsteht aus einem Dokument, das SCHON in der
Bibliothek liegt. Kein zweiter Upload-Weg: Sonst gäbe es Dateien,
die nur hier existieren und in der Bibliothek fehlen -- und niemand
wüsste, welche Fassung die richtige ist. */
async function neuFormular(behaelter) {
const f = el('form', 'schulung__neu');
f.append(el('p', 'feldschild', 'Unterweisung hinzufügen'));
f.append(el('p', 'schulung__unter',
'Wähle ein Dokument aus der Wissens-Bibliothek. Liegt es dort noch nicht, '
+ 'lade es zuerst dort hoch – so gibt es jede Datei nur einmal.'));
const raster = el('div', 'neu__raster');
const fTitel = el('div', null);
fTitel.append(el('label', 'feld-schild', 'Worum geht es?'));
const iTitel = el('input');
iTitel.maxLength = 160;
iTitel.placeholder = 'z. B. Community-Richtlinien durchgehen';
fTitel.append(iTitel);
raster.append(fTitel);
const fDok = el('div', null);
fDok.append(el('label', 'feld-schild', 'Dokument'));
const sDok = el('select');
sDok.append(new Option('— aus der Bibliothek wählen —', ''));
try {
const a = await hole('/workspace/api/wissen');
if (a.ok) {
const { eintraege } = await a.json();
for (const w of eintraege) sDok.append(new Option(w.titel, String(w.id)));
}
} catch { /* leer lassen */ }
fDok.append(sDok);
raster.append(fDok);
f.append(raster);
const fehler = el('p', 'fehler');
f.append(fehler);
const knopf = el('button', 'knopf knopf--klein', 'Hinzufügen');
knopf.type = 'submit';
f.append(knopf);
f.addEventListener('submit', async (e) => {
e.preventDefault();
fehler.textContent = '';
knopf.disabled = true;
try {
const a = await hole('/workspace/api/schulung', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ titel: iTitel.value, wissen_id: Number(sDok.value) || null }),
});
if (!a.ok) {
fehler.textContent = sagWas((await a.json().catch(() => ({}))).fehler, 'Ging nicht.');
return;
}
window.schulungBlock(behaelter);
} catch { /* umgeleitet */ } finally { knopf.disabled = false; }
});
return f;
}
})();