// ----------------------------- // Projektübersicht — Benachrichtigungen (Glocke im Menüband + Dashboard) // ----------------------------- // Eriks Wunsch: Wer eine neue Aufgabe zugewiesen bekommt, soll das in der // Übersicht sehen. Der Server legt die Einträge beim Speichern einer Sitzung // an (neueZuweisungenMelden(), server.js); dieses Modul zeigt sie: // // - die GLOCKE im Menüband (#glockeBtn) mit der Zahl der ungelesenen, ein // Klick öffnet die Liste (#glockeMenue). Ein Eintrag führt in den // Bereich "Aufgaben" seines Projekts und gilt damit als gelesen. // - auf "Meine Aufgaben" (#dashNeu, js/dashboard/dashboard.js) die // ungelesenen als eigener Block "Neu zugewiesen". // // Abgefragt wird beim Laden der Daten (renderAll()) und danach jede Minute — // eine Zuweisung durch einen Kollegen erscheint also spätestens nach einer // Minute, ohne neu zu laden. Kiosk-Konten fragen nicht (der Server würde sie // ohnehin mit 403 abweisen). // // Nutzt baseUrl() (js/server.js), currentUser (js/auth.js), esc()/ // formatWeekDate() (js/utils.js), currentWeek(), openProjectPlan() und // openSandboxPlan(). let benachrichtigungen = { ungelesen: 0, eintraege: [] }; let benachrichtigungenZuletzt = 0; const BENACHRICHTIGUNG_INTERVALL_MS = 60 * 1000; // ladeBenachrichtigungen: holt GET /benachrichtigungen und zeichnet Glocke, // offene Liste und Dashboard-Block neu. Mehrfachaufrufe innerhalb von fünf // Sekunden werden zusammengefasst (renderAll() läuft oft mehrmals hintereinander). // Parameter: erzwingen (Boolean, optional). Keine Rückgabe. async function ladeBenachrichtigungen(erzwingen){ if(!currentUser || !hatRecht('app')) return; if(!erzwingen && Date.now() - benachrichtigungenZuletzt < 5000) return; benachrichtigungenZuletzt = Date.now(); // Die Glocke erscheint für jedes angemeldete Konto, auch wenn der Abruf // scheitert (etwa ein noch nicht neu gestarteter Server ohne die Route) — // sie dann still auszublenden, sah aus, als fehle die Funktion ganz. try { const res = await fetch(baseUrl() + '/benachrichtigungen'); const data = await res.json().catch(() => null); if(res.ok && data && data.ok){ benachrichtigungen = { ungelesen: data.ungelesen || 0, eintraege: Array.isArray(data.eintraege) ? data.eintraege : [] }; } } catch(e){ /* still — die Glocke ist kein Grund für eine Fehlermeldung */ } renderGlocke(); if(glockeOffen()) renderGlockeMenue(); renderDashNeu(); } setInterval(() => ladeBenachrichtigungen(true), BENACHRICHTIGUNG_INTERVALL_MS); // renderGlocke: Zahl am Glockensymbol (ausgeblendet bei 0). function renderGlocke(){ const btn = document.getElementById('glockeBtn'); const zahl = document.getElementById('glockeZahl'); if(!btn || !zahl) return; btn.hidden = !currentUser || !hatRecht('app'); const n = benachrichtigungen.ungelesen; zahl.textContent = n > 9 ? '9+' : String(n); zahl.hidden = n === 0; btn.title = n ? n + (n === 1 ? ' neue Benachrichtigung' : ' neue Benachrichtigungen') : 'Benachrichtigungen'; btn.classList.toggle('hat-neue', n > 0); } function glockeOffen(){ const m = document.getElementById('glockeMenue'); return !!(m && !m.hidden); } // toggleGlocke: Liste öffnen/schließen. Beim Öffnen frisch laden. function toggleGlocke(){ const m = document.getElementById('glockeMenue'); const btn = document.getElementById('glockeBtn'); if(!m) return; if(glockeOffen()){ schliesseGlocke(); return; } m.hidden = false; if(btn) btn.setAttribute('aria-expanded', 'true'); renderGlockeMenue(); ladeBenachrichtigungen(true); setTimeout(() => { document.addEventListener('click', glockeAussenKlick); document.addEventListener('keydown', glockeEscape); }, 0); } function schliesseGlocke(){ const m = document.getElementById('glockeMenue'); const btn = document.getElementById('glockeBtn'); if(m) m.hidden = true; if(btn) btn.setAttribute('aria-expanded', 'false'); document.removeEventListener('click', glockeAussenKlick); document.removeEventListener('keydown', glockeEscape); } function glockeAussenKlick(e){ // composedPath() statt e.target.closest(): Ein Klick auf „ד oder „Alle // löschen“ zeichnet die Liste neu, bevor dieser Lauscher läuft — das // angeklickte Element hängt dann nicht mehr im Menü, closest() fände es // nicht, und das Fenster ginge zu. Der Pfad wird beim Auslösen festgehalten. const pfad = e.composedPath ? e.composedPath() : []; const drin = el => el && (pfad.includes(el) || e.target.closest && e.target.closest('#' + el.id)); if(drin(document.getElementById('glockeMenue')) || drin(document.getElementById('glockeBtn'))) return; schliesseGlocke(); } function glockeEscape(e){ if(e.key === 'Escape') schliesseGlocke(); } // zeitVor: "gerade eben", "vor 5 Min.", "vor 3 Std.", sonst das Datum. function zeitVor(iso){ const t = Date.parse(iso); if(isNaN(t)) return ''; const min = Math.round((Date.now() - t) / 60000); if(min < 1) return 'gerade eben'; if(min < 60) return 'vor ' + min + ' Min.'; const std = Math.round(min / 60); if(std < 24) return 'vor ' + std + ' Std.'; return new Date(t).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); } // benachrichtigungZeile: eine Zeile für Glocke und Dashboard. function benachrichtigungZeile(b){ const projekt = (b.projectNr ? b.projectNr + ' ' : '') + (b.projectName || ''); const faellig = b.faellig ? ' · fällig ' + esc(formatWeekDate(b.faellig)) : ''; // Zeile = Öffnen-Knopf + Löschen-Knopf daneben (Knöpfe dürfen nicht // ineinander stecken). return '