Kleine Verbesserungen und Teams, Outlook Integration

- Outlook: Kalender-Abo je Konto (eigene Aufgaben, Meilensteine,
  Abwesenheiten als .ics), einzurichten unter Einstellungen
- Teams: Nachricht in einen Kanal bei neuen Aufgaben, mit Link direkt
  auf die Aufgabe; einzurichten unter Admin → Teams
- Sitzungen nach Bedarf laden: beim Start nur die aktuelle und drei
  davor, im Verlauf „Weitere laden“ in Fünferschritten
- Termin aus Mail: Wahl Aufgabe/Meilenstein, Notiz auch bei
  Meilensteinen, nur aktiv mit eingerichteter KI
- Kalender zeigt die KW, auch statt der Browser-Datumsfelder
- Pfeile bei den Abwesenheiten, kleinere Server-Korrekturen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
erikandClaude Opus 5.5 committed 2026-10-30 08:23:02 +01:00
1 parent a2266662ad
commit 4c2e04e78b
19 files changed
+1100 -139

No files matched your search

+56 -23
View File
@@ -1,21 +1,31 @@
// 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.
// Einträge.
//
// 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).
// 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 '';
@@ -24,7 +34,7 @@
}
function ablageErlaubt(){
return ZIEL_ABSCHNITTE.includes(aktiverAbschnitt()) && app.project && !app.isLocked();
return kiAnbieter.length > 0 && ZIEL_ABSCHNITTE.includes(aktiverAbschnitt()) && app.project && !app.isLocked();
}
function bringtMail(dt){
@@ -44,7 +54,7 @@
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>`;
<span id="mailAblageText"></span></div>`;
document.body.appendChild(el);
}
return el;
@@ -139,12 +149,9 @@
// ---------- 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(() => '');
let zuletzt = '';
try { zuletzt = localStorage.getItem(ANBIETER_KEY) || ''; } catch(e){ /* egal */ }
return Promise.resolve(kiAnbieter.includes(zuletzt) ? zuletzt : (kiAnbieter[0] || ''));
}
function projektPersonen(){
@@ -229,7 +236,8 @@
app.showToast('In der Mail steht nichts für den Terminplan.');
return;
}
if(eintraege.length === 1){ nacheinanderOeffnen(d, eintraege); return; }
// Auch bei nur einem Vorschlag erst die Liste — dort wird gewählt, ob es
// eine Aufgabe oder ein Meilenstein wird.
auswahlZeigen(d, eintraege);
}
@@ -240,6 +248,16 @@
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,
@@ -271,20 +289,35 @@
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 = `<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>
<h2>${eintraege.length === 1 ? '1 Eintrag' : eintraege.length + ' Einträge'} in der Mail</h2>
<p class="hint">${esc(hinweisText(d))} Wähle je Eintrag, ob er eine Aufgabe oder ein Meilenstein wird. Die ausgewählten öffnen sich danach nacheinander zum Prüfen.</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="mail-auswahl-zeile">
<input type="checkbox" checked data-i="${i}" aria-label="${esc(e.titel)} übernehmen">
<span class="mail-auswahl-text"><span class="mail-auswahl-titel">${esc(e.titel)}</span>
<span class="mail-auswahl-datum">${esc(zeitraum(e))}</span></span>
<span class="segmented mail-auswahl-art" role="group" aria-label="Art" data-i="${i}">
<button type="button" class="mail-art-knopf" data-art="aufgabe" aria-pressed="${arten[i] === 'aufgabe'}">Aufgabe</button>
<button type="button" class="mail-art-knopf" data-art="meilenstein" aria-pressed="${arten[i] === 'meilenstein'}">Meilenstein</button>
</span>
</div>`).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.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 => eintraege[Number(cb.dataset.i)]);
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);
};