// ----------------------------- // Projektbereich — Notizen (Eriks Wahl „A – Liste + Editor“) // ----------------------------- // Gemeinsame Notizen je Projekt: links die Liste (angeheftete oben, danach // zuletzt geänderte zuerst, Suche über Titel und Text), rechts die gewählte // Notiz direkt bearbeitbar — Titel, dieselbe Werkzeugleiste wie beim // Sitzungskommentar (richtextToolbarHtml(), js/utils.js) und der Text. // Gespeichert wird automatisch kurz nach dem Tippen und beim Verlassen des // Feldes; „Gespeichert ✓“ zeigt den Stand. Mail per Drag & Drop (Text oder // .eml) bzw. Strg+V außerhalb von Eingabefeldern legt eine neue Notiz an, // ohne KI. Live: andere Browser erfahren Änderungen über das Ereignis // {typ:'notizen'} (notizenLiveAenderung(), aus js/store.js). // // Daten am Server: Tabelle projekt_notizen, Routen /projekt-notizen* // (server.js). Die Notizen hängen am Projekt (projectUid), nicht an der // Sitzung. Schreiben: Recht proj.plan; Löschen: Verfasser oder Admin. (function(){ const app = window.PlanApp; const SPEICHER_PAUSE_MS = 1200; let liste = []; // Notizen des geladenen Projekts let uidGeladen = null; // projectUid, zu dem `liste` gehört let aktivId = null; // gewählte Notiz let suche = ''; let basis = ''; // geaendertAm, auf dem die Bearbeitung beruht let ungespeichert = false; let speicherTimer = null; let speichertGerade = false; let beobachter = null; // MutationObserver auf dem Textfeld let loeschenFragt = null; // Timer der zweistufigen Löschen-Abfrage let ladeFehler = ''; const kannSchreiben = () => typeof hatRecht === 'function' && hatRecht('proj.plan'); const projektUid = () => app.project ? String(app.project.projectUid || app.project.id || '') : ''; const wrap = () => document.getElementById('planNotizenWrap'); const aktiv = () => liste.find(n => n.id === aktivId) || null; function zeit(iso){ if(!iso) return ''; const d = new Date(iso); if(isNaN(d)) return ''; const heute = new Date(); const gleicherTag = d.toDateString() === heute.toDateString(); const gestern = new Date(heute); gestern.setDate(heute.getDate() - 1); const uhr = d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); if(gleicherTag) 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' }); } function kurzname(name){ const teile = String(name || '').trim().split(/\s+/); return teile.length > 1 ? teile[0][0] + '. ' + teile.slice(1).join(' ') : (name || ''); } function klartext(html){ return 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(uid){ uidGeladen = uid; ladeFehler = ''; return fetch(baseUrl() + '/projekt-notizen?uid=' + encodeURIComponent(uid)) .then(r => r.json().then(d => ({ ok: r.ok, d }))) .then(({ ok, d }) => { if(uid !== uidGeladen) return; if(!ok || !d.ok) throw new Error(d.error || 'Laden fehlgeschlagen'); liste = d.notizen || []; sortieren(); }) .catch(e => { ladeFehler = e.message || 'Die Notizen konnten nicht geladen werden.'; liste = []; }); } // ---------- Darstellung ---------- window.renderPlanNotizen = function(){ const el = wrap(); if(!el || !app.project) return; const uid = projektUid(); if(uid !== uidGeladen){ jetztSpeichern(); aktivId = null; liste = []; suche = ''; el.innerHTML = '
Notizen werden geladen…
'; laden(uid).then(() => { if(projektUid() === uid) aufbauen(); }); return; } aufbauen(); }; function aufbauen(){ const el = wrap(); if(!el) return; if(!aktiv() && liste.length) aktivId = liste[0].id; el.innerHTML = `
`; listeZeichnen(); editorZeichnen(); } function listeZeichnen(){ const box = document.getElementById('notizenEintraege'); if(!box) return; if(ladeFehler){ box.innerHTML = '
' + esc(ladeFehler) + '
'; return; } const s = suche.trim().toLowerCase(); const treffer = s ? liste.filter(n => (n.titel + ' ' + klartext(n.html)).toLowerCase().includes(s)) : liste; if(!liste.length){ box.innerHTML = '
Noch keine Notizen.
'; return; } if(!treffer.length){ box.innerHTML = '
Keine Notiz enthält „' + esc(suche) + '“.
'; return; } 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('') : ''); } function editorZeichnen(){ const el = document.getElementById('notizenEditor'); if(!el) return; if(beobachter){ beobachter.disconnect(); beobachter = null; } const n = aktiv(); if(!n){ el.innerHTML = `
${liste.length ? 'Notiz links auswählen' : 'Noch keine Notizen in diesem Projekt'} ${kannSchreiben() ? 'Mit „+ Neu“ anlegen oder eine Mail auf diesen Bereich ziehen.' : 'Zum Anlegen fehlt das Recht, den Terminplan zu bearbeiten.'}
`; return; } const schreiben = kannSchreiben(); basis = n.geaendertAm; ungespeichert = false; el.innerHTML = `
${schreiben ? ` ` : ''}
${schreiben ? richtextToolbarHtml('.notizen-text') : ''} ${richtextFeldHtml('notizen-text', sanitizeStatusHtml(n.html || ''), 'Notiz schreiben…', 'id="notizText" onblur="notizJetztSpeichern()"', 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('notizText'); // Auch Fett/Farbe aus der Werkzeugleiste ändern den Inhalt, ohne ein // input-Ereignis auszulösen — daher ein MutationObserver. if(text && schreiben){ beobachter = new MutationObserver(() => notizGeaendert()); beobachter.observe(text, { childList: true, subtree: true, characterData: true, attributes: true }); } } function status(text, art){ const el = document.getElementById('notizStatus'); if(!el) return; el.className = 'notizen-status' + (art ? ' ist-' + art : ''); el.innerHTML = text; } // ---------- Bearbeiten und Speichern ---------- window.notizenSuchen = function(wert){ suche = wert || ''; listeZeichnen(); }; window.notizWaehlen = function(id){ if(id === aktivId) return; jetztSpeichern(); aktivId = id; listeZeichnen(); editorZeichnen(); }; window.notizGeaendert = function(){ if(!kannSchreiben() || !aktiv()) return; ungespeichert = true; status('Ungespeichert'); clearTimeout(speicherTimer); speicherTimer = setTimeout(jetztSpeichern, SPEICHER_PAUSE_MS); }; window.notizJetztSpeichern = function(){ jetztSpeichern(); }; // jetztSpeichern: schickt die gewählte Notiz, falls geändert. ohneBasis = // bewusst überschreiben (nach einem Konflikt „Meine Fassung behalten“). function jetztSpeichern(ohneBasis){ clearTimeout(speicherTimer); const n = aktiv(); if(!n || !ungespeichert || !kannSchreiben()) return Promise.resolve(); if(speichertGerade){ speicherTimer = setTimeout(jetztSpeichern, 400); return Promise.resolve(); } const titelEl = document.getElementById('notizTitel'); const textEl = document.getElementById('notizText'); const titel = titelEl ? titelEl.value : n.titel; const html = textEl ? textEl.innerHTML : n.html; ungespeichert = false; speichertGerade = true; status('Speichert…'); return fetch(baseUrl() + '/projekt-notizen/' + encodeURIComponent(n.id), { method: 'PUT', headers: { 'Content-Type': 'application/json; charset=utf-8' }, body: JSON.stringify({ titel, html, basis: ohneBasis ? '' : basis }) }).then(r => r.json().then(d => ({ r, d }))).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.notizKonflikt = 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(); }; window.notizNeu = function(){ const uid = projektUid(); if(!uid || !kannSchreiben()) return; jetztSpeichern(); 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'); neueNotizZeigen(d.notiz); const t = document.getElementById('notizTitel'); if(t) t.focus(); }).catch(e => app.showToast('Notiz konnte nicht angelegt werden: ' + e.message)); }; function neueNotizZeigen(notiz){ if(!notiz || notiz.uid !== uidGeladen) return; liste = liste.filter(x => x.id !== notiz.id); liste.push(notiz); sortieren(); aktivId = notiz.id; suche = ''; const s = document.getElementById('notizenSuche'); if(s) s.value = ''; listeZeichnen(); editorZeichnen(); } window.notizAnheften = function(){ const n = aktiv(); if(!n) return; jetztSpeichern().then(() => fetch(baseUrl() + '/projekt-notizen/' + encodeURIComponent(n.id), { method: 'PUT', headers: { 'Content-Type': 'application/json; charset=utf-8' }, body: JSON.stringify({ angeheftet: !n.angeheftet }) })).then(r => r.json()).then(d => { if(!d.ok) throw new Error(d.error || 'Fehler'); n.angeheftet = d.notiz.angeheftet; sortieren(); listeZeichnen(); const knopf = document.querySelector('.notizen-editor-kopf .notizen-symbol:not(.ist-gefahr)'); if(knopf){ knopf.classList.toggle('ist-an', n.angeheftet); knopf.textContent = n.angeheftet ? 'Angeheftet' : 'Anheften'; knopf.title = n.angeheftet ? 'Nicht mehr anheften' : 'Oben anheften'; knopf.setAttribute('aria-pressed', String(n.angeheftet)); } }).catch(e => app.showToast('Anheften fehlgeschlagen: ' + e.message)); }; // notizLoeschen: zweistufig im Knopf selbst (kein confirm()). window.notizLoeschen = function(){ const n = aktiv(); const knopf = document.getElementById('notizLoeschen'); if(!n || !knopf) return; if(!loeschenFragt){ knopf.textContent = 'Wirklich löschen?'; knopf.classList.add('ist-fragend'); loeschenFragt = setTimeout(() => { loeschenFragt = null; const k = document.getElementById('notizLoeschen'); if(k){ k.textContent = 'Löschen'; k.classList.remove('ist-fragend'); } }, 4000); return; } clearTimeout(loeschenFragt); loeschenFragt = null; clearTimeout(speicherTimer); ungespeichert = false; 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 = liste.length ? liste[0].id : null; listeZeichnen(); editorZeichnen(); }).catch(e => { status(esc(e.message), 'fehler'); const k = document.getElementById('notizLoeschen'); if(k){ k.textContent = 'Löschen'; k.classList.remove('ist-fragend'); } }); }; // ---------- Live: Änderungen aus anderen Browsern ---------- // Liste neu holen; die gewählte Notiz nur ersetzen, wenn hier gerade // nichts ungespeichert ist und niemand in ihr tippt. window.notizenLiveAenderung = function(uid){ if(!uid || uid !== uidGeladen) return; const n = aktiv(); laden(uid).then(() => { if(!wrap()) return; if(aktivId && !liste.some(x => x.id === aktivId)){ aktivId = liste.length ? liste[0].id : null; editorZeichnen(); } listeZeichnen(); const frisch = aktiv(); const tippt = document.activeElement && document.activeElement.closest && document.activeElement.closest('#notizenEditor'); if(frisch && n && frisch.id === n.id && !ungespeichert && !tippt && frisch.geaendertAm !== basis) editorZeichnen(); }); }; // ---------- Mail ablegen (Drag & Drop, Strg+V) ---------- function imNotizenBereich(){ const s = document.querySelector('.plan-section.is-active'); return typeof currentTab !== 'undefined' && currentTab === 'plan' && s && s.dataset.section === 'notizen' && !!app.project; } function ausMail(nutzlast){ const uid = projektUid(); if(!uid) return; jetztSpeichern(); fetch(baseUrl() + '/projekt-notizen/aus-mail', { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8' }, body: JSON.stringify(Object.assign({ uid }, nutzlast)) }).then(r => r.json()).then(d => { if(!d.ok) throw new Error(d.error || 'Fehlgeschlagen'); neueNotizZeigen(d.notiz); app.showToast('Notiz aus der Mail angelegt.'); }).catch(e => app.showToast('Mail konnte nicht übernommen werden: ' + e.message)); } function ausDaten(dt){ const dateien = Array.from(dt.files || []); const eml = dateien.find(f => /\.eml$/i.test(f.name || '') || f.type === 'message/rfc822'); if(eml){ if(eml.size > 5 * 1024 * 1024){ app.showToast('Die Mail ist zu groß (mehr als 5 MB).'); return; } eml.text().then(t => ausMail({ eml: t })); return; } if(dateien.some(f => /\.msg$/i.test(f.name || ''))){ app.showToast('Outlook-Dateien (.msg) lassen sich nicht lesen. Bitte den Mailtext markieren und hineinziehen.'); return; } let text = (dt.getData('text/plain') || '').trim(); if(!text){ const div = document.createElement('div'); div.innerHTML = (dt.getData('text/html') || '').replace(/<(script|style)[\s\S]*?<\/\1>/gi, ''); text = (div.innerText || div.textContent || '').trim(); } if(text) ausMail({ text }); } const bereich = () => document.getElementById('planSection-notizen'); document.addEventListener('dragover', e => { if(!imNotizenBereich() || !kannSchreiben() || !bereich().contains(e.target)) return; if(e.target.closest && e.target.closest('#notizenEditor [contenteditable="true"], #notizTitel')) return; e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; bereich().classList.add('notizen-zieht'); }); document.addEventListener('dragleave', e => { const b = bereich(); if(b && (!e.relatedTarget || !b.contains(e.relatedTarget))) b.classList.remove('notizen-zieht'); }); document.addEventListener('drop', e => { const b = bereich(); if(b) b.classList.remove('notizen-zieht'); if(!imNotizenBereich() || !kannSchreiben() || !b.contains(e.target)) return; // In das Textfeld gezogen: normal einfügen lassen, keine neue Notiz. if(e.target.closest && e.target.closest('#notizenEditor [contenteditable="true"], #notizTitel')) return; e.preventDefault(); ausDaten(e.dataTransfer); }); document.addEventListener('paste', e => { if(!imNotizenBereich() || !kannSchreiben()) return; const ziel = e.target; if(ziel && ziel.closest && ziel.closest('input, textarea, select, [contenteditable="true"], .modal-bg')) return; if(!e.clipboardData) return; e.preventDefault(); ausDaten(e.clipboardData); }); // Beim Verlassen der Seite noch speichern, was offen ist. window.addEventListener('pagehide', () => { if(ungespeichert) jetztSpeichern(); }); })();