/* ===================================================================== rundgang.js — Begrüßung und Rundgang beim ersten Anmelden (Etappe 6) Bauplan Kapitel 10. Ein Rundgang über die Zentrale — für die Community drei Schritte, für die Teamrollen fünf (zusätzlich die Arbeitslisten). Im letzten leuchten nur die Muss-Kacheln, am Ende steht man auf der Anleitung bei den Ersten Schritten. Welche der beiden Längen gilt, sagt der SERVER (`team: true/false`) — hier steht absichtlich kein einziger Rollenname. Sonst wäre jede neue Rolle, die irgendwann dazukommt, stillschweigend eine Teamrolle, und niemand käme auf die Idee, in dieser Datei nachzusehen. --------------------------------------------------------------------- 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.' }, /* NUR FUERS TEAM (Bauplan Kapitel 10). Die Community bekommt drei Stationen, die Teamrollen fuenf -- "zusaetzlich Deine Aufgaben" und die Arbeitslisten. Wer nur mitliest, hat hier nichts stehen, und eine Station vor einem leeren Kasten erklaert nichts. */ { wo: '#dran-seite', nurTeam: true, 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', nurTeam: true, 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 = []; /* Teamrolle oder Community? Kommt vom Server als Ja/Nein -- ein Rollenname stuende hier falsch (siehe den Kopf dieser Datei). */ let istTeam = true; 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() { /* ZWEI SIEBE, UND BEIDE SIND NOETIG: `nurTeam` nimmt der Community die Arbeitslisten (Bauplan Kapitel 10), `querySelector` nimmt jedem das, was auf SEINER Zentrale gar nicht steht. Ohne das zweite bliebe die Einweisung vor einem Kasten stehen, den es nicht gibt. */ stationen = STATIONEN .filter((s) => (s.nurTeam ? istTeam : true)) .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', /* OHNE NAMEN BEWUSST OHNE HAUSNAMEN: Dieselbe Datei läuft auf beiden Adressen. „Willkommen im Creator Workspace" wäre bei Team Dogi das falsche Haus, und ein Haus zu nennen, das man nicht kennt, ist schlimmer als keins zu nennen. */ d.name ? `Willkommen, ${d.name}!` : 'Willkommen!')); rumpf.append(el('p', 'rg-gruss__text', 'Schön, dass du da bist. In zwei Minuten zeige ich dir, ' + 'was hier für dich wichtig ist.')); 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 : []; istTeam = d.team !== false; /* 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)); })();