Files
projektuebersicht/public/js/plan/plan-mail-eintrag.js
T
erikandClaude Opus 5.5 b7e0bd0d92 Termin aus Mail, Rollen & Rechte, Abwesenheiten, Glas-Darstellung und Pi-Betrieb
- Termin aus Mail: Mailtext oder .eml per Drag & Drop/Strg+V auf Terminplan
  oder Aufgaben, KI schlägt Aufgaben vor, Prüfung im Aufgaben-Panel; neues
  Notizfeld je Aufgabe (plan_todos.notiz)
- Terminplan: Meilensteine ohne Aufgabenbezug je in eigener Zeile,
  "+ Neu"-Menü klappt neben schmaler Seitenleiste nach rechts, Leistungsphasen
- Rollen & Rechte, Abwesenheiten-Seite, Benachrichtigungen, Rundgang
- Glas-Darstellung, Logo als Favicon
- Pi-Betrieb: PDF-Arbeit im Kindprozess, Wächter, Einrichtungs- und
  Prüfskript inkl. WLAN-Wächter

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 14:16:52 +01:00

294 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
// Aufgaben. Der Terminplan ist nur die grafische Darstellung der
// Aufgabenliste — deshalb entstehen hier ausschließlich Aufgaben
// (plan.todos), keine Meilensteine.
//
// Nichts wird ohne Prüfung angelegt: Jeder Vorschlag öffnet das gewohnte
// Aufgaben-Panel vorausgefüllt (openTodoModal(null, prefill)), die Mail
// steht als Notiz darin (t.notiz). Mehrere Vorschläge erst als Liste zum
// Abhaken, dann nacheinander im Panel. 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;
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 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 = `<div class="mail-ablage-karte">${icon('mail')}
<b id="mailAblageTitel">Mail hier ablegen</b>
<span id="mailAblageText">Die KI macht daraus einen Eintrag im Terminplan. Der Mailtext geht dafür an den KI-Anbieter aus Einstellungen → KI-Zugangsdaten.</span></div>`;
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(){
return fetch(baseUrl() + '/my-ai-settings').then(r => r.json()).then(d => {
const bereit = ['chatgpt', 'claude'].filter(a => d && d[a] && d[a].configured);
let zuletzt = '';
try { zuletzt = localStorage.getItem(ANBIETER_KEY) || ''; } catch(e){ /* egal */ }
return bereit.includes(zuletzt) ? zuletzt : (bereit[0] || '');
}).catch(() => '');
}
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;
}
if(eintraege.length === 1){ nacheinanderOeffnen(d, eintraege); return; }
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;
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;
};
bg.innerHTML = `<div class="modal mail-auswahl" role="dialog" aria-label="Einträge aus der Mail">
<h2>${eintraege.length} Einträge in der Mail</h2>
<p class="hint">${esc(hinweisText(d))} Die ausgewählten öffnen sich nacheinander im Aufgaben-Panel.</p>
<div class="mail-auswahl-liste">${eintraege.map((e, i) => `
<label class="mail-auswahl-zeile"><input type="checkbox" checked data-i="${i}">
<span class="mail-auswahl-titel">${esc(e.titel)}</span>
<span class="mail-auswahl-datum">${esc(zeitraum(e))}</span></label>`).join('')}</div>
<div class="modal-actions">
<button type="button" class="secondary" data-aktion="abbrechen">Abbrechen</button>
<button type="button" data-aktion="weiter">Prüfen und anlegen</button>
</div></div>`;
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 => eintraege[Number(cb.dataset.i)]);
bg.style.display = 'none';
if(gewaehlt.length) nacheinanderOeffnen(d, gewaehlt);
};
bg.style.display = 'flex';
}
})();