// ----------------------------- // Projektplan — Aufgaben-Vorlagen je Leistungsphase // ----------------------------- // In einem Ingenieurbüro wiederholt sich der Ablauf einer Leistungsphase über // alle Projekte hinweg. Dieses Modul macht daraus wiederverwendbare Vorlagen: // Der Button "+ Vorlage" in der GANTT-Toolbar öffnet #lphTemplateModal, dort // wählt man eine Leistungsphase und ein Startdatum, hakt die gewünschten // Schritte an und fügt sie mit einem Klick als TODOs in den Projektplan ein. // // Der Honorar-Terminplan (js/plan/plan-honorar.js) ist ein eigener Dialog mit // eigener Schaltfläche "+ Terminplan" — beide teilten sich früher einen Dialog // mit Modus-Umschalter, was aber bedeutete, dass auf dem Knopf nur eines der // beiden Verfahren stand. // // Datenquelle sind die Routen /lph-templates (server.js, Tabelle // lph_templates): GET für alle angemeldeten Nutzer, POST/DELETE nur für // Admins — das Einfügen darf jeder, das Pflegen der firmenweiten Vorlagen // nicht. Die Pflege-Maske selbst steht seit dem Umbau NICHT mehr in diesem // Dialog, sondern im Bereich „Einstellungen" (js/settings/settings.js) — // hier bleibt für Administratoren nur ein Verweis dorthin. // // WICHTIG: Eingefügte Aufgaben sind danach ganz normale plan_todos ohne // Verbindung zur Vorlage. Eine später geänderte Vorlage verändert bereits // eingefügte Aufgaben also NICHT — ein laufender Projektplan soll sich nicht // rückwirkend unter der Hand ändern. // // Läuft wie alle plan-Module in einem IIFE; Zustand: window.PlanApp // (plan-state.js). (function(){ const app = window.PlanApp; // templateCache: zuletzt geladene Vorlagen (flaches Array), damit das // Umschalten der Leistungsphase im Modal ohne erneuten Serveraufruf geht. let templateCache = []; // TEMPLATE_LPHS: Leistungsphasen, für die Vorlagen angelegt werden können — // dieselbe Liste wie LPHS (js/globals.js) und wie VALID_LPHS in server.js. const TEMPLATE_LPHS = LPHS; // openTemplateModal: öffnet das Vorlagen-Modal, lädt die Vorlagen (einmal // pro Seitenaufruf) und baut Leistungsphasen-Auswahl sowie Startdatum auf. // Bei gesperrter Sitzung wird gar nicht erst geöffnet (wie bei den anderen // Bearbeitungs-Dialogen, siehe plan-modals.js). window.openTemplateModal = function(){ if(app.isLocked()) return; const lphSelect = document.getElementById('templateLphSelect'); if(lphSelect && !lphSelect.options.length){ // Vorbelegung: die amtierende Leistungsphase des Projekts, sonst LPH 1 const aktLp = Array.isArray(app.project.aktuelleLP) ? app.project.aktuelleLP[0] : ''; lphSelect.innerHTML = TEMPLATE_LPHS.map(l => `` ).join(''); } // Startdatum vorbelegen: heute const startInput = document.getElementById('templateStart'); const startDisplay = document.getElementById('templateStartDisplay'); if(startInput && !startInput.value){ const today = formatDateISO(new Date()); startInput.value = today; if(startDisplay) startDisplay.value = formatDateDE(parseDateISO(today)); } document.getElementById('lphTemplateModal').style.display = 'flex'; loadTemplates(); }; window.closeTemplateModal = function(){ document.getElementById('lphTemplateModal').style.display = 'none'; }; // loadTemplates: holt die Vorlagen vom Server und rendert danach die Liste. // Fehler werden im Listenbereich gemeldet (kein alert, damit das Modal // benutzbar bleibt). function loadTemplates(){ const listEl = document.getElementById('templateTaskList'); if(listEl) listEl.innerHTML = 'Lade Vorlagen…'; fetch(baseUrl() + '/lph-templates') .then(r => { if(!r.ok) throw new Error('Server HTTP ' + r.status); return r.json(); }) .then(data => { templateCache = Array.isArray(data.templates) ? data.templates : []; renderTemplateList(); }) .catch(err => { console.error('Vorlagen laden fehlgeschlagen:', err); if(listEl) listEl.innerHTML = 'Vorlagen konnten nicht geladen werden.'; }); } // templatesForSelectedLph: die Vorlagen-Schritte der gerade gewählten // Leistungsphase, in ihrer gepflegten Reihenfolge. Rückgabe: Array. function templatesForSelectedLph(){ const lphSelect = document.getElementById('templateLphSelect'); const lph = lphSelect ? lphSelect.value : ''; return templateCache.filter(t => t.lph === lph); } // renderTemplateList: baut die Auswahlliste der Schritte (Checkbox je // Schritt, standardmäßig alle angehakt) samt Vorschau der Termine, die sich // aus dem gewählten Startdatum ergeben. Für Admins wird darunter der // Pflege-Bereich gerendert. Wird bei jedem Wechsel von Leistungsphase oder // Startdatum erneut aufgerufen. Keine Parameter, keine Rückgabe. window.renderTemplateList = function(){ const listEl = document.getElementById('templateTaskList'); if(!listEl) return; const entries = templatesForSelectedLph(); const startIso = (document.getElementById('templateStart') || {}).value || ''; const startDate = parseDateISO(startIso); if(entries.length === 0){ listEl.innerHTML = 'Für diese Leistungsphase ist noch keine Vorlage hinterlegt.'; } else { listEl.innerHTML = entries.map(t => { // Vorschau der konkreten Termine (nur wenn ein Startdatum gesetzt ist) let dateHint = `Tag +${t.offsetDays}, ${t.durationDays} Tag${t.durationDays === 1 ? '' : 'e'}`; if(startDate){ const s = addDays(startDate, t.offsetDays); const e = addDays(s, Math.max(0, t.durationDays - 1)); dateHint = formatDateDE(s) + '–' + formatDateDE(e); } return ``; }).join(''); } renderTemplateHint(); }; // renderTemplateHint: blendet für Administratoren den Verweis auf den // Bereich „Einstellungen" ein. Das Anlegen und Löschen von Schritten stand // bis zum Umbau hier im Dialog; es liegt jetzt in js/settings/settings.js. // Dieser Dialog FÜGT Vorlagen nur noch ein. Keine Parameter, keine Rückgabe. function renderTemplateHint(){ const el = document.getElementById('templateAdminHint'); if(!el) return; el.style.display = (currentUser && currentUser.role === 'admin') ? '' : 'none'; } // applyTemplate: erzeugt aus den angehakten Vorlagen-Schritten neue TODOs im // aktuellen Projektplan. Termine ergeben sich aus dem gewählten Startdatum // plus offsetDays bzw. durationDays. Die neuen Aufgaben werden ans Ende der // Aufgabenliste gehängt, ohne Zuständige und ohne Abhängigkeiten — das wird // anschließend im TODO-Dialog ergänzt. window.applyTemplate = function(){ if(app.isLocked()) return; const startIso = (document.getElementById('templateStart') || {}).value || ''; const startDate = parseDateISO(startIso); if(!startDate){ alert('Bitte ein Startdatum wählen.'); return; } const listEl = document.getElementById('templateTaskList'); const checkedIds = listEl ? Array.from(listEl.querySelectorAll('input[type="checkbox"]:checked')).map(cb => cb.value) : []; if(checkedIds.length === 0){ alert('Bitte mindestens einen Schritt auswählen.'); return; } const entries = templatesForSelectedLph().filter(t => checkedIds.includes(t.id)); const plan = app.ensurePlanLocal(); entries.forEach(t => { const s = addDays(startDate, t.offsetDays); const e = addDays(s, Math.max(0, t.durationDays - 1)); plan.todos.push({ id: uid(), title: t.title, start: formatDateISO(s), end: formatDateISO(e), done: false, progress: 0, dependsOn: [], assignees: [], }); }); app.savePlan(); closeTemplateModal(); fitPlanGanttRange(); app.showToast(entries.length + ' Aufgabe(n) aus der Vorlage eingefügt.'); }; })();