Files
erikandClaude Opus 5.5 e12936444d Aufgaben im Wochenbericht, Meilensteine „Erreicht“, Panels als Planblatt
- Wochenbericht: Abschnitt „Aufgaben“ je Projekt (erledigt seit Vorwoche,
  überfällig, nächste zwei Wochen), rote Zahl in der Tagesordnung,
  Tabelle in der PDF-Akte; Klick öffnet das Aufgaben-Panel
- Meilensteine lassen sich als erreicht markieren (plan_milestones.erreicht);
  nur nicht erreichte vergangene gelten als überfällig
- Aufgabe- und Meilenstein-Panel neu aufgebaut: Stand oben, Lage im
  Terminplan, Angaben als Zeilen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 10:41:44 +01:00

182 lines
9.6 KiB
JavaScript
Raw Permalink 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.
// -----------------------------
// Projektbereich — Aufgaben- und Meilensteinliste
// -----------------------------
// Bereich "Aufgaben" des Projektbereichs (siehe js/plan/plan-sections.js):
// dieselben Daten wie im GANTT, nur als Liste — für alle, die Aufgaben lieber
// abarbeiten als sie zu terminieren. Es gibt hier bewusst KEINEN eigenen
// Datenbestand und keine eigenen Dialoge: eine Zeile öffnet denselben Dialog
// wie ein Klick im GANTT (openTodoModal/openMilestoneModal aus
// js/plan/plan-modals.js), das Häkchen ruft toggleTodoDone() auf, und neue
// Aufgaben entstehen über dieselben Schaltflächen wie im Terminplan
// (addTodo, addPlanMilestone, openTemplateModal, openHonorarModal).
//
// Die Reihenfolge entspricht der Reihenfolge im GANTT (plan.todos bzw.
// plan.milestones), damit der Index in der Liste und der im Dialog
// erwartete Index übereinstimmen und beide Ansichten dasselbe Bild zeigen.
//
// renderPlanTasks() wird am Ende von renderPlanGantt() (plan-gantt.js)
// mitgerendert — das ist die zentrale Neu-Render-Funktion, die alle
// Änderungen an Aufgaben und Meilensteinen ohnehin aufrufen.
//
// Läuft wie alle plan-Module in einem IIFE; Zustand: window.PlanApp
// (plan-state.js).
(function(){
const app = window.PlanApp;
// todoStatus: leitet den Status einer Aufgabe aus done/progress ab —
// dieselbe Dreiteilung wie das Status-Segment im TODO-Dialog
// (offen/arbeit/fertig, siehe plan-modals.js). Parameter: t (TODO).
// Rückgabe: 'offen' | 'arbeit' | 'fertig'.
function todoStatus(t){
if(t.done) return 'fertig';
return (Number(t.progress) || 0) > 0 ? 'arbeit' : 'offen';
}
// personById: sucht eine Person der aktuellen Sitzung anhand ihrer ID.
// Rückgabe: Personenobjekt oder null.
function personById(id){
return (app.week && Array.isArray(app.week.people) ? app.week.people : []).find(p => p.id === id) || null;
}
// assigneeAvatars: baut den überlappenden Avatar-Stapel der Zuständigen
// einer Aufgabe (dieselben Kreise wie im Projektboard, .board-avatar in
// styles.css). Parameter: ids (Array von Person-IDs). Rückgabe: HTML-String.
function assigneeAvatars(ids){
const chips = (Array.isArray(ids) ? ids : []).map(id => {
const person = personById(id);
if(!person) return '';
const role = Array.isArray(person.role) && person.role.length ? person.role[0] : '';
const title = (person.fullName || person.name) + (role ? ' · ' + role : '');
return `<span class="board-avatar ${app.planRoleClass(role)} state-1" title="${esc(title)}">${esc(person.name)}</span>`;
}).join('');
return chips ? `<span class="plan-task-assignees">${chips}</span>` : '';
}
// dateRangeText: formatiert den Zeitraum einer Aufgabe als "01.02.2026 –
// 05.02.2026" (bzw. nur ein Datum, wenn Start und Ende gleich sind).
// Parameter: t (TODO). Rückgabe: String (leer ohne Termine).
function dateRangeText(t){
const s = parseDateISO(t.start);
const e = parseDateISO(t.end);
if(!s && !e) return '';
if(s && e) return formatDateDE(s) === formatDateDE(e) ? formatDateDE(s) : formatDateDE(s) + ' – ' + formatDateDE(e);
return formatDateDE(s || e);
}
// isOverdue: true, wenn eine noch offene Aufgabe ihren Endtermin
// überschritten hat. Parameter: t (TODO). Rückgabe: Boolescher Wert.
function isOverdue(t){
if(t.done) return false;
const e = parseDateISO(t.end);
if(!e) return false;
const today = new Date();
today.setHours(0, 0, 0, 0);
return e < today;
}
// Phasenfilter der Aufgabenliste: '' = alle, 'LPH 5', oder '-' = ohne
// Phase. Gilt nur für das Projekt, für das er gesetzt wurde.
let filter = { projekt: null, phase: '' };
const filterPhase = () => filter.projekt === app.project ? filter.phase : '';
// setzeAufgabenFilter: von außen (Bereich Leistungsphasen) oder über die
// Filterknöpfe. Parameter: phase ('' | 'LPH x' | '-').
window.setzeAufgabenFilter = function(phase){
filter = { projekt: app.project, phase: phase || '' };
window.renderPlanTasks();
};
// renderPlanTasks: baut die Aufgaben- und die Meilensteinliste des Bereichs
// "Aufgaben" neu auf und schreibt die Zähler in die Kartenüberschriften.
// Keine Parameter, keine Rückgabe.
window.renderPlanTasks = function(){
if(!app.project) return;
const plan = app.ensurePlanLocal();
const locked = app.isLocked();
// ---- Aufgaben ----
const todoEl = document.getElementById('planTodoList');
const alleTodos = Array.isArray(plan.todos) ? plan.todos : [];
// Phasenbalken stehen im eigenen Bereich "Leistungsphasen"
// (plan-phasen.js), nicht in der Aufgabenliste. idx bleibt der Index in
// plan.todos, den die Dialoge erwarten.
const eintraege = alleTodos.map((t, idx) => ({ t, idx, phase: aufgabenPhase(t, alleTodos) }))
.filter(e => !balkenPhase(e.t));
const todos = eintraege.map(e => e.t);
const fp = filterPhase();
const sichtbar = eintraege.filter(e => !fp || (fp === '-' ? !e.phase : e.phase === fp));
const filterEl = document.getElementById('planTodoFilter');
if(filterEl){
const phasen = LPHS.filter(l => eintraege.some(e => e.phase === l));
const knopf = (wert, text) => `<button type="button" class="plan-task-filter-btn${fp === wert ? ' is-active' : ''}" onclick="setzeAufgabenFilter('${esc(wert)}')">${esc(text)}</button>`;
filterEl.innerHTML = phasen.length
? '<span>Phase:</span>' + knopf('', 'Alle') + phasen.map(l => knopf(l, l)).join('')
+ (eintraege.some(e => !e.phase) ? knopf('-', 'ohne Phase') : '')
: '';
}
if(todoEl){
// In gesperrten Sitzungen sind die Anlege-Schaltflächen ausgeblendet
// (renderPlanPage(), plan-state.js) — dann darf der Leertext auch nicht
// auf sie verweisen.
const leerTextTodos = locked
? 'Für diese Sitzung sind keine Aufgaben hinterlegt.'
: 'Noch keine Aufgaben. Über „+ Aufgabe“, „+ Vorlage“ oder „+ Terminplan“ anlegen.';
todoEl.innerHTML = todos.length === 0
? '<div class="plan-task-empty">' + leerTextTodos + '</div>'
: sichtbar.length === 0
? '<div class="plan-task-empty">Keine Aufgaben in dieser Auswahl.</div>'
: sichtbar.map(({ t, idx, phase }) => {
const status = todoStatus(t);
const range = dateRangeText(t);
const overdue = isOverdue(t);
const del = locked ? '' : `<button class="secondary small" title="Aufgabe löschen" onclick="event.stopPropagation(); deleteTodo(${idx})">✕</button>`;
const check = locked ? '' : `<input type="checkbox" ${t.done ? 'checked' : ''} title="Erledigt" onclick="event.stopPropagation(); toggleTodoDone(${idx})">`;
const progress = (!t.done && (Number(t.progress) || 0) > 0) ? `<span class="plan-task-meta">${Math.round(Number(t.progress) || 0)} %</span>` : '';
// In gesperrten Sitzungen weder Klick-Handler noch Einladung zum
// Bearbeiten: openTodoModal() bricht dort sowieso wortlos ab.
const rowAction = locked ? '' : ` onclick="openTodoModal(${idx})" title="Zum Bearbeiten klicken"`;
return `<div class="plan-task-row${t.done ? ' is-done' : ''}${locked ? ' is-locked' : ''}"${rowAction}>
${check}
<span class="plan-task-dot ${status}"></span>
<span class="plan-task-title">${esc(t.title || 'Aufgabe')}</span>
${t.notiz ? `<span class="plan-task-notiz" title="${esc(String(t.notiz).slice(0, 300))}">${icon('fileText')}</span>` : ''}
${phase ? `<span class="plan-task-phase" title="Liegt im Zeitraum dieser Leistungsphase">${esc(phase)}</span>` : ''}
${assigneeAvatars(t.assignees)}
${progress}
<span class="plan-task-meta${overdue ? ' overdue' : ''}" title="${range ? '' : 'Ohne Termin — erscheint nicht im Terminplan'}">${range ? esc(range) : '<span class="plan-task-no-schedule">ohne Termin</span>'}</span>
${del}
</div>`;
}).join('');
}
const todoCount = document.getElementById('planTodoCount');
if(todoCount){
const offen = todos.filter(t => !t.done).length;
todoCount.textContent = todos.length === 0 ? '' : `${offen} von ${todos.length} offen`;
}
// ---- Meilensteine ----
const msEl = document.getElementById('planMilestoneList');
const milestones = Array.isArray(plan.milestones) ? plan.milestones : [];
if(msEl){
msEl.innerHTML = milestones.length === 0
? '<div class="plan-task-empty">' + (locked ? 'Für diese Sitzung sind keine Meilensteine hinterlegt.' : 'Noch keine Meilensteine. Über „+ Meilenstein“ anlegen.') + '</div>'
: milestones.map((m, idx) => {
const d = parseDateISO(m.date);
const today = new Date();
today.setHours(0, 0, 0, 0);
const overdue = d && d < today && !m.erreicht;
const msAction = locked ? '' : ` onclick="openMilestoneModal(${idx})" title="Zum Bearbeiten klicken"`;
return `<div class="plan-task-row${locked ? ' is-locked' : ''}"${msAction}>
<span class="plan-task-dot"></span>
<span class="plan-task-title">${esc(m.name || 'Meilenstein')}</span>
<span class="plan-task-meta${overdue ? ' overdue' : ''}">${m.erreicht ? 'erreicht · ' : ''}${esc(d ? formatDateDE(d) : 'ohne Datum')}</span>
</div>`;
}).join('');
}
const msCount = document.getElementById('planMilestoneCount');
if(msCount) msCount.textContent = milestones.length === 0 ? '' : `${milestones.filter(m => m.erreicht).length} von ${milestones.length} erreicht`;
};
})();