// ----------------------------- // Einführung — geführter Rundgang // ----------------------------- // Eriks Wunsch: Jeder Nutzer, auch bestehende, macht einmal einen geführten // Rundgang durch die Anwendung; unter Einstellungen → Mein Konto lässt er // sich jederzeit neu starten. Der Rundgang hebt den jeweiligen Eintrag der // Seitenleiste hervor (Aussparung in einer abgedunkelten Ebene), wechselt in // den Bereich und erklärt ihn in einer Sprechblase. // // Gezeigt wird nur, was die Rolle darf (hatRecht(), Rollen & Rechte) — ein // Konto ohne „Anwendung nutzen“ (Wandtafel) bekommt keinen Rundgang. // Abgeschlossen oder übersprungen → POST /einfuehrung (users.einfuehrung_am), // danach startet er nicht mehr von selbst. Esc bzw. „Später“ schließt nur // für diesen Seitenaufruf (sessionStorage) — beim nächsten Laden kommt er // wieder. // // Ein Schritt: { id, recht?, wenn?(), tab?, ziel? (CSS-Selektor), titel, // text (HTML, aus festen Texten) }. Ohne ziel steht die Blase in der Mitte. const EINFUEHRUNG_SCHRITTE = [ { id: 'start', titel: 'Willkommen in der Projektübersicht', text: () => `Hallo ${esc((currentUser && (currentUser.displayName || '').split(' ')[0]) || '')}! In gut zwei Minuten zeigen wir dir, wo du was findest. Mit den Pfeiltasten oder „Weiter“ geht es durch, „Später“ schließt den Rundgang für jetzt.` }, { id: 'dashboard', recht: 'app', tab: 'dashboard', ziel: '.sidebar-nav [data-tab="dashboard"]', titel: 'Dashboard', text: () => 'Deine Startseite: die Aufgaben, die dir zugewiesen sind, die nächsten Termine deiner Projekte und ein Kalender mit Meilensteinen und Abwesenheiten. Überfälliges steht oben.' }, { id: 'glocke', recht: 'app', ziel: '#glockeBtn', titel: 'Benachrichtigungen', text: () => 'Die Glocke zeigt, wenn dir jemand eine neue Aufgabe zugewiesen hat. Ein Klick springt direkt in die Aufgaben des Projekts.' }, { id: 'projekte', recht: 'app', tab: 'board', ziel: '.sidebar-nav [data-tab="board"]', titel: 'Projekte', text: () => 'Alle Projekte der laufenden Sitzung, nach Gruppen.' + (hatRecht('proj.anlegen') ? ' Über „+ Projekt“ legst du ein neues an — ein Assistent fragt Gewerke, Auftrag, Team und Terminplan ab.' : '') }, { id: 'projektbereich', recht: 'app', tab: 'board', ziel: '#board .board-name', titel: 'Der Projektbereich', text: () => 'Ein Klick auf den Projektnamen öffnet den Bereich des Projekts. Oben stehen die Reiter: Dashboard, Terminplan, Leistungsphasen, Aufgaben, Leistung, Dateien, Protokolle und Einstellungen.' }, { id: 'terminplan', recht: 'proj.plan', tab: 'board', titel: 'Terminplan und Vorlagen', text: () => 'Im Reiter Terminplan legst du über „+ Neu“ Leistungsphasen, Aufgaben und Meilensteine an. „+ Vorlage“ fügt die Grundleistungen der HOAI für dein Gewerk ein — die Dauer verteilt sich nach den Siemon-Anteilen über den Phasenbalken. Gib Aufgaben einen Aufwand in Stunden, dann zählen sie in der Auslastung.' }, { id: 'wochenbericht', recht: 'app', tab: 'wochenbericht', ziel: '.sidebar-nav [data-tab="wochenbericht"]', titel: 'Wochenbericht', text: () => 'Die Tagesordnung der wöchentlichen Besprechung, je Gewerk. Links die Projekte, rechts das gewählte.' + (hatRecht('wb.kommentar') ? ' Hier schreibst du den Kommentar der Woche und den Fortschritt der Leistungsphasen; „Besprochen & weiter“ führt zum nächsten Projekt.' : '') + ' „Als PDF ablegen“ speichert den Bericht.' }, { id: 'auslastung', recht: 'app', tab: 'load', ziel: '.sidebar-nav [data-tab="load"]', titel: 'Auslastung', text: () => 'Wie voll die nächsten zwölf Wochen für jede Person sind. Die Stunden kommen aus den Aufgaben mit Aufwand — eine Aufgabe für zwei Personen zählt für jede zur Hälfte.' }, { id: 'abwesenheiten', wenn: () => typeof darfIrgendeineAbwesenheit === 'function' && darfIrgendeineAbwesenheit(), tab: 'abwesenheit', ziel: '.sidebar-nav [data-tab="abwesenheit"]', titel: 'Abwesenheiten', text: () => 'Hier trägst du Urlaub, Krankheit und Fortbildung ein. Unten siehst du das Team im Monat — wer außerhalb deines Bereichs liegt, erscheint nur als „abwesend“.' + (hatRecht('team.abwAlle') || hatRecht('team.abwGewerk') || hatRecht('team.abwProjekte') ? ' Ein Klick auf einen Tag in der Zeile einer Person trägt für sie ein.' : '') }, { id: 'planung', recht: 'wz.planung', tab: 'planung', ziel: '.sidebar-nav [data-tab="planung"]', titel: 'Planung → Terminplan', text: () => 'Terminpläne ohne echtes Projekt — zum Ausprobieren, für Angebote oder um Kapazität abzuschätzen. Aus einem Honorar errechnet oder frei angelegt.' }, { id: 'planchecker', recht: 'wz.checker', tab: 'planchecker', ziel: '.sidebar-nav [data-tab="planchecker"]', titel: 'Plan Checker', text: () => 'Lädt einen Plan als PDF hoch und lässt ihn von einer KI gegen eine Liste von Merkmalen prüfen. Dafür brauchst du einen eigenen KI-Schlüssel unter Einstellungen.' }, { id: 'einstellungen', tab: 'settings', ziel: '.sidebar-nav [data-tab="settings"]', titel: 'Einstellungen', text: () => 'Dein Konto: Passwort, E-Mail-Adresse, deine KI-Schlüssel und die Darstellung. Hier kannst du diesen Rundgang jederzeit neu starten.' }, { id: 'admin', wenn: () => typeof hatAdminRecht === 'function' && hatAdminRecht(), ziel: '#adminAccordionToggle', titel: 'Admin', text: () => 'Du darfst Teile der Verwaltung pflegen: ' + esc(typeof ADMIN_SECTIONS !== 'undefined' ? ADMIN_SECTIONS.filter(adminAbschnittErlaubt).map(s => s.title).join(', ') : '') + '.' }, { id: 'ende', tab: 'dashboard', titel: 'Fertig!', text: () => { const hinweise = []; if(currentUser && !currentUser.personId){ hinweise.push('Dein Konto ist noch keiner Person zugeordnet — „Meine Aufgaben“ bleibt deshalb leer. Bitte einen Administrator, das in der Benutzerverwaltung nachzutragen.'); } return 'Das war der Überblick. Bei Fragen hilft dir ein Kollege — oder du startest den Rundgang unter Einstellungen noch einmal.' + (hinweise.length ? '' : ''); } }, ]; let tourSchritte = []; let tourIndex = 0; let tourAktiv = false; let tourVorTab = null; // einfuehrungBeiBedarf: startet den Rundgang, wenn er für dieses Konto noch // aussteht — sobald die Daten geladen sind und kein Dialog (Passwort) offen // ist. Von afterAuthenticated() und nach dem Pflicht-Passwortwechsel gerufen. function einfuehrungBeiBedarf(){ if(!currentUser || !currentUser.einfuehrungFaellig || !hatRecht('app')) return; try { if(sessionStorage.getItem('projuebersicht-tour-spaeter')) return; } catch(e){} let versuche = 0; const pruefen = () => { if(tourAktiv) return; const geladen = typeof currentWeek === 'function' && currentWeek(); const dialogOffen = [...document.querySelectorAll('.modal-bg')].some(m => m.style.display && m.style.display !== 'none'); const login = document.getElementById('loginOverlay'); const loginOffen = login && login.style.display !== 'none' && !login.hidden && login.offsetParent !== null; if(geladen && !dialogOffen && !loginOffen) return einfuehrungStarten(); if(++versuche < 120) setTimeout(pruefen, 500); }; setTimeout(pruefen, 600); } // einfuehrungStarten: Rundgang von vorn (auch aus Einstellungen). function einfuehrungStarten(){ tourSchritte = EINFUEHRUNG_SCHRITTE.filter(s => (!s.recht || hatRecht(s.recht)) && (!s.wenn || s.wenn())); tourIndex = 0; tourAktiv = true; tourVorTab = typeof currentTab !== 'undefined' ? currentTab : null; let el = document.getElementById('tourEbene'); if(!el){ el = document.createElement('div'); el.id = 'tourEbene'; el.className = 'tour'; el.innerHTML = '
'; document.body.appendChild(el); window.addEventListener('resize', () => { if(tourAktiv) tourPositionieren(); }); document.addEventListener('keydown', tourTaste, true); } el.hidden = false; tourZeigen(); } function tourTaste(e){ if(!tourAktiv) return; if(e.key === 'Escape'){ e.preventDefault(); e.stopPropagation(); tourSpaeter(); } else if(e.key === 'ArrowRight' || (e.key === 'Enter' && !e.target.closest('button'))){ e.preventDefault(); tourWeiter(); } else if(e.key === 'ArrowLeft'){ e.preventDefault(); tourZurueck(); } } // tourZeigen: aktuellen Schritt aufbauen (Bereich wechseln, Blase füllen, // danach Aussparung und Blase an das Ziel legen). function tourZeigen(){ const s = tourSchritte[tourIndex]; if(!s) return tourBeenden(true); if(s.tab && typeof showTab === 'function' && currentTab !== s.tab) showTab(s.tab); const blase = document.querySelector('#tourEbene .tour-blase'); const letzter = tourIndex === tourSchritte.length - 1; blase.innerHTML = `
${tourIndex + 1} von ${tourSchritte.length}

