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:
1 parent
a2266662ad
commit
4c2e04e78b
19 files changed
+1100
-139
No files matched your search
@@ -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);
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user