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>
This commit is contained in:
erikandClaude Opus 5.5 committed 2026-11-10 10:41:44 +01:00
1 parent 4ac3d8d53d
commit e12936444d
13 files changed
+693 -137

No files matched your search

+130
View File
@@ -666,12 +666,14 @@ function renderBerichtListe(){
const gruppenkopf = gruppe !== gruppeVorher ? `<div class="bericht-rail-gruppe">${esc(gruppe)}</div>` : '';
gruppeVorher = gruppe;
const name = (p.nr ? p.nr + ' · ' : '') + p.name;
const ueber = berichtAufgabenDaten(p, w, prevWeek).ueberfaellig.length;
return `${gruppenkopf}<div class="bericht-rail-item${istAktiv ? ' is-aktiv' : ''}${kommentarVon(p, wochenberichtGewerk).besprochen ? ' is-ab' : ''}"
role="button" tabindex="0" data-uid="${esc(p.projectUid || '')}"
aria-current="${istAktiv ? 'true' : 'false'}">
<span class="bericht-rail-num">${String(position).padStart(2, '0')}</span>
${info.bucket === 'unchanged' ? '' : `<span class="bericht-rail-dot is-${esc(info.bucket)}" aria-hidden="true"></span>`}
<span class="bericht-rail-name">${esc(name)}</span>
${ueber ? `<span class="bericht-rail-ueber" title="${ueber} überfällige Aufgabe${ueber === 1 ? '' : 'n'}">${ueber}</span>` : ''}
</div>`;
}).join('');
@@ -713,6 +715,7 @@ function renderBerichtDetail(p, w, prevWeek, locked){
<div class="bericht-sub">${subText}</div>
${fehlen.length ? `<span class="bericht-abwesend-hinweis">Heute abwesend: ${esc(fehlen.join(', '))} — eventuell vertagen.</span>` : ''}
${renderBoardDrawer(p, allPeople, kLocked, w.label, Object.assign({}, BERICHT_OPTS, { ohneVerlauf: true }))}
${renderBerichtAufgaben(p, w, prevWeek, locked)}
<div class="bericht-detail-fuss">
${!locked && hatRecht('proj.plan') ? `<button type="button" class="secondary small" onclick="openTodoAusKommentar('${esc(p.id)}')">
Aufgabe aus Kommentar…
@@ -876,6 +879,133 @@ function berichtVerlaufNeu(){
el.outerHTML = renderBerichtVerlauf(p, w, isWeekLocked(w));
}
// ---------- Aufgaben des Projekts (Eriks Wahl „B“) ----------
//
// Die drei Fragen der Runde: was ist seit der Vorwoche erledigt, was ist
// überfällig, was steht in den nächsten zwei Wochen an. Nur Anzeige — ein
// Klick öffnet dasselbe Aufgaben-/Meilenstein-Panel wie im Terminplan
// ("Ein Ort je Angabe"). Keine neue Datenhaltung: "erledigt seit" ist der
// Vergleich mit der Kopie des Projekts in der vorigen geladenen Sitzung.
// Phasenbalken (t.lph) zählen nicht, sie stehen als Leistungsphasen in der Karte.
const BERICHT_AUFGABEN_TAGE = 14;
const BERICHT_AUFGABEN_JE_SPALTE = 6;
// berichtAufgabenDaten: Parameter p, w, prevWeek. Rückgabe:
// {erledigt, ueberfaellig, bald, weitere, mitVorwoche} — Listen von
// {art, idx, titel, datum, progress, neu, kuerzel}. Aufgaben mit Zuständigen
// aus dem gewählten Gewerk stehen in jeder Liste zuerst. Auch von js/pdf.js genutzt.
function berichtAufgabenDaten(p, w, prevWeek){
const plan = p.plan || {};
const heute = heuteIso();
const bis = isoPlusTage(BERICHT_AUFGABEN_TAGE);
const vorher = prevWeek && p.projectUid ? (prevWeek.projects || []).find(x => x.projectUid === p.projectUid) : null;
const vorherTodos = new Map(((vorher && vorher.plan && vorher.plan.todos) || []).map(t => [String(t.id), t]));
const leute = new Map((w.people || []).map(person => [person.id, person]));
const g = wochenberichtGewerk;
const erledigt = [], ueberfaellig = [], bald = [];
let weitere = 0;
(plan.todos || []).forEach((t, idx) => {
if(t.lph) return;
const wer = (t.assignees || []).map(id => leute.get(id)).filter(Boolean);
const alt = vorherTodos.get(String(t.id));
const e = {
art: 'aufgabe', idx, titel: t.title || 'Aufgabe', datum: t.end || '',
progress: parseInt(t.progress, 10) || 0, fertig: !!t.done,
neu: !!vorher && !alt,
kuerzel: wer.map(person => person.name).join(', '),
imGewerk: !!g && wer.some(person => personGewerk(person) === g)
};
if(t.done){
if(vorher && (!alt || !alt.done)) erledigt.push(e);
} else if(t.end && t.end < heute) ueberfaellig.push(e);
else if(t.end && t.end <= bis) bald.push(e);
else weitere++;
});
(plan.milestones || []).forEach((m, idx) => {
if(!m.date || m.erreicht) return;
const e = { art: 'meilenstein', idx, titel: m.name || 'Meilenstein', datum: m.date, progress: 0, fertig: false, neu: false, kuerzel: '', imGewerk: false };
if(m.date < heute) ueberfaellig.push(e);
else if(m.date <= bis) bald.push(e);
});
const sortieren = l => l.sort((a, b) => (b.imGewerk - a.imGewerk) || String(a.datum).localeCompare(String(b.datum)));
return { erledigt: sortieren(erledigt), ueberfaellig: sortieren(ueberfaellig), bald: sortieren(bald), weitere, mitVorwoche: !!vorher };
}
// renderBerichtAufgaben: der Abschnitt unter der Projektkarte.
// Parameter: p, w, prevWeek, locked. Rückgabe: HTML.
function renderBerichtAufgaben(p, w, prevWeek, locked){
const d = berichtAufgabenDaten(p, w, prevWeek);
const klickbar = !locked && hatRecht('proj.plan');
const vorKw = prevWeek ? berichtKurzKw(prevWeek.label) : '';
const zeile = e => {
const datum = e.datum ? formatDateDE(parseDateISO(e.datum)).slice(0, 6) : '';
const art = e.art === 'meilenstein'
? '<span class="ba-raute" aria-label="Meilenstein"></span>'
: `<span class="ba-haken${e.fertig ? ' is-an' : ''}" aria-hidden="true"></span>`;
const inhalt = `${art}
<span class="ba-titel">${esc(e.titel)}${e.neu ? '<span class="ba-neu">neu</span>' : ''}${!e.fertig && e.progress > 0 && e.progress < 100 ? `<span class="ba-fort">${e.progress} %</span>` : ''}</span>
<span class="ba-wer">${esc(e.kuerzel)}</span>
<span class="ba-datum">${esc(datum)}</span>`;
return klickbar
? `<button type="button" class="ba-zeile" onclick="berichtEintragOeffnen('${esc(p.id)}','${e.art}',${e.idx})">${inhalt}</button>`
: `<div class="ba-zeile">${inhalt}</div>`;
};
const spalte = (titel, liste, leer, klasse) => `<div class="ba-spalte">
<div class="ba-spalte-kopf${klasse || ''}"><span>${esc(titel)}</span><span class="ba-zahl">${liste.length}</span></div>
${liste.length ? liste.slice(0, BERICHT_AUFGABEN_JE_SPALTE).map(zeile).join('') : `<div class="ba-leer">${esc(leer)}</div>`}
${liste.length > BERICHT_AUFGABEN_JE_SPALTE ? `<div class="ba-leer">${liste.length - BERICHT_AUFGABEN_JE_SPALTE} weitere</div>` : ''}
</div>`;
const hinweis = [];
if(d.weitere) hinweis.push(d.weitere === 1 ? '1 weitere offene Aufgabe später oder ohne Termin' : d.weitere + ' weitere offene Aufgaben später oder ohne Termin');
if(klickbar) hinweis.push('Klick auf eine Zeile öffnet die Aufgabe');
return `<section class="bericht-aufgaben" id="berichtAufgaben" aria-label="Aufgaben">
<div class="bv-kopf">
<h2 class="bv-titel">Aufgaben</h2>
<span class="bv-anzahl">${vorKw ? 'seit ' + esc(vorKw) : 'keine Vorwoche geladen'}${wochenberichtGewerk ? ' · Zuständige aus ' + esc(wochenberichtGewerk) + ' zuerst' : ''}</span>
<span class="bericht-fuss-spacer"></span>
<button type="button" class="secondary small" onclick="berichtAufgabenImProjekt('${esc(p.id)}')">Alle im Projekt</button>
</div>
<div class="ba-spalten">
${spalte(vorKw ? 'Erledigt seit ' + vorKw : 'Erledigt', d.erledigt, d.mitVorwoche ? 'Nichts erledigt' : 'Kein Vergleich möglich')}
${spalte('Überfällig', d.ueberfaellig, 'Nichts überfällig', d.ueberfaellig.length ? ' is-rot' : '')}
${spalte('Nächste zwei Wochen', d.bald, 'Nichts fällig')}
</div>
${hinweis.length ? `<div class="ba-hinweis">${esc(hinweis.join(' · '))}</div>` : ''}
</section>`;
}
// berichtAufgabenNeu: zeichnet nur diesen Abschnitt und die Tagesordnung neu
// (nach dem Schließen des Panels), ein angefangener Kommentar bleibt.
function berichtAufgabenNeu(){
const w = currentWeek();
const p = w && aktuellesBerichtProjekt(w);
const el = document.getElementById('berichtAufgaben');
if(!p || !el) return;
el.outerHTML = renderBerichtAufgaben(p, w, findPreviousWeek(w), isWeekLocked(w));
renderBerichtListe();
}
// berichtEintragOeffnen: öffnet das Aufgaben- bzw. Meilenstein-Panel über dem
// Wochenbericht. Die Panels arbeiten auf window.PlanApp; showTab('plan')
// setzt week/project über openPlanView() ohnehin neu.
function berichtEintragOeffnen(pid, art, idx){
const w = currentWeek();
const p = w && (w.projects || []).find(x => x.id === pid);
if(!p) return;
window.PlanApp.week = w;
window.PlanApp.project = p;
if(art === 'meilenstein') openMilestoneModal(idx);
else openTodoModal(idx);
}
// berichtAufgabenImProjekt: springt in den Bereich "Aufgaben" des Projekts.
function berichtAufgabenImProjekt(pid){
location.hash = 'aufgaben';
openProjectPlan(pid);
}
// berichtVerlaufUebernehmen: schreibt den Kommentar jener Sitzung in das
// Kommentarfeld der aktuellen (takeCommentFromHistory(), js/board/board.js).
function berichtVerlaufUebernehmen(weekLabel){