code auf einzelne Datein aufgeteilt
This commit is contained in:
1 parent
7451e0bf78
commit
8c94fc91f6
30 files changed
+5765
-27
No files matched your search
@@ -0,0 +1,288 @@
|
||||
// -----------------------------
|
||||
// Projektplan — Kritischer Pfad, Auto-Schedule & Mail-Link
|
||||
// -----------------------------
|
||||
// Dieses Modul berechnet über computeCriticalPath die Aufgabe(n) auf dem
|
||||
// kritischen Pfad (längste Vorgänger-Kette) und hebt sie im GANTT hervor,
|
||||
// verschiebt bei autoScheduleTodos TODOs und Meilensteine automatisch hinter
|
||||
// ihre Vorgänger und erstellt bei todoMail einen mailto-Link an alle
|
||||
// zugeordneten Personen. Die Hervorhebung übergibt renderPlanGantt als
|
||||
// Option criticalPath an buildGanttHtml (plan-build-gantt.js); den Schalter
|
||||
// criticalPathActive hält der Zustand in window.PlanApp (plan-state.js).
|
||||
//
|
||||
// Läuft wie alle plan-Module in einem IIFE; Zustand: window.PlanApp
|
||||
// (plan-state.js).
|
||||
|
||||
(function(){
|
||||
const app = window.PlanApp;
|
||||
|
||||
// ---------- Kritischer Pfad & Auto-Schedule ----------
|
||||
|
||||
// computeCriticalPath: liefert Array der TODO-IDs, die auf einem kritischen Pfad liegen
|
||||
// (längste Kette von Vorgänger-Abhängigkeiten, End-to-Start)
|
||||
window.computeCriticalPath = function(project){
|
||||
if(!project || !project.plan) return [];
|
||||
const todos = (project.plan.todos || []).filter(t => t.start && t.end);
|
||||
const milestones = (project.plan.milestones || []).filter(m => m.date);
|
||||
if(todos.length === 0 && milestones.length === 0) return [];
|
||||
|
||||
// Einheitliche Knoten: TODOs und Meilensteine (Dauer 1)
|
||||
const nodes = todos.map(t => ({ type: 'todo', id: t.id, obj: t, start: parseDateISO(t.start), end: parseDateISO(t.end), duration: Math.max(1, daysBetween(parseDateISO(t.start), parseDateISO(t.end)) + 1) }));
|
||||
milestones.forEach(m => {
|
||||
const d = parseDateISO(m.date);
|
||||
nodes.push({ type: 'milestone', id: 'm_' + m.id, obj: m, start: d, end: d, duration: 1 });
|
||||
});
|
||||
const nodeMap = new Map(nodes.map(n => [n.id, n]));
|
||||
|
||||
// depsOf: liefert die Vorgänger-Liste eines Knotens (leeres Array, falls
|
||||
// keine vorhanden).
|
||||
function depsOf(n){
|
||||
return Array.isArray(n.obj.dependsOn) ? n.obj.dependsOn : [];
|
||||
}
|
||||
|
||||
// Frühester Start (ES) und frühestes Ende (EF) berechnen.
|
||||
// visiting-Set schützt vor Endlos-Rekursion bei zyklischen Abhängigkeiten.
|
||||
const es = {}, ef = {};
|
||||
const esVisiting = new Set();
|
||||
// computeES: berechnet rekursiv den frühesten Start (ES) und frühestes
|
||||
// Ende (EF) eines Knotens aus seinen Vorgängern. Liefert ES oder null
|
||||
// bei Zyklen (visiting-Set).
|
||||
function computeES(n){
|
||||
if(es[n.id] !== undefined) return es[n.id];
|
||||
if(esVisiting.has(n.id)){ es[n.id] = null; ef[n.id] = null; return null; } // Zyklus
|
||||
if(!n.start || !n.end){ es[n.id] = null; ef[n.id] = null; return null; }
|
||||
esVisiting.add(n.id);
|
||||
const deps = depsOf(n);
|
||||
let maxEF = null;
|
||||
deps.forEach(depId => {
|
||||
const dep = nodeMap.get(depId);
|
||||
if(!dep) return;
|
||||
const depStart = computeES(dep);
|
||||
if(!depStart) return;
|
||||
const depEnd = addDays(depStart, dep.duration - 1);
|
||||
if(maxEF === null || depEnd > maxEF) maxEF = depEnd;
|
||||
});
|
||||
esVisiting.delete(n.id);
|
||||
es[n.id] = maxEF === null ? n.start : addDays(maxEF, 1);
|
||||
ef[n.id] = addDays(es[n.id], n.duration - 1);
|
||||
return es[n.id];
|
||||
}
|
||||
nodes.forEach(n => computeES(n));
|
||||
|
||||
const validNodes = nodes.filter(n => es[n.id]);
|
||||
if(validNodes.length === 0) return [];
|
||||
const maxEF = validNodes.reduce((max, n) => { const d = ef[n.id]; return !max || d > max ? d : max; }, null);
|
||||
|
||||
// Spätestes Ende (LF) und spätester Start (LS) rückwärts berechnen.
|
||||
// Wichtig: LF eines Knotens ist der Tag VOR dem spätesten Start (LS)
|
||||
// seines frühesten Nachfolgers – nicht vor dessen LF.
|
||||
const lf = {}, ls = {};
|
||||
const lfVisiting = new Set();
|
||||
// computeLF: berechnet rekursiv den spätesten Start (LS) und das späteste
|
||||
// Ende (LF) eines Knotens rückwärts aus seinen Nachfolgern. Liefert LF
|
||||
// oder null bei Zyklen (visiting-Set).
|
||||
function computeLF(n){
|
||||
if(lf[n.id] !== undefined) return lf[n.id];
|
||||
if(lfVisiting.has(n.id)){ lf[n.id] = null; ls[n.id] = null; return null; } // Zyklus
|
||||
if(!n.start || !n.end){ lf[n.id] = null; ls[n.id] = null; return null; }
|
||||
lfVisiting.add(n.id);
|
||||
const successors = validNodes.filter(succ => depsOf(succ).includes(n.id));
|
||||
let minLF = null;
|
||||
successors.forEach(succ => {
|
||||
if(!computeLF(succ)) return;
|
||||
const succLS = ls[succ.id];
|
||||
if(!succLS) return;
|
||||
const candidate = addDays(succLS, -1);
|
||||
if(minLF === null || candidate < minLF) minLF = candidate;
|
||||
});
|
||||
lfVisiting.delete(n.id);
|
||||
lf[n.id] = minLF === null ? maxEF : minLF;
|
||||
ls[n.id] = addDays(lf[n.id], -(n.duration - 1));
|
||||
return lf[n.id];
|
||||
}
|
||||
validNodes.forEach(n => computeLF(n));
|
||||
|
||||
return validNodes.filter(n => n.type === 'todo' && es[n.id] && ls[n.id] && daysBetween(es[n.id], ls[n.id]) === 0).map(n => n.id);
|
||||
};
|
||||
|
||||
// toggleCriticalPath: schaltet die Hervorhebung des kritischen Pfads im
|
||||
// GANTT ein/aus (Wert in app.criticalPathActive), aktualisiert den
|
||||
// Toolbar-Button und zeigt über einen Toast an, wie viele Aufgaben auf
|
||||
// dem kritischen Pfad liegen.
|
||||
window.toggleCriticalPath = function(){
|
||||
app.criticalPathActive = !app.criticalPathActive;
|
||||
const btn = document.getElementById('criticalPathBtn');
|
||||
if(btn) btn.classList.toggle('active', app.criticalPathActive);
|
||||
renderPlanGantt();
|
||||
if(app.criticalPathActive){
|
||||
const path = computeCriticalPath(app.project);
|
||||
if(path.length === 0){
|
||||
app.showToast('Kein kritischer Pfad ermittelt (zu wenig Aufgaben oder Abhängigkeiten).');
|
||||
} else {
|
||||
app.showToast(path.length + ' Aufgabe(n) auf dem kritischen Pfad hervorgehoben.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 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) 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);
|
||||
}
|
||||
|
||||
todos.forEach(t => scheduleTodo(t));
|
||||
milestones.forEach(m => scheduleMilestone(m));
|
||||
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;
|
||||
};
|
||||
})();
|
||||
Reference in new issue
Block a user