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
+311
-89
@@ -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);
|
||||
};
|
||||
})();
|
||||
Reference in new issue
Block a user