Anleitung, Etappen 3 bis 7: alle vier Fassungen, Pflege, Suche, Rundgang, Abnahme
Der Bauplan ist damit vollstaendig umgesetzt.
ETAPPE 3 -- SCOUT, MANAGER UND SPICY MEDIA.
Woertlich aus Kapitel 07 bis 09, zusammen 84 Karten. Die Zahlen des
Bauplans stimmen auf die Karte genau: 18/21/22/23, verteilt 5/6/7,
5/7/9, 6/9/7, 5/5/13.
EINE ZWEIDEUTIGKEIT IM PDF, GEMESSEN STATT GERATEN. Beim Manager
nennt die Merke-Spalte "61, 58, 51 feste Punkte", und Layout- und
Raw-Auszug ordnen sie verschiedenen Kacheln zu. Nachgezaehlt im Code:
LIVE 69, Content-Ideen 61, Community 58, Technik 51. Damit ist die
zeilenweise Lesart bewiesen richtig -- die Layout-Lesart haette "61"
der LIVE-Analyse gegeben, die 69 hat.
Die Zuordnungspruefung laeuft jetzt fuer alle vier Rollen, und die
erwartete Kartenzahl wird AUS DER KACHELLISTE abgeleitet, nicht aus
der Abnahmeliste abgeschrieben. Dazu eine Gegenprobe, dass sich die
vier Fassungen wirklich unterscheiden -- sonst waeren alle Zeilen
gruen, solange die Kachellisten zufaellig passen.
ETAPPE 4 -- DIE PFLEGE, fuer DogFather UND Spicy.
Bearbeitet wird an Ort und Stelle: Stufe und die drei Zeilen je Karte,
Titel und Text je Listenzeile, Leitgedanke je Rolle. Fehlende Karten
werden benannt und lassen sich mit einem Griff anlegen -- das ist der
Fall "Fuer diese Kachel fehlt ein Eintrag" aus Kapitel 11. Dazu "Wer
ist wie weit": eine Arbeitslage, keine Ueberwachung.
KEINE ZWEITE ROLLENAUSWAHL. Die Vorschau gibt es schon -- sie heisst
"Meine Sicht" und steht in der Kopfleiste. Geprueft wird fuers Pflegen
`req.person` und nicht `req.sicht`: Wer durch fremde Augen sieht, soll
nicht aus Versehen in fremdem Namen aendern.
DIE MARKE "NEU" HAT EIN ENDE: 14 Tage ODER bis die Person die Seite
nach der Aenderung geoeffnet hat. Der Bauplan sagt dazu nichts, und
ohne Ende waere nach einem halben Jahr alles neu.
ETAPPE 5 -- DIE SUCHE.
Die Karten stehen in der Kopfleisten-Suche, aber nur die der EIGENEN
Rolle: Faende ein Creator die Manager-Karte, stuende dort "Zuteilen,
freigeben" neben einer Kachel, die er nicht hat. Der Treffer traegt
den Zusatz hinter dem Fragezeichen, sonst landen fuenf Karten auf
derselben Seite.
ETAPPE 6 -- BEGRUESSUNG UND RUNDGANG.
Fuenf Schritte ueber die Zentrale, im letzten leuchten nur die
Muss-Kacheln. EIGENE DATEI (`rundgang.js`) und nicht ein Stueck
start.js: Er laeuft auf der meistbenutzten Seite des Hauses, und wenn
hier etwas schiefgeht, darf davon nichts anderes betroffen sein.
Keine gerechnete Koordinate, kein Loch in einer Abdeckung -- die Seite
wird leiser, das Ziel bleibt hell. Faellt das Skript aus, ist beim
naechsten Laden alles normal.
DER ROLLENWECHSEL IST KEINE SONDERREGEL, sondern der Schluessel:
`(Person, Rolle)`. Die neue Rolle hat schlicht noch keine Zeile, also
startet die Einweisung von selbst noch einmal. Eine Spalte
"zuruecksetzen", an die jemand denken muesste, waere die Stelle, an
der es vergessen wird.
EIN FUND AUS DER MESSUNG: Gemerkt wurde zuerst erst am ENDE des
Rundgangs. Wer "Los geht's" drueckte und dann wegging, bekam die
Begruessung bei jedem Aufruf wieder -- fuer immer. Jetzt wird beim
ZEIGEN gemerkt; das ist die Tatsache, um die es geht.
ETAPPE 7 -- DIE ABNAHME.
Dabei fiel auf, dass der Rollen-Rundgang SPICY MEDIA gar nicht kannte:
fuenf Durchgaenge, aber zweimal Manager, zweimal Scout und Spicy nie.
Ihr gehoert die Gruppe "Rund um das Team" samt Team-Lage -- diese
Kachel ist in keinem Durchgang je geoeffnet worden. Jetzt laeuft sie
mit: 452 statt 406 Pruefungen.
MITGENOMMEN, WEIL ES ROT WAR: `pruef-eventkarte` und
`pruef-haus-luecke` bildeten ihr Tagesdatum aus UTC. Zwischen 00:00
und 02:00 deutscher Zeit liegt UTC im Vortag -- ein naechtlicher Lauf
haette an einem Datum gesucht, das er selbst nicht geschrieben hat.
Beide benutzen jetzt `helfer-zeit`. `pruef-struktur` ist damit gruen.
Gemessen (alle gruen, kein einziger Befund)
pruef-anleitung 125 Pruefungen (vorher 70)
pruef-rollen 452 Pruefungen (vorher 406) -- mit Spicy Media
pruef-struktur gruen (vorher rot)
pruef-rechtetafel, pruef-css-klassen, pruef-kachel-universum,
pruef-suchfeld, pruef-kachelraster, pruef-start-ansicht,
pruef-sicht, pruef-eventkarte alle gruen
mess-anleitung Rundgang fuenf Schritte, im letzten genau 5
Muss-Kacheln hervorgehoben; beim zweiten
Aufruf keine Begruessung mehr; kein
Querschieben bei 412 und 1440 px
Sicherung: ~/sicherungen/workspace-vor-anleitung-e37-20261006-1536.db
OFFEN, NICHT VON MIR UND NICHT AUS DIESEM BAUPLAN: pruef-haus-luecke
endet mit Rueckgabewert 3 ("konnte nicht nachsehen") -- eine der
beiden Adressen liefert das Anschlagbrett nicht. Nachgemessen: Das war
vor dieser Arbeit genauso.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -21,6 +21,12 @@
|
||||
|
||||
const BASIS = '/workspace/api/anleitung';
|
||||
|
||||
/* Der zuletzt geladene Stand. Gebraucht, weil die Pflege nach dem
|
||||
Speichern neu zeichnen muss -- und weil der Schalter "bearbeiten"
|
||||
dieselben Daten noch einmal braucht, ohne sie erneut zu holen. */
|
||||
let daten = null;
|
||||
let pflegeAn = false;
|
||||
|
||||
const el = (tag, klasse, text) => {
|
||||
const k = document.createElement(tag);
|
||||
if (klasse) k.className = klasse;
|
||||
@@ -148,7 +154,13 @@
|
||||
schlecht unterscheidet, läse sonst eine Karte ohne Stufe — und
|
||||
die Stufe ist das Einzige, was diese Seite überhaupt sortiert. */
|
||||
namen.append(el('span', 'an-karte__stufe', stufe.name));
|
||||
/* DIE MARKE "NEU" steht als WORT da, nicht als farbiger Punkt.
|
||||
Ein Punkt ohne Wort ist eine Frage, keine Auskunft. Sie
|
||||
verschwindet von selbst: 14 Tage oder bis die Person die Seite
|
||||
nach der Änderung geöffnet hat -- gerechnet wird das im Server. */
|
||||
if (eintrag.neu) namen.append(el('span', 'an-karte__neu', 'Neu'));
|
||||
kopf.append(namen);
|
||||
if (pflegeAn) kopf.append(aendernKnopf(() => karteOeffnen(eintrag, kachel)));
|
||||
karte.append(kopf);
|
||||
|
||||
const zeilen = el('dl', 'an-karte__zeilen');
|
||||
@@ -252,8 +264,11 @@
|
||||
AUFBAUEN
|
||||
===================================================================== */
|
||||
function zeigen(d) {
|
||||
daten = d;
|
||||
$('ohne-fassung').hidden = true;
|
||||
$('fassung').hidden = false;
|
||||
$('pflege').hidden = !d.darf_pflegen;
|
||||
document.body.dataset.pflege = pflegeAn ? 'ja' : 'nein';
|
||||
|
||||
$('leitgedanke').textContent = d.leitgedanke || '';
|
||||
$('leitgedanke').hidden = !d.leitgedanke;
|
||||
@@ -295,6 +310,7 @@
|
||||
rumpf.append(el('span', 'an-schritt__name', s.titel));
|
||||
rumpf.append(el('span', 'an-schritt__text', s.text));
|
||||
li.append(rumpf);
|
||||
if (pflegeAn) li.append(aendernKnopf(() => zeileOeffnen(s, 'schritt')));
|
||||
return li;
|
||||
}));
|
||||
standSetzen(d.fortschritt);
|
||||
@@ -318,6 +334,7 @@
|
||||
const li = el('li', 'an-nicht__zeile');
|
||||
li.append(el('span', 'an-nicht__was', z.titel));
|
||||
li.append(el('span', 'an-nicht__wer', z.text));
|
||||
if (pflegeAn) li.append(aendernKnopf(() => zeileOeffnen(z, 'nicht')));
|
||||
return li;
|
||||
});
|
||||
liste('takt', 'takt-block', d.takt || [], (z) => {
|
||||
@@ -330,8 +347,10 @@
|
||||
const falte = el('details', 'an-frage');
|
||||
falte.append(el('summary', 'an-frage__frage', z.titel));
|
||||
falte.append(el('p', 'an-frage__antwort', z.text));
|
||||
if (pflegeAn) falte.append(aendernKnopf(() => zeileOeffnen(z, 'frage'), 'an-frage__aendern'));
|
||||
return falte;
|
||||
});
|
||||
fehlendeZeigen(d);
|
||||
}
|
||||
|
||||
function ohneFassung(d) {
|
||||
@@ -345,6 +364,178 @@
|
||||
p.hidden = false;
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
DIE PFLEGE (Etappe 4) — nur für DogFather und Spicy Media
|
||||
=====================================================================
|
||||
|
||||
Bearbeitet wird AN ORT UND STELLE. Eine getrennte
|
||||
Verwaltungsansicht hieße, zwischen zwei Fenstern zu vergleichen –
|
||||
und genau dabei entstehen Texte, die nicht zueinander passen.
|
||||
|
||||
Die Rolle wird nicht hier gewählt: Dafür gibt es den
|
||||
Sicht-Umschalter in der Kopfleiste.
|
||||
===================================================================== */
|
||||
|
||||
function aendernKnopf(beiKlick, klasse) {
|
||||
const k = el('button', 'an-aendern' + (klasse ? ' ' + klasse : ''), '✎');
|
||||
k.type = 'button';
|
||||
k.title = 'Bearbeiten';
|
||||
k.setAttribute('aria-label', 'Bearbeiten');
|
||||
k.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); beiKlick(); });
|
||||
return k;
|
||||
}
|
||||
|
||||
/* Welche Kacheln dieser Rolle haben noch keine Karte?
|
||||
Derselbe Abgleich, den pruef-anleitung in beide Richtungen prüft –
|
||||
hier als Angebot, ihn zu schließen. */
|
||||
function fehlendeZeigen(d) {
|
||||
const kasten = $('fehlend-block');
|
||||
if (!d.darf_pflegen || !pflegeAn) { kasten.hidden = true; return; }
|
||||
const alle = kachelnFuer(d.rolle);
|
||||
alle.delete('anleitung');
|
||||
const haben = new Set((d.karten || []).map((k) => k.kachel));
|
||||
const fehlt = [...alle.entries()].filter(([s]) => !haben.has(s));
|
||||
kasten.hidden = !fehlt.length;
|
||||
$('fehlend').replaceChildren(...fehlt.map(([schluessel, kachel]) => {
|
||||
const li = el('li', 'an-fehlend__zeile');
|
||||
li.append(el('span', 'an-fehlend__name', kachel.name));
|
||||
const k = el('button', 'an-fehlend__knopf', 'Eintrag anlegen');
|
||||
k.type = 'button';
|
||||
k.addEventListener('click', async () => {
|
||||
k.disabled = true;
|
||||
try {
|
||||
const a = await hole(`${BASIS}/karte`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ rolle: d.rolle, kachel: schluessel, stufe: 'bedarf',
|
||||
wozu: '', tust: '', merke: '' }),
|
||||
});
|
||||
if (!a.ok) throw new Error('abgelehnt');
|
||||
await laden();
|
||||
} catch { melde('Der Eintrag ließ sich nicht anlegen.'); k.disabled = false; }
|
||||
});
|
||||
li.append(k);
|
||||
return li;
|
||||
}));
|
||||
}
|
||||
|
||||
/* ---- Das Fenster -------------------------------------------------- */
|
||||
let imFenster = null; // { art: 'karte'|'zeile', id, unterart }
|
||||
|
||||
function feld(id, schildId, schild, wert, sichtbar) {
|
||||
$(id).hidden = !sichtbar;
|
||||
if (schildId) $(schildId).textContent = schild;
|
||||
const eingabe = $(id).querySelector('input, textarea, select');
|
||||
if (eingabe) eingabe.value = wert ?? '';
|
||||
}
|
||||
|
||||
function karteOeffnen(eintrag, kachel) {
|
||||
imFenster = { art: 'karte', id: eintrag.id };
|
||||
$('aendern-titel').textContent = kachel.name;
|
||||
$('aendern-unter').textContent = 'Stufe und die drei Zeilen dieser Karte.';
|
||||
const wahl = $('e-stufe');
|
||||
wahl.replaceChildren(...(daten.stufen || []).map((st) => {
|
||||
const o = document.createElement('option');
|
||||
o.value = st.schluessel;
|
||||
o.textContent = st.name;
|
||||
return o;
|
||||
}));
|
||||
wahl.value = eintrag.stufe;
|
||||
$('feld-stufe').hidden = false;
|
||||
feld('feld-a', 'schild-a', 'Wozu', eintrag.wozu, true);
|
||||
feld('feld-b', 'schild-b', 'Was du hier tust', eintrag.tust, true);
|
||||
feld('feld-c', 'schild-c', 'Merke', eintrag.merke, true);
|
||||
$('feld-ziel').hidden = true;
|
||||
$('aendern-fehler').textContent = '';
|
||||
$('aendern').showModal();
|
||||
}
|
||||
|
||||
const ZEILEN_SCHILD = {
|
||||
schritt: ['Kachel', 'Was zu tun ist'],
|
||||
nicht: ['Was du liegen lassen kannst', 'Wer es erledigt'],
|
||||
takt: ['Wann', 'Was du aufmachst'],
|
||||
frage: ['Frage', 'Antwort'],
|
||||
};
|
||||
|
||||
function zeileOeffnen(z, art) {
|
||||
imFenster = { art: 'zeile', id: z.id, unterart: art };
|
||||
const [a, b] = ZEILEN_SCHILD[art] || ['Titel', 'Text'];
|
||||
$('aendern-titel').textContent = z.titel || 'Eintrag';
|
||||
$('aendern-unter').textContent = '';
|
||||
$('feld-stufe').hidden = true;
|
||||
feld('feld-a', 'schild-a', a, z.titel, true);
|
||||
feld('feld-b', 'schild-b', b, z.text, true);
|
||||
$('feld-c').hidden = true;
|
||||
/* Ein Ziel gibt es nur beim Schritt -- bei den anderen wäre das
|
||||
Feld eine Zusage, die nirgends eingelöst wird. */
|
||||
feld('feld-ziel', null, null, z.ziel, art === 'schritt');
|
||||
$('aendern-fehler').textContent = '';
|
||||
$('aendern').showModal();
|
||||
}
|
||||
|
||||
async function aendernSpeichern(e) {
|
||||
e.preventDefault();
|
||||
if (!imFenster) return;
|
||||
const knopf = $('aendern-speichern');
|
||||
knopf.disabled = true;
|
||||
try {
|
||||
const wohin = imFenster.art === 'karte'
|
||||
? `${BASIS}/karte/${imFenster.id}` : `${BASIS}/zeile/${imFenster.id}`;
|
||||
const rumpf = imFenster.art === 'karte'
|
||||
? { stufe: $('e-stufe').value, wozu: $('e-a').value,
|
||||
tust: $('e-b').value, merke: $('e-c').value }
|
||||
: { titel: $('e-a').value, text: $('e-b').value,
|
||||
ziel: imFenster.unterart === 'schritt' ? $('e-ziel').value : null };
|
||||
const a = await hole(wohin, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(rumpf),
|
||||
});
|
||||
if (!a.ok) throw new Error('abgelehnt');
|
||||
$('aendern').close();
|
||||
await laden();
|
||||
} catch {
|
||||
$('aendern-fehler').textContent = 'Das ließ sich nicht speichern.';
|
||||
} finally {
|
||||
knopf.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Wer ist wie weit --------------------------------------------- */
|
||||
async function standZeigen() {
|
||||
const ziel = $('stand-liste');
|
||||
ziel.replaceChildren(el('p', 'leise', 'wird geladen …'));
|
||||
$('stand-dialog').showModal();
|
||||
try {
|
||||
const a = await hole(`${BASIS}/stand`);
|
||||
if (!a.ok) throw new Error('abgelehnt');
|
||||
const r = await a.json();
|
||||
if (!r.leute.length) {
|
||||
ziel.replaceChildren(el('p', 'leise', 'Noch niemand im Team.'));
|
||||
return;
|
||||
}
|
||||
ziel.replaceChildren(...r.leute.map((p) => {
|
||||
const zeile = el('div', 'an-wer');
|
||||
if (p.fertig) zeile.dataset.fertig = 'ja';
|
||||
zeile.append(el('span', 'an-wer__name', p.name));
|
||||
zeile.append(el('span', 'an-wer__rolle', p.rolle));
|
||||
zeile.append(el('span', 'an-wer__stand',
|
||||
p.gesamt === 0 ? 'keine Schritte hinterlegt'
|
||||
: p.fertig ? 'fertig' : `${p.erledigt} von ${p.gesamt}`));
|
||||
/* Das Datum nur, wenn es eines gibt -- „zuletzt: nie" ist eine
|
||||
Zeile, die nichts sagt und trotzdem Platz braucht. */
|
||||
if (p.zuletzt) {
|
||||
zeile.append(el('span', 'an-wer__wann',
|
||||
new Date(p.zuletzt).toLocaleDateString('de-DE',
|
||||
{ day: '2-digit', month: '2-digit', year: 'numeric' })));
|
||||
}
|
||||
return zeile;
|
||||
}));
|
||||
} catch {
|
||||
ziel.replaceChildren(el('p', 'leise', 'Der Stand ließ sich nicht laden.'));
|
||||
}
|
||||
}
|
||||
|
||||
function melde(grund) {
|
||||
$('ladefehler-grund').textContent = grund || '';
|
||||
$('ladefehler').hidden = false;
|
||||
@@ -364,6 +555,17 @@
|
||||
zeigen(d);
|
||||
}
|
||||
|
||||
$('pflege-an').addEventListener('click', () => {
|
||||
pflegeAn = !pflegeAn;
|
||||
$('pflege-an').setAttribute('aria-pressed', String(pflegeAn));
|
||||
$('pflege-satz').textContent = pflegeAn
|
||||
? 'Änderungen gelten sofort für alle dieser Rolle.' : '';
|
||||
if (daten) zeigen(daten);
|
||||
});
|
||||
$('pflege-stand').addEventListener('click', () => { standZeigen().catch(() => {}); });
|
||||
$('stand-zu').addEventListener('click', () => $('stand-dialog').close());
|
||||
$('aendern-form').addEventListener('submit', (e) => { aendernSpeichern(e).catch(() => {}); });
|
||||
$('aendern-abbruch').addEventListener('click', () => $('aendern').close());
|
||||
$('nochmal').addEventListener('click', () => { laden().catch(() => {}); });
|
||||
laden().catch(() => melde('Unerwarteter Fehler.'));
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
/* =====================================================================
|
||||
rundgang.js — Begrüßung und Rundgang beim ersten Anmelden (Etappe 6)
|
||||
|
||||
Bauplan Kapitel 10. Fünf Schritte über die Zentrale, im letzten
|
||||
leuchten nur die Muss-Kacheln, am Ende steht man auf der Anleitung
|
||||
bei den Ersten Schritten.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
ER DARF DIE SEITE UNTER KEINEN UMSTÄNDEN BLOCKIEREN
|
||||
|
||||
Das ist die wichtigste Eigenschaft dieser Datei, und sie ist der
|
||||
Grund, warum sie eine eigene ist. Sie läuft auf `start.html` —
|
||||
der meistbenutzten Seite des Hauses, für jede Rolle, bei jedem
|
||||
Aufruf. Geht hier etwas schief, darf davon nichts anderes betroffen
|
||||
sein.
|
||||
|
||||
Deshalb:
|
||||
* eigene Datei, eigenes `defer` — kein Platz in start.js, wo ein
|
||||
Fehler die Kacheln mitnähme;
|
||||
* alles in `try`, und der Fehlerfall ist „nichts tun";
|
||||
* der Server antwortet IMMER mit 200 (notfalls `zeigen: false`),
|
||||
damit in der Konsole nichts Rotes steht;
|
||||
* wird ein Zielbereich nicht gefunden, wird der Schritt
|
||||
übersprungen statt auf ihn zu warten. Eine Einweisung, die auf
|
||||
ein Element wartet, das es nicht gibt, ist genau die Sorte
|
||||
Stillstand, die schlimmer ist als ein Fehlschlag.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
ER STARTET GENAU EINMAL JE PERSON UND ROLLE
|
||||
|
||||
Gemerkt wird das serverseitig (`anl_einweisung`), nicht im Browser:
|
||||
Wer sich am Telefon anmeldet und später am Rechner, hat sie schon
|
||||
gesehen. Und weil der Schlüssel (Person, Rolle) ist, startet sie
|
||||
nach einem Rollenwechsel von selbst noch einmal — ohne dass jemand
|
||||
daran denken muss.
|
||||
===================================================================== */
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
const BASIS = '/workspace/api/anleitung';
|
||||
|
||||
const el = (tag, klasse, text) => {
|
||||
const k = document.createElement(tag);
|
||||
if (klasse) k.className = klasse;
|
||||
if (text !== undefined) k.textContent = text;
|
||||
return k;
|
||||
};
|
||||
|
||||
/* Die fünf Stationen. Wird ein Ziel nicht gefunden, fällt der
|
||||
Schritt weg — die Zentrale sieht je nach Rolle anders aus, und
|
||||
eine Einweisung, die auf einem fehlenden Kasten stehen bleibt,
|
||||
wäre schlimmer als eine um einen Schritt kürzere. */
|
||||
const STATIONEN = [
|
||||
{ wo: '.willkommen', titel: 'Deine Zentrale',
|
||||
text: 'Hier siehst du auf einen Blick, wie dein Tag steht: der Ring, '
|
||||
+ 'der nächste Termin und was offen ist.' },
|
||||
{ wo: '#dran-seite', titel: 'Was ist dran',
|
||||
text: 'Die wichtigste Liste des Hauses. Was hier steht, will etwas von dir – '
|
||||
+ 'und es verschwindet von selbst, sobald es erledigt ist.' },
|
||||
{ wo: '#heute-seite', titel: 'Heute',
|
||||
text: 'Deine Termine für heute. Steht nichts da, steht wirklich nichts an.' },
|
||||
{ wo: '#zahlen-block', titel: 'Deine Aufgaben',
|
||||
text: 'Offen, in Arbeit, zur Freigabe, erledigt. Ein Tippen bringt dich '
|
||||
+ 'ins Aufgabenbrett.' },
|
||||
{ wo: '#bereiche', titel: 'Mit diesen fängst du an',
|
||||
text: 'Alles andere kannst du erst einmal liegen lassen. Die Anleitung '
|
||||
+ 'sagt dir zu jeder Kachel, wofür sie da ist.', nurMuss: true },
|
||||
];
|
||||
|
||||
let schritt = 0;
|
||||
let stationen = [];
|
||||
let muss = [];
|
||||
let schale = null;
|
||||
|
||||
/* ---------- Das Hervorheben ---------------------------------------
|
||||
Kein Loch in einer Abdeckung (das müsste bei jedem Scrollen neu
|
||||
gerechnet werden), sondern ein Rahmen um das Ziel plus ein
|
||||
Merkmal am Element. Die Abdunklung macht CSS über
|
||||
`body[data-rundgang]` — damit bleibt die Seite benutzbar, falls
|
||||
dieses Skript mittendrin stirbt: Ein Attribut am body ist weg,
|
||||
sobald die Seite neu lädt. */
|
||||
function hervorheben(ziel) {
|
||||
for (const e of document.querySelectorAll('[data-rundgang-ziel]')) {
|
||||
e.removeAttribute('data-rundgang-ziel');
|
||||
}
|
||||
for (const e of document.querySelectorAll('[data-rundgang-muss]')) {
|
||||
e.removeAttribute('data-rundgang-muss');
|
||||
}
|
||||
if (ziel) ziel.setAttribute('data-rundgang-ziel', 'ja');
|
||||
}
|
||||
|
||||
/* Nur die Muss-Kacheln leuchten lassen. Der Schlüssel kommt aus
|
||||
derselben Funktion, mit der auch die Anleitung ihre Karten
|
||||
verbindet — eine zweite Rechnung hier wäre die, die beim nächsten
|
||||
Fragezeichen in einer Adresse danebenliegt. */
|
||||
function mussMarkieren() {
|
||||
const B = window.Bereiche;
|
||||
if (!B || typeof B.schluesselVonZiel !== 'function') return 0;
|
||||
let n = 0;
|
||||
for (const a of document.querySelectorAll('#bereiche .kachel__link')) {
|
||||
const s = B.schluesselVonZiel(a.getAttribute('href') || '');
|
||||
if (muss.includes(s)) {
|
||||
a.closest('.kachel')?.setAttribute('data-rundgang-muss', 'ja');
|
||||
n++;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
function zeichnen() {
|
||||
const st = stationen[schritt];
|
||||
if (!st) { beenden(true); return; }
|
||||
const ziel = document.querySelector(st.wo);
|
||||
hervorheben(ziel);
|
||||
if (st.nurMuss) mussMarkieren();
|
||||
|
||||
schale.querySelector('.rg__zaehler').textContent =
|
||||
`Schritt ${schritt + 1} von ${stationen.length}`;
|
||||
schale.querySelector('.rg__titel').textContent = st.titel;
|
||||
schale.querySelector('.rg__text').textContent = st.text;
|
||||
schale.querySelector('.rg__weiter').textContent =
|
||||
schritt === stationen.length - 1 ? 'Zur Anleitung' : 'Weiter';
|
||||
|
||||
/* Sanft hinscrollen, aber nur, wenn jemand das möchte. */
|
||||
try {
|
||||
ziel?.scrollIntoView({
|
||||
behavior: matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
} catch { /* ältere Browser: dann eben ohne */ }
|
||||
}
|
||||
|
||||
async function gesehenMerken() {
|
||||
try {
|
||||
await fetch(`${BASIS}/einweisung/gesehen`, {
|
||||
method: 'POST', credentials: 'same-origin',
|
||||
});
|
||||
} catch { /* Beim nächsten Mal eben noch einmal -- kein Schaden. */ }
|
||||
}
|
||||
|
||||
function beenden(zurAnleitung) {
|
||||
document.body.removeAttribute('data-rundgang');
|
||||
hervorheben(null);
|
||||
schale?.remove();
|
||||
schale = null;
|
||||
gesehenMerken().finally(() => {
|
||||
if (zurAnleitung) location.assign('anleitung.html#schritte');
|
||||
});
|
||||
}
|
||||
|
||||
function rundgangStarten() {
|
||||
stationen = STATIONEN.filter((s) => document.querySelector(s.wo));
|
||||
if (!stationen.length) { beenden(false); return; }
|
||||
schritt = 0;
|
||||
document.body.dataset.rundgang = 'ja';
|
||||
|
||||
schale = el('div', 'rg');
|
||||
schale.setAttribute('role', 'dialog');
|
||||
schale.setAttribute('aria-modal', 'false');
|
||||
schale.setAttribute('aria-label', 'Rundgang');
|
||||
const kasten = el('div', 'rg__kasten');
|
||||
kasten.append(el('p', 'rg__zaehler'));
|
||||
kasten.append(el('h2', 'rg__titel'));
|
||||
kasten.append(el('p', 'rg__text'));
|
||||
const knoepfe = el('div', 'rg__knoepfe');
|
||||
const abbruch = el('button', 'rg__still', 'Abbrechen');
|
||||
abbruch.type = 'button';
|
||||
abbruch.addEventListener('click', () => beenden(false));
|
||||
const weiter = el('button', 'rg__weiter knopf', 'Weiter');
|
||||
weiter.type = 'button';
|
||||
weiter.addEventListener('click', () => { schritt++; zeichnen(); });
|
||||
knoepfe.append(abbruch, weiter);
|
||||
kasten.append(knoepfe);
|
||||
schale.append(kasten);
|
||||
document.body.append(schale);
|
||||
|
||||
/* Escape beendet -- wie bei jedem Fenster im Haus. */
|
||||
document.addEventListener('keydown', function esc(e) {
|
||||
if (e.key !== 'Escape' || !schale) return;
|
||||
document.removeEventListener('keydown', esc);
|
||||
beenden(false);
|
||||
});
|
||||
|
||||
zeichnen();
|
||||
weiter.focus();
|
||||
}
|
||||
|
||||
/* ---------- Die Begrüßung ------------------------------------------ */
|
||||
function begruessen(d) {
|
||||
const fenster = el('dialog', 'dialog rg-gruss');
|
||||
const rumpf = el('div', 'rg-gruss__rumpf');
|
||||
rumpf.append(el('h2', 'dialog__titel',
|
||||
d.name ? `Willkommen, ${d.name}!` : 'Willkommen im Creator Workspace'));
|
||||
rumpf.append(el('p', 'rg-gruss__text',
|
||||
'In fünf Minuten zeige ich dir, was für dich wichtig ist und was nicht.'));
|
||||
const knoepfe = el('div', 'dialog__knoepfe');
|
||||
const spaeter = el('button', 'knopf-still', 'Später');
|
||||
spaeter.type = 'button';
|
||||
const los = el('button', 'knopf', 'Los geht’s');
|
||||
los.type = 'button';
|
||||
knoepfe.append(spaeter, los);
|
||||
rumpf.append(knoepfe);
|
||||
fenster.append(rumpf);
|
||||
document.body.append(fenster);
|
||||
|
||||
/* „SPÄTER" IST ERLAUBT und zählt trotzdem als gezeigt. Ein
|
||||
Fenster, das so lange wiederkommt, bis man das Richtige
|
||||
anklickt, ist keine Einladung. Dass noch etwas offen ist, sagt
|
||||
danach der Hinweis „Erste Schritte noch offen" — der ist leiser
|
||||
und steht da, bis er erledigt ist. */
|
||||
spaeter.addEventListener('click', () => {
|
||||
fenster.close();
|
||||
fenster.remove();
|
||||
gesehenMerken();
|
||||
});
|
||||
los.addEventListener('click', () => {
|
||||
fenster.close();
|
||||
fenster.remove();
|
||||
rundgangStarten();
|
||||
});
|
||||
/* GEMERKT WIRD BEIM ZEIGEN, NICHT BEIM BEENDEN (06.10.2026).
|
||||
|
||||
Zuerst stand das nur in `beenden()`. Gemessen mit
|
||||
mess-anleitung: Wer "Los geht's" drueckt und dann weggeht --
|
||||
Seite gewechselt, Telefon weggelegt, Fenster zu --, hat den
|
||||
Rundgang nie beendet. Beim naechsten Aufruf kam die Begruessung
|
||||
wieder. Und beim uebernaechsten. Fuer immer.
|
||||
|
||||
Die Tatsache, die gespeichert werden soll, ist "sie wurde
|
||||
gezeigt" -- und die steht in dem Moment fest, in dem das
|
||||
Fenster aufgeht. Alles andere ist eine Bedingung, die der
|
||||
Mensch erfuellen muss, damit ihn die Software in Ruhe laesst. */
|
||||
gesehenMerken();
|
||||
fenster.showModal();
|
||||
los.focus();
|
||||
}
|
||||
|
||||
/* ---------- Start --------------------------------------------------- */
|
||||
async function pruefen() {
|
||||
let a;
|
||||
try { a = await fetch(`${BASIS}/einweisung`, { credentials: 'same-origin' }); }
|
||||
catch { return; }
|
||||
if (!a.ok) return;
|
||||
let d;
|
||||
try { d = await a.json(); } catch { return; }
|
||||
muss = Array.isArray(d.muss) ? d.muss : [];
|
||||
|
||||
/* Der Knopf „Rundgang noch einmal starten" steht auf der
|
||||
Anleitung; von dort kommt man mit `#rundgang` zurück. Das geht
|
||||
auch, wenn die Einweisung längst gezeigt wurde — sonst wäre
|
||||
der Knopf dort einer, der nichts tut. */
|
||||
if (location.hash === '#rundgang') {
|
||||
history.replaceState(null, '', location.pathname);
|
||||
rundgangStarten();
|
||||
return;
|
||||
}
|
||||
if (d.zeigen) begruessen(d);
|
||||
}
|
||||
|
||||
/* Erst wenn die Kacheln stehen: Der letzte Schritt leuchtet sie an,
|
||||
und vorher gibt es sie nicht. Zwei Sekunden sind gemessen reichlich
|
||||
(die Kacheln stehen nach rund 300 ms) -- und falls doch nicht,
|
||||
fällt nur die Markierung weg, nicht der Rundgang. */
|
||||
if (document.readyState === 'complete') setTimeout(pruefen, 1200);
|
||||
else window.addEventListener('load', () => setTimeout(pruefen, 1200));
|
||||
})();
|
||||
Reference in New Issue
Block a user