Plan Checker, Wochenbericht-Fokus-Modus und Wandtafel-Dashboard-Feinschliff
Plan Checker (neues Studenten-Werkzeug, Seitenleiste + Admin → Plan-Checker- Merkmale): lädt einen Bauplan als PDF hoch und lässt ChatGPT oder Claude ihn gegen eine gepflegte Merkmalsliste prüfen. PDF-Rendering (pdf.js + @napi-rs/canvas) berücksichtigt jetzt Ebenen (Optional Content Groups) und WASM-Decoder korrekt, sendet eine Übersicht plus mehrere hochaufgelöste Kachel-Ausschnitte statt eines einzelnen, zu groben Bilds, und beide Anbieter bekommen dasselbe Bildmaterial. Jedes Prüfergebnis kann auf eines der gesendeten Bilder als Beleg verweisen (bildIndex) — als Screenshot im Ergebnis anklickbar, öffnet vergrößert in einem Modal statt eines von Chrome blockierten target=_blank-Links auf eine data:-URL. KI-Modellname ist jetzt pro Konto überschreibbar (Einstellungen → Mein Konto) für Drittanbieter mit abweichendem Modell-Katalog. Wochenbericht: dichte Kartenansicht bekommt einen Fokus-Modus — nur der Kommentar liegt offen, Leistungsphasen und Beteiligte stecken in aufklappbaren Abschnitten statt dauerhaft nebeneinander zu stehen. Projektbereich: Terminplan-Auslastung rechnet jetzt exakt wie der Auslastungs-Tab/die Wandtafel (statt einer unabhängigen, tageweisen Schätzung); die LPH-Fortschrittsliste im Dashboard zeigt nur noch die beauftragten Leistungsphasen. Wandtafel: Dashboard-Kopf mit Kennzahlen-Reihe und Panel-Reihe neu strukturiert, in den Blautönen des Firmenlogos; dieselbe Palette wurde anschließend auf den gesamten App-weiten Dark Mode übernommen. Dateibereinigung: nicht mehr benötigte Test-Uploads/Backups entfernt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
db63a29365
commit
6ba93b2a92
308 files changed
+64441
-743
No files matched your search
@@ -9,9 +9,12 @@
|
||||
// (computeResourceLoads/renderResourceLoad/openResourceModal, Cockpit-Layout:
|
||||
// Detailkarten im Auslastungs-Modal, Vorschau in der Kompakt-Karte darunter)
|
||||
// und die KPI-Kachelzeile über dem GANTT (renderPlanKpis — Gesamtfortschritt,
|
||||
// offene Aufgaben, nächster Meilenstein, Team-Auslastung, alle aus denselben
|
||||
// Plan-Daten abgeleitet). renderPlanGantt ist die zentrale Neu-Render-
|
||||
// Funktion (rendert am Ende auch Auslastung und KPIs neu) und wird nach
|
||||
// offene Aufgaben und nächster Meilenstein aus denselben Plan-Daten;
|
||||
// Team-Auslastung dagegen über berechneAuslastung() 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
|
||||
// neu) und wird nach
|
||||
// Änderungen von plan-modals.js, plan-drag.js und plan-schedule.js erneut
|
||||
// aufgerufen.
|
||||
//
|
||||
@@ -171,15 +174,15 @@
|
||||
milestoneTile = kpiTile('Nächster Meilenstein', '–', milestones.length > 0 ? 'Keine anstehenden Meilensteine' : 'Keine Meilensteine angelegt');
|
||||
}
|
||||
|
||||
const { loads, maxDays } = computeResourceLoads();
|
||||
const { loads } = computeResourceLoads();
|
||||
const loadValues = Object.values(loads);
|
||||
let loadTile;
|
||||
if(loadValues.length === 0){
|
||||
loadTile = kpiTile('Team-Auslastung', '–', 'Keine Beteiligten zugeordnet');
|
||||
} else {
|
||||
const avgPct = Math.round(loadValues.reduce((s, l) => s + (l.days / maxDays * 100), 0) / loadValues.length);
|
||||
const top = loadValues.slice().sort((a, b) => b.days - a.days)[0];
|
||||
loadTile = kpiTile('Team-Auslastung', avgPct + '%', top.days > 0 ? 'Höchste Auslastung: ' + top.person.name : 'Noch keine Aufgaben zugewiesen');
|
||||
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');
|
||||
}
|
||||
|
||||
row.innerHTML = progressTile + openTile + milestoneTile + loadTile;
|
||||
@@ -304,66 +307,83 @@
|
||||
|
||||
// ---------- Ressourcen-Auslastung ----------
|
||||
|
||||
// computeResourceLoads: berechnet die Auslastung der Projektbeteiligten in
|
||||
// Werktagen (ohne Wochenenden) anhand der ihnen zugeordneten TODOs.
|
||||
// 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).
|
||||
//
|
||||
// Gemeinsam genutzt von renderResourceLoad() (Detailkarten im
|
||||
// Auslastungs-Modal) und renderPlanKpis() (Team-Auslastung-Kachel), damit
|
||||
// beide dieselbe Zahl zeigen.
|
||||
//
|
||||
// Neben der Auslastung IN DIESEM Projekt (days/todos) wird zusätzlich
|
||||
// erfasst, wie stark dieselbe Person in den ÜBRIGEN Projekten derselben
|
||||
// Sitzung gebunden ist (otherDays/otherProjects). Ohne diesen Blick über den
|
||||
// Projektrand ist eine Überbuchung strukturell unsichtbar: jemand kann hier
|
||||
// entspannt aussehen und trotzdem in vier weiteren Projekten verplant sein.
|
||||
// Die Daten dafür sind bereits vorhanden — app.week.projects enthält alle
|
||||
// Projekte der Sitzung (GET /load-all lädt sie ohnehin komplett).
|
||||
//
|
||||
// Rückgabe: { loads, maxDays } — loads ist ein Objekt Person-ID ->
|
||||
// { person, roles, days, todos, otherDays, otherProjects }, maxDays das
|
||||
// Maximum über alle Personen (mindestens 1, für Prozent-Berechnungen; bezieht
|
||||
// sich bewusst nur auf die projekteigenen Tage, damit die Balken im Modal
|
||||
// untereinander vergleichbar bleiben).
|
||||
// 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).
|
||||
function computeResourceLoads(){
|
||||
const plan = app.ensurePlanLocal();
|
||||
const todos = Array.isArray(plan.todos) ? plan.todos.filter(t => t.start && t.end) : [];
|
||||
const todos = Array.isArray(plan.todos) ? plan.todos : [];
|
||||
const assignees = projectAssignees(app.project, app.week);
|
||||
const vollzeit = Number(APP_SETTINGS.vollzeitStundenProWoche) || 40;
|
||||
|
||||
const loads = {};
|
||||
assignees.forEach(a => {
|
||||
if(!loads[a.person.id]){
|
||||
loads[a.person.id] = { person: a.person, roles: [], days: 0, todos: [], otherDays: 0, otherProjects: [] };
|
||||
loads[a.person.id] = {
|
||||
person: a.person, roles: [], todos: [],
|
||||
kapazitaet: kapazitaetsFaktor(a.person) * vollzeit,
|
||||
stundenProjekt: 0, stundenAndere: 0, andereProjekte: [],
|
||||
};
|
||||
}
|
||||
if(!loads[a.person.id].roles.includes(a.role)) loads[a.person.id].roles.push(a.role);
|
||||
});
|
||||
|
||||
todos.forEach(t => {
|
||||
if(!Array.isArray(t.assignees) || t.assignees.length === 0) return;
|
||||
const days = workdaysBetween(parseDateISO(t.start), parseDateISO(t.end));
|
||||
if(!Array.isArray(t.assignees)) return;
|
||||
t.assignees.forEach(pid => {
|
||||
if(!loads[pid]) return;
|
||||
loads[pid].days += days;
|
||||
loads[pid].todos.push(t.title);
|
||||
if(loads[pid]) loads[pid].todos.push(t.title);
|
||||
});
|
||||
});
|
||||
|
||||
// Auslastung in den anderen Projekten derselben Sitzung
|
||||
((app.week && app.week.projects) || []).forEach(other => {
|
||||
if(other.id === app.project.id) return;
|
||||
const otherTodos = (other.plan && Array.isArray(other.plan.todos)) ? other.plan.todos : [];
|
||||
otherTodos.forEach(t => {
|
||||
if(!t.start || !t.end || !Array.isArray(t.assignees)) return;
|
||||
const days = workdaysBetween(parseDateISO(t.start), parseDateISO(t.end));
|
||||
t.assignees.forEach(pid => {
|
||||
if(!loads[pid]) return; // nur Personen, die auch hier beteiligt sind
|
||||
loads[pid].otherDays += days;
|
||||
const label = other.nr ? (other.nr + ' ' + (other.name || '')) : (other.name || 'Projekt');
|
||||
if(!loads[pid].otherProjects.includes(label)) loads[pid].otherProjects.push(label);
|
||||
});
|
||||
((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 label = pr.nr ? (pr.nr + ' ' + (pr.name || '')) : (pr.name || 'Projekt');
|
||||
if(!loads[pid].andereProjekte.includes(label)) loads[pid].andereProjekte.push(label);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const maxDays = Math.max(1, Object.values(loads).reduce((m, l) => Math.max(m, l.days), 0));
|
||||
return { loads, maxDays };
|
||||
Object.values(loads).forEach(l => {
|
||||
l.stunden = l.stundenProjekt + l.stundenAndere;
|
||||
l.quote = l.kapazitaet > 0 ? l.stunden / l.kapazitaet : 0;
|
||||
});
|
||||
|
||||
return { loads };
|
||||
}
|
||||
|
||||
// openResourceModal: springt in den Bereich "Terminplan", in dem die
|
||||
@@ -387,7 +407,7 @@
|
||||
const container = document.getElementById('resourceLoadContainer');
|
||||
const summaryEl = document.getElementById('planResourceSummary');
|
||||
if(!container || !app.project) return;
|
||||
const { loads, maxDays } = computeResourceLoads();
|
||||
const { loads } = computeResourceLoads();
|
||||
const loadValues = Object.values(loads);
|
||||
|
||||
if(loadValues.length === 0){
|
||||
@@ -398,11 +418,10 @@
|
||||
|
||||
if(summaryEl){
|
||||
const cells = loadValues.map(l => {
|
||||
const pct = Math.round(l.days / maxDays * 100);
|
||||
const otherHint = l.otherDays > 0 ? ` (+${l.otherDays} anderswo)` : '';
|
||||
return `<div class="plan-heat-cell" style="opacity:${Math.max(.15, pct / 100)}" title="${esc(l.person.name)}: ${l.days} Tage${otherHint}"></div>`;
|
||||
const otherHint = l.stundenAndere > 0 ? ` (+${stundenFormat(l.stundenAndere)} h anderswo)` : '';
|
||||
return `<div class="plan-heat-cell" style="opacity:${Math.max(.15, Math.min(1, l.quote))}" title="${esc(l.person.name)}: ${stundenFormat(l.stunden)} h${otherHint}"></div>`;
|
||||
}).join('');
|
||||
const alsoBooked = loadValues.filter(l => l.otherDays > 0).length;
|
||||
const alsoBooked = loadValues.filter(l => l.stundenAndere > 0).length;
|
||||
const chip = alsoBooked > 0
|
||||
? `<span class="plan-mini-chip warn">${alsoBooked} auch in anderen Projekten</span>`
|
||||
: `<span class="plan-mini-chip">nur in diesem Projekt</span>`;
|
||||
@@ -410,10 +429,11 @@
|
||||
<div class="plan-mini-chip-row">${chip}</div>`;
|
||||
}
|
||||
|
||||
let html = '<div class="section-title" style="margin-bottom:8px;font-size:13px">Ressourcen-Auslastung (Aufgabentage, ohne Wochenenden)</div>';
|
||||
let html = '<div class="section-title" style="margin-bottom:8px;font-size:13px">Ressourcen-Auslastung (Wochenstunden, wie im Auslastungs-Tab)</div>';
|
||||
html += '<div class="load-grid plan-resource-grid">';
|
||||
Object.values(loads).forEach(l => {
|
||||
const pct = Math.round(l.days / maxDays * 100);
|
||||
const ampel = loadAmpelColor(l.quote);
|
||||
const breite = Math.min(100, l.quote * 100);
|
||||
const absCount = app.countPersonAbsences(l.person.id);
|
||||
const absBadge = absCount > 0 ? `<div class="hint" style="font-size:10px;margin-top:2px">${absCount} Abwesenheit(en)</div>` : '';
|
||||
|
||||
@@ -427,19 +447,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Gegenüberstellung: Tage in diesem Projekt vs. in den übrigen Projekten
|
||||
// derselben Sitzung (siehe computeResourceLoads) — macht sichtbar, wenn
|
||||
// jemand hier entspannt aussieht, aber anderswo ausgebucht ist.
|
||||
const otherBadge = l.otherDays > 0
|
||||
? `<div class="plan-resource-other" title="${esc(l.otherProjects.join('\n'))}">
|
||||
+${l.otherDays} Tage in ${l.otherProjects.length} weiteren Projekt${l.otherProjects.length === 1 ? '' : 'en'}
|
||||
// Gegenüberstellung: Stunden aus diesem Projekt vs. aus den übrigen
|
||||
// Projekten der Sitzung (siehe computeResourceLoads) — macht sichtbar,
|
||||
// wenn jemand hier entspannt aussieht, aber anderswo ausgebucht ist.
|
||||
const otherBadge = l.stundenAndere > 0
|
||||
? `<div class="plan-resource-other" title="${esc(l.andereProjekte.join('\n'))}">
|
||||
+${stundenFormat(l.stundenAndere)} h in ${l.andereProjekte.length} weiteren Projekt${l.andereProjekte.length === 1 ? '' : 'en'}
|
||||
</div>`
|
||||
: `<div class="plan-resource-other muted">keine weiteren Projekte</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">${l.days}</div>
|
||||
<div class="bar" style="height:6px;margin-top:4px"><div style="width:${pct}%;background:${pct>75?'#c53030':pct>45?'#d69e2e':'#2f855a'}"></div></div>
|
||||
<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="bar" style="height:6px;margin-top:4px"><div style="width:${breite}%;background:${ampel}"></div></div>
|
||||
${otherBadge}
|
||||
<div class="load-detail" style="font-size:10px;margin-top:4px">${todoListHtml}</div>
|
||||
${absBadge}
|
||||
|
||||
Reference in new issue
Block a user