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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user