// plan-mail-eintrag.js — "Termin aus Mail" (Eriks Wunsch): Mailtext per // Drag & Drop (markierter Text oder eine ganze .eml-Datei) oder Strg+V auf // den Terminplan bzw. die Aufgabenliste eines Projekts; eine KI macht daraus // Einträge. // // Nichts wird ohne Prüfung angelegt: Die Vorschläge stehen erst in einer // Liste — je Zeile abhaken und „Aufgabe | Meilenstein“ wählen (Eriks // Wunsch; die KI schlägt die Art vor) —, dann öffnet sich je Eintrag das // gewohnte Panel vorausgefüllt (openTodoModal/openMilestoneModal mit // prefill), die Mail steht als Notiz darin (t.notiz bzw. m.notiz). // Server: POST /plan-eintrag-aus-mail (speichert nichts). (function(){ const app = window.PlanApp; const ANBIETER_KEY = 'projuebersicht-mail-ki'; const ZIEL_ABSCHNITTE = ['terminplan', 'aufgaben']; let tiefe = 0; // dragenter/-leave-Zähler (Kindelemente feuern mit) let laeuft = false; // kiAnbieter: die Anbieter mit hinterlegtem Schlüssel. Ohne eingerichtete // KI ist die Funktion ganz aus (Eriks Wunsch) — keine Ablagefläche, kein // Abfangen von Strg+V. Geladen nach der Anmeldung und nach jeder Änderung // unter Einstellungen → KI-Zugangsdaten (mailKiStatusLaden()). let kiAnbieter = []; window.mailKiStatusLaden = function(){ return fetch(baseUrl() + '/my-ai-settings').then(r => r.json()).then(d => { kiAnbieter = ['chatgpt', 'claude'].filter(a => d && d[a] && d[a].configured); }).catch(() => { kiAnbieter = []; }); }; function aktiverAbschnitt(){ if(typeof currentTab === 'undefined' || currentTab !== 'plan') return ''; const s = document.querySelector('.plan-section.is-active'); return s ? s.dataset.section : ''; } function ablageErlaubt(){ return kiAnbieter.length > 0 && ZIEL_ABSCHNITTE.includes(aktiverAbschnitt()) && app.project && !app.isLocked(); } function bringtMail(dt){ if(!dt) return false; const typen = Array.from(dt.types || []); return typen.includes('Files') || typen.includes('text/plain') || typen.includes('text/html'); } // ---------- Ablagefläche ---------- function flaeche(){ let el = document.getElementById('mailAblage'); if(!el){ el = document.createElement('div'); el.id = 'mailAblage'; el.className = 'mail-ablage'; el.hidden = true; el.innerHTML = `
${icon('mail')} Mail hier ablegen
`; document.body.appendChild(el); } return el; } function zeigeFlaeche(titel, text){ const el = flaeche(); document.getElementById('mailAblageTitel').textContent = titel; if(text !== undefined) document.getElementById('mailAblageText').textContent = text; el.classList.toggle('ist-beschaeftigt', laeuft); el.hidden = false; } function versteckeFlaeche(){ const el = document.getElementById('mailAblage'); if(el) el.hidden = true; } document.addEventListener('dragenter', e => { if(laeuft || !ablageErlaubt() || !bringtMail(e.dataTransfer)) return; tiefe++; zeigeFlaeche('Mail hier ablegen', 'Die KI macht daraus einen Eintrag im Terminplan. Der Mailtext geht dafür an den KI-Anbieter aus Einstellungen → KI-Zugangsdaten.'); }); document.addEventListener('dragover', e => { if(laeuft || !ablageErlaubt() || !bringtMail(e.dataTransfer)) return; e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; }); document.addEventListener('dragleave', () => { if(tiefe === 0) return; tiefe--; if(tiefe === 0 && !laeuft) versteckeFlaeche(); }); document.addEventListener('drop', e => { if(tiefe === 0 && !ablageErlaubt()) return; tiefe = 0; if(laeuft || !ablageErlaubt() || !bringtMail(e.dataTransfer)){ if(!laeuft) versteckeFlaeche(); return; } e.preventDefault(); ausDataTransfer(e.dataTransfer); }); // Strg+V als Ausweg, wenn ein Mailprogramm das Ziehen nicht unterstützt — // nur außerhalb von Eingabefeldern, damit normales Einfügen unberührt bleibt. document.addEventListener('paste', e => { if(laeuft || !ablageErlaubt()) return; const ziel = e.target; if(ziel && (ziel.closest('input, textarea, select, [contenteditable="true"], .modal-bg'))) return; const dt = e.clipboardData; if(!dt) return; const datei = Array.from(dt.files || []).find(istEml); const text = (dt.getData('text/plain') || '').trim(); if(!datei && !text) return; e.preventDefault(); ausDataTransfer(dt); }); function istEml(f){ return f && /\.eml$/i.test(f.name || '') || (f && f.type === 'message/rfc822'); } function ausDataTransfer(dt){ const dateien = Array.from(dt.files || []); const eml = dateien.find(istEml); if(eml){ if(eml.size > 5 * 1024 * 1024){ fehler('Die Mail ist zu groß (mehr als 5 MB).'); return; } eml.text().then(t => auswerten({ eml: t })).catch(() => fehler('Die Mail-Datei ließ sich nicht lesen.')); return; } if(dateien.some(f => /\.msg$/i.test(f.name || ''))){ fehler('Outlook-Dateien (.msg) lassen sich nicht direkt lesen. Bitte den Mailtext markieren und hineinziehen oder mit Strg+V einfügen.'); return; } let text = (dt.getData('text/plain') || '').trim(); if(!text){ const html = dt.getData('text/html') || ''; const div = document.createElement('div'); div.innerHTML = html.replace(/<(script|style)[\s\S]*?<\/\1>/gi, ''); text = (div.innerText || div.textContent || '').trim(); } if(!text){ if(dateien.length) fehler('Diese Datei ist keine Mail. Abgelegt werden können markierter Mailtext oder .eml-Dateien.'); else versteckeFlaeche(); return; } auswerten({ text }); } function fehler(meldung){ laeuft = false; versteckeFlaeche(); app.showToast(meldung, 'error'); } // ---------- Auswerten ---------- function anbieterWaehlen(){ let zuletzt = ''; try { zuletzt = localStorage.getItem(ANBIETER_KEY) || ''; } catch(e){ /* egal */ } return Promise.resolve(kiAnbieter.includes(zuletzt) ? zuletzt : (kiAnbieter[0] || '')); } function projektPersonen(){ const gesehen = new Set(); const liste = []; projectAssignees(app.project, app.week).forEach(a => { if(gesehen.has(a.person.id)) return; gesehen.add(a.person.id); liste.push(a.person); }); return liste; } function kontext(){ const todos = (app.project.plan && app.project.plan.todos) || []; return { heute: heuteIso(), projekt: [app.project.nr, app.project.name].filter(Boolean).join(' '), personen: projektPersonen().map(p => p.fullName || p.name).filter(Boolean), phasen: todos.filter(t => t.lph && t.start && t.end).map(t => ({ lph: t.lph, start: t.start, ende: t.end })) }; } function auswerten(nutzlast){ laeuft = true; zeigeFlaeche('Mail wird ausgewertet …', 'Die KI liest die Mail. Das dauert einige Sekunden.'); anbieterWaehlen().then(anbieter => fetch(baseUrl() + '/plan-eintrag-aus-mail', { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8' }, body: JSON.stringify(Object.assign({ anbieter, kontext: kontext() }, nutzlast)) })) .then(r => r.json().then(d => ({ ok: r.ok, d }))) .then(({ ok, d }) => { laeuft = false; versteckeFlaeche(); if(!ok || !d.ok){ app.showToast(d.error || 'Die Mail konnte nicht ausgewertet werden.', 'error'); return; } if(d.erkennungsart === 'chatgpt' || d.erkennungsart === 'claude'){ try { localStorage.setItem(ANBIETER_KEY, d.erkennungsart); } catch(e){ /* egal */ } } verarbeiten(d); }) .catch(() => fehler('Der Server ist nicht erreichbar.')); } // ---------- Vorschläge ins Panel ---------- // personenZuordnen: Namen aus der KI-Antwort → Personen-IDs des Projekts // (voller Name, sonst Nachname, sonst Kürzel; Groß-/Kleinschreibung egal). function personenZuordnen(namen){ const leute = projektPersonen(); const ids = []; (namen || []).forEach(n => { const s = String(n || '').trim().toLowerCase(); if(!s) return; const treffer = leute.find(p => String(p.fullName || '').toLowerCase() === s) || leute.find(p => { const nn = String(p.fullName || '').toLowerCase().split(/\s+/).pop(); return nn && (s === nn || s.split(/\s+/).pop() === nn); }) || leute.find(p => String(p.name || '').toLowerCase() === s); if(treffer && !ids.includes(treffer.id)) ids.push(treffer.id); }); return ids; } function notizText(mail, eintrag){ const kopf = []; const datum = mail.datum ? formatDateDE(parseDateISO(mail.datum)) : ''; kopf.push('Aus Mail' + (mail.absender ? ' von ' + mail.absender : '') + (datum ? ' vom ' + datum : '')); if(mail.betreff) kopf.push('Betreff: ' + mail.betreff); if(eintrag.zitat) kopf.push('Bezug: „' + eintrag.zitat + '“'); return kopf.join('\n') + '\n\n' + String(mail.text || '').slice(0, 8000); } function hinweisText(d){ if(d.erkennungsart === 'chatgpt' || d.erkennungsart === 'claude'){ return 'Vorschlag der KI aus der Mail — bitte prüfen und speichern.'; } return 'Die KI war nicht verfügbar (' + (d.erkennungshinweis || 'unbekannter Fehler') + '). Termine bitte selbst eintragen.'; } function verarbeiten(d){ const eintraege = Array.isArray(d.eintraege) ? d.eintraege : []; if(eintraege.length === 0){ app.showToast('In der Mail steht nichts für den Terminplan.'); return; } // Auch bei nur einem Vorschlag erst die Liste — dort wird gewählt, ob es // eine Aufgabe oder ein Meilenstein wird. auswahlZeigen(d, eintraege); } function nacheinanderOeffnen(d, liste){ const rest = liste.slice(); const gesamt = rest.length; const naechster = () => { const e = rest.shift(); if(!e){ renderPlanGantt(); return; } const nr = gesamt - rest.length; if(e.art === 'meilenstein'){ openMilestoneModal(null, { name: e.titel, date: e.ende || e.start, notiz: notizText(d.mail || {}, e), mailHinweis: (gesamt > 1 ? `Vorschlag ${nr} von ${gesamt}. ` : '') + hinweisText(d), nachSchliessen: naechster }); return; } openTodoModal(null, { title: e.titel, start: e.start, end: e.ende, assignees: personenZuordnen(e.zustaendig), aufwand: e.aufwandStunden, notiz: notizText(d.mail || {}, e), mailHinweis: (gesamt > 1 ? `Vorschlag ${nr} von ${gesamt}. ` : '') + hinweisText(d), nachSchliessen: naechster }); }; naechster(); } // ---------- Mehrere Vorschläge: Auswahl ---------- function auswahlZeigen(d, eintraege){ let bg = document.getElementById('mailAuswahlModal'); if(!bg){ bg = document.createElement('div'); bg.id = 'mailAuswahlModal'; bg.className = 'modal-bg'; bg.style.display = 'none'; bg.addEventListener('click', e => { if(e.target === bg) bg.style.display = 'none'; }); document.body.appendChild(bg); } const zeitraum = e => { if(!e.start) return 'ohne Termin'; const a = formatDateDE(parseDateISO(e.start)), b = formatDateDE(parseDateISO(e.ende)); return a === b ? a : a + ' – ' + b; }; const arten = eintraege.map(e => e.art === 'meilenstein' ? 'meilenstein' : 'aufgabe'); bg.innerHTML = ``; bg.querySelectorAll('.mail-auswahl-art button').forEach(b => { b.onclick = () => { const i = Number(b.parentElement.dataset.i); arten[i] = b.dataset.art; b.parentElement.querySelectorAll('button').forEach(x => x.setAttribute('aria-pressed', String(x === b))); }; }); bg.querySelector('[data-aktion="abbrechen"]').onclick = () => { bg.style.display = 'none'; }; bg.querySelector('[data-aktion="weiter"]').onclick = () => { const gewaehlt = Array.from(bg.querySelectorAll('input[type="checkbox"]:checked')) .map(cb => { const i = Number(cb.dataset.i); return Object.assign({}, eintraege[i], { art: arten[i] }); }); bg.style.display = 'none'; if(gewaehlt.length) nacheinanderOeffnen(d, gewaehlt); }; bg.style.display = 'flex'; } })();