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
@@ -132,18 +132,17 @@
|
||||
return m[2] === String(new Date().getFullYear()) ? m[1] : m[1] + ' / ' + m[2];
|
||||
}
|
||||
|
||||
const PLAN_VERLAUF_KURZ = 3;
|
||||
let planVerlaufAlle = false;
|
||||
let planVerlaufProjekt = null;
|
||||
|
||||
function renderPlanKommentarVerlauf(verlauf){
|
||||
const el = document.getElementById('planStatusVerlauf');
|
||||
if(!el) return;
|
||||
const uid = app.project && (app.project.projectUid || app.project.id);
|
||||
if(uid !== planVerlaufProjekt){ planVerlaufProjekt = uid; planVerlaufAlle = false; }
|
||||
if(!verlauf.length){ el.hidden = true; el.innerHTML = ''; return; }
|
||||
// Gezeigt wird, was geladen ist — beim Start die drei Sitzungen vor der
|
||||
// aktuellen; „Weitere laden“ holt je fünf ältere (weitereLadenKnopf(),
|
||||
// js/store.js). Die Sandbox hat keine früheren Fassungen.
|
||||
const weitere = app.isSandboxPlan() ? '' : weitereLadenKnopf();
|
||||
if(!verlauf.length && !weitere){ el.hidden = true; el.innerHTML = ''; return; }
|
||||
el.hidden = false;
|
||||
const zeigen = planVerlaufAlle ? verlauf : verlauf.slice(0, PLAN_VERLAUF_KURZ);
|
||||
const zeigen = verlauf;
|
||||
const zeilen = zeigen.map((e, i) => `
|
||||
<li class="plan-verlauf-eintrag${i === 0 ? ' ist-juengster' : ''}">
|
||||
<span class="plan-verlauf-kw">${esc(kurzKw(e.week.label))}</span>
|
||||
@@ -153,21 +152,15 @@
|
||||
<div class="plan-verlauf-meta">${esc(e.gewerk)}${e.autor ? ' · ' + esc(e.autor) : ''}</div>
|
||||
</div>
|
||||
</li>`).join('');
|
||||
const mehr = verlauf.length > PLAN_VERLAUF_KURZ
|
||||
? `<button type="button" class="plan-verlauf-mehr" onclick="planVerlaufUmschalten()">${planVerlaufAlle ? 'Weniger anzeigen' : 'Alle ' + verlauf.length + ' anzeigen'}</button>`
|
||||
: '';
|
||||
const mehr = weitere ? `<div class="plan-verlauf-mehr-zeile">${weitere}</div>` : '';
|
||||
el.innerHTML = `
|
||||
<div class="plan-verlauf-kopf">
|
||||
<span class="plan-verlauf-titel">Verlauf</span>
|
||||
<span class="plan-verlauf-anzahl">${verlauf.length === 1 ? '1 früherer Kommentar' : verlauf.length + ' frühere Kommentare'}</span>
|
||||
<span class="plan-verlauf-anzahl">${verlauf.length === 0 ? 'in den geladenen Sitzungen kein früherer Kommentar' : verlauf.length === 1 ? '1 früherer Kommentar' : verlauf.length + ' frühere Kommentare'}</span>
|
||||
</div>
|
||||
<ol class="plan-verlauf-liste">${zeilen}</ol>${mehr}`;
|
||||
${zeilen ? `<ol class="plan-verlauf-liste">${zeilen}</ol>` : ''}${mehr}`;
|
||||
}
|
||||
|
||||
window.planVerlaufUmschalten = function(){
|
||||
planVerlaufAlle = !planVerlaufAlle;
|
||||
window.renderPlanInfo();
|
||||
};
|
||||
|
||||
|
||||
window.renderPlanInfo = function(){
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -33,8 +33,15 @@
|
||||
// (Index des Meilensteins oder null für einen neuen). Befüllt Name, Datum,
|
||||
// Positions-Auswahl und die Vorgänger-Checkboxen (andere Meilensteine und
|
||||
// TODOs) aus dem Plan.
|
||||
window.openMilestoneModal = function(index){
|
||||
// msNachSchliessen: wie todoNachSchliessen — "Termin aus Mail" öffnet
|
||||
// damit nach dem Schließen den nächsten Vorschlag.
|
||||
let msNachSchliessen = null;
|
||||
|
||||
// openMilestoneModal: Parameter index (null = neu), prefill (optional, nur
|
||||
// bei neu: name, date, notiz, mailHinweis, nachSchliessen).
|
||||
window.openMilestoneModal = function(index, prefill){
|
||||
if(app.isLocked()) return;
|
||||
const pre = prefill || {};
|
||||
const plan = app.ensurePlanLocal();
|
||||
if(!Array.isArray(plan.milestones)) plan.milestones = [];
|
||||
// Migration alter Meilensteine
|
||||
@@ -43,10 +50,18 @@
|
||||
|
||||
const isNew = index === null || plan.milestones[index] === undefined;
|
||||
const currentMs = isNew ? null : plan.milestones[index];
|
||||
document.getElementById('milestoneEditName').value = isNew ? '' : (currentMs.name || '');
|
||||
document.getElementById('milestoneEditName').value = isNew ? (pre.name || '') : (currentMs.name || '');
|
||||
const msNotiz = document.getElementById('milestoneEditNotiz');
|
||||
if(msNotiz) msNotiz.value = isNew ? (pre.notiz || '') : (currentMs.notiz || '');
|
||||
const msMailHinweis = document.getElementById('milestoneEditMailHinweis');
|
||||
if(msMailHinweis){
|
||||
msMailHinweis.hidden = !(isNew && pre.mailHinweis);
|
||||
msMailHinweis.textContent = (isNew && pre.mailHinweis) || '';
|
||||
}
|
||||
msNachSchliessen = isNew && typeof pre.nachSchliessen === 'function' ? pre.nachSchliessen : null;
|
||||
// Verstecktes ISO-Feld + sichtbares Anzeigefeld (deutsches Format) —
|
||||
// dieselbe Aufteilung wie bei allen anderen Datumseingaben der Seite.
|
||||
const msDateIso = isNew ? '' : (currentMs.date || '');
|
||||
const msDateIso = isNew ? (pre.date || '') : (currentMs.date || '');
|
||||
document.getElementById('milestoneEditDate').value = msDateIso;
|
||||
const msDateDisplay = document.getElementById('milestoneEditDateDisplay');
|
||||
if(msDateDisplay) msDateDisplay.value = msDateIso ? formatDateDE(parseDateISO(msDateIso)) : '';
|
||||
@@ -152,6 +167,9 @@
|
||||
window.closeMilestoneModal = function(){
|
||||
document.getElementById('milestoneModal').style.display = 'none';
|
||||
app.currentMilestoneIndex = null;
|
||||
const danach = msNachSchliessen;
|
||||
msNachSchliessen = null;
|
||||
if(danach) setTimeout(danach, 0);
|
||||
};
|
||||
|
||||
// saveMilestoneEdit: übernimmt die Modal-Eingaben (Name, Datum, Position,
|
||||
@@ -169,18 +187,21 @@
|
||||
|
||||
const positionSelect = document.getElementById('milestoneEditPosition');
|
||||
const position = positionSelect ? positionSelect.value : '';
|
||||
const msNotizEl = document.getElementById('milestoneEditNotiz');
|
||||
const notiz = msNotizEl ? msNotizEl.value.trim() : '';
|
||||
|
||||
const plan = app.ensurePlanLocal();
|
||||
if(!Array.isArray(plan.milestones)) plan.milestones = [];
|
||||
|
||||
if(app.currentMilestoneIndex === null){
|
||||
plan.milestones.push({ id: uid(), name, date, dependsOn, position });
|
||||
plan.milestones.push({ id: uid(), name, date, dependsOn, position, notiz });
|
||||
} else if(plan.milestones[app.currentMilestoneIndex]){
|
||||
const m = plan.milestones[app.currentMilestoneIndex];
|
||||
m.name = name;
|
||||
m.date = date;
|
||||
m.dependsOn = dependsOn;
|
||||
m.position = position;
|
||||
m.notiz = notiz;
|
||||
}
|
||||
plan.milestones.sort((a,b) => parseDateISO(a.date) - parseDateISO(b.date));
|
||||
app.savePlan();
|
||||
|
||||
@@ -99,8 +99,21 @@ window.PlanApp = {
|
||||
// showTab('plan') gerufen (und von dort aus auch nach jedem erneuten
|
||||
// Laden, siehe load() in js/store.js). Rückgabe: true, wenn das Projekt
|
||||
// gefunden und gerendert wurde.
|
||||
// startAufgabe: Link mit ?aufgabe=<id> (z. B. aus einer Teams-Nachricht):
|
||||
// diese Aufgabe nach dem Öffnen des Projekts gleich im Aufgaben-Panel
|
||||
// zeigen — einmalig, beim ersten passenden Öffnen.
|
||||
let startAufgabe = '';
|
||||
try { startAufgabe = new URLSearchParams(window.location.search).get('aufgabe') || ''; } catch(e){ /* egal */ }
|
||||
|
||||
window.openPlanView = function(){
|
||||
app.showPlanNav();
|
||||
// Ein Aufgaben-Link stammt aus der Sitzung, in der zugewiesen wurde. Ist
|
||||
// inzwischen eine neuere da, dort öffnen — Aufgaben behalten über den
|
||||
// Sitzungswechsel ihre id (ensureCurrentWeekRollover()).
|
||||
if(startAufgabe && app.weekLabel !== SANDBOX_WEEK_LABEL && Array.isArray(db.wochenListe) && db.wochenListe.length){
|
||||
const juengste = db.wochenListe[db.wochenListe.length - 1].label;
|
||||
if(juengste && juengste !== app.weekLabel) app.weekLabel = juengste;
|
||||
}
|
||||
|
||||
// Planung-Sandbox: eigener, einfacherer Zweig — eine feste, reservierte
|
||||
// "Sitzung" (db.sandboxWeek, nie Teil von db.weeks, siehe js/store.js)
|
||||
@@ -143,6 +156,19 @@ window.PlanApp = {
|
||||
}
|
||||
app.week = db.weeks.find(w => w.label === app.weekLabel) || null;
|
||||
if(!app.week){
|
||||
// Eine ältere Sitzung, die beim Start nicht mitgeladen wurde (nur die
|
||||
// jüngsten, js/store.js): einzeln nachholen und dann neu öffnen.
|
||||
const bekannt = (db.wochenListe || []).some(w => w.label === app.weekLabel);
|
||||
if(bekannt && typeof sitzungSicherLaden === 'function'){
|
||||
const t = document.getElementById('planSubtitle');
|
||||
if(t) t.textContent = 'Sitzung ' + app.weekLabel + ' wird geladen…';
|
||||
const label = app.weekLabel;
|
||||
sitzungSicherLaden(label).then(w => {
|
||||
if(w && app.weekLabel === label && currentTab === 'plan') showTab('plan');
|
||||
else if(!w) planFail('Nicht gefunden', 'Sitzung "' + label + '" konnte nicht geladen werden.');
|
||||
}).catch(() => planFail('Nicht gefunden', 'Sitzung "' + label + '" konnte nicht geladen werden.'));
|
||||
return false;
|
||||
}
|
||||
planFail('Nicht gefunden', 'Sitzung "' + app.weekLabel + '" konnte nicht geladen werden.');
|
||||
return false;
|
||||
}
|
||||
@@ -286,6 +312,22 @@ window.PlanApp = {
|
||||
// js/plan/plan-sections.js. Steht bewusst am Ende: erst wenn alle
|
||||
// Bereiche befüllt sind, wird einer davon eingeblendet.
|
||||
app.initPlanSections();
|
||||
|
||||
if(startAufgabe){
|
||||
const id = startAufgabe;
|
||||
startAufgabe = '';
|
||||
// Adresszeile auf die tatsächlich geöffnete Sitzung bringen (der Link
|
||||
// nannte evtl. eine ältere, siehe openPlanView()).
|
||||
if(typeof updateRouteUrl === 'function') updateRouteUrl('plan');
|
||||
const idx = (plan.todos || []).findIndex(t => String(t.id) === id);
|
||||
if(idx < 0){ showToast('Die Aufgabe aus dem Link gibt es in diesem Projekt nicht mehr.'); return; }
|
||||
if(location.hash !== '#aufgaben') showPlanSection('aufgaben');
|
||||
// Panel erst nach dem Zeichnen öffnen (das Gantt misst seine Breite).
|
||||
setTimeout(() => {
|
||||
if(isLocked()){ showToast('Aufgabe: ' + (plan.todos[idx].title || '') + ' — diese Sitzung ist gesperrt.'); return; }
|
||||
openTodoModal(idx);
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
// Entfernt eine gelöschte ID aus allen dependsOn-Listen (TODOs und Meilensteine)
|
||||
|
||||
Reference in new issue
Block a user