Filipe: "Es darf vor allem keine Stellen geben, an denen ein Benutzer
etwas falsch machen kann, nur weil die Seite es nicht verstaendlich
genug erklaert."
Gemessen: 30 Stellen in 16 Dateien benutzten confirm() oder prompt().
Das Haus hatte die richtige Bauweise laengst -- einen <dialog>, in
aufgaben.html sogar ausfuehrlich begruendet -- aber sie stand IN EINER
SEITE. Wer anderswo etwas loeschen liess, hatte sie nicht.
confirm('Wirklich loeschen?') stellt die falsche Frage: Es fragt, ob
man sicher ist, und nennt nicht, WAS passiert, was BLEIBT und ob es
ZURUECK geht. Jetzt beantwortet jeder der 41 Dialoge alle drei.
Neu: workspace/assets/js/nachfrage.js -- window.frageNach() mit
Pflichtgrund, Zahlenfeld, einzeiliger Eingabe und Abtippsicherung.
Drei Ausgaenge: <dialog> / confirm()-Notnagel fuer Safari vor 15.4 /
Abbruch (Esc, Klick daneben, "Doch nicht" -- immer false).
DREIMAL DERSELBE FALLSTRICK, dreimal nachgemessen statt vermutet:
.dialog stand in aufgaben.css und leistung.css -> auf dateien.html
waere der Dialog ein weisser Systemkasten gewesen. 14 Regeln
klammergenau nach module.css verschoben (Klammern gezaehlt, nicht
per Muster geschnitten -- heute frueh hat ein nicht-gieriges
Muster schon einmal CSS zerrissen).
Das Formular trug .neu neu--blank -- und .neu gibt seine Abstaende
nur in aufgaben.css. Gemessen: padding 0px, und die Felder
verloren ihre height:44px. Jetzt steht alles unter
.nachfrage__form in module.css; der Dialog borgt nichts mehr.
Die erste Fassung der Pruefung zaehlte nachfrage.js SELBST als
Nutzer -- damit war jede Seite trivialerweise "Nutzer" und die
Pruefung gruen ohne Inhalt. Jetzt ausdruecklich ausgenommen.
ZWEI FUNDE NEBENBEI:
hilfeAufraeumen() wird im Betrieb NIE aufgerufen. Der Kommentar
behauptete "wird beim Start aufgerufen (siehe index.js)" -- das
war nie wahr; einziger Aufrufer ist die eigene Pruefung. Folge:
geschlossene vertrauliche Faelle bleiben unbegrenzt stehen. NICHT
eingeschaltet (das loescht echte Daten und ist Filipes
Entscheidung), sondern der Kommentar richtiggestellt.
Einen Hilfe-Fall zu schliessen ist endgueltig -- es gibt keine
Route, die ihn wieder oeffnet. Vorher stand darueber nur die
Frage nach einem Schlusswort. Jetzt sagt der Dialog es.
pruef-struktur hat meine eigene Pruefung von heute Nachmittag
erwischt: Sie bildete ihr Datum aus UTC. Beim Beheben erst
heuteLokal(datum) genommen -- die Funktion nimmt gar kein Argument
und haette still "heute" statt "+3 Tage" geliefert. Jetzt tagLokal(3),
nachgerechnet: Abstand 3 Tage.
Am Bildschirm angesehen (Rechner 1280, Handy 390): passt rein, Esc
ergibt false, Fokus liegt auf dem harmlosen Knopf, Knoepfe 44px auf
Touch. Der Platzhalter im Abtippfeld zeigte den erwarteten Namen --
das sah aus wie ein schon ausgefuelltes Feld, entfernt.
Neu: server/pruef-nachfrage.mjs -- 17/0, mit sechs Gegenproben und
beiden Richtungen (wer fragt, laedt die Datei; wer nie fragt, laedt
sie nicht -- sonst truege die Anmeldewand 4,8 KB fuer nichts).
pruef-meldungen 8/0, pruef-css-klassen gruen, pruef-struktur gruen,
pruef-leistung gruen.
273 lines
10 KiB
JavaScript
273 lines
10 KiB
JavaScript
/* ===================================================================
|
||
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 (!await frageNach({
|
||
titel: 'Gemeinsam durchgegangen?',
|
||
was: 'Deine Bestätigung wird mit Datum festgehalten.',
|
||
bleibt: 'Sobald BEIDE bestätigt haben, ist der Nachweis endgültig '
|
||
+ 'und lässt sich nicht mehr ändern. Solange nur du bestätigt '
|
||
+ 'hast, ist noch nichts festgeschrieben.',
|
||
ja: 'Ja, bestätigen',
|
||
})) 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;
|
||
}
|
||
})();
|