// ----------------------------- // Seitenleiste „Notizen“ (Tab `notizen`, /Notizen) // ----------------------------- // Eriks Wunsch: Jedes Konto sieht hier die Notizen ALLER seiner Projekte und // legt auch projektunabhängige Notizen an, die sich beim Bearbeiten einem // Projekt zuordnen lassen. Gleicher Aufbau wie der Reiter „Notizen“ im // Projektbereich (js/plan/plan-notizen.js: Liste + Editor, Werkzeugleiste, // automatisches Speichern), dazu ein Projektfilter und im Editor die Auswahl // „Projekt“. Eigene Element-ids (mn…), weil der Projekt-Reiter gleichzeitig // im DOM liegt. // // Daten: GET /meine-notizen (Notizen meiner Projekte + meine // projektunabhängigen, project_uid = ''), sonst dieselben Routen // /projekt-notizen* wie der Projekt-Reiter; PUT nimmt `uid` zum Zuordnen. // Projektunabhängige Notizen sieht und ändert nur, wer sie angelegt hat. (function(){ const SPEICHER_PAUSE_MS = 1200; const OHNE = '__ohne__'; let liste = []; let meineProjekte = new Set(); let geladen = false; let ladeFehler = ''; let filter = ''; // '' = alle, OHNE = ohne Projekt, sonst uid let suche = ''; let aktivId = null; let basis = ''; let ungespeichert = false; let speicherTimer = null; let speichertGerade = false; let beobachter = null; let loeschenFragt = null; let eigenesSchreiben = 0; // Zeitpunkt der letzten eigenen Änderung (Live-Echo auslassen) const el = () => document.getElementById('notizen'); const aktiv = () => liste.find(n => n.id === aktivId) || null; const darf = n => !n || !n.uid ? true : (typeof hatRecht === 'function' && hatRecht('proj.plan')); const darfProjekte = () => typeof hatRecht === 'function' && hatRecht('proj.plan'); // Projekte der aktuellen Sitzung je projectUid (Name und Nummer). function projekte(){ const w = typeof currentWeek === 'function' ? currentWeek() : null; const m = new Map(); ((w && w.projects) || []).forEach(p => { if(p.projectUid) m.set(String(p.projectUid), p); }); return m; } function projektName(uid, karte){ if(!uid) return 'Ohne Projekt'; const p = (karte || projekte()).get(uid); return p ? [p.nr, p.name].filter(Boolean).join(' ') : 'Projekt nicht in der aktuellen Sitzung'; } function zeit(iso){ if(!iso) return ''; const d = new Date(iso); if(isNaN(d)) return ''; const heute = new Date(); const gestern = new Date(heute); gestern.setDate(heute.getDate() - 1); const uhr = d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); if(d.toDateString() === heute.toDateString()) return 'heute ' + uhr; if(d.toDateString() === gestern.toDateString()) return 'gestern ' + uhr; return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: d.getFullYear() === heute.getFullYear() ? undefined : 'numeric' }); } const klartext = html => typeof statusPlainText === 'function' ? statusPlainText(html) : String(html || '').replace(/<[^>]+>/g, ' '); function sortieren(){ liste.sort((a, b) => (b.angeheftet - a.angeheftet) || String(b.geaendertAm).localeCompare(String(a.geaendertAm))); } // ---------- Laden ---------- function laden(){ ladeFehler = ''; return fetch(baseUrl() + '/meine-notizen') .then(r => r.json().then(d => ({ ok: r.ok, d }))) .then(({ ok, d }) => { if(!ok || !d.ok) throw new Error(d.error || 'Laden fehlgeschlagen'); liste = d.notizen || []; meineProjekte = new Set(d.meineProjekte || []); sortieren(); geladen = true; }) .catch(e => { ladeFehler = e.message || 'Die Notizen konnten nicht geladen werden.'; liste = []; geladen = true; }); } // ---------- Darstellung ---------- window.renderMeineNotizen = function(){ const box = el(); if(!box) return; box.innerHTML = '
Notizen werden geladen…
'; jetztSpeichern(); laden().then(aufbauen); }; function sichtbar(){ const s = suche.trim().toLowerCase(); return liste.filter(n => (!filter || (filter === OHNE ? !n.uid : n.uid === filter)) && (!s || (n.titel + ' ' + klartext(n.html) + ' ' + projektName(n.uid)).toLowerCase().includes(s))); } function filterOptionen(){ const karte = projekte(); const mitNotizen = new Set(liste.filter(n => n.uid).map(n => n.uid)); const uids = Array.from(new Set([...meineProjekte, ...mitNotizen])) .sort((a, b) => projektName(a, karte).localeCompare(projektName(b, karte), 'de')); const zahl = f => liste.filter(n => f === OHNE ? !n.uid : n.uid === f).length; return '' + '' + uids.map(u => '').join(''); } function aufbauen(){ const box = el(); if(!box) return; const treffer = sichtbar(); if(!treffer.some(n => n.id === aktivId)) aktivId = treffer.length ? treffer[0].id : null; box.innerHTML = `
`; listeZeichnen(); editorZeichnen(); } function listeZeichnen(){ const box = document.getElementById('mnEintraege'); if(!box) return; if(ladeFehler){ box.innerHTML = '
' + esc(ladeFehler) + '
'; return; } const treffer = sichtbar(); if(!liste.length){ box.innerHTML = '
Noch keine Notizen. Mit „+ Neu“ eine eigene anlegen.
'; return; } if(!treffer.length){ box.innerHTML = '
' + (suche ? 'Keine Notiz enthält „' + esc(suche) + '“.' : 'Keine Notizen in dieser Auswahl.') + '
'; return; } const karte = projekte(); const zeile = n => { const vorschau = klartext(n.html).replace(/\s+/g, ' ').trim().slice(0, 90); return ``; }; const oben = treffer.filter(n => n.angeheftet), rest = treffer.filter(n => !n.angeheftet); box.innerHTML = (oben.length ? '
Angeheftet
' + oben.map(zeile).join('') : '') + (rest.length ? (oben.length ? '
Weitere
' : '') + rest.map(zeile).join('') : ''); } // projektAuswahl: Auswahl im Editor — „Ohne Projekt“, meine Projekte, dann // die übrigen der Sitzung. Ohne Recht proj.plan nur „Ohne Projekt“ (und das // aktuelle Projekt, damit der Stand lesbar bleibt). function projektAuswahl(n){ const karte = projekte(); const name = u => projektName(u, karte); const sort = arr => arr.sort((a, b) => name(a).localeCompare(name(b), 'de')); const opt = u => ''; const meine = sort(Array.from(meineProjekte).filter(u => karte.has(u))); const weitere = sort(Array.from(karte.keys()).filter(u => !meineProjekte.has(u))); const sperren = !darf(n) || (!darfProjekte() && !n.uid); let html = ''; if(darfProjekte()){ if(meine.length) html += '' + meine.map(opt).join('') + ''; if(weitere.length) html += '' + weitere.map(opt).join('') + ''; } if(n.uid && !karte.has(n.uid)) html += opt(n.uid); return ''; } function editorZeichnen(){ const box = document.getElementById('mnEditor'); if(!box) return; if(beobachter){ beobachter.disconnect(); beobachter = null; } const n = aktiv(); if(!n){ box.innerHTML = `
${liste.length ? 'Notiz links auswählen' : 'Noch keine Notizen'} Mit „+ Neu“ eine Notiz anlegen. Sie gehört zunächst nur Ihnen und lässt sich oben rechts einem Projekt zuordnen.
`; return; } const schreiben = darf(n); basis = n.geaendertAm; ungespeichert = false; const projekt = n.uid ? projekte().get(n.uid) : null; box.innerHTML = `
${schreiben ? ` ` : ''}
${projektAuswahl(n)} ${projekt ? `Im Projekt öffnen` : ''} ${!n.uid ? 'Nur für Sie sichtbar, bis sie einem Projekt zugeordnet ist' : ''}
${schreiben ? richtextToolbarHtml('#mnText') : ''} ${richtextFeldHtml('notizen-text', sanitizeStatusHtml(n.html || ''), 'Notiz schreiben…', 'id="mnText" onblur="mnJetztSpeichern()"', schreiben)}
Angelegt von ${esc(n.erstelltVon)} ${esc(zeit(n.erstelltAm))}${n.geaendertAm !== n.erstelltAm ? ' · zuletzt geändert von ' + esc(n.geaendertVon) + ' ' + esc(zeit(n.geaendertAm)) : ''}
`; const text = document.getElementById('mnText'); if(text && schreiben){ beobachter = new MutationObserver(() => mnGeaendert()); beobachter.observe(text, { childList: true, subtree: true, characterData: true, attributes: true }); } } function status(text, art){ const s = document.getElementById('mnStatus'); if(!s) return; s.className = 'notizen-status' + (art ? ' ist-' + art : ''); s.innerHTML = text; } function hinweis(text){ if(typeof zeigeHinweis === 'function') zeigeHinweis(text); else status(esc(text), 'fehler'); } // ---------- Bedienung ---------- window.mnFilter = function(wert){ jetztSpeichern(); filter = wert || ''; aufbauen(); }; window.mnSuchen = function(wert){ suche = wert || ''; listeZeichnen(); }; window.mnWaehlen = function(id){ if(id === aktivId) return; jetztSpeichern(); aktivId = id; listeZeichnen(); editorZeichnen(); }; window.mnGeaendert = function(){ const n = aktiv(); if(!n || !darf(n)) return; ungespeichert = true; status('Ungespeichert'); clearTimeout(speicherTimer); speicherTimer = setTimeout(jetztSpeichern, SPEICHER_PAUSE_MS); }; window.mnJetztSpeichern = function(){ jetztSpeichern(); }; function senden(n, nutzlast){ eigenesSchreiben = Date.now(); return fetch(baseUrl() + '/projekt-notizen/' + encodeURIComponent(n.id), { method: 'PUT', headers: { 'Content-Type': 'application/json; charset=utf-8' }, body: JSON.stringify(nutzlast) }).then(r => r.json().then(d => ({ r, d }))); } function jetztSpeichern(ohneBasis){ clearTimeout(speicherTimer); const n = aktiv(); if(!n || !ungespeichert || !darf(n)) return Promise.resolve(); if(speichertGerade){ speicherTimer = setTimeout(jetztSpeichern, 400); return Promise.resolve(); } const titelEl = document.getElementById('mnTitel'); const textEl = document.getElementById('mnText'); const titel = titelEl ? titelEl.value : n.titel; const html = textEl ? textEl.innerHTML : n.html; ungespeichert = false; speichertGerade = true; status('Speichert…'); return senden(n, { titel, html, basis: ohneBasis ? '' : basis }).then(({ r, d }) => { if(r.status === 409 && d.konflikt){ ungespeichert = true; status(esc(d.error) + ' ' + ' ', 'warnung'); n._fremd = d.notiz; return; } if(!r.ok || !d.ok) throw new Error(d.error || 'Speichern fehlgeschlagen'); Object.assign(n, d.notiz, { titel, html }); if(n.id === aktivId) basis = d.notiz.geaendertAm; sortieren(); listeZeichnen(); if(!ungespeichert) status('Gespeichert ✓', 'ok'); }).catch(e => { ungespeichert = true; status('Nicht gespeichert: ' + esc(e.message) + ' — wird erneut versucht.', 'fehler'); speicherTimer = setTimeout(jetztSpeichern, 5000); }).finally(() => { speichertGerade = false; }); } window.mnKonflikt = function(meine){ const n = aktiv(); if(!n) return; if(meine){ ungespeichert = true; jetztSpeichern(true); return; } if(n._fremd) Object.assign(n, n._fremd); delete n._fremd; ungespeichert = false; listeZeichnen(); editorZeichnen(); }; // mnNeu: neue Notiz — im gefilterten Projekt, sonst ohne Projekt. window.mnNeu = function(){ jetztSpeichern(); const uid = filter && filter !== OHNE && darfProjekte() ? filter : ''; eigenesSchreiben = Date.now(); fetch(baseUrl() + '/projekt-notizen', { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8' }, body: JSON.stringify({ uid, titel: '', html: '' }) }).then(r => r.json()).then(d => { if(!d.ok) throw new Error(d.error || 'Anlegen fehlgeschlagen'); liste = liste.filter(x => x.id !== d.notiz.id).concat([d.notiz]); sortieren(); aktivId = d.notiz.id; suche = ''; aufbauen(); const t = document.getElementById('mnTitel'); if(t) t.focus(); }).catch(e => hinweis('Notiz konnte nicht angelegt werden: ' + e.message)); }; // mnZuordnen: Notiz einem Projekt zuordnen (oder wieder lösen). Offene // Änderungen werden vorher gespeichert. window.mnZuordnen = function(uid){ const n = aktiv(); if(!n) return; jetztSpeichern().then(() => senden(n, { uid: uid || '' })).then(({ r, d }) => { if(!r.ok || !d.ok) throw new Error(d.error || 'Zuordnen fehlgeschlagen'); Object.assign(n, d.notiz); if(n.uid) meineProjekte.add(n.uid); if(filter && filter !== (n.uid || OHNE)) filter = n.uid || OHNE; aufbauen(); status(n.uid ? 'Dem Projekt zugeordnet ✓' : 'Vom Projekt gelöst ✓', 'ok'); }).catch(e => { hinweis(e.message); editorZeichnen(); }); }; window.mnAnheften = function(){ const n = aktiv(); if(!n) return; jetztSpeichern().then(() => senden(n, { angeheftet: !n.angeheftet })).then(({ d }) => { if(!d.ok) throw new Error(d.error || 'Fehler'); n.angeheftet = d.notiz.angeheftet; sortieren(); listeZeichnen(); editorZeichnen(); }).catch(e => hinweis('Anheften fehlgeschlagen: ' + e.message)); }; window.mnLoeschen = function(){ const n = aktiv(); const knopf = document.getElementById('mnLoeschen'); if(!n || !knopf) return; if(!loeschenFragt){ knopf.textContent = 'Wirklich löschen?'; knopf.classList.add('ist-fragend'); loeschenFragt = setTimeout(() => { loeschenFragt = null; const k = document.getElementById('mnLoeschen'); if(k){ k.textContent = 'Löschen'; k.classList.remove('ist-fragend'); } }, 4000); return; } clearTimeout(loeschenFragt); loeschenFragt = null; clearTimeout(speicherTimer); ungespeichert = false; eigenesSchreiben = Date.now(); fetch(baseUrl() + '/projekt-notizen/' + encodeURIComponent(n.id), { method: 'DELETE' }) .then(r => r.json().then(d => ({ r, d }))).then(({ r, d }) => { if(!r.ok || !d.ok) throw new Error(d.error || 'Löschen fehlgeschlagen'); liste = liste.filter(x => x.id !== n.id); aktivId = null; aufbauen(); }).catch(e => { status(esc(e.message), 'fehler'); const k = document.getElementById('mnLoeschen'); if(k){ k.textContent = 'Löschen'; k.classList.remove('ist-fragend'); } }); }; window.mnProjektOeffnen = function(pid){ jetztSpeichern(); location.hash = 'notizen'; if(typeof openProjectPlan === 'function') openProjectPlan(pid); return false; }; // ---------- Live: Änderungen aus anderen Browsern ---------- window.meineNotizenLive = function(uid){ if(typeof currentTab === 'undefined' || currentTab !== 'notizen' || !geladen) return; if(uid && !meineProjekte.has(uid)) return; const vorher = aktiv(); const tippt = document.activeElement && document.activeElement.closest && document.activeElement.closest('#mnEditor'); // Das Echo der eigenen Speicherung (kommt auch in diesem Tab an) auslassen. if(ungespeichert || tippt || Date.now() - eigenesSchreiben < 3000) return; laden().then(() => { if(vorher && liste.some(x => x.id === vorher.id)) aktivId = vorher.id; aufbauen(); }); }; // Klick irgendwo anders: die Rückfrage „Wirklich löschen?“ zurücknehmen. document.addEventListener('pointerdown', e => { if(!loeschenFragt || (e.target.closest && e.target.closest('#mnLoeschen'))) return; clearTimeout(loeschenFragt); loeschenFragt = null; const k = document.getElementById('mnLoeschen'); if(k){ k.textContent = 'Löschen'; k.classList.remove('ist-fragend'); } }, true); window.addEventListener('pagehide', () => { if(ungespeichert) jetztSpeichern(); }); })();