${esc(s.titel)}

${s.text()}
${letzter ? '' : ''} ${tourIndex > 0 ? '' : ''}
`; // Nach dem Bereichswechsel kurz warten, bis die Seite steht. requestAnimationFrame(() => { tourPositionieren(); setTimeout(tourPositionieren, 260); }); const knopf = document.getElementById('tourWeiter'); if(knopf) knopf.focus({ preventScroll: true }); } // tourZiel: sichtbares Ziel-Element des Schritts oder null (dann Mitte). function tourZiel(s){ if(!s || !s.ziel) return null; const el = document.querySelector(s.ziel); if(!el || el.hidden || el.offsetParent === null) return null; const r = el.getBoundingClientRect(); if(r.width < 2 || r.height < 2 || r.right < 0 || r.left > innerWidth) return null; return el; } function tourPositionieren(){ const ebene = document.getElementById('tourEbene'); if(!ebene || !tourAktiv) return; const s = tourSchritte[tourIndex]; const loch = ebene.querySelector('.tour-loch'); const blase = ebene.querySelector('.tour-blase'); const ziel = tourZiel(s); const schmal = innerWidth < 640; if(!ziel){ ebene.classList.add('ohne-ziel'); blase.style.left = Math.max(12, (innerWidth - blase.offsetWidth) / 2) + 'px'; blase.style.top = schmal ? '' : Math.max(12, (innerHeight - blase.offsetHeight) / 2) + 'px'; blase.classList.toggle('ist-unten', schmal); return; } ebene.classList.remove('ohne-ziel'); ziel.scrollIntoView({ block: 'nearest', inline: 'nearest' }); const r = ziel.getBoundingClientRect(); const pad = 6; Object.assign(loch.style, { left: (r.left - pad) + 'px', top: (r.top - pad) + 'px', width: (r.width + 2 * pad) + 'px', height: (r.height + 2 * pad) + 'px' }); blase.classList.toggle('ist-unten', schmal); if(schmal){ blase.style.left = '12px'; blase.style.top = ''; return; } const bw = blase.offsetWidth, bh = blase.offsetHeight, abstand = 16; // Bevorzugt rechts vom Ziel (Seitenleiste links), sonst darunter/darüber. let left, top; if(r.right + abstand + bw < innerWidth - 12){ left = r.right + abstand; top = r.top + r.height / 2 - bh / 2; } else if(r.bottom + abstand + bh < innerHeight - 12){ left = r.left + r.width / 2 - bw / 2; top = r.bottom + abstand; } else { left = r.left + r.width / 2 - bw / 2; top = r.top - abstand - bh; } blase.style.left = Math.min(Math.max(12, left), innerWidth - bw - 12) + 'px'; blase.style.top = Math.min(Math.max(12, top), innerHeight - bh - 12) + 'px'; } function tourWeiter(){ if(tourIndex >= tourSchritte.length - 1) return tourBeenden(true); tourIndex++; tourZeigen(); } function tourZurueck(){ if(tourIndex === 0) return; tourIndex--; tourZeigen(); } // tourSpaeter: schließt nur für diesen Seitenaufruf (kommt beim nächsten // Laden wieder, solange er nicht abgeschlossen ist). function tourSpaeter(){ try { sessionStorage.setItem('projuebersicht-tour-spaeter', '1'); } catch(e){} tourSchliessen(); if(tourVorTab && typeof showTab === 'function') showTab(tourVorTab); } // tourBeenden: abgeschlossen oder übersprungen — am Server vermerken. function tourBeenden(){ tourSchliessen(); if(currentUser) currentUser.einfuehrungFaellig = false; fetch(baseUrl() + '/einfuehrung', { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8' }, body: JSON.stringify({ erledigt: true }) }).catch(err => console.error('Einführung konnte nicht vermerkt werden:', err)); if(typeof showTab === 'function') showTab('dashboard'); } function tourSchliessen(){ tourAktiv = false; const el = document.getElementById('tourEbene'); if(el) el.hidden = true; }