Termin aus Mail, Rollen & Rechte, Abwesenheiten, Glas-Darstellung und Pi-Betrieb
- Termin aus Mail: Mailtext oder .eml per Drag & Drop/Strg+V auf Terminplan oder Aufgaben, KI schlägt Aufgaben vor, Prüfung im Aufgaben-Panel; neues Notizfeld je Aufgabe (plan_todos.notiz) - Terminplan: Meilensteine ohne Aufgabenbezug je in eigener Zeile, "+ Neu"-Menü klappt neben schmaler Seitenleiste nach rechts, Leistungsphasen - Rollen & Rechte, Abwesenheiten-Seite, Benachrichtigungen, Rundgang - Glas-Darstellung, Logo als Favicon - Pi-Betrieb: PDF-Arbeit im Kindprozess, Wächter, Einrichtungs- und Prüfskript inkl. WLAN-Wächter Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f4fb3c550b
commit
b7e0bd0d92
56 files changed
+12265
-2974
No files matched your search
+187
-76
@@ -10,7 +10,7 @@
|
||||
// Detailkarten im Auslastungs-Modal, Vorschau in der Kompakt-Karte darunter)
|
||||
// und die KPI-Kachelzeile über dem GANTT (renderPlanKpis — Gesamtfortschritt,
|
||||
// offene Aufgaben und nächster Meilenstein aus denselben Plan-Daten;
|
||||
// Team-Auslastung dagegen über berechneAuslastung() aus js/load/load.js,
|
||||
// Team-Auslastung dagegen über aufgabenVerlaufJePerson() aus js/load/load.js,
|
||||
// siehe computeResourceLoads() weiter unten — dieselbe Rechnung wie im
|
||||
// Auslastungs-Tab, nicht aus den Plan-Daten selbst). renderPlanGantt ist die
|
||||
// zentrale Neu-Render-Funktion (rendert am Ende auch Auslastung und KPIs
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
// renderPlanGantt: baut das komplette GANTT-Diagramm des aktuellen Projekts
|
||||
// neu auf. Nutzt die Datumseingaben der Seite (oder berechnet den
|
||||
// Projektzeitraum), berücksichtigt Urlaub, kritischen Pfad, Sperrstatus und
|
||||
// Projektzeitraum), berücksichtigt Urlaub, Sperrstatus und
|
||||
// Zoomstufe und ruft buildGanttHtml auf. Im Anschluss werden die
|
||||
// Abhängigkeitslinien gezeichnet und die Ressourcen-Auslastung aktualisiert.
|
||||
window.renderPlanGantt = function(){
|
||||
@@ -41,16 +41,23 @@
|
||||
document.getElementById('planGanttStart').value = formatDateISO(start);
|
||||
document.getElementById('planGanttEnd').value = formatDateISO(end);
|
||||
}
|
||||
// Zeitraum auch sichtbar ins Feld schreiben (es zeigte sonst nur den
|
||||
// Platzhalter, solange niemand einen Zeitraum gewählt hatte).
|
||||
const zeitraumAnzeige = document.getElementById('planGanttRange');
|
||||
if(zeitraumAnzeige) zeitraumAnzeige.value = formatDateDE(start) + ' – ' + formatDateDE(end);
|
||||
const showVacation = document.getElementById('planShowVacation').checked;
|
||||
const vacationPeople = projectAssignees(app.project, app.week).map(a => a.person.id);
|
||||
const criticalPath = app.criticalPathActive ? computeCriticalPath(app.project) : [];
|
||||
const ghost = collectGhostDates();
|
||||
container.innerHTML = buildGanttHtml([app.project], start, end, {
|
||||
showVacation, editable: !app.isLocked(), highlightProjectId: app.project.id,
|
||||
showTodos: true, vacationPeople, pxPerDay: app.pxPerDay, criticalPath,
|
||||
showTodos: true, vacationPeople, pxPerDay: app.pxPerDay,
|
||||
ghostTodos: ghost.todos, ghostMilestones: ghost.milestones, ghostLabel: ghost.label,
|
||||
filter: app.ganttFilter || 'alle', phasenZu: app.phasenZu || (app.phasenZu = {}),
|
||||
eigenePersonId: (typeof currentUser !== 'undefined' && currentUser && currentUser.personId) || '',
|
||||
});
|
||||
renderOhneTermin();
|
||||
container.scrollLeft = prevScroll;
|
||||
if(typeof ganttGriffeEinsetzen === 'function') ganttGriffeEinsetzen(container);
|
||||
requestAnimationFrame(() => renderGanttDependencies(container));
|
||||
renderPlanWarnings();
|
||||
renderResourceLoad();
|
||||
@@ -61,6 +68,7 @@
|
||||
// Neu-Render-Funktion nach jeder Änderung ist — so bleiben alle drei
|
||||
// Bereiche ohne zusätzliche Aufrufe in den ändernden Modulen gleich.
|
||||
if(typeof renderPlanTasks === 'function') renderPlanTasks();
|
||||
if(typeof renderPlanPhasen === 'function') renderPlanPhasen();
|
||||
if(typeof renderPlanDashboard === 'function') renderPlanDashboard();
|
||||
};
|
||||
|
||||
@@ -121,71 +129,68 @@
|
||||
</div>`;
|
||||
};
|
||||
|
||||
// kpiTile: baut das HTML einer einzelnen Kachel für renderPlanKpis().
|
||||
// Parameter: label (Kachel-Titel), value (Hauptzahl), sub (Unterzeile),
|
||||
// warn (optional, hebt den Wert farblich hervor, z. B. bei überfälligen
|
||||
// Aufgaben). Rückgabe: HTML-String.
|
||||
function kpiTile(label, value, sub, warn){
|
||||
return `<div class="plan-kpi-tile${warn ? ' warn' : ''}">
|
||||
// bandZelle: eine Kennzahl im Lageband des Dashboards. Parameter: label,
|
||||
// value, sub (Texte), warn (Unterzeile rot). Rückgabe: HTML-String.
|
||||
function bandZelle(label, value, sub, warn){
|
||||
return `<div class="plan-lage-zelle">
|
||||
<div class="kpi-label">${esc(label)}</div>
|
||||
<div class="kpi-value">${esc(value)}</div>
|
||||
<div class="kpi-sub">${esc(sub)}</div>
|
||||
<div class="kpi-sub${warn ? ' ist-rot' : ''}">${esc(sub)}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// renderPlanKpis: füllt die KPI-Kachelzeile über dem GANTT (Cockpit-Layout,
|
||||
// #planKpiRow in App.html) mit vier Kennzahlen, die alle aus den
|
||||
// bereits vorhandenen Plan-Daten abgeleitet sind (keine neue Datenquelle):
|
||||
// Gesamtfortschritt (Ø-Fortschritt aller TODOs), offene Aufgaben (inkl.
|
||||
// überfällige), nächster anstehender Meilenstein und Team-Auslastung
|
||||
// (Ø-Prozentwert aus computeResourceLoads(), dieselbe Zahl wie im
|
||||
// Auslastungs-Modal). Wird nach jedem GANTT-Rendering aufgerufen, damit sie
|
||||
// nach TODO-/Meilenstein-/Zuordnungsänderungen sofort aktuell ist.
|
||||
// renderPlanKpis: füllt das Lageband oben im Dashboard (#planKpiRow,
|
||||
// Anordnung "Lagebild"): links die laufende Leistungsphase mit Ring
|
||||
// (amtierende LPH und ihr Fortschritt aus dem Wochenbericht), daneben offene
|
||||
// Aufgaben (mit Überfälligem), nächster Meilenstein und Team-Auslastung
|
||||
// (computeResourceLoads(), dieselbe Zahl wie im Auslastungs-Tab). Der
|
||||
// frühere "Gesamtfortschritt" (Ø der Aufgaben) ist entfallen — für den
|
||||
// Stand eines Projekts zählt die Leistungsphase. Wird nach jedem
|
||||
// GANTT-Rendering aufgerufen, damit es sofort aktuell ist.
|
||||
window.renderPlanKpis = function(){
|
||||
const row = document.getElementById('planKpiRow');
|
||||
if(!row || !app.project) return;
|
||||
const p = app.project;
|
||||
const plan = app.ensurePlanLocal();
|
||||
const todos = Array.isArray(plan.todos) ? plan.todos : [];
|
||||
const milestones = Array.isArray(plan.milestones) ? plan.milestones : [];
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
let progressTile;
|
||||
if(todos.length === 0){
|
||||
progressTile = kpiTile('Gesamtfortschritt', '–', 'Keine Aufgaben angelegt');
|
||||
} else {
|
||||
const avg = Math.round(todos.reduce((s, t) => s + (parseInt(t.progress, 10) || 0), 0) / todos.length);
|
||||
const doneCount = todos.filter(t => t.done).length;
|
||||
progressTile = kpiTile('Gesamtfortschritt', avg + '%', doneCount + ' von ' + todos.length + ' Aufgaben erledigt');
|
||||
}
|
||||
const lph = (typeof lpActiveList === 'function' ? lpActiveList(p) : [])[0] || '';
|
||||
const roh = lph && p.lph ? p.lph[lph] : '';
|
||||
const pct = roh === undefined || roh === '' ? null : Math.max(0, Math.min(100, Number(roh) || 0));
|
||||
const phase = `<div class="plan-lage-zelle plan-lage-phase">
|
||||
<span class="plan-lage-ring" style="--p:${pct || 0}"></span>
|
||||
<div><div class="kpi-label">Laufende Phase</div>
|
||||
<div class="kpi-value">${lph ? esc(lph) + (pct === null ? '' : ' · ' + pct + ' %') : '–'}</div>
|
||||
<div class="kpi-sub">${lph ? (pct === 100 ? 'abgeschlossen' : 'Fortschritt laut Wochenbericht') : 'Keine amtierende Phase gesetzt'}</div></div>
|
||||
</div>`;
|
||||
|
||||
const open = todos.filter(t => !t.done);
|
||||
const overdue = open.filter(t => t.end && parseDateISO(t.end) < today);
|
||||
const openSub = overdue.length > 0 ? overdue.length + ' überfällig' : (open.length > 0 ? 'Im Zeitplan' : 'Alle Aufgaben erledigt');
|
||||
const openTile = kpiTile('Offene Aufgaben', String(open.length), openSub, overdue.length > 0);
|
||||
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)
|
||||
.sort((a, b) => parseDateISO(a.date) - parseDateISO(b.date));
|
||||
let milestoneTile;
|
||||
if(upcoming.length > 0){
|
||||
milestoneTile = kpiTile('Nächster Meilenstein', formatDateDE(parseDateISO(upcoming[0].date)), upcoming[0].name || 'Meilenstein');
|
||||
} else {
|
||||
milestoneTile = kpiTile('Nächster Meilenstein', '–', milestones.length > 0 ? 'Keine anstehenden Meilensteine' : 'Keine Meilensteine angelegt');
|
||||
}
|
||||
const msZelle = upcoming.length > 0
|
||||
? bandZelle('Nächster Meilenstein', formatDateDE(parseDateISO(upcoming[0].date)), upcoming[0].name || 'Meilenstein')
|
||||
: bandZelle('Nächster Meilenstein', '–', milestones.length > 0 ? 'keine anstehenden' : 'keine angelegt');
|
||||
|
||||
const { loads } = computeResourceLoads();
|
||||
const loadValues = Object.values(loads);
|
||||
let loadTile;
|
||||
let loadZelle;
|
||||
if(loadValues.length === 0){
|
||||
loadTile = kpiTile('Team-Auslastung', '–', 'Keine Beteiligten zugeordnet');
|
||||
loadZelle = bandZelle('Team-Auslastung', '–', 'niemand zugeordnet');
|
||||
} else {
|
||||
const avgPct = Math.round(loadValues.reduce((s, l) => s + l.quote * 100, 0) / loadValues.length);
|
||||
const top = loadValues.slice().sort((a, b) => b.quote - a.quote)[0];
|
||||
loadTile = kpiTile('Team-Auslastung', avgPct + '%', top.stunden > 0 ? 'Höchste Auslastung: ' + top.person.name : 'Noch nicht bewertbar');
|
||||
loadZelle = bandZelle('Team-Auslastung', avgPct + ' %', top.stunden > 0
|
||||
? 'höchste: ' + (top.person.fullName || top.person.name) + ' ' + Math.round(top.quote * 100) + ' %' : 'noch nicht bewertbar');
|
||||
}
|
||||
|
||||
row.innerHTML = progressTile + openTile + milestoneTile + loadTile;
|
||||
row.innerHTML = phase + bandZelle('Offene Aufgaben', String(open.length), openSub, overdue.length > 0) + msZelle + loadZelle;
|
||||
};
|
||||
|
||||
// renderGanttDependencies: zeichnet für alle TODO-/Meilenstein-Balken die
|
||||
@@ -202,7 +207,13 @@
|
||||
let svg = chart.querySelector('.gantt-dependencies-svg');
|
||||
if(svg) svg.remove();
|
||||
|
||||
const nodeBars = Array.from(chart.querySelectorAll('.gantt-bar.todo[data-todo-id], .gantt-milestone[data-todo-id]'));
|
||||
// Linien abgeschaltet (Menü "Mehr") — dann nichts zeichnen.
|
||||
const pfeile = document.getElementById('planShowPfeile');
|
||||
if(pfeile && !pfeile.checked) return;
|
||||
// Ausgeblendete Zeilen (zugeklappte Phase, Filter) haben keine Lage —
|
||||
// Linien dorthin würden ins Leere zeigen.
|
||||
const nodeBars = Array.from(chart.querySelectorAll('.gantt-bar.todo[data-todo-id], .gantt-milestone[data-todo-id]'))
|
||||
.filter(b => b.offsetParent !== null);
|
||||
if(nodeBars.length === 0) return;
|
||||
|
||||
const chartRect = chart.getBoundingClientRect();
|
||||
@@ -283,6 +294,97 @@
|
||||
renderPlanGantt();
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ruhige Leiste (Eriks Wahl "A"): Maßstab, Heute, Filter, Menüs.
|
||||
// ---------------------------------------------------------------------------
|
||||
const TP_STUFEN = { fit: null, woche: 8, tag: 32 };
|
||||
|
||||
// tpMassstab: Maßstab "Übersicht" (an die Breite angepasst), "Wochen" oder
|
||||
// "Tage" — feste Pixel je Tag statt der früheren −/+-Knöpfe.
|
||||
window.tpMassstab = function(stufe){
|
||||
app.pxPerDay = TP_STUFEN[stufe] === undefined ? null : TP_STUFEN[stufe];
|
||||
document.querySelectorAll('#tpMassstab button').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.stufe === stufe)));
|
||||
renderPlanGantt();
|
||||
if(app.pxPerDay) tpZuHeuteScrollen();
|
||||
};
|
||||
|
||||
function tpZuHeuteScrollen(){
|
||||
const c = document.getElementById('planGanttContainer');
|
||||
const heute = c && c.querySelector('.gantt-grid-today');
|
||||
if(!c || !heute) return;
|
||||
const r = heute.getBoundingClientRect(), cr = c.getBoundingClientRect();
|
||||
c.scrollLeft += (r.left - cr.left) - c.clientWidth / 3;
|
||||
}
|
||||
|
||||
// tpHeute: Zeitraum ab zwei Wochen vor heute, gleiche Länge wie bisher
|
||||
// (mindestens 12 Wochen), und heute ins Bild holen.
|
||||
window.tpHeute = function(){
|
||||
const s = parseDateISO(document.getElementById('planGanttStart').value);
|
||||
const e = parseDateISO(document.getElementById('planGanttEnd').value);
|
||||
const tage = s && e ? Math.max(84, daysBetween(s, e)) : 84;
|
||||
const von = new Date(); von.setHours(0, 0, 0, 0); von.setDate(von.getDate() - 14);
|
||||
const bis = new Date(von); bis.setDate(bis.getDate() + tage);
|
||||
document.getElementById('planGanttStart').value = formatDateISO(von);
|
||||
document.getElementById('planGanttEnd').value = formatDateISO(bis);
|
||||
const anzeige = document.getElementById('planGanttRange');
|
||||
if(anzeige) anzeige.value = formatDateDE(von) + ' – ' + formatDateDE(bis);
|
||||
renderPlanGantt();
|
||||
if(app.pxPerDay) tpZuHeuteScrollen();
|
||||
};
|
||||
|
||||
// tpFilter: Alle / Meine (eigene Zuständigkeit) / Überfällig. Gefilterte
|
||||
// Zeilen werden nur ausgeblendet, nicht weggelassen — das Umsortieren per
|
||||
// Ziehen (plan-drag.js) zählt die Zeilen im DOM.
|
||||
window.tpFilter = function(filter){
|
||||
app.ganttFilter = filter;
|
||||
document.querySelectorAll('#tpFilter button').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.filter === filter)));
|
||||
renderPlanGantt();
|
||||
};
|
||||
|
||||
// tpPhaseUmschalten: klappt eine Leistungsphase im Terminplan auf/zu.
|
||||
window.tpPhaseUmschalten = function(phase){
|
||||
if(!app.phasenZu) app.phasenZu = {};
|
||||
app.phasenZu[phase] = !app.phasenZu[phase];
|
||||
renderPlanGantt();
|
||||
};
|
||||
|
||||
// tpMenueZu: schließt die Menüs "Mehr" und "+ Neu".
|
||||
window.tpMenueZu = function(){
|
||||
document.querySelectorAll('.tp-menue[open]').forEach(d => d.removeAttribute('open'));
|
||||
};
|
||||
// Ein Klick außerhalb schließt ein offenes Menü; ein Menü öffnen schließt
|
||||
// das andere.
|
||||
document.addEventListener('click', e => {
|
||||
document.querySelectorAll('.tp-menue[open]').forEach(d => { if(!d.contains(e.target)) d.removeAttribute('open'); });
|
||||
});
|
||||
document.addEventListener('keydown', e => { if(e.key === 'Escape') window.tpMenueZu(); });
|
||||
// Die Liste hängt rechtsbündig unter dem Knopf. Bricht die Leiste in einem
|
||||
// schmalen Fenster um, steht "+ Neu" links und die Liste rutschte unter die
|
||||
// Seitenleiste — dann linksbündig aufklappen (Klasse .nach-rechts).
|
||||
document.addEventListener('toggle', e => {
|
||||
const d = e.target;
|
||||
if(!d.classList || !d.classList.contains('tp-menue') || !d.open) return;
|
||||
const liste = d.querySelector('.tp-menue-liste');
|
||||
if(!liste) return;
|
||||
liste.classList.remove('nach-rechts');
|
||||
const seite = document.querySelector('.side-menu');
|
||||
const r = seite ? seite.getBoundingClientRect() : null;
|
||||
const linkeGrenze = r && r.width && getComputedStyle(seite).position !== 'fixed' ? r.right : 0;
|
||||
if(liste.getBoundingClientRect().left < linkeGrenze + 8) liste.classList.add('nach-rechts');
|
||||
}, true);
|
||||
|
||||
// renderOhneTermin: Hinweis unter dem Diagramm auf Aufgaben, die keinen
|
||||
// Balken haben (ohne Zeitraum oder bewusst ausgeblendet) — sie stehen nur
|
||||
// unter "Aufgaben". Vorher fehlte jeder Hinweis, dass es sie gibt.
|
||||
function renderOhneTermin(){
|
||||
const el = document.getElementById('planOhneTermin');
|
||||
if(!el) return;
|
||||
const todos = (app.project && app.project.plan && Array.isArray(app.project.plan.todos)) ? app.project.plan.todos : [];
|
||||
const ohne = todos.filter(t => !t.done && !(t.start && t.end && t.showInGantt !== false));
|
||||
el.hidden = ohne.length === 0;
|
||||
el.innerHTML = ohne.length ? `<span class="tp-ohne-zahl">${ohne.length}</span> ${ohne.length === 1 ? 'offene Aufgabe' : 'offene Aufgaben'} ohne Termin <span class="tp-ohne-hinweis">stehen nur unter „Aufgaben“ →</span>` : '';
|
||||
}
|
||||
|
||||
// zoomPlanGantt: ändert die Pixel-pro-Tag-Stufe des Diagramms.
|
||||
// Nach unten hinaus wird wieder auf die Containerbreite angepasst (fit).
|
||||
window.zoomPlanGantt = function(dir){
|
||||
@@ -291,7 +393,7 @@
|
||||
let end = parseDateISO(document.getElementById('planGanttEnd').value);
|
||||
if(!start || !end) return;
|
||||
const totalDays = Math.max(1, daysBetween(start, end) + 1);
|
||||
const cw = container ? Math.max(200, container.clientWidth - 250) : 900;
|
||||
const cw = container ? Math.max(200, container.clientWidth - (ganttSpaltenBreite() + 30)) : 900;
|
||||
const fitPx = Math.max(3, cw / totalDays);
|
||||
const base = app.pxPerDay || fitPx;
|
||||
let next;
|
||||
@@ -308,33 +410,20 @@
|
||||
// ---------- Ressourcen-Auslastung ----------
|
||||
|
||||
// computeResourceLoads: berechnet die Auslastung der Projektbeteiligten
|
||||
// NACH DERSELBEN RECHNUNG wie der Auslastungs-Tab und die Wandtafel
|
||||
// (berechneAuslastung()/loadAmpelColor()/kapazitaetsFaktor(),
|
||||
// js/load/load.js) — Wochenstunden aus Honorar × Phasenanteil ×
|
||||
// Restfortschritt, bezogen auf die Kapazität der Person.
|
||||
//
|
||||
// Vorher zählte diese Stelle etwas ganz anderes: Aufgabentage aus den
|
||||
// TODO-Zeiträumen DIESES Terminplans, ohne jeden Bezug zu Honorar oder
|
||||
// Kapazität — ein Blick auf dieselbe Person zeigte hier eine andere Zahl
|
||||
// als im Auslastungs-Tab. Erik: "Die Auslastung der Projektbeteiligten im
|
||||
// Terminplan ist eine andere als die in der sonstigen Anwendung... gleich-
|
||||
// setzen mit der in der sonstigen Anwendung." Jetzt wird für JEDES Projekt
|
||||
// der Sitzung (dieses UND alle übrigen) genau derselbe
|
||||
// berechneAuslastung()-Aufruf gemacht wie in renderLoad() — dieselbe
|
||||
// Person zeigt hier also exakt dieselbe Gesamtzahl wie im Auslastungs-Tab,
|
||||
// nur aufgeteilt in den Anteil DIESES Projekts und den Anteil der übrigen
|
||||
// Projekte der Sitzung (sonst wäre eine Überbuchung hier unsichtbar:
|
||||
// jemand kann in diesem Projekt entspannt aussehen und trotzdem in vier
|
||||
// weiteren ausgebucht sein — dieselbe Überlegung wie zuvor, nur jetzt in
|
||||
// Stunden statt Tagen).
|
||||
// NACH DERSELBEN RECHNUNG wie der Auslastungs-Tab und die Wandtafel:
|
||||
// Stunden dieser Woche aus Aufgaben mit Aufwand, gleich geteilt auf die
|
||||
// Zuständigen (aufgabenVerlaufJePerson(), js/load/load.js), bezogen auf die
|
||||
// Kapazität der Person. Für JEDES Projekt der Sitzung, aufgeteilt in den
|
||||
// Anteil DIESES Projekts und den der übrigen — sonst sähe jemand hier
|
||||
// entspannt aus, der in vier anderen Projekten ausgebucht ist.
|
||||
//
|
||||
// Gemeinsam genutzt von renderResourceLoad() (Detailkarten im
|
||||
// Terminplan-Bereich) und renderPlanKpis() (Team-Auslastung-Kachel), damit
|
||||
// beide dieselbe Zahl zeigen. Rückgabe: { loads } — loads ist ein Objekt
|
||||
// Person-ID -> { person, roles, kapazitaet, stundenProjekt, stundenAndere,
|
||||
// stunden (Summe), quote (stunden/kapazitaet), andereProjekte, todos
|
||||
// (Titel der eigenen TODOs in DIESEM Projekt, rein informativ — zählt
|
||||
// nicht mehr in die Stundenrechnung mit ein).
|
||||
// (Titel der eigenen Aufgaben in DIESEM Projekt, ohne Phasenbalken),
|
||||
// offenProjekt (offene Aufgabenstunden dieses Projekts, alle Wochen).
|
||||
function computeResourceLoads(){
|
||||
const plan = app.ensurePlanLocal();
|
||||
const todos = Array.isArray(plan.todos) ? plan.todos : [];
|
||||
@@ -353,25 +442,42 @@
|
||||
if(!loads[a.person.id].roles.includes(a.role)) loads[a.person.id].roles.push(a.role);
|
||||
});
|
||||
|
||||
// Zuständige einer Aufgabe zählen mit, auch wenn sie unter „Beteiligte“
|
||||
// fehlen — wie auf der Auslastungsseite. Sonst fielen ihre
|
||||
// Aufgabenstunden hier stillschweigend weg.
|
||||
const personen = (app.week && app.week.people) || [];
|
||||
todos.forEach(t => {
|
||||
if(!Array.isArray(t.assignees)) return;
|
||||
if(!Array.isArray(t.assignees) || balkenPhase(t)) return;
|
||||
t.assignees.forEach(pid => {
|
||||
if(loads[pid]) loads[pid].todos.push(t.title);
|
||||
if(!loads[pid]){
|
||||
const person = personen.find(x => x.id === pid);
|
||||
if(!person) return;
|
||||
loads[pid] = {
|
||||
person, roles: ['nur Aufgabe'], todos: [],
|
||||
kapazitaet: kapazitaetsFaktor(person) * vollzeit,
|
||||
stundenProjekt: 0, stundenAndere: 0, andereProjekte: [],
|
||||
};
|
||||
}
|
||||
loads[pid].todos.push(t.title);
|
||||
});
|
||||
});
|
||||
|
||||
// Offene Aufgabenstunden dieses Projekts insgesamt (nicht nur diese
|
||||
// Woche) — liegt eine Aufgabe erst später, zeigte die Wochenzahl sonst 0.
|
||||
Object.values(loads).forEach(l => { l.offenProjekt = 0; });
|
||||
offeneAufwandsAufgaben(app.project).forEach(({ t, rest }) => {
|
||||
t.assignees.forEach(pid => { if(loads[pid]) loads[pid].offenProjekt += rest / t.assignees.length; });
|
||||
});
|
||||
|
||||
// Stunden dieser Woche allein aus Aufgaben mit Aufwand — dieselbe
|
||||
// Rechnung wie auf der Auslastungsseite (aufgabenVerlaufJePerson(), load.js).
|
||||
((app.week && app.week.projects) || []).forEach(pr => {
|
||||
const aktivePhase = lpActiveList(pr)[0];
|
||||
Object.keys(pr.assign || {}).forEach(aKey => {
|
||||
const pid = aKey.slice(0, aKey.indexOf('|'));
|
||||
if(!loads[pid]) return; // nur Personen, die auch in DIESEM Projekt beteiligt sind
|
||||
const rolle = aKey.slice(aKey.indexOf('|') + 1);
|
||||
const al = berechneAuslastung(null, pr, aktivePhase, rolle, pr.assign[aKey], pr.assign);
|
||||
if(al.stunden <= 0) return;
|
||||
if(pr.id === app.project.id){
|
||||
loads[pid].stundenProjekt += al.stunden;
|
||||
} else {
|
||||
loads[pid].stundenAndere += al.stunden;
|
||||
const av = typeof aufgabenVerlaufJePerson === 'function' ? aufgabenVerlaufJePerson(pr) : {};
|
||||
Object.keys(av).forEach(pid => {
|
||||
if(!loads[pid] || !(av[pid][0] > 0)) return;
|
||||
if(pr.id === app.project.id) loads[pid].stundenProjekt += av[pid][0];
|
||||
else {
|
||||
loads[pid].stundenAndere += av[pid][0];
|
||||
const label = pr.nr ? (pr.nr + ' ' + (pr.name || '')) : (pr.name || 'Projekt');
|
||||
if(!loads[pid].andereProjekte.includes(label)) loads[pid].andereProjekte.push(label);
|
||||
}
|
||||
@@ -456,10 +562,15 @@
|
||||
</div>`
|
||||
: `<div class="plan-resource-other muted">keine weiteren Projekte</div>`;
|
||||
|
||||
const offenText = l.offenProjekt > 0
|
||||
? `<div class="plan-resource-other muted">${stundenFormat(l.offenProjekt)} h offen in Aufgaben dieses Projekts</div>`
|
||||
: `<div class="plan-resource-other muted">keine Aufgaben mit Aufwand</div>`;
|
||||
|
||||
html += `<div class="load-card plan-resource-card plan-resource-clickable" onclick="openAbsenceModal('${esc(l.person.id)}')" title="Klicken, um Urlaube/Abwesenheiten zu bearbeiten">
|
||||
<h3>${esc(l.person.name)} <span style="font-size:10px;color:#888;font-weight:normal">(${esc(l.roles.join(' / '))})</span></h3>
|
||||
<div class="load-num" style="font-size:18px;color:${ampel}">${stundenFormat(l.stunden)}<span style="font-size:11px;font-weight:400">h</span></div>
|
||||
<div class="load-num" style="font-size:18px;color:${ampel}">${stundenFormat(l.stunden)}<span style="font-size:11px;font-weight:400">h diese Woche</span></div>
|
||||
<div class="bar" style="height:6px;margin-top:4px"><div style="width:${breite}%;background:${ampel}"></div></div>
|
||||
${offenText}
|
||||
${otherBadge}
|
||||
<div class="load-detail" style="font-size:10px;margin-top:4px">${todoListHtml}</div>
|
||||
${absBadge}
|
||||
|
||||
Reference in new issue
Block a user