- Gestaltung „Bauplan“: ruhigere Farben, Schriftskala, Abstände im 4-px-Raster, Kalenderwochen und Nummern in Mono-Ziffern, Glas beruhigt - Dashboard „Meine Aufgaben“: Kopf mit Kalenderwoche, Band „Diese Woche“ (Mo–Fr), je Projekt Meine Rolle / Team / Notizen; offene Termine blau hervorgehoben; Monatskalender unverändert - Neuer Bereich „Notizen“ in der Seitenleiste: Notizen aller eigenen Projekte und eigene Notizen ohne Projekt, später einem Projekt zuordenbar - Seitenleiste neu gegliedert (Projekte, Team, Notizen, Studenten, Einstellungen/Admin unten), Admin-Punkte in Personen/Vorgaben/System - Reiter im Projektkopf wie die Seitenleiste hervorgehoben - Behoben: „&amp;…“ in Kommentaren wuchs bei jedem Speichern; bestehende Kommentare werden beim Start repariert - Behoben: Lösch-Rückfrage bei Notizen unlesbar bzw. blieb stehen, Cursor im Notiztitel verdeckt, Dropdown im Dunkelmodus gekachelt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
189 lines
7.5 KiB
JavaScript
189 lines
7.5 KiB
JavaScript
// -----------------------------
|
||
// Projektplan — Auto-Schedule & Mail-Link
|
||
// -----------------------------
|
||
// Dieses Modul verschiebt bei autoScheduleTodos TODOs und Meilensteine
|
||
// automatisch hinter ihre Vorgänger (aufgerufen beim Ziehen eines Balkens,
|
||
// plan-drag.js, und über "Auto-Schedule" im Bereich Aufgaben) und erstellt
|
||
// bei todoMail einen mailto-Link an alle zugeordneten Personen. Die frühere
|
||
// Hervorhebung des kritischen Pfads ist auf Eriks Wunsch entfallen.
|
||
//
|
||
// Läuft wie alle plan-Module in einem IIFE; Zustand: window.PlanApp
|
||
// (plan-state.js).
|
||
|
||
(function(){
|
||
const app = window.PlanApp;
|
||
|
||
// ---------- Auto-Schedule ----------
|
||
|
||
// autoScheduleTodos: verschiebt alle TODOs und Meilensteine, die von
|
||
// anderen Elementen abhängen, automatisch auf den frühesten möglichen
|
||
// Termin (einen Tag nach dem Ende des spätesten Vorgängers). Erkennt
|
||
// zyklische Abhängigkeiten und verhindert Endlos-Rekursion; danach
|
||
// Speichern, Neu-Rendern und Hinweis per Toast.
|
||
window.autoScheduleTodos = function(){
|
||
if(app.isLocked()) return;
|
||
const plan = app.ensurePlanLocal();
|
||
if(!Array.isArray(plan.todos) || plan.todos.length === 0) return;
|
||
const todos = plan.todos.filter(t => t.start && t.end);
|
||
const todoMap = new Map(todos.map(t => [t.id, t]));
|
||
const milestones = Array.isArray(plan.milestones) ? plan.milestones.filter(m => m.date) : [];
|
||
const milestoneMap = new Map(milestones.map(m => ['m_' + m.id, m]));
|
||
|
||
// duration: liefert die Dauer einer TODO in Tagen (mindestens 1 Tag).
|
||
function duration(t){ const s = parseDateISO(t.start), e = parseDateISO(t.end); return s && e ? Math.max(1, daysBetween(s, e) + 1) : 1; }
|
||
|
||
// processed: bereits fertig eingeplante Knoten
|
||
// visiting: aktuell im Rekursionspfad -> schützt vor Endlos-Rekursion bei Zyklen
|
||
// moved: Knoten, deren Datum sich tatsächlich geändert hat
|
||
const processed = new Set();
|
||
const visiting = new Set();
|
||
const moved = new Set();
|
||
let hasCycle = false;
|
||
|
||
// scheduleTodo: plant eine TODO rekursiv ein – nutzt den spätesten
|
||
// Vorgänger-Termin (TODO oder Meilenstein) als frühesten Start und
|
||
// verschiebt Start/Ende entsprechend (Eintrag in moved bei Änderung).
|
||
function scheduleTodo(t){
|
||
if(processed.has(t.id)) return;
|
||
if(visiting.has(t.id)){ hasCycle = true; return; }
|
||
const tStart = parseDateISO(t.start);
|
||
if(!tStart){ processed.add(t.id); return; }
|
||
visiting.add(t.id);
|
||
const deps = Array.isArray(t.dependsOn) ? t.dependsOn : [];
|
||
let earliest = tStart;
|
||
deps.forEach(depId => {
|
||
let depEnd = null;
|
||
const depTodo = todoMap.get(depId);
|
||
if(depTodo){
|
||
scheduleTodo(depTodo);
|
||
depEnd = parseDateISO(depTodo.end);
|
||
} else {
|
||
const depMs = milestoneMap.get(depId);
|
||
if(depMs){
|
||
// Den Meilenstein ZUERST einplanen: Vorher wurde gegen sein ALTES
|
||
// Datum gerechnet, und weil er erst nach allen Aufgaben verschoben
|
||
// wurde, konnte eine Aufgabe vor ihrem eigenen Vorgänger beginnen.
|
||
scheduleMilestone(depMs);
|
||
depEnd = parseDateISO(depMs.date);
|
||
}
|
||
}
|
||
if(!depEnd) return;
|
||
const candidate = addDays(depEnd, 1);
|
||
if(candidate > earliest) earliest = candidate;
|
||
});
|
||
visiting.delete(t.id);
|
||
const dur = duration(t);
|
||
const newStart = formatDateISO(earliest);
|
||
const newEnd = formatDateISO(addDays(earliest, dur - 1));
|
||
if(newStart !== t.start || newEnd !== t.end){
|
||
t.start = newStart;
|
||
t.end = newEnd;
|
||
moved.add(t.id);
|
||
}
|
||
processed.add(t.id);
|
||
}
|
||
|
||
// scheduleMilestone: plant einen Meilenstein ein, sofern er von anderen
|
||
// TODOs/Meilensteinen abhängig ist, und verschiebt sein Datum auf den
|
||
// frühestmöglichen Termin (Eintrag in moved bei Änderung).
|
||
function scheduleMilestone(m){
|
||
const mId = 'm_' + m.id;
|
||
if(processed.has(mId)) return;
|
||
if(visiting.has(mId)){ hasCycle = true; return; }
|
||
const deps = Array.isArray(m.dependsOn) ? m.dependsOn : [];
|
||
let earliest = parseDateISO(m.date);
|
||
if(!earliest){ processed.add(mId); return; }
|
||
visiting.add(mId);
|
||
deps.forEach(depId => {
|
||
let depEnd = null;
|
||
const depTodo = todoMap.get(depId);
|
||
if(depTodo){
|
||
scheduleTodo(depTodo);
|
||
depEnd = parseDateISO(depTodo.end);
|
||
} else {
|
||
const depMs = milestoneMap.get(depId);
|
||
if(depMs && depMs.id !== m.id){
|
||
scheduleMilestone(depMs);
|
||
depEnd = parseDateISO(depMs.date);
|
||
}
|
||
}
|
||
if(!depEnd) return;
|
||
const candidate = addDays(depEnd, 1);
|
||
if(candidate > earliest) earliest = candidate;
|
||
});
|
||
visiting.delete(mId);
|
||
const newDate = formatDateISO(earliest);
|
||
if(newDate !== m.date){
|
||
m.date = newDate;
|
||
moved.add(mId);
|
||
}
|
||
processed.add(mId);
|
||
}
|
||
|
||
// Meilensteine zuerst: scheduleTodo() ruft scheduleMilestone() für seine
|
||
// Vorgänger zwar selbst auf, aber ein Meilenstein OHNE Nachfolger-Aufgabe
|
||
// würde sonst erst am Ende verschoben.
|
||
milestones.forEach(m => scheduleMilestone(m));
|
||
todos.forEach(t => scheduleTodo(t));
|
||
if(moved.size > 0){
|
||
app.savePlan();
|
||
renderPlanGantt();
|
||
app.showToast(moved.size + ' Element(e) automatisch verschoben.' + (hasCycle ? ' Zyklische Abhängigkeit gefunden — bitte Vorgänger prüfen.' : ''));
|
||
} else if(hasCycle){
|
||
app.showToast('Achtung: zyklische Abhängigkeit gefunden – bitte Vorgänger prüfen.');
|
||
} else {
|
||
app.showToast('Keine Änderungen nötig – alle Termine passen zu den Abhängigkeiten.');
|
||
}
|
||
};
|
||
|
||
// ---------- Mail-Link für alle zugeordneten Personen ----------
|
||
|
||
// todoMail: baut aus den E-Mail-Adressen der zugeordneten Personen und
|
||
// Projektinformationen einen mailto-Link zusammen und öffnet ihn. Zeigt
|
||
// Warnungen, wenn keine E-Mail-Adressen vorhanden sind oder der Link sehr
|
||
// lang wird.
|
||
window.todoMail = function(){
|
||
if(!app.project || !app.week){
|
||
console.warn('Kein aktuelles Projekt/keine aktuelle Woche gefunden – Mail kann nicht erstellt werden.');
|
||
return;
|
||
}
|
||
|
||
const assignees = projectAssignees(app.project, app.week);
|
||
|
||
const empfaengerListe = assignees
|
||
.map(a => a.email || (a.person && a.person.email) || '')
|
||
.filter(email => email && email.trim() !== '');
|
||
|
||
const eindeutigeEmpfaenger = [...new Set(empfaengerListe)];
|
||
|
||
if(eindeutigeEmpfaenger.length === 0){
|
||
console.warn('Keine gültigen E-Mail-Adressen der zugeordneten Personen gefunden.');
|
||
return;
|
||
}
|
||
|
||
const empfaenger = eindeutigeEmpfaenger.join(',');
|
||
|
||
const titleEl = document.getElementById('planTitle');
|
||
const projektname = titleEl ? titleEl.textContent.trim() : 'Projekt';
|
||
const seitenLink = window.location.href;
|
||
const betreff = 'Neue Aufgaben im Projekt ' + projektname;
|
||
|
||
const nachricht =
|
||
'Guten Tag,\n\n' +
|
||
'es wurden neue Aufgaben im Projekt ' + projektname + ' hinzugefügt.\n\n' +
|
||
'Link zum Projekt:\n' +
|
||
seitenLink + '\n\n' +
|
||
'Mit freundlichen Grüßen';
|
||
|
||
const mailtoLink =
|
||
'mailto:' + encodeURIComponent(empfaenger).replace(/%2C/g, ',') +
|
||
'?subject=' + encodeURIComponent(betreff) +
|
||
'&body=' + encodeURIComponent(nachricht);
|
||
|
||
if(mailtoLink.length > 1800){
|
||
console.warn('mailto-Link ist sehr lang (' + mailtoLink.length + ' Zeichen) – manche Mail-Clients könnten ihn abschneiden.');
|
||
}
|
||
|
||
window.location.href = mailtoLink;
|
||
};
|
||
})(); |