/* ===================================================================== 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)); })();