Notizen je Projekt statt Reiter Leistungsphasen
- Neuer Reiter „Notizen“ im Projektbereich: Liste mit Suche und angehefteten Notizen, Editor mit Formatierung, automatisches Speichern, Mail per Drag & Drop oder Strg+V als neue Notiz, live bei allen - Reiter „Leistungsphasen“ entfernt (Phasen weiter im Terminplan) - Behoben: Umlaute in .eml-Dateien ohne Transfer-Kodierung gingen verloren (betraf auch „Termin aus Mail“) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
51dfbe8cd8
commit
7d65ff813f
10 files changed
+697
-265
No files matched your search
@@ -1,21 +1,10 @@
|
||||
// -----------------------------
|
||||
// Projektbereich — Leistungsphasen
|
||||
// Projektbereich — Zuordnung Aufgabe → Leistungsphase
|
||||
// -----------------------------
|
||||
// Bereich "Leistungsphasen" (Eriks Wahl "B"): die Phasenbalken des
|
||||
// Terminplans (Aufgaben mit t.lph, balkenPhase() in load.js) als eigener
|
||||
// Bereich statt als Zeilen zwischen den Aufgaben. Oben ein Ablaufbalken über
|
||||
// alle Phasen mit Heute-Linie, darunter eine Kachel je Phase: Fortschritt
|
||||
// (aus dem Wochenbericht, p.lph), Zeitraum (aus dem Balken), Aufwand
|
||||
// (Summe der Stunden ihrer Aufgaben) und die Aufgaben, die in ihrem
|
||||
// Zeitraum beginnen — dieselbe Zuordnung wie die Gliederung im Terminplan
|
||||
// (plan-build-gantt.js).
|
||||
//
|
||||
// Keine eigene Datenhaltung: Bearbeiten öffnet dasselbe Panel wie im
|
||||
// Terminplan (openTodoModal() im Phasen-Modus), Fortschritt bleibt im
|
||||
// Wochenbericht ("Ein Ort je Angabe").
|
||||
//
|
||||
// renderPlanPhasen() wird von renderPlanGantt() (plan-gantt.js) und beim
|
||||
// Wechsel in den Bereich (showPlanSection(), plan-sections.js) aufgerufen.
|
||||
// Der frühere Reiter „Leistungsphasen“ (Ablaufbalken + Kachel je Phase) ist
|
||||
// auf Eriks Wunsch entfallen; an seiner Stelle steht „Notizen“. Geblieben ist
|
||||
// nur aufgabenPhase(), die die Aufgabenliste (plan-tasks.js) und das
|
||||
// Terminplan-PDF (pdf.js) nutzen.
|
||||
|
||||
// aufgabenPhase: zu welcher Leistungsphase eine (gewöhnliche) Aufgabe gehört —
|
||||
// der Phasenbalken, in dessen Zeitraum sie beginnt. Dieselbe Regel wie die
|
||||
@@ -31,150 +20,3 @@ function aufgabenPhase(t, todos){
|
||||
.find(b => s0 >= parseDateISO(b.start) && s0 <= parseDateISO(b.end));
|
||||
return k ? balkenPhase(k) : '';
|
||||
}
|
||||
|
||||
(function(){
|
||||
const app = window.PlanApp;
|
||||
|
||||
const LPH_NAMEN = {
|
||||
'LPH 1': 'Grundlagenermittlung', 'LPH 2': 'Vorplanung', 'LPH 3': 'Entwurfsplanung',
|
||||
'LPH 4': 'Genehmigungsplanung', 'LPH 5': 'Ausführungsplanung', 'LPH 6': 'Vorbereitung der Vergabe',
|
||||
'LPH 7': 'Mitwirkung bei der Vergabe', 'LPH 8': 'Objektüberwachung', 'LPH 9': 'Objektbetreuung',
|
||||
'W+M': 'Wartung und Monitoring'
|
||||
};
|
||||
|
||||
const kurzDatum = d => d ? d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }) : '';
|
||||
const langDatum = d => d ? formatDateDE(d) : '';
|
||||
|
||||
// phasenDaten: je Phase alles, was Ablaufbalken und Kachel brauchen.
|
||||
// Rückgabe: Array, sortiert läuft → geplant → fertig, darin nach Beginn.
|
||||
function phasenDaten(){
|
||||
const p = app.project;
|
||||
const plan = app.ensurePlanLocal();
|
||||
const todos = Array.isArray(plan.todos) ? plan.todos : [];
|
||||
const beauftragt = Array.isArray(p.beauftragteLP) ? p.beauftragteLP : [];
|
||||
|
||||
// Phasen: beauftragte plus alle, die einen Balken haben.
|
||||
const namen = LPHS.filter(l => beauftragt.includes(l) || todos.some(t => balkenPhase(t) === l));
|
||||
const amtierend = (typeof lpActiveList === 'function' ? lpActiveList(p) : [])[0] || null;
|
||||
const laufend = typeof arbeitsPhase === 'function' ? arbeitsPhase(p, amtierend) : amtierend;
|
||||
|
||||
return namen.map(l => {
|
||||
const idx = todos.findIndex(t => balkenPhase(t) === l);
|
||||
const balken = idx >= 0 ? todos[idx] : null;
|
||||
const start = balken ? parseDateISO(balken.start) : null;
|
||||
const ende = balken ? parseDateISO(balken.end) : null;
|
||||
const prozent = typeof fortschrittProzent === 'function' ? fortschrittProzent(p, l) : 0;
|
||||
const aufgaben = todos.filter(t => aufgabenPhase(t, todos) === l);
|
||||
// Aufwand = Summe der Stunden ihrer Aufgaben (dieselbe Quelle wie die
|
||||
// Auslastung, js/load/load.js), nicht mehr aus dem Honorar.
|
||||
const stunden = Math.round(aufgaben.reduce((s, t) => s + (Number(t.aufwand) || 0), 0) * 10) / 10;
|
||||
const status = prozent >= 100 ? 'fertig' : (l === laufend ? 'laeuft' : 'geplant');
|
||||
return { l, idx, balken, start, ende, prozent, stunden, aufgaben, status,
|
||||
beauftragt: !beauftragt.length || beauftragt.includes(l) };
|
||||
}).sort((a, b) => {
|
||||
const rang = { laeuft: 0, geplant: 1, fertig: 2 };
|
||||
if(rang[a.status] !== rang[b.status]) return rang[a.status] - rang[b.status];
|
||||
return LPHS.indexOf(a.l) - LPHS.indexOf(b.l);
|
||||
});
|
||||
}
|
||||
|
||||
// ablaufHtml: der schmale Balken über alle Phasen mit Zeitraum. Phasen
|
||||
// ohne Zeitraum fehlen hier (sie stehen als Kachel mit Hinweis darunter).
|
||||
function ablaufHtml(daten){
|
||||
const mit = daten.filter(d => d.start && d.ende && d.ende >= d.start)
|
||||
.sort((a, b) => a.start - b.start);
|
||||
if(!mit.length) return '';
|
||||
const von = mit[0].start.getTime();
|
||||
const bis = Math.max(...mit.map(d => d.ende.getTime()));
|
||||
const spanne = Math.max(1, bis - von);
|
||||
const pos = t => Math.min(100, Math.max(0, (t - von) / spanne * 100));
|
||||
const heute = new Date(); heute.setHours(0, 0, 0, 0);
|
||||
const heutePos = heute.getTime() >= von && heute.getTime() <= bis ? pos(heute.getTime()) : null;
|
||||
const segmente = mit.map(d => {
|
||||
const links = pos(d.start.getTime());
|
||||
const breite = Math.max(1.5, pos(d.ende.getTime() + 86400000) - links);
|
||||
const titel = `${d.l}: ${langDatum(d.start)} – ${langDatum(d.ende)}, ${d.prozent} %`;
|
||||
return `<button type="button" class="phasen-ablauf-seg is-${d.status}" style="left:${links}%;width:${breite}%;--fort:${d.prozent}%"
|
||||
title="${esc(titel)}" onclick="phasenKachelZeigen('${esc(d.l)}')"><span>${esc(breite > 6 ? d.l : d.l.replace('LPH ', ''))}</span></button>`;
|
||||
}).join('');
|
||||
const heuteHtml = heutePos === null ? '' : `<span class="phasen-ablauf-heute" style="left:${heutePos}%" title="heute"></span>`;
|
||||
const monat = d => d.toLocaleDateString('de-DE', { month: 'short', year: 'numeric' });
|
||||
return `<div class="plan-panel phasen-ablauf">
|
||||
<div class="phasen-kopf">Ablauf</div>
|
||||
<div class="phasen-ablauf-spur">${segmente}${heuteHtml}</div>
|
||||
<div class="phasen-ablauf-achse"><span>${esc(monat(mit[0].start))}</span>${heutePos === null ? '' : `<span class="phasen-ablauf-heute-text" style="left:${heutePos}%">heute, ${esc(kurzDatum(heute))}</span>`}<span>${esc(monat(new Date(bis)))}</span></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// kachelHtml: eine Kachel je Phase.
|
||||
function kachelHtml(d, locked){
|
||||
const statusText = { laeuft: 'läuft', geplant: 'geplant', fertig: 'fertig' }[d.status];
|
||||
const erledigt = d.aufgaben.filter(t => t.done).length;
|
||||
const aufgabenText = d.aufgaben.length ? `${erledigt} von ${d.aufgaben.length} erledigt` : 'keine';
|
||||
const zeitraum = d.start && d.ende
|
||||
? `${d.start.getFullYear() === d.ende.getFullYear() ? kurzDatum(d.start) : langDatum(d.start)} – ${langDatum(d.ende)}`
|
||||
: '<span class="phasen-leer">kein Zeitraum</span>';
|
||||
const bearbeiten = locked ? ''
|
||||
: (d.balken
|
||||
? `<button class="secondary small" onclick="openTodoModal(${d.idx})">Bearbeiten</button>`
|
||||
: `<button class="secondary small" onclick="addPlanPhase('${esc(d.l)}')">Zeitraum festlegen</button>`);
|
||||
const ansehen = d.aufgaben.length
|
||||
? `<button class="secondary small" onclick="phasenAufgabenZeigen('${esc(d.l)}')">Aufgaben ansehen</button>` : '';
|
||||
return `<div class="phasen-kachel is-${d.status}" data-phase="${esc(d.l)}">
|
||||
<div class="phasen-kachel-kopf">
|
||||
<div class="phasen-ring" style="--fort:${d.prozent}%"><span>${d.prozent} %</span></div>
|
||||
<div class="phasen-kachel-name">
|
||||
<b>${esc(d.l)}</b>
|
||||
<span>${esc(LPH_NAMEN[d.l] || '')}</span>
|
||||
</div>
|
||||
<span class="phasen-chip is-${d.status}">${statusText}</span>
|
||||
</div>
|
||||
${d.beauftragt ? '' : '<div class="phasen-hinweis">nicht beauftragt</div>'}
|
||||
<div class="phasen-werte">
|
||||
<span><small>Zeitraum</small><b>${zeitraum}</b></span>
|
||||
<span><small>Aufwand</small><b>${d.stunden ? d.stunden.toLocaleString('de-DE') + ' h' : '<span class="phasen-leer">keine Stunden</span>'}</b></span>
|
||||
<span><small>Aufgaben</small><b>${aufgabenText}</b></span>
|
||||
</div>
|
||||
${bearbeiten || ansehen ? `<div class="phasen-knoepfe">${bearbeiten}${ansehen}</div>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// renderPlanPhasen: baut den Bereich neu auf. Keine Parameter.
|
||||
window.renderPlanPhasen = function(){
|
||||
const el = document.getElementById('planPhasenWrap');
|
||||
if(!el || !app.project) return;
|
||||
const locked = app.isLocked();
|
||||
const daten = phasenDaten();
|
||||
if(!daten.length){
|
||||
el.innerHTML = `<div class="plan-panel"><div class="plan-task-empty">${locked
|
||||
? 'Für dieses Projekt sind keine Leistungsphasen hinterlegt.'
|
||||
: 'Noch keine Leistungsphase. Unter „Einstellungen“ beauftragen oder über „+ Leistungsphase“ bzw. „Terminplan aus Honorar“ anlegen.'}</div></div>`;
|
||||
return;
|
||||
}
|
||||
const summeH = daten.reduce((s, d) => s + d.stunden, 0);
|
||||
const enden = daten.filter(d => d.ende).map(d => d.ende.getTime());
|
||||
const laufend = daten.find(d => d.status === 'laeuft');
|
||||
const teile = [daten.filter(d => d.beauftragt).length + ' beauftragt'];
|
||||
if(laufend) teile.push('läuft: ' + laufend.l);
|
||||
if(summeH) teile.push(summeH.toLocaleString('de-DE') + ' h in Aufgaben');
|
||||
if(enden.length) teile.push('Ende voraussichtlich ' + langDatum(new Date(Math.max(...enden))));
|
||||
el.innerHTML = `<div class="phasen-zusammenfassung">${esc(teile.join(' · '))}<span>Fortschritt aus dem Wochenbericht</span></div>
|
||||
${ablaufHtml(daten)}
|
||||
<div class="phasen-raster">${daten.map(d => kachelHtml(d, locked)).join('')}</div>`;
|
||||
};
|
||||
|
||||
// phasenKachelZeigen: Klick auf ein Segment im Ablauf — Kachel ins Bild
|
||||
// holen und kurz hervorheben.
|
||||
window.phasenKachelZeigen = function(l){
|
||||
const k = document.querySelector(`#planPhasenWrap .phasen-kachel[data-phase="${CSS.escape(l)}"]`);
|
||||
if(!k) return;
|
||||
k.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
k.classList.remove('ist-markiert'); void k.offsetWidth; k.classList.add('ist-markiert');
|
||||
};
|
||||
|
||||
// phasenAufgabenZeigen: in den Bereich Aufgaben, gefiltert auf diese Phase.
|
||||
window.phasenAufgabenZeigen = function(l){
|
||||
if(typeof setzeAufgabenFilter === 'function') setzeAufgabenFilter(l);
|
||||
showPlanSection('aufgaben');
|
||||
};
|
||||
})();
|
||||
Reference in new issue
Block a user