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:
1 parent
4ac3d8d53d
commit
e12936444d
13 files changed
+693
-137
No files matched your search
@@ -212,14 +212,14 @@ function dashDaten(){
|
||||
});
|
||||
(plan.milestones || []).forEach(m => {
|
||||
if(!m.date) return;
|
||||
termine.push({ art: 'meilenstein', titel: m.name || 'Meilenstein', datum: m.date, meins: false, wer: [] });
|
||||
termine.push({ art: 'meilenstein', titel: m.name || 'Meilenstein', datum: m.date, meins: false, wer: [], erreicht: !!m.erreicht });
|
||||
});
|
||||
const hatEigene = termine.some(t => t.meins);
|
||||
if(!rollen.length && !hatEigene) return;
|
||||
// Erledigte Vergangenheit interessiert hier nicht: Meilensteine vor heute
|
||||
// fallen weg (sie haben kein Erledigt-Kennzeichen), offene Aufgaben vor
|
||||
// heute bleiben als überfällig stehen.
|
||||
const relevant = termine.filter(t => !(t.art === 'meilenstein' && t.datum < heute));
|
||||
// Erreichte Meilensteine fallen weg (im Kalender bleiben sie stehen);
|
||||
// nicht erreichte vor heute gelten wie offene Aufgaben als überfällig —
|
||||
// dieselbe Regel wie die Zahl im Band (GET /my-dashboard).
|
||||
const relevant = termine.filter(t => !(t.art === 'meilenstein' && t.erreicht));
|
||||
relevant.sort((a, b) => (a.datum || '9999').localeCompare(b.datum || '9999'));
|
||||
projekte.push({ p, rollen, termine: relevant, alleTermine: termine });
|
||||
});
|
||||
|
||||
+31
-2
@@ -518,13 +518,14 @@ function pdfWbProjektAkte(p, w, prevWeek, allePersonen){
|
||||
color: '#D70015', margin: [0, 6, 0, 0] });
|
||||
}
|
||||
|
||||
const innerContent = {
|
||||
const aufgaben = pdfWbAufgaben(p, w, prevWeek);
|
||||
const innerContent = { stack: [{
|
||||
columns: [
|
||||
{ width: '*', stack: [kopf, aenderung, kommentar].concat(vorher ? [vorher] : []) },
|
||||
{ width: 190, stack: rightStack }
|
||||
],
|
||||
columnGap: 12
|
||||
};
|
||||
}].concat(aufgaben ? [aufgaben] : []) };
|
||||
|
||||
return {
|
||||
table: {
|
||||
@@ -548,6 +549,34 @@ function pdfWbProjektAkte(p, w, prevWeek, allePersonen){
|
||||
};
|
||||
}
|
||||
|
||||
// pdfWbAufgaben: die Aufgaben der Akte wie auf dem Bildschirm
|
||||
// (berichtAufgabenDaten() in wochenbericht.js) — erledigt seit der Vorwoche,
|
||||
// überfällig, nächste zwei Wochen, je höchstens fünf. Rückgabe: pdfmake-Block
|
||||
// oder null, wenn in keiner Gruppe etwas steht.
|
||||
function pdfWbAufgaben(p, w, prevWeek){
|
||||
if(typeof berichtAufgabenDaten !== 'function') return null;
|
||||
const d = berichtAufgabenDaten(p, w, prevWeek);
|
||||
const gruppen = [
|
||||
[prevWeek ? 'Erledigt seit ' + berichtKurzKw(prevWeek.label) : 'Erledigt', d.erledigt, null],
|
||||
['Überfällig', d.ueberfaellig, '#D70015'],
|
||||
['Nächste zwei Wochen', d.bald, null]
|
||||
].filter(g => g[1].length);
|
||||
if(!gruppen.length) return null;
|
||||
const body = [];
|
||||
gruppen.forEach(([titel, liste, farbe]) => {
|
||||
liste.slice(0, 5).forEach((e, i) => body.push([
|
||||
{ text: i === 0 ? titel + ' (' + liste.length + ')' : '', style: 'td', bold: true, color: farbe || undefined },
|
||||
{ text: (e.art === 'meilenstein' ? 'Meilenstein: ' : '') + e.titel + (e.neu ? ' neu' : ''), style: 'td' },
|
||||
{ text: e.kuerzel, style: 'td' },
|
||||
{ text: e.datum ? formatDateDE(parseDateISO(e.datum)) : '', style: 'td', alignment: 'right', color: farbe || undefined }
|
||||
]));
|
||||
});
|
||||
return { stack: [
|
||||
{ text: 'AUFGABEN', fontSize: 6.5, bold: true, color: '#8E8E93', margin: [0, 10, 0, 3] },
|
||||
{ table: { widths: [100, '*', 70, 50], body }, layout: pdfWbGridLayout(false) }
|
||||
] };
|
||||
}
|
||||
|
||||
// pdfWbVorigerKommentar: Block mit dem jüngsten früheren Kommentar des
|
||||
// Gewerks. Steht er in der direkten Vorwoche, heißt er "Vorwoche KW …",
|
||||
// sonst "Zuletzt KW …". Parameter: p (Projekt), w (Sitzung). Rückgabe:
|
||||
|
||||
@@ -171,7 +171,7 @@
|
||||
const openSub = overdue.length > 0 ? overdue.length + ' überfällig' : (open.length > 0 ? 'im Zeitplan' : 'alle erledigt');
|
||||
|
||||
const upcoming = milestones
|
||||
.filter(m => m.date && parseDateISO(m.date) >= today)
|
||||
.filter(m => m.date && !m.erreicht && parseDateISO(m.date) >= today)
|
||||
.sort((a, b) => parseDateISO(a.date) - parseDateISO(b.date));
|
||||
const msZelle = upcoming.length > 0
|
||||
? bandZelle('Nächster Meilenstein', formatDateDE(parseDateISO(upcoming[0].date)), upcoming[0].name || 'Meilenstein')
|
||||
|
||||
@@ -65,6 +65,9 @@
|
||||
document.getElementById('milestoneEditDate').value = msDateIso;
|
||||
const msDateDisplay = document.getElementById('milestoneEditDateDisplay');
|
||||
if(msDateDisplay) msDateDisplay.value = msDateIso ? formatDateDE(parseDateISO(msDateIso)) : '';
|
||||
const msErreicht = document.getElementById('milestoneEditErreicht');
|
||||
if(msErreicht) msErreicht.checked = !isNew && !!currentMs.erreicht;
|
||||
panelWoSetzen('milestoneModalWo', '');
|
||||
|
||||
const positionSelect = document.getElementById('milestoneEditPosition');
|
||||
if(positionSelect){
|
||||
@@ -129,6 +132,7 @@
|
||||
dependsOnList.innerHTML = listHtml;
|
||||
}
|
||||
|
||||
msLageBildNeu();
|
||||
document.getElementById('milestoneModal').style.display = 'flex';
|
||||
document.getElementById('milestoneEditName').focus();
|
||||
};
|
||||
@@ -167,6 +171,7 @@
|
||||
window.closeMilestoneModal = function(){
|
||||
document.getElementById('milestoneModal').style.display = 'none';
|
||||
app.currentMilestoneIndex = null;
|
||||
if(currentTab === 'wochenbericht') berichtAufgabenNeu();
|
||||
const danach = msNachSchliessen;
|
||||
msNachSchliessen = null;
|
||||
if(danach) setTimeout(danach, 0);
|
||||
@@ -189,12 +194,14 @@
|
||||
const position = positionSelect ? positionSelect.value : '';
|
||||
const msNotizEl = document.getElementById('milestoneEditNotiz');
|
||||
const notiz = msNotizEl ? msNotizEl.value.trim() : '';
|
||||
const msErreichtEl = document.getElementById('milestoneEditErreicht');
|
||||
const erreicht = !!(msErreichtEl && msErreichtEl.checked);
|
||||
|
||||
const plan = app.ensurePlanLocal();
|
||||
if(!Array.isArray(plan.milestones)) plan.milestones = [];
|
||||
|
||||
if(app.currentMilestoneIndex === null){
|
||||
plan.milestones.push({ id: uid(), name, date, dependsOn, position, notiz });
|
||||
plan.milestones.push({ id: uid(), name, date, dependsOn, position, notiz, erreicht });
|
||||
} else if(plan.milestones[app.currentMilestoneIndex]){
|
||||
const m = plan.milestones[app.currentMilestoneIndex];
|
||||
m.name = name;
|
||||
@@ -202,6 +209,7 @@
|
||||
m.dependsOn = dependsOn;
|
||||
m.position = position;
|
||||
m.notiz = notiz;
|
||||
m.erreicht = erreicht;
|
||||
}
|
||||
plan.milestones.sort((a,b) => parseDateISO(a.date) - parseDateISO(b.date));
|
||||
app.savePlan();
|
||||
@@ -266,6 +274,140 @@
|
||||
return progress > 0 ? 'arbeit' : 'offen';
|
||||
}
|
||||
|
||||
// ---------- Planblatt: Kopfzeile, Lage im Terminplan, Stand ----------
|
||||
|
||||
// kurzDatum: "2026-11-19" -> "19.11." Parameter: iso. Rückgabe: String.
|
||||
function kurzDatum(iso){ return formatDateDE(parseDateISO(iso)).slice(0, 6); }
|
||||
|
||||
// planKnoten: Aufgabe oder Meilenstein zu einer Knoten-id (Meilensteine mit
|
||||
// Präfix "m_", wie in dependsOn). Rückgabe: {titel, start, end, ms, fertig} oder null.
|
||||
function planKnoten(plan, id){
|
||||
id = String(id || '');
|
||||
if(id.indexOf('m_') === 0){
|
||||
const m = (plan.milestones || []).find(x => 'm_' + x.id === id);
|
||||
return m ? { id, titel: m.name || 'Meilenstein', start: m.date || '', end: m.date || '', ms: true, fertig: !!m.erreicht } : null;
|
||||
}
|
||||
const t = (plan.todos || []).find(x => String(x.id) === id);
|
||||
return t ? { id, titel: t.title || 'Aufgabe', start: t.start || t.end || '', end: t.end || t.start || '', ms: false, fertig: !!t.done } : null;
|
||||
}
|
||||
|
||||
// panelWoSetzen: Projekt (Nummer, Name) und optional Phase in der Kopfzeile.
|
||||
function panelWoSetzen(elId, phase){
|
||||
const el = document.getElementById(elId);
|
||||
if(!el) return;
|
||||
const p = app.project || {};
|
||||
el.innerHTML = (p.nr ? `<span class="pd-nr">${esc(p.nr)}</span> ` : '') + esc(p.name || '') + (phase ? ' · ' + esc(phase) : '');
|
||||
}
|
||||
|
||||
// LAGE_TAGE: der Ausschnitt im Panelkopf — sechs Kalenderwochen ab dem
|
||||
// Montag der Woche VOR dem Eintrag.
|
||||
const LAGE_TAGE = 42;
|
||||
|
||||
// lageBildHtml: Ausschnitt des Terminplans um einen Eintrag. Oben die
|
||||
// Vorgänger (grau, wenn erledigt), in der Mitte der Eintrag selbst, unten
|
||||
// die Nachfolger; dazu die Heute-Linie. Nur Anzeige.
|
||||
// e = {id (Knoten-id oder null), start, end, ms, progress, fertig, deps}.
|
||||
// Rückgabe: HTML.
|
||||
function lageBildHtml(e){
|
||||
const plan = app.ensurePlanLocal();
|
||||
const s = parseDateISO(e.start || e.end), en = parseDateISO(e.end || e.start);
|
||||
if(!s || !en) return '<div class="pd-lage-leer">Noch kein Termin gewählt</div>';
|
||||
const vor = (e.deps || []).map(id => planKnoten(plan, id)).filter(k => k && k.start);
|
||||
// Ausschnitt beginnt eine Woche vor dem Eintrag, früher, wenn ein
|
||||
// Vorgänger davor endet (höchstens drei Wochen), damit er sichtbar bleibt.
|
||||
const mo = new Date(s);
|
||||
mo.setDate(mo.getDate() - 7);
|
||||
const grenze = new Date(s); grenze.setDate(grenze.getDate() - 21);
|
||||
vor.forEach(k => { const v = parseDateISO(k.end); if(v && v < mo) mo.setTime(Math.max(v.getTime() - 2 * 864e5, grenze.getTime())); });
|
||||
mo.setDate(mo.getDate() - ((mo.getDay() + 6) % 7));
|
||||
const tag = 100 / LAGE_TAGE;
|
||||
const x = d => daysBetween(mo, d) * tag;
|
||||
const nach = e.id ? (plan.todos || []).filter(t => (t.dependsOn || []).includes(e.id)).map(t => planKnoten(plan, t.id))
|
||||
.concat((plan.milestones || []).filter(m => (m.dependsOn || []).includes(e.id)).map(m => planKnoten(plan, 'm_' + m.id)))
|
||||
.filter(k => k && k.start) : [];
|
||||
const form = (k, y, klasse, progress) => {
|
||||
const a = parseDateISO(k.start), b = parseDateISO(k.end) || a;
|
||||
if(k.ms){
|
||||
const l = x(a) + tag / 2;
|
||||
return l < 0 || l > 100 ? '' : `<span class="pd-lage-raute ${klasse}" style="left:${l}%;top:${y}px" title="${esc(k.titel)}"></span>`;
|
||||
}
|
||||
const l = Math.max(0, x(a)), r = Math.min(100, x(b) + tag);
|
||||
if(r <= l) return '';
|
||||
return `<span class="pd-lage-bar ${klasse}" style="left:${l}%;width:${r - l}%;top:${y}px" title="${esc(k.titel)}">`
|
||||
+ (progress > 0 ? `<b style="width:${Math.min(100, progress)}%"></b>` : '') + '</span>';
|
||||
};
|
||||
const ich = { titel: '', start: e.start || e.end, end: e.end || e.start, ms: e.ms };
|
||||
const ichKlasse = 'is-diese' + (e.fertig ? ' is-fertig' : '') + (e.ueber ? ' is-ueber' : '');
|
||||
let flaeche = vor.slice(0, 3).map(k => form(k, 4, k.fertig ? 'is-fertig' : 'is-vor', 0)).join('')
|
||||
+ form(ich, e.ms ? 18 : 20, ichKlasse, e.ms ? 0 : e.progress)
|
||||
+ nach.slice(0, 3).map(k => form(k, 36, 'is-nach', 0)).join('');
|
||||
const heute = parseDateISO(heuteIso());
|
||||
const hx = x(heute) + tag / 2;
|
||||
if(hx >= 0 && hx <= 100) flaeche += `<span class="pd-lage-heute" style="left:${hx}%"></span>`;
|
||||
const skala = [0, 1, 2, 3, 4, 5].map(i => {
|
||||
const d = new Date(mo); d.setDate(d.getDate() + i * 7);
|
||||
return `<span style="left:${i * 100 / 6}%">KW ${String(getWeekNumber(d)).padStart(2, '0')}</span>`;
|
||||
}).join('');
|
||||
const legende = [];
|
||||
if(vor[0]) legende.push(`<span>${esc(vor[0].titel)}${vor[0].fertig ? ', erledigt' : ''}</span>`);
|
||||
if(hx >= 0 && hx <= 100) legende.push(`<span>heute ${esc(kurzDatum(heuteIso()))}</span>`);
|
||||
if(nach[0]) legende.push(`<span>danach ${esc(nach[0].titel)} ${esc(kurzDatum(nach[0].start))}</span>`);
|
||||
return `<div class="pd-lage-skala">${skala}</div><div class="pd-lage-flaeche">${flaeche}</div>`
|
||||
+ (legende.length ? `<div class="pd-lage-legende">${legende.join('')}</div>` : '');
|
||||
}
|
||||
|
||||
// todoLageNeu: zeichnet die Lage der Aufgabe aus den aktuellen Eingaben.
|
||||
window.todoLageNeu = function(){
|
||||
const el = document.getElementById('todoLageBild');
|
||||
if(!el || !app.project) return;
|
||||
const plan = app.ensurePlanLocal();
|
||||
const t = app.currentTodoIndex !== null ? plan.todos[app.currentTodoIndex] : null;
|
||||
const status = currentTodoEditStatus();
|
||||
const slider = document.getElementById('todoEditProgress');
|
||||
el.innerHTML = lageBildHtml({
|
||||
id: t ? t.id : null,
|
||||
start: document.getElementById('todoEditStart').value,
|
||||
end: document.getElementById('todoEditEnd').value,
|
||||
ms: false, fertig: status === 'fertig',
|
||||
progress: status === 'fertig' ? 100 : (status === 'arbeit' && slider ? Number(slider.value) : 0),
|
||||
deps: Array.from(document.querySelectorAll('#todoEditDependsOn input[type="checkbox"]:checked')).map(cb => cb.value)
|
||||
});
|
||||
};
|
||||
|
||||
// msLageBildNeu: Lage, KW-Marke und Überfällig-Hinweis des Meilensteins.
|
||||
window.msLageBildNeu = function(){
|
||||
const el = document.getElementById('msLageBild');
|
||||
if(!el || !app.project) return;
|
||||
const plan = app.ensurePlanLocal();
|
||||
const m = app.currentMilestoneIndex !== null ? plan.milestones[app.currentMilestoneIndex] : null;
|
||||
const datum = document.getElementById('milestoneEditDate').value;
|
||||
const erreicht = document.getElementById('milestoneEditErreicht').checked;
|
||||
const heute = heuteIso();
|
||||
const ueber = !!datum && !erreicht && datum < heute;
|
||||
el.innerHTML = lageBildHtml({
|
||||
id: m ? 'm_' + m.id : null, start: datum, end: datum, ms: true, fertig: erreicht, ueber,
|
||||
deps: Array.from(document.querySelectorAll('#milestoneEditDependsOn input[type="checkbox"]:checked')).map(cb => cb.value)
|
||||
});
|
||||
const kw = document.getElementById('milestoneEditKw');
|
||||
const d = parseDateISO(datum);
|
||||
if(kw){ kw.hidden = !d; kw.textContent = d ? 'KW ' + String(getWeekNumber(d)).padStart(2, '0') : ''; }
|
||||
document.querySelectorAll('#milestoneEditStand button').forEach(b =>
|
||||
b.setAttribute('aria-pressed', String((b.dataset.stand === 'erreicht') === erreicht)));
|
||||
const warn = document.getElementById('milestoneEditWarnung');
|
||||
if(warn){
|
||||
const tage = ueber ? daysBetween(parseDateISO(datum), parseDateISO(heute)) : 0;
|
||||
warn.hidden = !ueber;
|
||||
warn.textContent = ueber ? 'seit ' + (tage === 1 ? '1 Tag' : tage + ' Tagen') + ' überfällig' : '';
|
||||
}
|
||||
};
|
||||
|
||||
// msStandSetzen: Umschalter "Offen | Erreicht" — schreibt das (versteckte)
|
||||
// Feld #milestoneEditErreicht, das saveMilestoneEdit() liest.
|
||||
window.msStandSetzen = function(erreicht){
|
||||
document.getElementById('milestoneEditErreicht').checked = !!erreicht;
|
||||
msLageBildNeu();
|
||||
};
|
||||
|
||||
// applyTodoStatus: schreibt den Status in das Segment und blendet den
|
||||
// Fortschrittsregler nur im Zustand "In Arbeit" ein (offen = 0 %,
|
||||
// erledigt = 100 % sind implizit). Parameter: status (String),
|
||||
@@ -296,6 +438,7 @@
|
||||
slider.value = value;
|
||||
if(label) label.textContent = value + ' %';
|
||||
}
|
||||
todoLageNeu();
|
||||
}
|
||||
|
||||
// setTodoEditStatus: Klick-Handler des Status-Segments im TODO-Modal.
|
||||
@@ -313,6 +456,7 @@
|
||||
todoProgressMemo = value;
|
||||
const label = document.getElementById('todoEditProgressVal');
|
||||
if(label) label.textContent = value + ' %';
|
||||
todoLageNeu();
|
||||
};
|
||||
|
||||
// currentTodoEditStatus: liest den aktuell gewählten Segment-Status aus dem
|
||||
@@ -335,6 +479,7 @@
|
||||
badge.hidden = days <= 0;
|
||||
badge.textContent = days > 0 ? (days + ' AT') : '';
|
||||
clearFieldError('todoEditRange');
|
||||
todoLageNeu();
|
||||
};
|
||||
|
||||
// setFieldError / clearFieldError: zeigen einen Validierungshinweis direkt
|
||||
@@ -401,6 +546,17 @@
|
||||
const count = list.querySelectorAll('input[type="checkbox"]:checked').length;
|
||||
badge.textContent = count;
|
||||
badge.classList.toggle('zero', count === 0);
|
||||
const summary = document.getElementById('todoEditDependsSummary');
|
||||
if(summary){
|
||||
const plan = app.ensurePlanLocal();
|
||||
summary.innerHTML = Array.from(list.querySelectorAll('input[type="checkbox"]:checked'))
|
||||
.map(cb => planKnoten(plan, cb.value)).filter(Boolean)
|
||||
.map(k => `<div class="pd-vorg-zeile"><span class="pd-vorg-stand${k.fertig ? ' is-fertig' : ''}${k.ms ? ' is-ms' : ''}" aria-hidden="true"></span>`
|
||||
+ `<span class="pd-vorg-titel">${esc(k.titel)}</span>`
|
||||
+ `<span class="pd-vorg-datum">${k.end ? 'bis ' + esc(kurzDatum(k.end)) : 'ohne Termin'}</span></div>`).join('');
|
||||
summary.hidden = !summary.innerHTML;
|
||||
}
|
||||
todoLageNeu();
|
||||
};
|
||||
|
||||
// filterTodoDepends: blendet in der Vorgänger-Liste alle Einträge aus, die
|
||||
@@ -459,6 +615,8 @@
|
||||
const neu = document.getElementById('todoEditSaveNew');
|
||||
if(abs) abs.hidden = !an;
|
||||
if(wer) wer.hidden = an;
|
||||
const aufwandAbs = document.getElementById('todoEditAufwandAbs');
|
||||
if(aufwandAbs) aufwandAbs.hidden = an;
|
||||
if(neu) neu.hidden = an;
|
||||
const titelEl = document.getElementById('todoModalTitle');
|
||||
if(titelEl) titelEl.textContent = an ? (isNew ? 'Neue Leistungsphase' : 'Leistungsphase bearbeiten') : (isNew ? 'Neue Aufgabe' : 'Aufgabe bearbeiten');
|
||||
@@ -644,6 +802,7 @@
|
||||
}
|
||||
todoAufwandInfo();
|
||||
todoPhasenModusSetzen(isNew ? !!pre.phase : !!currentTodo.lph, isNew ? null : currentTodo, isNew, isNew ? pre.lph : null);
|
||||
panelWoSetzen('todoModalWo', currentTodo && typeof aufgabenPhase === 'function' ? aufgabenPhase(currentTodo, plan.todos) : '');
|
||||
|
||||
const storedProgress = isNew ? 0 : (parseInt(currentTodo.progress, 10) || 0);
|
||||
todoProgressMemo = (storedProgress > 0 && storedProgress < 100) ? storedProgress : 50;
|
||||
@@ -687,9 +846,10 @@
|
||||
const searchInput = document.getElementById('todoEditDependsSearch');
|
||||
if(searchInput) searchInput.value = '';
|
||||
refreshTodoDependsCount();
|
||||
// Bereich nur aufklappen, wenn es dort auch etwas zu sehen gibt
|
||||
// Gewählte Vorgänger stehen offen darüber (#todoEditDependsSummary), die
|
||||
// Auswahlliste bleibt zu, bis man sie ändern will.
|
||||
const fold = document.getElementById('todoEditDependsFold');
|
||||
if(fold) fold.open = !!(currentTodo && Array.isArray(currentTodo.dependsOn) && currentTodo.dependsOn.length > 0);
|
||||
if(fold) fold.open = false;
|
||||
|
||||
const assigneesDiv = document.getElementById('todoEditAssignees');
|
||||
if(assigneesDiv){
|
||||
@@ -761,6 +921,7 @@
|
||||
}
|
||||
document.getElementById('todoModal').style.display = 'none';
|
||||
app.currentTodoIndex = null;
|
||||
if(currentTab === 'wochenbericht') berichtAufgabenNeu();
|
||||
todoModalSnapshot = '';
|
||||
const danach = todoNachSchliessen;
|
||||
todoNachSchliessen = null;
|
||||
|
||||
@@ -107,9 +107,8 @@
|
||||
if(nav) nav.style.display = '';
|
||||
}
|
||||
|
||||
// upcomingEntries: offene Aufgaben mit Endtermin und Meilensteine ab heute
|
||||
// (vergangene Meilensteine haben kein Erledigt-Kennzeichen und gelten als
|
||||
// gewesen), chronologisch. idx verweist in plan.todos/plan.milestones, damit
|
||||
// upcomingEntries: offene Aufgaben mit Endtermin und nicht erreichte
|
||||
// Meilensteine (vergangene gelten als überfällig), chronologisch. idx verweist in plan.todos/plan.milestones, damit
|
||||
// ein Klick denselben Dialog öffnet wie im Terminplan.
|
||||
// Rückgabe: Array von { art, idx, titel, datum, iso, wer, meins }.
|
||||
function upcomingEntries(){
|
||||
@@ -126,7 +125,7 @@
|
||||
meins: !!ich && (t.assignees || []).includes(ich) });
|
||||
});
|
||||
(plan.milestones || []).forEach((m, idx) => {
|
||||
if(!m.date || m.date < heute) return;
|
||||
if(!m.date || m.erreicht) return;
|
||||
rows.push({ art: 'meilenstein', idx, titel: m.name || 'Meilenstein', iso: m.date, wer: [], meins: false });
|
||||
});
|
||||
rows.sort((a, b) => a.iso.localeCompare(b.iso));
|
||||
|
||||
@@ -166,16 +166,16 @@
|
||||
const d = parseDateISO(m.date);
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const overdue = d && d < today;
|
||||
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' : ''}">${esc(d ? formatDateDE(d) : 'ohne Datum')}</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.length} gesamt`;
|
||||
if(msCount) msCount.textContent = milestones.length === 0 ? '' : `${milestones.filter(m => m.erreicht).length} von ${milestones.length} erreicht`;
|
||||
};
|
||||
})();
|
||||
@@ -822,7 +822,7 @@ function wtTageDaten(w){
|
||||
// (offen/überfällig) sind über die Kennzahlen oben erreichbar, die heute
|
||||
// Abwesenden über die Kopfleiste.
|
||||
(plan.milestones || []).forEach(m => {
|
||||
if(m.date && m.date >= heute && spalteFuer[m.date]) spalteFuer[m.date].eintraege.push({ art: 'ms', titel: m.name || 'Meilenstein', projekt: hinweisFuer(m.date) + projekt, pid: pr.id, abschnitt: 'terminplan', sort: m.date });
|
||||
if(m.date && !m.erreicht && m.date >= heute && spalteFuer[m.date]) spalteFuer[m.date].eintraege.push({ art: 'ms', titel: m.name || 'Meilenstein', projekt: hinweisFuer(m.date) + projekt, pid: pr.id, abschnitt: 'terminplan', sort: m.date });
|
||||
});
|
||||
});
|
||||
tage.forEach(t => t.eintraege.sort((a, b) => String(a.sort).localeCompare(String(b.sort))));
|
||||
|
||||
@@ -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){
|
||||
|
||||
Reference in new issue
Block a user