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:
erikandClaude Opus 5.5 committed 2026-10-29 14:16:52 +01:00
1 parent f4fb3c550b
commit b7e0bd0d92
56 files changed
+12265 -2974

No files matched your search

+311 -89
View File
@@ -51,10 +51,285 @@
// Wechsel in den Bereich (showPlanSection(), plan-sections.js) und nach
// jeder Änderung an Honorar oder beauftragten Leistungsphasen aufgerufen.
// Keine Parameter, keine Rückgabe.
// HOAI-Bezeichnungen der Leistungsphasen für die Zeilen im Abschnitt
// "Leistungsphasen" (nur Anzeige).
const LPH_BEZEICHNUNG = {
'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'
};
// Gewählter Abschnitt der Einstellungen (Allgemein/Auftrag/Phasen/Team).
let einstTeil = 'allgemein';
window.zeigeEinstTeil = function(teil){
einstTeil = teil;
document.querySelectorAll('#planSection-einstellungen .plan-einst-reiter').forEach(b => {
const an = b.dataset.teil === teil;
b.classList.toggle('is-active', an);
b.setAttribute('aria-current', an ? 'true' : 'false');
});
document.querySelectorAll('#planSection-einstellungen .plan-einst-teil').forEach(t => {
t.hidden = t.dataset.teil !== teil;
});
};
// einstTeamPersonen: alle Personen, die im Abschnitt "Beteiligte" vorkommen
// können — jede Person der Sitzung mit mindestens einer Fachrolle
// (peopleByRole()), je mit den Rollen, die sie hat. Dazu, wer dem Projekt
// zugeordnet ist, obwohl sein Konto die Rolle inzwischen nicht mehr trägt —
// sonst wäre so jemand unsichtbar und ließe sich nicht mehr entfernen.
// Rückgabe: Array {person, rollen:Set}.
function einstTeamPersonen(){
const grouped = peopleByRole(app.week);
const map = new Map();
ROLES.forEach(r => (grouped[r] || []).forEach(entry => {
if(!map.has(entry.person.id)) map.set(entry.person.id, { person: entry.person, rollen: new Set() });
map.get(entry.person.id).rollen.add(r);
}));
const p = app.project;
const leute = (app.week && app.week.people) || [];
Object.keys((p && p.assign) || {}).forEach(key => {
if(!(p.assign[key] > 0)) return;
const [id, r] = key.split('|');
if(map.has(id)) { map.get(id).rollen.add(r); return; }
const person = leute.find(x => x.id === id);
if(person) map.set(id, { person, rollen: new Set([r]) });
});
return [...map.values()];
}
const einstName = person => person.fullName || person.name || '';
const einstNachname = person => einstName(person).split(' ').slice(-1)[0];
const einstBeteiligt = (p, id) => ROLES.some(r => (p.assign[assignKey(id, r)] || 0) > 0);
function einstGewerkRang(p){
const reihenfolge = Object.keys(DEPARTMENTS);
const projGewerke = reihenfolge.filter(g => (p.gewerke || []).indexOf(g) !== -1);
return { projGewerke, rang: g => g === '' ? 99 : (projGewerke.indexOf(g) !== -1 ? projGewerke.indexOf(g) : 10 + reihenfolge.indexOf(g)) };
}
// Zustand des Abschnitts "Beteiligte": zuletzt hinzugefügte Person (kurz
// hervorgehoben) und die zuletzt entfernte samt ihrer Zuordnung, damit
// "Rückgängig" sie wiederherstellen kann.
let einstNeu = '';
let einstEntfernt = null;
let einstToastTimer = null;
// renderEinstTeam: Besetzungsleiste, Sperrhinweis, Suchfeld und die
// Personenliste nach Gewerk (Eriks Wahl "B"). Keine Rückgabe.
function renderEinstTeam(p, locked){
if(!p.assign) p.assign = {};
const personen = einstTeamPersonen();
const { projGewerke, rang } = einstGewerkRang(p);
const sperre = document.getElementById('planEinstSperre');
if(sperre) sperre.hidden = !locked;
const sucheWrap = document.getElementById('planEinstSucheWrap');
if(sucheWrap) sucheWrap.hidden = locked;
// Besetzungsleiste: je Rolle, wer die Hauptverantwortung trägt.
const besetzung = document.getElementById('planEinstBesetzung');
if(besetzung){
besetzung.innerHTML = ROLES.map(r => {
const haupt = personen.filter(e => p.assign[assignKey(e.person.id, r)] === 1);
const neben = personen.filter(e => p.assign[assignKey(e.person.id, r)] === 2);
const fehlt = haupt.length === 0;
const text = fehlt
? (neben.length ? 'nur Unterstützung, Haupt fehlt' : 'niemand zugeordnet')
: haupt.map(e => einstNachname(e.person)).join(', ') + (neben.length ? ' + ' + neben.length + ' Unterst.' : '');
return `<div class="plan-einst-besetzt${fehlt ? ' ist-luecke' : ''}"><b>${esc(r)}</b><span>${esc(text)}</span></div>`;
}).join('');
}
const el = document.getElementById('planAssignMatrix');
if(!el) return;
const drin = personen.filter(e => einstBeteiligt(p, e.person.id));
if(!drin.length){
el.innerHTML = personen.length
? `<div class="plan-einst-leer"><b>Noch niemand zugeordnet</b><span>Über das Suchfeld Personen hinzufügen. Vorgeschlagen werden zuerst die Mitarbeiter aus den Gewerken des Projekts.</span></div>`
: '<div class="plan-einst-leer"><b>Niemand verfügbar</b><span>In dieser Sitzung ist niemand mit einer Fachrolle hinterlegt (Admin → Benutzerverwaltung).</span></div>';
} else {
const gruppen = new Map();
drin.forEach(e => {
const g = personGewerk(e.person) || '';
if(!gruppen.has(g)) gruppen.set(g, []);
gruppen.get(g).push(e);
});
el.innerHTML = [...gruppen.keys()].sort((x, y) => rang(x) - rang(y)).map(g => {
const liste = gruppen.get(g).sort((a, b) => einstName(a.person).localeCompare(einstName(b.person), 'de'));
const titel = g === '' ? 'Ohne Gewerk' : ((typeof GEWERK_LABELS === 'object' && GEWERK_LABELS[g]) || '')
+ (projGewerke.indexOf(g) === -1 ? ', kein Gewerk des Projekts' : '');
return `<div class="plan-einst-gruppe">
${g ? `<span class="plan-gewerk-schild">${esc(g)}</span>` : ''}
<span class="plan-gewerk-titel">${esc(titel)}</span>
<span class="plan-gewerk-linie"></span>
<span class="plan-gewerk-anzahl">${liste.length === 1 ? '1 Person' : liste.length + ' Personen'}</span>
</div>` + liste.map(e => {
const id = e.person.id;
const name = einstName(e.person);
const kapseln = ROLES.map(r => {
const hat = e.rollen.has(r);
const st = p.assign[assignKey(id, r)] || 0;
const art = st === 1 ? 'ist-haupt' : (st === 2 ? 'ist-neben' : '');
const titel = !hat ? `${name} hat die Rolle ${r} nicht` : `${r}: ${st === 1 ? 'Hauptverantwortung' : st === 2 ? 'Unterstützung' : 'nicht beteiligt'}`;
return `<button type="button" class="plan-einst-kapsel ${art}${hat ? '' : ' ist-fremd'}" aria-pressed="${st > 0}"
${!hat || locked ? 'disabled' : ''} title="${esc(titel)}"
onclick="planCycleAssign('${esc(id)}','${esc(r)}')">${esc(r)}</button>`;
}).join('');
return `<div class="plan-einst-zeile${einstNeu === id ? ' ist-neu' : ''}">
<span class="plan-einst-person"><span class="plan-einst-ava">${esc(e.person.name)}</span><b>${esc(name)}</b></span>
<span class="plan-einst-kapseln" role="group" aria-label="Rollen von ${esc(name)}">${kapseln}</span>
${locked ? '' : `<button type="button" class="plan-einst-weg" title="Aus dem Projekt entfernen" aria-label="${esc(name)} aus dem Projekt entfernen" onclick="einstEntfernen('${esc(id)}')">×</button>`}
</div>`;
}).join('');
}).join('');
}
const toast = document.getElementById('planEinstToast');
if(toast){
toast.hidden = !einstEntfernt || locked;
toast.innerHTML = einstEntfernt
? `<span>${esc(einstEntfernt.name)} aus dem Projekt entfernt</span><button type="button" onclick="einstRueckgaengig()">Rückgängig</button>` : '';
}
}
// einstNachAenderung: speichern und alles nachziehen, was die Zuordnung
// zeigt (dieselben Wege wie planCycleAssign()).
function einstNachAenderung(){
app.savePlan();
renderPlanSettings();
if(typeof renderBoard === 'function') renderBoard();
if(typeof renderWochenbericht === 'function') renderWochenbericht();
}
// einstEntfernen: nimmt eine Person mit allen Rollen aus dem Projekt und
// merkt sich die Zuordnung für "Rückgängig" (8 Sekunden).
window.einstEntfernen = function(personId){
const p = app.project;
if(!p || app.stammGesperrt()) return;
const eintrag = {};
ROLES.forEach(r => {
const key = assignKey(personId, r);
if(p.assign[key]) { eintrag[key] = p.assign[key]; delete p.assign[key]; }
});
const e = einstTeamPersonen().find(x => x.person.id === personId);
const person = e ? e.person : ((app.week.people || []).find(x => x.id === personId) || { name: personId });
einstEntfernt = { name: einstName(person), eintrag };
einstNeu = '';
clearTimeout(einstToastTimer);
einstToastTimer = setTimeout(() => { einstEntfernt = null; const t = document.getElementById('planEinstToast'); if(t) t.hidden = true; }, 8000);
einstNachAenderung();
};
window.einstRueckgaengig = function(){
const p = app.project;
if(!p || !einstEntfernt || app.stammGesperrt()) return;
Object.assign(p.assign, einstEntfernt.eintrag);
const key = Object.keys(einstEntfernt.eintrag)[0];
einstNeu = key ? key.split('|')[0] : '';
einstEntfernt = null;
clearTimeout(einstToastTimer);
einstNachAenderung();
};
// Suchfeld "Person hinzufügen": Vorschläge aus allen Personen mit
// Fachrolle, die noch nicht beteiligt sind — erst die Gewerke des
// Projekts, dann die übrigen. Gerendert wird nur die Vorschlagsliste,
// damit das Eingabefeld beim Tippen seinen Fokus behält.
function einstVorschlaege(){
const p = app.project;
const feld = document.getElementById('planEinstSuche');
const s = (feld ? feld.value : '').trim().toLowerCase();
const { projGewerke } = einstGewerkRang(p);
const frei = einstTeamPersonen()
.filter(e => !einstBeteiligt(p, e.person.id))
.filter(e => !s || einstName(e.person).toLowerCase().indexOf(s) !== -1 || String(e.person.name || '').toLowerCase().indexOf(s) !== -1)
.sort((a, b) => einstName(a.person).localeCompare(einstName(b.person), 'de'));
const eigen = frei.filter(e => projGewerke.indexOf(personGewerk(e.person)) !== -1);
const andere = frei.filter(e => projGewerke.indexOf(personGewerk(e.person)) === -1);
return [['Aus den Gewerken des Projekts', eigen], ['Andere Gewerke', andere]].filter(([, l]) => l.length);
}
window.einstSucheZeigen = function(){
const box = document.getElementById('planEinstVorschlaege');
if(!box || !app.project || app.stammGesperrt()) return;
const gruppen = einstVorschlaege();
if(!gruppen.length){
const feld = document.getElementById('planEinstSuche');
box.innerHTML = feld && feld.value.trim() ? '<div class="plan-einst-vorschlag-leer">Niemand gefunden.</div>' : '';
box.hidden = !box.innerHTML;
return;
}
box.innerHTML = gruppen.map(([titel, liste]) => `<div class="plan-einst-vorschlag-titel">${esc(titel)}</div>`
+ liste.map(e => `<button type="button" class="plan-einst-vorschlag" onmousedown="event.preventDefault()"
onclick="einstHinzufuegen('${esc(e.person.id)}')">
<span class="plan-einst-ava">${esc(e.person.name)}</span>
<span><b>${esc(einstName(e.person))}</b><span>${esc([personGewerk(e.person), ROLES.filter(r => e.rollen.has(r)).join(', ')].filter(Boolean).join(', '))}</span></span>
</button>`).join('')).join('');
box.hidden = false;
};
window.einstSucheSchliessen = function(){
setTimeout(() => { const box = document.getElementById('planEinstVorschlaege'); if(box) box.hidden = true; }, 120);
};
// Enter übernimmt den ersten Vorschlag, Esc schließt die Liste.
window.einstSucheTaste = function(event){
if(event.key === 'Escape'){ const box = document.getElementById('planEinstVorschlaege'); if(box) box.hidden = true; return; }
if(event.key !== 'Enter') return;
event.preventDefault();
const erste = einstVorschlaege()[0];
if(erste && erste[1][0]) window.einstHinzufuegen(erste[1][0].person.id);
};
// einstHinzufuegen: nimmt eine Person mit ihrer ersten Fachrolle als
// Hauptverantwortung auf; weitere Rollen setzt man über die Kapseln.
window.einstHinzufuegen = function(personId){
const p = app.project;
if(!p || app.stammGesperrt()) return;
const e = einstTeamPersonen().find(x => x.person.id === personId);
if(!e) return;
const rolle = ROLES.find(r => e.rollen.has(r));
if(!rolle) return;
if(!p.assign) p.assign = {};
p.assign[assignKey(personId, rolle)] = 1;
einstNeu = personId;
einstEntfernt = null;
const feld = document.getElementById('planEinstSuche');
if(feld) feld.value = '';
const box = document.getElementById('planEinstVorschlaege');
if(box) box.hidden = true;
einstNachAenderung();
};
// renderEinstUebersicht: die Kurzfassung unter jedem Abschnitt der
// Unternavigation und welcher Abschnitt sichtbar ist.
function renderEinstUebersicht(){
const p = app.project;
if(!p) return;
const sandbox = app.isSandboxPlan();
const setze = (id, text) => { const el = document.getElementById(id); if(el) el.textContent = text; };
const gewerke = Array.isArray(p.gewerke) ? p.gewerke : [];
const amtierend = Array.isArray(p.aktuelleLP) && p.aktuelleLP[0] ? p.aktuelleLP[0] : '';
setze('einstSumAllgemein', sandbox ? [p.name || '', amtierend].filter(Boolean).join(' · ')
: [p.nr || 'ohne Nummer', gewerke.join(', ') || 'kein Gewerk', amtierend].filter(Boolean).join(' · '));
const honorar = Number(p.honorar) || 0;
const honorarText = honorar > 0 ? honorar.toLocaleString('de-DE') + ' €' : 'kein Honorar';
const status = (typeof AUFTRAGSSTATUS !== 'undefined' && typeof auftragsstatusOf === 'function')
? (AUFTRAGSSTATUS.find(s => s.key === auftragsstatusOf(p)) || {}).label : '';
setze('einstSumAuftrag', sandbox ? honorarText : [status, honorarText].filter(Boolean).join(' · '));
const beauftragt = Array.isArray(p.beauftragteLP) ? p.beauftragteLP.length : 0;
const laeuft = Array.isArray(p.aktuelleLP) && p.aktuelleLP[0] ? ' · läuft: ' + p.aktuelleLP[0] : '';
setze('einstSumPhasen', `${beauftragt} von ${LPHS.length} beauftragt${laeuft}`);
const ids = new Set(Object.keys(p.assign || {}).filter(k => p.assign[k] > 0).map(k => k.split('|')[0]));
const unbesetzt = gewerke.filter(g => !einstTeamPersonen().some(e => ids.has(e.person.id) && personGewerk(e.person) === g));
setze('einstSumTeam', (ids.size === 1 ? '1 Person' : ids.size + ' Personen') + (unbesetzt.length ? ' · ' + unbesetzt.join(', ') + ' unbesetzt' : ''));
window.zeigeEinstTeil(einstTeil);
}
window.renderPlanSettings = function(){
const p = app.project;
if(!p) return;
const locked = app.isLocked();
const locked = app.stammGesperrt();
const sandbox = app.isSandboxPlan();
const lockHint = document.getElementById('planSettingsLockHint');
@@ -62,16 +337,20 @@
// Felder, die nur bei einem echten Projekt Bedeutung haben (Auftrags-
// Workflow, Dateien-Zuordnung über die Nummer, Gewerke-Filter des
// Wochenberichts, Auslastungs-Ersatzgröße) — für die Planung-Sandbox
// Wochenberichts) — für die Planung-Sandbox
// ausgeblendet, siehe CLAUDE.md "Planung — projektunabhängige
// Terminplan-Sandbox". Honorar + beauftragte Leistungsphasen (Grundlage
// des Terminplan-Rechners) und die Beteiligten-Matrix bleiben sichtbar.
['settingsProjNr', 'settingsAuftragsstatus', 'settingsGewerke', 'settingsGroesse']
['settingsProjNr', 'settingsAuftragsstatus', 'settingsGewerke']
.forEach(id => {
const el = document.getElementById(id);
const row = el && el.closest('.form-row');
if(row) row.style.display = sandbox ? 'none' : '';
});
['settingsProjNrHinweis'].forEach(id => {
const el = document.getElementById(id);
if(el) el.hidden = sandbox;
});
// Projektnummer und -name: das fokussierte Feld nicht überschreiben
// (dieselbe Vorsichtsmaßnahme wie beim Honorar weiter unten).
@@ -86,25 +365,6 @@
nameEl.disabled = locked;
}
// Projektgröße: Sie ist der Ersatz für ein fehlendes Honorar, deshalb
// steht unter dem Regler, ob sie gerade überhaupt zählt. Eine Zahl, die
// wirkungslos ist, ohne dass man es sieht, wäre schlimmer als keine.
const groesseEl = document.getElementById('settingsGroesse');
if(groesseEl){
const g = Number(p.groesse) || 0;
if(document.activeElement !== groesseEl) groesseEl.value = g > 0 ? g : '';
groesseEl.disabled = locked;
const hinweisEl = document.getElementById('settingsGroesseHinweis');
if(hinweisEl){
const honorar = Number(p.honorar) || 0;
hinweisEl.textContent = honorar > 0
? 'Ein Honorar ist hinterlegt — die Auslastung rechnet damit, die Größe bleibt unberücksichtigt.'
: (g > 0
? 'Kein Honorar hinterlegt: Die Auslastung rechnet mit dieser Größe. 0 = keine Angabe, 1 = großes Projekt.'
: 'Ohne Honorar und ohne Größe fließt dieses Projekt in keine Auslastung ein. 0 = keine Angabe, 1 = großes Projekt.');
}
}
// Schlagwörter: dieselbe Chip-Liste wie im früheren Projekte-Tab.
const tagsEl = document.getElementById('settingsProjTags');
if(tagsEl){
@@ -124,11 +384,12 @@
const gewerkeEl = document.getElementById('settingsGewerke');
if(gewerkeEl && typeof GEWERK_LABELS === 'object'){
const gesetzt = new Set(Array.isArray(p.gewerke) ? p.gewerke : []);
gewerkeEl.innerHTML = Object.keys(GEWERK_LABELS).map(g => `<label class="beauftragte-lp-item" title="${esc(GEWERK_LABELS[g])}">
<input type="checkbox" value="${esc(g)}" ${gesetzt.has(g) ? 'checked' : ''} ${locked ? 'disabled' : ''}
onchange="togglePlanGewerk('${esc(g)}', this.checked)">
${esc(g)}
</label>`).join('');
// Als Umschalt-Knöpfe (Einstellungen "C"): angetippt = gesetzt.
gewerkeEl.innerHTML = Object.keys(GEWERK_LABELS).map(g => {
const an = gesetzt.has(g);
return `<button type="button" class="plan-einst-chip${an ? ' an' : ''}" aria-pressed="${an}" title="${esc(GEWERK_LABELS[g])}"
${locked ? 'disabled' : ''} onclick="togglePlanGewerk('${esc(g)}', ${!an})">${esc(g)}</button>`;
}).join('');
}
// Auftragsstatus: Das Auswahlfeld kommt fertig aus js/auftrag.js, damit
@@ -139,8 +400,6 @@
statusEl.innerHTML = auftragsstatusSelectHtml(p, { disabled: locked });
}
const aufwandBtn = document.getElementById('settingsAufwandBtn');
if(aufwandBtn) aufwandBtn.disabled = locked || typeof openLpModal !== 'function';
// Honorar: das fokussierte Feld nicht überschreiben, sonst springt beim
// Tippen der Cursor (gleiche Vorsichtsmaßnahme wie im Honorar-Dialog).
@@ -152,36 +411,32 @@
honorarEl.disabled = locked;
}
// Beauftragte Leistungsphasen: dieselbe Kreuzchen-Liste wie im
// Honorar-Dialog, gespeichert über toggleBeauftragteLp() (plan-honorar.js).
// Beauftragte Leistungsphasen: je Phase eine Zeile mit Umschalter
// "Beauftragt | Nicht beauftragt", die laufende Phase trägt "läuft".
// Gespeichert über toggleBeauftragteLp() (plan-honorar.js).
const lpEl = document.getElementById('settingsBeauftragteLp');
if(lpEl){
const gesetzt = new Set(Array.isArray(p.beauftragteLP) ? p.beauftragteLP : []);
lpEl.innerHTML = LPHS.map(l => `<label class="beauftragte-lp-item">
<input type="checkbox" value="${esc(l)}" ${gesetzt.has(l) ? 'checked' : ''} ${locked ? 'disabled' : ''}
onchange="toggleBeauftragteLp('${esc(l)}', this.checked)">
${esc(l)}
</label>`).join('');
const laeuft = Array.isArray(p.aktuelleLP) ? p.aktuelleLP[0] : '';
lpEl.innerHTML = LPHS.map(l => {
const an = gesetzt.has(l);
return `<div class="plan-einst-phase${an ? ' ist-an' : ''}">
<span class="plan-einst-phase-name"><b>${esc(l)}</b><span>${esc(LPH_BEZEICHNUNG[l] || '')}</span>${l === laeuft ? '<em>läuft</em>' : ''}</span>
<span class="plan-einst-seg" role="group" aria-label="${esc(l)}">
<button type="button" class="${an ? 'an' : ''}" ${locked ? 'disabled' : ''} onclick="toggleBeauftragteLp('${esc(l)}', true)">Beauftragt</button>
<button type="button" class="${an ? '' : 'an'}" ${locked ? 'disabled' : ''} onclick="toggleBeauftragteLp('${esc(l)}', false)">Nicht beauftragt</button>
</span>
</div>`;
}).join('');
}
// Beteiligte — Stammdatum, hier editierbar (siehe Kopfkommentar). Nicht
// zugeordnete Personen erscheinen blass, damit erkennbar bleibt, wer zur
// Auswahl stünde; ein Klick schaltet den Zustand durch (planCycleAssign()).
const assignMatrix = document.getElementById('planAssignMatrix');
if(assignMatrix){
const grouped = peopleByRole(app.week);
if(!p.assign) p.assign = {};
assignMatrix.innerHTML = ROLES.map(r => {
const entries = grouped[r] || [];
const chips = entries.length ? entries.map(entry => {
const state = p.assign[assignKey(entry.person.id, r)] || 0;
const click = locked ? '' : `onclick="planCycleAssign('${entry.person.id}','${r}')"`;
const title = (entry.person.fullName || entry.person.name) + ' · ' + r;
return `<span class="board-avatar ${app.planRoleClass(r)} state-${state}${locked ? ' locked-cell' : ''}" ${click} title="${esc(title)}">${esc(entry.person.name)}</span>`;
}).join('') : `<span class="hint" style="margin:0">Niemand hinterlegt</span>`;
return `<div class="board-role-row"><span class="board-role-label ${app.planRoleClass(r)}">${esc(r)}</span><div class="board-role-chips">${chips}</div></div>`;
}).join('');
}
if(assignMatrix) renderEinstTeam(p, locked);
renderEinstUebersicht();
};
// planCycleAssign: wechselt die Zuordnung einer Person zu DIESEM Projekt
@@ -191,7 +446,7 @@
// Rückgabe.
window.planCycleAssign = function(personId, role){
const p = app.project;
if(!p || app.isLocked()) return;
if(!p || app.stammGesperrt()) return;
if(!p.assign) p.assign = {};
const key = assignKey(personId, role);
const cur = p.assign[key] || 0;
@@ -215,7 +470,7 @@
// Dateien nicht mehr; deshalb die Rückfrage. Parameter: val. Keine Rückgabe.
window.setPlanProjNr = function(val){
const p = app.project;
if(!p || app.isLocked()) return;
if(!p || app.stammGesperrt()) return;
const neuVal = (val || '').trim();
if(neuVal === (p.nr || '')) return;
if(p.nr && !confirm('Die Projektnummer "' + p.nr + '" wird zu "' + neuVal + '".\n\n'
@@ -231,7 +486,7 @@
// setPlanProjName: ändert die Projektbezeichnung. Parameter: val.
window.setPlanProjName = function(val){
const p = app.project;
if(!p || app.isLocked()) return;
if(!p || app.stammGesperrt()) return;
p.name = (val || '').trim();
app.savePlan();
// Die Bezeichnung steht an drei weiteren Stellen: in der Kopfzeile des
@@ -249,28 +504,12 @@
renderPlanSettings();
};
// setPlanGroesse: übernimmt die Projektgröße (0–1). Gespeichert wird erst
// beim Loslassen des Reglers, nicht bei jedem Zwischenwert — sonst liefe
// ein Zug über die Skala als zwanzig Speichervorgänge auf den Server.
// Parameter: val. Keine Rückgabe.
window.setPlanGroesse = function(val){
const p = app.project;
if(!p || app.isLocked()) return;
const n = Math.max(0, Math.min(1, parseFloat(val) || 0));
if(n === (Number(p.groesse) || 0)) return;
p.groesse = n;
app.savePlan();
renderPlanSettings();
// Die Auslastung rechnet mit diesem Wert.
if(typeof renderLoad === 'function' && document.getElementById('loadGrid')) renderLoad();
};
// planTagKeydown: übernimmt ein Schlagwort mit Enter. Parameter: event.
window.planTagKeydown = function(event){
if(event.key !== 'Enter') return;
event.preventDefault();
const p = app.project;
if(!p || app.isLocked()) return;
if(!p || app.stammGesperrt()) return;
const tag = (event.target.value || '').trim();
if(!tag) return;
if(!Array.isArray(p.tags)) p.tags = [];
@@ -283,7 +522,7 @@
// planRemoveTag: entfernt ein Schlagwort. Parameter: tag.
window.planRemoveTag = function(tag){
const p = app.project;
if(!p || app.isLocked() || !Array.isArray(p.tags)) return;
if(!p || app.stammGesperrt() || !Array.isArray(p.tags)) return;
p.tags = p.tags.filter(t => t !== tag);
app.savePlan();
renderPlanSettings();
@@ -296,7 +535,7 @@
// GEWERK_LABELS (js/projekt-wizard.js), damit sie nicht bei jeder Änderung
// durcheinandergerät. Parameter: gewerk, checked (Boolean).
window.togglePlanGewerk = function(gewerk, checked){
if(app.isLocked()) return;
if(app.stammGesperrt()) return;
const p = app.project;
const set = new Set(Array.isArray(p.gewerke) ? p.gewerke : []);
if(checked) set.add(gewerk); else set.delete(gewerk);
@@ -304,21 +543,4 @@
app.savePlan();
renderPlanSettings();
};
// planOpenAufwand: öffnet die Aufwands-Gewichtungsmatrix. Der Dialog selbst
// liegt in js/load/load.js (openLpModal) und wird hier nur aufgerufen —
// er gehörte immer zum Projekt, stand aber bisher nur im Projekte-Tab.
window.planOpenAufwand = function(){
const p = app.project;
if(!p || typeof openLpModal !== 'function') return;
// openLpModal() sucht das Projekt in currentWeek(). Zeigt der
// Projektbereich eine ANDERE Sitzung, findet es dort nichts und täte
// stillschweigend nichts — deshalb hier der ausdrückliche Hinweis.
const w = typeof currentWeek === 'function' ? currentWeek() : null;
if(!w || !Array.isArray(w.projects) || !w.projects.some(x => x.id === p.id)){
app.showToast('Der Aufwand lässt sich nur in der laufenden Sitzung bearbeiten.');
return;
}
openLpModal(p.id);
};
})();