code auf einzelne Datein aufgeteilt
This commit is contained in:
1 parent
7451e0bf78
commit
8c94fc91f6
30 files changed
+5765
-27
No files matched your search
@@ -0,0 +1,352 @@
|
||||
// -----------------------------
|
||||
// Projektübersicht — Auslastung (Load) & Aufwand je Leistungsphase
|
||||
// -----------------------------
|
||||
// Modul für den Auslastungs-Tab der Hauptseite (Test.html, Ladereihenfolge:
|
||||
// globals → utils → ui → date-picker → csv → server → store → weeks → board →
|
||||
// people → projects → load → gantt → …). Es stellt renderLoad(), das LP-
|
||||
// Aufwand-Modal und das Neues-Projekt-Modal bereit. Genutzt werden LPHS/ROLES
|
||||
// aus js/globals.js, currentWeek()/save() aus js/store.js, guardWeekLocked()
|
||||
// aus js/weeks.js sowie peopleByRole()/assignKey() aus js/board.js/js/utils.js.
|
||||
// Die Auslastungs-Logik (berechneAuslastung, lpActiveList, faktorFormat,
|
||||
// pdfLoadAmpelColor, LOAD_BAR_MAX) wird zusätzlich von js/pdf.js für den
|
||||
// PDF-Export wiederverwendet.
|
||||
//
|
||||
// Datenmodell je Projekt (rollen- und leistungsphasenspezifisch):
|
||||
// pr.aufwandProLP = {
|
||||
// 'LPH 1': { Planung: 0.4, Projektleitung: 0.6, Bauüberwachung: 0.0 },
|
||||
// ...
|
||||
// } // Gewichtungsfaktor je Rolle (0.0–1.0)
|
||||
// pr.aktuelleLP = ['LPH 6'] // aktuell amtierende Phase (max. 1)
|
||||
//
|
||||
// Es zählt genau eine amtierende Phase je Projekt ("Nur amtierende Einzelphase",
|
||||
// Absprache mit dem Auftraggeber). Altdaten mit mehreren aktiven Phasen
|
||||
// verwenden deterministisch die erste aktive Phase der Liste.
|
||||
//
|
||||
// Berechnungslogik:
|
||||
// Auslastung(Mitarbeiter) = Σ Faktor der amtierenden LP je Projekt,
|
||||
// in dem der Mitarbeiter (in seiner Rolle) zugewiesen ist.
|
||||
// Der Faktor zählt 1:1 – die Zuordnungsart (Haupt/Optional) skaliert nicht.
|
||||
// Leere Matrix-Felder zählen bei gesetzter aktiver LP mit dem Standard-Faktor
|
||||
// 1,0; ohne gesetzte aktive LP zählt das Projekt 0 (nicht eingerechnet).
|
||||
// "In Anbahnung" wird nicht eingerechnet.
|
||||
//
|
||||
// HINWEIS (Fachplaner-Fragen, bewusst NICHT eigenständig entschieden):
|
||||
// - Ampel-Schwellen (50 % / 75 % des Maximalwerts) sind PLATZHALTER und müssen
|
||||
// fachlich abgestimmt werden.
|
||||
// - Mitarbeiterkapazität (Teilzeit/Vollzeit) existiert nicht – offener Punkt.
|
||||
|
||||
// AUFWAND_LPS: Phasen des Aufwandsmodells (LPH 1–8; "W+M" ist bewusst nicht
|
||||
// enthalten, entspricht der Entscheidung "LP-Schema: lp1-8").
|
||||
const AUFWAND_LPS = LPHS.slice(0, 8);
|
||||
|
||||
// LP_AUFWAND_VORLAGE: Standardwerte der Maske "Aufwand je Leistungsphase".
|
||||
// Vorbefüllung beim Anlegen neuer Projekte sowie beim Öffnen der Maske für
|
||||
// bestehende Projekte, die in einzelnen Feldern keine Werte haben – beim
|
||||
// Speichern werden sie übernommen. Werte 0.0–1.0 je Rolle; 0 = kein Aufwand.
|
||||
// Nicht gelistete Phasen/Rollen bleiben leer und zählen als Standard-Faktor 1,0.
|
||||
// Hier im Code anpassen.
|
||||
const LP_AUFWAND_VORLAGE = {
|
||||
'LPH 1': { Planung: 0.4, Projektleitung: 0.2, Bauüberwachung: 0 },
|
||||
'LPH 2': { Planung: 0.4, Projektleitung: 0.2, Bauüberwachung: 0 },
|
||||
'LPH 3': { Planung: 1.0, Projektleitung: 0.3, Bauüberwachung: 0 },
|
||||
'LPH 4': { Planung: 0.0, Projektleitung: 0.3, Bauüberwachung: 0 },
|
||||
'LPH 5': { Planung: 1.0, Projektleitung: 0.4, Bauüberwachung: 0 },
|
||||
'LPH 6': { Planung: 0.2, Projektleitung: 0.7, Bauüberwachung: 0.3 },
|
||||
'LPH 7': { Planung: 0.1, Projektleitung: 0.3, Bauüberwachung: 0.5 },
|
||||
'LPH 8': { Planung: 0, Projektleitung: 0.2, Bauüberwachung: 1.0 }
|
||||
};
|
||||
|
||||
// PLATZHALTER! Ampel-Schwellen als Anteil des Maximalwerts LOAD_BAR_MAX
|
||||
// (gelb ab 50 %, rot ab 75 %). Müssen vom Fachplaner bestätigt werden.
|
||||
const THRESHOLD_RATIO_CRITICAL = 0.75;
|
||||
const THRESHOLD_RATIO_ELEVATED = 0.50;
|
||||
|
||||
// lpActiveList: liefert die amtierenden Leistungsphasen eines Projekts,
|
||||
// gefiltert auf das Aufwandsmodell (LPH 1–8). Parameter: pr = Projektobjekt.
|
||||
// Rückgabe: Array von LP-Namen (leer, wenn keine aktive Phase passt).
|
||||
function lpActiveList(pr){
|
||||
const a = pr && pr.aktuelleLP;
|
||||
if(Array.isArray(a)) return a.filter(l => AUFWAND_LPS.includes(l));
|
||||
if(typeof a === 'string' && AUFWAND_LPS.includes(a)) return [a];
|
||||
return [];
|
||||
}
|
||||
|
||||
// aufwandMatrixWert: Rollenwert eines Projekts für eine Leistungsphase
|
||||
// (Gewichtungsfaktor, 0,0–1,0). Liefert null, wenn keine (rollenspezifische) Matrix
|
||||
// bzw. kein Wert für diese Phase/Rolle hinterlegt ist. 0 ist ein gültiger Wert.
|
||||
function aufwandMatrixWert(projekt, lp, rolle){
|
||||
const m = projekt && projekt.aufwandProLP;
|
||||
const entry = (m && typeof m === 'object') ? m[lp] : undefined;
|
||||
if(!entry || typeof entry !== 'object' || Array.isArray(entry)) return null;
|
||||
const v = parseFloat(entry[rolle]);
|
||||
return isNaN(v) ? null : v;
|
||||
}
|
||||
|
||||
// berechneAuslastung: zentrale Auslastungsberechnung einer Zuordnung
|
||||
// (Person/Rolle) zu einem Projekt für die amtierende Leistungsphase.
|
||||
// Ohne gesetzte aktive LP zählt das Projekt 0 (nicht eingerechnet).
|
||||
// Mit aktiver LP: Liegt aufwandProLP[aktuelleLP][rolle] vor, zählt dieser
|
||||
// Faktor 1:1 – die Zuordnungsart (Haupt/Optional) skaliert bewusst nicht mehr.
|
||||
// Fehlt der Matrix-Wert, gilt der Standard-Faktor 1,0.
|
||||
// "In Anbahnung" wird nicht eingerechnet.
|
||||
function berechneAuslastung(mitarbeiter, projekt, aktuelleLP, rolle, status){
|
||||
if(!projekt || projekt.group === 'In Anbahnung') return { load: 0, anbahnung: true };
|
||||
if(!aktuelleLP) return { load: 0 };
|
||||
const wert = aufwandMatrixWert(projekt, aktuelleLP, rolle);
|
||||
if(wert !== null) return { load: wert };
|
||||
return { load: 1.0 };
|
||||
}
|
||||
|
||||
// faktorFormat: formatiert einen Auslastungs-Faktor in deutscher Notation
|
||||
// (Komma, max. 2 Nachkommastellen), z. B. 1.1 → "1,10". 1,0 = 100 % Kapazität.
|
||||
function faktorFormat(x){
|
||||
const n = Math.round((x || 0) * 100) / 100;
|
||||
return n.toFixed(2).replace('.', ',');
|
||||
}
|
||||
|
||||
// loadAmpelColor: Ampel-Farbe relativ zum Maximalwert (gelb ab 50 %,
|
||||
// rot ab 75 % von LOAD_BAR_MAX; PLATZHALTER).
|
||||
function loadAmpelColor(x){
|
||||
return x >= LOAD_BAR_MAX * THRESHOLD_RATIO_CRITICAL ? '#c53030' : x >= LOAD_BAR_MAX * THRESHOLD_RATIO_ELEVATED ? '#d69e2e' : '#2f855a';
|
||||
}
|
||||
// pdfLoadAmpelColor: Ampel-Farbe für den PDF-Export (js/pdf.js) mit gleichen
|
||||
// Schwellen wie loadAmpelColor, aber dunkleren, druckfreundlichen Farbtönen.
|
||||
function pdfLoadAmpelColor(x){
|
||||
return x >= LOAD_BAR_MAX * THRESHOLD_RATIO_CRITICAL ? '#D70015' : x >= LOAD_BAR_MAX * THRESHOLD_RATIO_ELEVATED ? '#B25000' : '#248A3D';
|
||||
}
|
||||
|
||||
// ---------- LP-Aufwand Eingabemaske ----------
|
||||
|
||||
// lpModalProjectId: Projekt-ID des aktuell offenen LP-Aufwand-Modals (null = geschlossen).
|
||||
let lpModalProjectId = null;
|
||||
|
||||
// lpRowsHtml: erzeugt die Aufwand-Maske als Matrix-Tabelle.
|
||||
// Zeilen = Leistungsphasen (LPH 1–8), Spalten = Rollen (Planung/Projektleitung/
|
||||
// Bauüberwachung). Die amtierende Leistungsphase wird pro Projekt im Tab
|
||||
// "Projekte" gewählt (renderProjects / editProjectActiveLP).
|
||||
// prefix: 'lp' = Bearbeiten-Modal, 'np' = Neues-Projekt-Modal (eindeutige IDs).
|
||||
// Leere Felder werden mit den Standardwerten aus LP_AUFWAND_VORLAGE vorbefüllt.
|
||||
// Die Feld-IDs bleiben identisch, damit collectLpRows() unverändert funkioniert.
|
||||
function lpRowsHtml(p, prefix){
|
||||
const head = ROLES.map(r => `<th>${esc(r)}</th>`).join('');
|
||||
const rows = AUFWAND_LPS.map(l => {
|
||||
const idx = l.replace(/[^\d]/g,'');
|
||||
const row = (p.aufwandProLP && typeof p.aufwandProLP[l] === 'object') ? p.aufwandProLP[l] : {};
|
||||
const cells = ROLES.map((r, ri) => {
|
||||
const raw = row[r];
|
||||
const vorlage = LP_AUFWAND_VORLAGE && LP_AUFWAND_VORLAGE[l] ? LP_AUFWAND_VORLAGE[l][r] : undefined;
|
||||
const base = (raw !== undefined && raw !== null && raw !== '') ? raw
|
||||
: (vorlage !== undefined && vorlage !== null && vorlage !== '') ? vorlage : '';
|
||||
const val = base !== '' ? String(parseFloat(base)).replace('.', ',') : '';
|
||||
return `<td><input type="text" inputmode="decimal" id="${prefix}Val${idx}R${ri}" value="${val}" placeholder="1,0"></td>`;
|
||||
}).join('');
|
||||
return `<tr><th class="lp-lph">${l}</th>${cells}</tr>`;
|
||||
}).join('');
|
||||
return `<table class="lp-matrix">
|
||||
<thead><tr><th class="lp-lph">Leistungsphase</th>${head}</tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>`;
|
||||
}
|
||||
|
||||
// collectLpRows: liest die Aufwand-Maske aus und validiert die Werte (0–1).
|
||||
// Rückgabe: { aufwand, hasValue } oder null bei Eingabefehler.
|
||||
function collectLpRows(prefix){
|
||||
const aufwand = {};
|
||||
let hasValue = false;
|
||||
for(const l of AUFWAND_LPS){
|
||||
const idx = l.replace(/[^\d]/g,'');
|
||||
const entry = {};
|
||||
let rowHasValue = false;
|
||||
let invalid = false;
|
||||
for(let ri = 0; ri < ROLES.length; ri++){
|
||||
const r = ROLES[ri];
|
||||
const input = document.getElementById(prefix + 'Val' + idx + 'R' + ri);
|
||||
if(!input) continue;
|
||||
const raw = input.value.trim().replace(',', '.');
|
||||
if(raw === '') continue;
|
||||
const v = parseFloat(raw);
|
||||
if(isNaN(v) || v < 0 || v > 1){
|
||||
alert('Bitte Werte zwischen 0 und 1 eintragen (z. B. 0,4) – oder das Feld leer lassen. (' + l + ', ' + r + ')');
|
||||
invalid = true;
|
||||
break;
|
||||
}
|
||||
entry[r] = v;
|
||||
rowHasValue = true;
|
||||
}
|
||||
if(invalid) return null;
|
||||
if(rowHasValue){ aufwand[l] = entry; hasValue = true; }
|
||||
}
|
||||
return { aufwand, hasValue };
|
||||
}
|
||||
|
||||
// applyLpRows: übernimmt das Ergebnis von collectLpRows auf ein Projekt.
|
||||
// Die Maske ist mit den Standardwerten (LP_AUFWAND_VORLAGE) vorbefüllt;
|
||||
// Felder ohne Vorbefüllung, die leer bleiben, zählen bei gesetzter aktiver LP
|
||||
// später als Standard-Faktor 1,0; ohne aktive LP zählt das Projekt 0.
|
||||
// Die amtierende Leistungsphase wird separat über den Tab "Projekte" gesetzt.
|
||||
function applyLpRows(p, lp){
|
||||
if(!lp) return false;
|
||||
p.aufwandProLP = lp.hasValue ? lp.aufwand : undefined;
|
||||
return true;
|
||||
}
|
||||
|
||||
// openLpModal: öffnet das Modal "Aufwand je Leistungsphase" für ein Projekt.
|
||||
// Parameter: pid = Projekt-ID; gesperrte Wochen werden abgewiesen. Kein Rückgabewert.
|
||||
function openLpModal(pid){
|
||||
if(guardWeekLocked()) return;
|
||||
const p = currentWeek().projects.find(x=>x.id===pid);
|
||||
if(!p) return;
|
||||
lpModalProjectId = pid;
|
||||
const titleEl = document.getElementById('lpModalProject');
|
||||
if(titleEl) titleEl.textContent = (p.nr ? p.nr + ' – ' : '') + (p.name || '');
|
||||
const rowsEl = document.getElementById('lpModalRows');
|
||||
if(rowsEl) rowsEl.innerHTML = lpRowsHtml(p, 'lp');
|
||||
document.getElementById('lpModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
// closeLpModal: blendet das LP-Aufwand-Modal aus und setzt lpModalProjectId zurück.
|
||||
function closeLpModal(){
|
||||
document.getElementById('lpModal').style.display = 'none';
|
||||
lpModalProjectId = null;
|
||||
}
|
||||
|
||||
// saveLpModal: liest die LP-Maske aus, übernimmt sie auf das Projekt, speichert
|
||||
// und rendert Board, Projekte und Auslastung neu; danach schließt sich das Modal.
|
||||
function saveLpModal(){
|
||||
if(guardWeekLocked()) return;
|
||||
const p = currentWeek().projects.find(x=>x.id===lpModalProjectId);
|
||||
if(!p) return;
|
||||
const lp = collectLpRows('lp');
|
||||
if(lp === null) return;
|
||||
if(applyLpRows(p, lp)) save();
|
||||
renderBoard(); renderProjects(); renderLoad();
|
||||
closeLpModal();
|
||||
}
|
||||
|
||||
// ---------- Neues Projekt (mit LP-Aufwand beim Anlegen) ----------
|
||||
|
||||
// openNewProjModal: öffnet das Modal "Neues Projekt" mit geleerten Eingaben,
|
||||
// der Auswahl der aktiven LP und einer leeren Aufwand-Maske (prefix 'np').
|
||||
function openNewProjModal(){
|
||||
if(guardWeekLocked()) return;
|
||||
document.getElementById('newProjNr').value = '';
|
||||
document.getElementById('newProjName').value = '';
|
||||
document.getElementById('newProjGroup').value = 'Aktive Projekte';
|
||||
const lpSel = document.getElementById('newProjActiveLP');
|
||||
if(lpSel){
|
||||
lpSel.innerHTML = '<option value="">Keine</option>' + AUFWAND_LPS.map(l => `<option value="${l}">${l}</option>`).join('');
|
||||
lpSel.value = '';
|
||||
}
|
||||
const rowsEl = document.getElementById('newProjLpRows');
|
||||
if(rowsEl) rowsEl.innerHTML = lpRowsHtml({ aufwandProLP: {}, aktuelleLP: [] }, 'np');
|
||||
document.getElementById('newProjModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
// closeNewProjModal: blendet das Neues-Projekt-Modal aus.
|
||||
function closeNewProjModal(){
|
||||
document.getElementById('newProjModal').style.display = 'none';
|
||||
}
|
||||
|
||||
// saveNewProj: legt ein neues Projekt an (Nr., Name, Gruppe, aktive LP,
|
||||
// optionaler LP-Aufwand), berechnet seine Reihenfolge in der Gruppe, speichert
|
||||
// und rendert Projekte und Board neu. Kein Rückgabewert.
|
||||
function saveNewProj(){
|
||||
if(guardWeekLocked()) return;
|
||||
const nr = document.getElementById('newProjNr').value.trim();
|
||||
const name = document.getElementById('newProjName').value.trim();
|
||||
if(!name){ alert("Bitte Projektbezeichnung eingeben."); return; }
|
||||
const group = document.getElementById('newProjGroup').value;
|
||||
const activeLP = document.getElementById('newProjActiveLP').value;
|
||||
const lp = collectLpRows('np');
|
||||
if(lp === null) return;
|
||||
const w = currentWeek();
|
||||
const nextOrder = w.projects
|
||||
.filter(p=>p.group===group)
|
||||
.reduce((max,p)=> Math.max(max, p.order!==undefined ? p.order : 0), -1) + 1;
|
||||
const pr = {id:uid(),nr,name,group,status:'',lph:{},assign:{}, statusState: undefined, order: nextOrder, plan: null};
|
||||
if(activeLP) pr.aktuelleLP = [activeLP];
|
||||
applyLpRows(pr, lp);
|
||||
w.projects.push(pr);
|
||||
save(); renderProjects(); renderBoard();
|
||||
closeNewProjModal();
|
||||
}
|
||||
|
||||
// LOAD_BAR_MAX: Maximalwert der Balken-Skala im Auslastungs-Tab (und im
|
||||
// PDF-Export). 1,0 = 100 % Kapazität; Werte > 1,0 = Überlastung (rot).
|
||||
const LOAD_BAR_MAX = 8;
|
||||
|
||||
// renderLoad: berechnet die Belastung der Personen auf Basis des
|
||||
// rollenspezifischen LP-Gewichtungsfaktors je amtierender Phase (0,0–1,0,
|
||||
// Standard 1,0). Leere Matrix-Werte zählen bei gesetzter aktiver LP als 1,0;
|
||||
// ohne gesetzte aktive LP zählt das Projekt 0 (nicht eingerechnet).
|
||||
// Ampel: Schwellen relativ zum Maximalwert LOAD_BAR_MAX
|
||||
// (gelb ab 50 %, rot ab 75 % – PLATZHALTER).
|
||||
// Balken: Skala auf das feste Maximum LOAD_BAR_MAX (Standard 8).
|
||||
function renderLoad(){
|
||||
const scope = document.getElementById('loadScope').value;
|
||||
const barMax = LOAD_BAR_MAX;
|
||||
const selectedWeek = currentWeek();
|
||||
const weeks = scope==='all' ? db.weeks : (selectedWeek ? [selectedWeek] : []);
|
||||
if(weeks.length === 0){
|
||||
document.getElementById('loadGrid').innerHTML = '<div class="hint">Keine Sitzung ausgewählt oder die Daten werden noch geladen.</div>';
|
||||
return;
|
||||
}
|
||||
const grouped = scope==='all' ? peopleByRole(db.weeks) : peopleByRole(selectedWeek);
|
||||
const stats = {};
|
||||
ROLES.forEach(r=>{
|
||||
grouped[r].forEach(entry=>{
|
||||
stats[assignKey(entry.person.id, r)] = {load:0,projects:{},anbahnung:0};
|
||||
});
|
||||
});
|
||||
|
||||
weeks.forEach(w=>{
|
||||
w.projects.forEach(pr=>{
|
||||
const aktivePhase = lpActiveList(pr)[0];
|
||||
Object.keys(pr.assign).forEach(aKey=>{
|
||||
const s = stats[aKey];
|
||||
if(!s) return;
|
||||
const st = pr.assign[aKey];
|
||||
const rolle = aKey.slice(aKey.indexOf('|') + 1);
|
||||
const al = berechneAuslastung(null, pr, aktivePhase, rolle, st);
|
||||
if(al.anbahnung){
|
||||
if(st===1 || st===2) s.anbahnung++;
|
||||
return;
|
||||
}
|
||||
if(al.load > 0){
|
||||
s.load += al.load;
|
||||
s.projects[pr.name] = (s.projects[pr.name]||0)+1;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Hochgerechnete Auslastung: durchschnittliche Wochenlast als Faktor
|
||||
// der Kapazität (1,0 = 100 %). Werte > 1,0 = Überlastung.
|
||||
const nWeeks = Math.max(1, weeks.length);
|
||||
const weekAvg = k => stats[k].load / nWeeks;
|
||||
let html="";
|
||||
ROLES.forEach(r=>{
|
||||
html += `<div class="load-row"><div class="load-row-title">${r}</div>`;
|
||||
if(grouped[r].length===0){
|
||||
html += `<div class="hint">Keine Personen.</div>`;
|
||||
}
|
||||
grouped[r].forEach(entry=>{
|
||||
const s = stats[assignKey(entry.person.id, r)];
|
||||
const avg = weekAvg(assignKey(entry.person.id, r));
|
||||
// Balken relativ zur einstellbaren Skala (Standard 8); Überlastung > 1,0
|
||||
// bleibt über die Ampel-Farbe sichtbar, der Zahlenwert bleibt ungedeckelt.
|
||||
const pct = Math.min(100, Math.round(avg / barMax * 100));
|
||||
const detail = Object.keys(s.projects).map(k=>`<div><span>${esc(k)}</span><b>${s.projects[k]}×</b></div>`).join('') || '<div class="hint">keine Zuordnung</div>';
|
||||
const anbahnungBadge = (s.anbahnung && s.anbahnung>0) ? `<span style="font-size:11px;color:#3182ce;font-weight:normal;margin-left:0px">(${s.anbahnung} in Anbahnung)</span>` : '';
|
||||
html += `<div class="load-card">
|
||||
<h3>${esc(entry.person.fullName)} <span style="font-size:11px;color:#888;font-weight:normal">(${r})</span></h3>
|
||||
<h3>${anbahnungBadge}</h3>
|
||||
<div class="load-num">${faktorFormat(avg)}</div>
|
||||
<div class="bar" style="height:10px;margin-top:6px" title="Skala: 0 – ${faktorFormat(barMax)}"><div style="width:${pct}%;background:${loadAmpelColor(avg)}"></div></div>
|
||||
<div class="load-detail">${detail}</div>
|
||||
</div>`;
|
||||
});
|
||||
html += `</div>`;
|
||||
});
|
||||
document.getElementById('loadGrid').innerHTML=html;
|
||||
}
|
||||
Reference in new issue
Block a user