// ----------------------------- // Projektplan — Modals (Meilenstein, TODO, Abwesenheit) // ----------------------------- // Dieses Modul stellt die Öffnen-/Schließen-/Speichern-/Löschen-Funktionen // der drei Dialoge der Projektplan-Seite bereit: Meilenstein, TODO und // Abwesenheiten. Der Bearbeitungszustand liegt in window.PlanApp // (plan-state.js): currentMilestoneIndex, currentTodoIndex und // currentAbsencePersonId. Alle Änderungen werden über app.savePlan bzw. // app.saveGlobalPeople gespeichert und das GANTT über renderPlanGantt // (plan-gantt.js) neu gezeichnet. Bei gesperrten Wochen (app.isLocked) sind // alle Bearbeitungsfunktionen deaktiviert. // // Läuft wie alle plan-Module in einem IIFE; Zustand: window.PlanApp // (plan-state.js). (function(){ const app = window.PlanApp; // ---------- Meilenstein-Modal ---------- // openMilestoneModal: öffnet das Meilenstein-Modal. Parameter: index // (Index des Meilensteins oder null für einen neuen). Befüllt Name, Datum, // Positions-Auswahl und die Vorgänger-Checkboxen (andere Meilensteine und // TODOs) aus dem Plan. window.openMilestoneModal = function(index){ if(app.isLocked()) return; const plan = app.ensurePlanLocal(); if(!Array.isArray(plan.milestones)) plan.milestones = []; // Migration alter Meilensteine plan.milestones.forEach(m => { if(!Array.isArray(m.dependsOn)) m.dependsOn = []; }); app.currentMilestoneIndex = index; const isNew = index === null || plan.milestones[index] === undefined; const currentMs = isNew ? null : plan.milestones[index]; document.getElementById('milestoneEditName').value = isNew ? '' : (currentMs.name || ''); document.getElementById('milestoneEditDate').value = isNew ? '' : (currentMs.date || ''); const positionSelect = document.getElementById('milestoneEditPosition'); if(positionSelect){ const currentPosition = currentMs && currentMs.position ? currentMs.position : ''; const defaultAfterTodo = isNew && plan.todos && plan.todos.length > 0 ? plan.todos[plan.todos.length - 1].id : null; let opts = ''; if(plan.todos && plan.todos.length > 0){ plan.todos.forEach(t => { const beforeVal = 'before:' + t.id; const afterVal = 'after:' + t.id; const beforeSel = currentPosition === beforeVal ? 'selected' : ''; let afterSel = currentPosition === afterVal ? 'selected' : ''; if(isNew && defaultAfterTodo === t.id && !currentPosition) afterSel = 'selected'; opts += ``; opts += ``; }); } positionSelect.innerHTML = opts; } const dependsOnList = document.getElementById('milestoneEditDependsOn'); if(dependsOnList){ const currentDeps = currentMs && Array.isArray(currentMs.dependsOn) ? currentMs.dependsOn : []; let listHtml = ''; // Andere Meilensteine als Vorgänger anbieten if(plan.milestones.length > 1){ listHtml += '
Meilensteine
'; plan.milestones.forEach(m => { if(currentMs && m.id === currentMs.id) return; const mId = 'm_' + m.id; const checked = currentDeps.includes(mId) ? 'checked' : ''; listHtml += ``; }); } // TODOs als Vorgänger anbieten if(plan.todos && plan.todos.length > 0){ listHtml += '
Aufgaben
'; (plan.todos || []).forEach(t => { const checked = currentDeps.includes(t.id) ? 'checked' : ''; const datesStr = (t.start || t.end) ? ` (${formatDateDE(parseDateISO(t.start))}–${formatDateDE(parseDateISO(t.end))})` : ''; listHtml += ``; }); } if(listHtml === '') listHtml = 'Keine anderen Aufgaben oder Meilensteine vorhanden.'; dependsOnList.innerHTML = listHtml; } document.getElementById('milestoneModal').style.display = 'flex'; document.getElementById('milestoneEditName').focus(); }; // closeMilestoneModal: blendet das Meilenstein-Modal aus und setzt den // Bearbeitungsindex zurück. window.closeMilestoneModal = function(){ document.getElementById('milestoneModal').style.display = 'none'; app.currentMilestoneIndex = null; }; // saveMilestoneEdit: übernimmt die Modal-Eingaben (Name, Datum, Position, // Vorgänger) in den Plan – als neuer Eintrag oder als Update des // aktuell bearbeiteten Meilensteins. Sortiert danach nach Datum, speichert // und rendert neu. window.saveMilestoneEdit = function(){ if(app.isLocked()) return; const name = document.getElementById('milestoneEditName').value.trim(); const date = document.getElementById('milestoneEditDate').value; if(!name || !date){ alert('Bitte Name und Datum angeben.'); return; } const dependsOnList = document.getElementById('milestoneEditDependsOn'); const dependsOn = dependsOnList ? Array.from(dependsOnList.querySelectorAll('input[type="checkbox"]:checked')).map(cb => cb.value).filter(Boolean) : []; const positionSelect = document.getElementById('milestoneEditPosition'); const position = positionSelect ? positionSelect.value : ''; const plan = app.ensurePlanLocal(); if(!Array.isArray(plan.milestones)) plan.milestones = []; if(app.currentMilestoneIndex === null){ plan.milestones.push({ id: uid(), name, date, dependsOn, position }); } else if(plan.milestones[app.currentMilestoneIndex]){ const m = plan.milestones[app.currentMilestoneIndex]; m.name = name; m.date = date; m.dependsOn = dependsOn; m.position = position; } plan.milestones.sort((a,b) => parseDateISO(a.date) - parseDateISO(b.date)); app.savePlan(); closeMilestoneModal(); renderPlanGantt(); }; // deleteMilestoneEdit: löscht nach Bestätigung den aktuell bearbeiteten // Meilenstein samt seiner Vorgänger-Verweise (app.removeDependencyRefs), // speichert und rendert neu. window.deleteMilestoneEdit = function(){ if(app.isLocked()) return; if(app.currentMilestoneIndex === null){ closeMilestoneModal(); return; } if(!confirm('Meilenstein löschen?')) return; const plan = app.ensurePlanLocal(); if(plan.milestones && plan.milestones[app.currentMilestoneIndex]){ const deletedId = 'm_' + plan.milestones[app.currentMilestoneIndex].id; plan.milestones.splice(app.currentMilestoneIndex, 1); app.removeDependencyRefs(plan, deletedId); app.savePlan(); } closeMilestoneModal(); renderPlanGantt(); }; // addPlanMilestone: öffnet das Meilenstein-Modal für einen neuen Eintrag. window.addPlanMilestone = function(){ openMilestoneModal(null); }; // ---------- TODO-Modal ---------- // openTodoModal: öffnet das TODO-Modal. Parameter: index (Index der Aufgabe // oder null für eine neue). Befüllt Titel, Start/Ende, Erledigt-Status, // Fortschritt, Vorgänger-Checkboxen und die aus dem Projektboard // stammenden Bearbeiter-Auswahl. window.openTodoModal = function(index){ if(app.isLocked()) return; const plan = app.ensurePlanLocal(); if(!Array.isArray(plan.todos)) plan.todos = []; app.currentTodoIndex = index; const isNew = index === null || plan.todos[index] === undefined; const currentTodo = !isNew ? plan.todos[index] : null; document.getElementById('todoEditTitle').value = isNew ? '' : (currentTodo.title || ''); document.getElementById('todoEditStart').value = isNew ? '' : (currentTodo.start || ''); document.getElementById('todoEditEnd').value = isNew ? '' : (currentTodo.end || ''); document.getElementById('todoEditDone').checked = isNew ? false : !!currentTodo.done; const progress = isNew ? 0 : (parseInt(currentTodo.progress, 10) || 0); document.getElementById('todoEditProgress').value = progress; document.getElementById('todoEditProgressVal').textContent = progress + '%'; const dependsOnList = document.getElementById('todoEditDependsOn'); if(dependsOnList){ const currentDeps = currentTodo && Array.isArray(currentTodo.dependsOn) ? currentTodo.dependsOn : []; let listHtml = ''; // Meilensteine als Vorgänger anbieten if(Array.isArray(plan.milestones) && plan.milestones.length > 0){ listHtml += '
Meilensteine
'; plan.milestones.forEach(m => { const mId = 'm_' + m.id; const checked = currentDeps.includes(mId) ? 'checked' : ''; listHtml += ``; }); } // Andere TODOs als Vorgänger anbieten if(plan.todos && plan.todos.length > 1){ listHtml += '
Aufgaben
'; (plan.todos || []).forEach(t => { if(currentTodo && t.id === currentTodo.id) return; const checked = currentDeps.includes(t.id) ? 'checked' : ''; const datesStr = (t.start || t.end) ? ` (${formatDateDE(parseDateISO(t.start))}–${formatDateDE(parseDateISO(t.end))})` : ''; listHtml += ``; }); } if(listHtml === '') listHtml = 'Keine anderen Aufgaben oder Meilensteine vorhanden.'; dependsOnList.innerHTML = listHtml; } const assigneesDiv = document.getElementById('todoEditAssignees'); if(assigneesDiv){ const assignees = projectAssignees(app.project, app.week); const currentAssignees = currentTodo && Array.isArray(currentTodo.assignees) ? currentTodo.assignees : []; const seenPeople = new Set(); const uniquePeople = []; assignees.forEach(a => { if(!seenPeople.has(a.person.id)){ seenPeople.add(a.person.id); uniquePeople.push(a.person); } }); assigneesDiv.innerHTML = uniquePeople.map(person => { const checked = currentAssignees.includes(person.id) ? 'checked' : ''; return ``; }).join('') || 'Keine Projektzuordnungen im Board.'; } document.getElementById('todoModal').style.display = 'flex'; document.getElementById('todoEditTitle').focus(); }; // closeTodoModal: blendet das TODO-Modal aus und setzt den Bearbeitungsindex // zurück. window.closeTodoModal = function(){ document.getElementById('todoModal').style.display = 'none'; app.currentTodoIndex = null; }; // saveTodoEdit: übernimmt die Modal-Eingaben in den Plan – als neuer TODO // oder Update des aktuell bearbeiteten. Fehlende End-/Startwerte werden // als eintägige Aufgabe ergänzt, „Erledigt“ setzt den Fortschritt auf // 100 %. Speichert und rendert neu. window.saveTodoEdit = function(){ if(app.isLocked()) return; const title = document.getElementById('todoEditTitle').value.trim(); let start = document.getElementById('todoEditStart').value; let end = document.getElementById('todoEditEnd').value; // Nur ein Datum angegeben: als eintägige Aufgabe behandeln, // sonst würde kein Balken im GANTT erscheinen. if(start && !end) end = start; if(end && !start) start = end; const done = document.getElementById('todoEditDone').checked; let progress = parseInt(document.getElementById('todoEditProgress').value, 10) || 0; // "Erledigt" impliziert 100 % Fortschritt if(done && progress < 100) progress = 100; const dependsOnList = document.getElementById('todoEditDependsOn'); const dependsOn = dependsOnList ? Array.from(dependsOnList.querySelectorAll('input[type="checkbox"]:checked')).map(cb => cb.value).filter(Boolean) : []; const assigneesDiv = document.getElementById('todoEditAssignees'); const assignees = assigneesDiv ? Array.from(assigneesDiv.querySelectorAll('input:checked')).map(cb => cb.value) : []; if(!title){ alert('Bitte einen Titel eingeben.'); return; } if(start && end && parseDateISO(end) < parseDateISO(start)){ alert('Enddatum liegt vor Startdatum.'); return; } const plan = app.ensurePlanLocal(); if(!Array.isArray(plan.todos)) plan.todos = []; if(app.currentTodoIndex === null){ plan.todos.push({ id: uid(), title, start, end, done, progress, dependsOn, assignees }); } else if(plan.todos[app.currentTodoIndex]){ const t = plan.todos[app.currentTodoIndex]; t.title = title; t.start = start; t.end = end; t.done = done; t.progress = progress; t.dependsOn = dependsOn; t.assignees = assignees; } app.savePlan(); closeTodoModal(); renderPlanGantt(); }; // deleteTodoEdit: löscht nach Bestätigung die aktuell bearbeitete Aufgabe, // entfernt ihre Vorgänger-Verweise und setzt die relative Position // betroffener Meilensteine zurück. Speichert und rendert neu. window.deleteTodoEdit = function(){ if(app.isLocked()) return; if(app.currentTodoIndex === null){ closeTodoModal(); return; } if(!confirm('TODO löschen?')) return; const plan = app.ensurePlanLocal(); if(plan.todos && plan.todos[app.currentTodoIndex]){ const deletedId = plan.todos[app.currentTodoIndex].id; plan.todos.splice(app.currentTodoIndex, 1); app.removeDependencyRefs(plan, deletedId); // Meilensteine, die relativ zu dieser TODO positioniert waren, zurücksetzen (plan.milestones || []).forEach(m => { if(m.position === 'before:' + deletedId || m.position === 'after:' + deletedId) m.position = ''; }); app.savePlan(); } closeTodoModal(); renderPlanGantt(); }; // addTodoFromList: legt eine neue Aufgabe aus den Eingabefeldern der // TODO-Liste an (Titel, Start/Ende), leert die Felder und rendert neu. window.addTodoFromList = function(){ if(app.isLocked()) return; const title = document.getElementById('newTodoTitle').value.trim(); let start = document.getElementById('newTodoStart').value; let end = document.getElementById('newTodoEnd').value; if(start && !end) end = start; if(end && !start) start = end; if(!title){ alert('Bitte einen Titel eingeben.'); return; } if(start && end && parseDateISO(end) < parseDateISO(start)){ alert('Enddatum liegt vor Startdatum.'); return; } const plan = app.ensurePlanLocal(); plan.todos.push({ id: uid(), title, start, end, done: false, progress: 0, dependsOn: [], assignees: [] }); document.getElementById('newTodoTitle').value = ''; document.getElementById('newTodoStart').value = ''; document.getElementById('newTodoEnd').value = ''; app.savePlan(); renderPlanGantt(); }; // addTodo: öffnet das TODO-Modal für eine neue Aufgabe. window.addTodo = function(){ openTodoModal(null); }; // updateTodo: setzt ein einzelnes Feld (z. B. done) einer Aufgabe direkt // und speichert sowie rendert neu. Parameter: idx (Index), field // (Feldname), value (neuer Wert). window.updateTodo = function(idx, field, value){ if(app.isLocked()) return; const plan = app.ensurePlanLocal(); if(!plan.todos[idx]) return; plan.todos[idx][field] = value; app.savePlan(); renderPlanGantt(); }; // toggleTodoDone: schaltet den Erledigt-Status der Aufgabe am Index um, // speichert und rendert neu. window.toggleTodoDone = function(idx){ if(app.isLocked()) return; const plan = app.ensurePlanLocal(); if(!plan.todos[idx]) return; plan.todos[idx].done = !plan.todos[idx].done; app.savePlan(); renderPlanGantt(); }; // deleteTodo: löscht nach Bestätigung die Aufgabe am Index, bereinigt die // Vorgänger-Verweise und die relative Meilenstein-Position. Speichert und // rendert neu. window.deleteTodo = function(idx){ if(app.isLocked()) return; if(!confirm('TODO löschen?')) return; const plan = app.ensurePlanLocal(); if(!plan.todos[idx]) return; const deletedId = plan.todos[idx].id; plan.todos.splice(idx, 1); app.removeDependencyRefs(plan, deletedId); (plan.milestones || []).forEach(m => { if(m.position === 'before:' + deletedId || m.position === 'after:' + deletedId) m.position = ''; }); app.savePlan(); renderPlanGantt(); }; // ---------- Abwesenheits-Modal (Urlaube der Projektbeteiligten) ---------- // openAbsenceModal: öffnet das Abwesenheits-Modal für die übergebene // Person-ID, setzt den Titel, leert die Formularfelder und rendert die // Liste der bestehenden Abwesenheiten. window.openAbsenceModal = function(personId){ const person = app.findGlobalPerson(personId); if(!person) return; app.currentAbsencePersonId = personId; document.getElementById('absenceModalTitle').textContent = 'Abwesenheiten – ' + (person.fullName || person.name); // Formularfelder zurücksetzen const startInput = document.getElementById('absenceEditStart'); const endInput = document.getElementById('absenceEditEnd'); const rangeDisplay = document.getElementById('absenceEditRange'); if(startInput) startInput.value = ''; if(endInput) endInput.value = ''; if(rangeDisplay) rangeDisplay.value = ''; renderAbsenceModalList(); document.getElementById('absenceModal').style.display = 'flex'; }; // closeAbsenceModal: blendet das Abwesenheits-Modal aus und setzt die // aktuelle Person-ID zurück. window.closeAbsenceModal = function(){ document.getElementById('absenceModal').style.display = 'none'; app.currentAbsencePersonId = null; }; // renderAbsenceModalList: rendert die Liste der Abwesenheiten der aktuellen // Person im Modal (jeweils Typ, Zeitraum, Notiz und Lösch-Button). Bei // gesperrter Woche wird das Formular ausgeblendet. function renderAbsenceModalList(){ const listDiv = document.getElementById('absenceModalList'); const person = app.findGlobalPerson(app.currentAbsencePersonId); if(!listDiv || !person) return; const absences = Array.isArray(person.absences) ? person.absences : []; const locked = app.isLocked(); if(absences.length === 0){ listDiv.innerHTML = 'Keine Abwesenheiten eingetragen.'; } else { listDiv.innerHTML = '
' + absences.map(a => `
${esc(a.type || 'Abwesenheit')}: ${formatDateDE(parseDateISO(a.start))}–${formatDateDE(parseDateISO(a.end))} ${a.note ? `${esc(a.note)}` : ''} ${locked ? '' : ``}
`).join('') + '
'; } const form = document.getElementById('absenceModalForm'); if(form) form.style.display = locked ? 'none' : ''; } // addPlanAbsence: legt aus den Formularfeldern eine neue Abwesenheit für die // aktuelle Person an – in der globalen Personenliste und in der lokalen // Sitzungs-Kopie. Speichert beides und rendert Liste sowie GANTT neu. window.addPlanAbsence = function(){ if(app.isLocked()) return; const person = app.findGlobalPerson(app.currentAbsencePersonId); if(!person) return; const start = document.getElementById('absenceEditStart').value; const end = document.getElementById('absenceEditEnd').value; const type = document.getElementById('absenceEditType').value; const note = document.getElementById('absenceEditNote').value.trim(); if(!start || !end){ alert('Bitte Start- und Enddatum angeben.'); return; } if(parseDateISO(end) < parseDateISO(start)){ alert('Enddatum liegt vor Startdatum.'); return; } if(!Array.isArray(person.absences)) person.absences = []; const absence = { id: uid(), start, end, type, note }; person.absences.push(absence); person.absences.sort((a,b) => parseDateISO(a.start) - parseDateISO(b.start)); // Auch die Sitzungs-lokale Kopie aktualisieren, damit die Anzeige konsistent bleibt const localPerson = (app.week.people || []).find(p => p.id === app.currentAbsencePersonId); if(localPerson){ if(!Array.isArray(localPerson.absences)) localPerson.absences = []; localPerson.absences.push({ ...absence }); localPerson.absences.sort((a,b) => parseDateISO(a.start) - parseDateISO(b.start)); } app.saveGlobalPeople(); app.savePlan(); document.getElementById('absenceEditStart').value = ''; document.getElementById('absenceEditEnd').value = ''; const rangeDisplay = document.getElementById('absenceEditRange'); if(rangeDisplay) rangeDisplay.value = ''; document.getElementById('absenceEditNote').value = ''; renderAbsenceModalList(); renderPlanGantt(); }; // deletePlanAbsence: entfernt die Abwesenheit mit der übergebenen ID aus // der globalen Personenliste und der lokalen Sitzungs-Kopie, speichert // beides und rendert Liste sowie GANTT neu. window.deletePlanAbsence = function(absenceId){ if(app.isLocked()) return; const person = app.findGlobalPerson(app.currentAbsencePersonId); if(!person || !Array.isArray(person.absences)) return; person.absences = person.absences.filter(a => a.id !== absenceId); const localPerson = (app.week.people || []).find(p => p.id === app.currentAbsencePersonId); if(localPerson && Array.isArray(localPerson.absences)){ localPerson.absences = localPerson.absences.filter(a => a.id !== absenceId); } app.saveGlobalPeople(); app.savePlan(); renderAbsenceModalList(); renderPlanGantt(); }; })();