Termin aus Mail, Rollen & Rechte, Abwesenheiten, Glas-Darstellung und Pi-Betrieb
- Termin aus Mail: Mailtext oder .eml per Drag & Drop/Strg+V auf Terminplan oder Aufgaben, KI schlägt Aufgaben vor, Prüfung im Aufgaben-Panel; neues Notizfeld je Aufgabe (plan_todos.notiz) - Terminplan: Meilensteine ohne Aufgabenbezug je in eigener Zeile, "+ Neu"-Menü klappt neben schmaler Seitenleiste nach rechts, Leistungsphasen - Rollen & Rechte, Abwesenheiten-Seite, Benachrichtigungen, Rundgang - Glas-Darstellung, Logo als Favicon - Pi-Betrieb: PDF-Arbeit im Kindprozess, Wächter, Einrichtungs- und Prüfskript inkl. WLAN-Wächter Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f4fb3c550b
commit
b7e0bd0d92
56 files changed
+12265
-2974
No files matched your search
@@ -1,13 +1,11 @@
|
||||
// -----------------------------
|
||||
// Projektplan — Kritischer Pfad, Auto-Schedule & Mail-Link
|
||||
// Projektplan — 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).
|
||||
// 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).
|
||||
@@ -15,113 +13,7 @@
|
||||
(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.');
|
||||
}
|
||||
}
|
||||
};
|
||||
// ---------- Auto-Schedule ----------
|
||||
|
||||
// autoScheduleTodos: verschiebt alle TODOs und Meilensteine, die von
|
||||
// anderen Elementen abhängen, automatisch auf den frühesten möglichen
|
||||
|
||||
Reference in new issue
Block a user