// ----------------------------- // 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 ? ' Achtung: zyklische Abhängigkeit gefunden!' : '')); } 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; }; })();