Files
projektuebersicht/public/js/plan/plan-schedule.js
T
erikandClaude Opus 5.5 e291ca8438 Neues Design „Bauplan“, Dashboard, Notizen-Bereich, Seitenleiste
- 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: „&…“ 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>
2026-11-02 15:07:32 +01:00

189 lines
7.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// -----------------------------
// 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;
};
})();