- Meine Aufgaben: Projekte in der Reihenfolge der Projektliste, je Konto per Ziehen umsortierbar (users.dashboard_reihenfolge) - Projektliste: Öffnen/Löschen hinter einem "⋯"-Menü - Avatare je Person statt je Rolle; mehrere Rollen als senkrechte Farbstreifen; Titelleiste zieht Rollenänderungen sofort nach - Nach dem Anmelden vollständiges Konto über /whoami laden (Seitenleiste zeigte sonst erst nach Neuladen alle Einträge) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
551 lines
28 KiB
JavaScript
551 lines
28 KiB
JavaScript
// -----------------------------
|
||
// Projektbereich — Einstellungen
|
||
// -----------------------------
|
||
// Bereich "Einstellungen" des Projektbereichs (siehe js/plan/plan-sections.js).
|
||
// Hier stehen die Angaben, die das Projekt als Ganzes betreffen, an einer
|
||
// Stelle zusammen:
|
||
//
|
||
// * Projektnummer, -name und Schlagwörter (hier bearbeitbar — der frühere
|
||
// Projekte-Tab, in dem dieselben Felder ein zweites Mal standen, ist in
|
||
// die Projektliste aufgegangen und zeigt sie nur noch an)
|
||
// * LP-Aufwand (die Gewichtungsmatrix, vormals ebenfalls im Projekte-Tab)
|
||
// * Gruppe und amtierende Leistungsphase (Auswahlfelder; gefüllt und
|
||
// gespeichert von renderPlanInfo()/setPlanGroup()/setPlanActiveLp() in
|
||
// js/plan/plan-info.js — dieses Modul rührt sie nicht an)
|
||
// * Honorar (netto) und die BEAUFTRAGTEN LEISTUNGSPHASEN
|
||
// * Beteiligte (Rollen-/Personen-Matrix, #planAssignMatrix)
|
||
//
|
||
// Honorar und beauftragte Leistungsphasen standen früher ausschließlich im
|
||
// Dialog "Terminplan aus Honorar" (js/plan/plan-honorar.js). Sie sind
|
||
// Stammdaten des Projekts und gehören deshalb in die Projekteinstellungen;
|
||
// im Rechen-Dialog stehen sie weiterhin mit, damit dieser für sich allein
|
||
// bedienbar bleibt. Beide Masken schreiben dieselben Projektfelder
|
||
// (p.honorar, p.beauftragteLP) über setHonorarBetrag()/toggleBeauftragteLp()
|
||
// aus plan-honorar.js und rendern einander nach jeder Änderung neu, sodass
|
||
// sie nicht auseinanderlaufen können.
|
||
//
|
||
// Die Beteiligten-Matrix war früher ein reines Anzeigefeld, gefüllt von
|
||
// plan-info.js, mit einem Verweis in den Wochenbericht — dort entstand die
|
||
// Zuordnung. Auf Eriks ausdrücklichen Wunsch ("In den Einstellungen der
|
||
// Projekte will ich auch die Projektbeteiligten einstellen können") ist das
|
||
// umgekehrt: Zuordnung ist jetzt ein Stammdatum wie Honorar, und zwar NUR
|
||
// HIER editierbar — der Wochenbericht zeigt sie seitdem nur noch zum Lesen
|
||
// (board.js, renderBoardDrawer()) mit einem Verweis hierher. planCycleAssign()
|
||
// unten ersetzt für diesen Zweck cycleAssign() (js/board/board.js): jene
|
||
// Funktion sucht das Projekt über currentWeek() und fände es bei einem
|
||
// Tiefenlink auf eine andere Sitzung oder einem Planung-Sandbox-Plan dort
|
||
// nicht — dieselbe Falle, vor der planOpenAufwand() unten warnt.
|
||
//
|
||
// Gespeichert wird über den normalen Weg app.savePlan() (die Felder sind Teil
|
||
// des Wochen-JSON, Tabellen projects.honorar/project_assign bzw.
|
||
// project_beauftragte_lph) — es gibt dafür keinen eigenen Endpunkt.
|
||
//
|
||
// Läuft wie alle plan-Module in einem IIFE; Zustand: window.PlanApp
|
||
// (plan-state.js).
|
||
|
||
(function(){
|
||
const app = window.PlanApp;
|
||
|
||
// renderPlanSettings: hält den Einstellungen-Bereich auf dem Stand des
|
||
// Projekts. Wird beim Seitenaufbau (renderPlanPage(), plan-state.js), beim
|
||
// 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;
|
||
// Kürzel in der Titelleiste mitziehen — sonst zeigte sie eine geänderte
|
||
// Rolle erst nach dem Neuladen (renderPlanPage() baut sie nur beim Öffnen).
|
||
const kopfTeam = document.getElementById('planPeopleList');
|
||
if(kopfTeam) kopfTeam.innerHTML = app.renderProjectAssignees();
|
||
const locked = app.stammGesperrt();
|
||
const sandbox = app.isSandboxPlan();
|
||
|
||
const lockHint = document.getElementById('planSettingsLockHint');
|
||
if(lockHint) lockHint.style.display = locked ? '' : 'none';
|
||
|
||
// Felder, die nur bei einem echten Projekt Bedeutung haben (Auftrags-
|
||
// Workflow, Dateien-Zuordnung über die Nummer, Gewerke-Filter des
|
||
// 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']
|
||
.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).
|
||
const nrEl = document.getElementById('settingsProjNr');
|
||
if(nrEl && document.activeElement !== nrEl){
|
||
nrEl.value = p.nr || '';
|
||
nrEl.disabled = locked;
|
||
}
|
||
const nameEl = document.getElementById('settingsProjName');
|
||
if(nameEl && document.activeElement !== nameEl){
|
||
nameEl.value = p.name || '';
|
||
nameEl.disabled = locked;
|
||
}
|
||
|
||
// Schlagwörter: dieselbe Chip-Liste wie im früheren Projekte-Tab.
|
||
const tagsEl = document.getElementById('settingsProjTags');
|
||
if(tagsEl){
|
||
const tags = Array.isArray(p.tags) ? p.tags : [];
|
||
tagsEl.innerHTML = tags.map(t => `<span class="tag-chip" data-tag="${esc(t)}">${esc(t)}${locked ? '' :
|
||
` <span class="tag-remove" title="Schlagwort entfernen" onclick="planRemoveTag(this.closest('.tag-chip').dataset.tag)">×</span>`}</span>`).join('')
|
||
+ (locked ? '' : `<input type="text" class="tag-input" placeholder="+ Schlagwort" onkeydown="planTagKeydown(event)">`);
|
||
}
|
||
|
||
// Gewerke: bestimmen, in welcher Gewerke-Besprechung des Wochenberichts
|
||
// dieses Projekt erscheint (berichtProjekte(),
|
||
// js/wochenbericht/wochenbericht.js). Bis zur Einführung dieses Felds
|
||
// ließ sich das Gewerk nur beim Anlegen im Assistenten setzen; ältere
|
||
// Projekte wurden einmalig aus ihrem Abteilungs-Schlagwort übernommen
|
||
// (migrateBackfillProjectGewerkeAusTags(), server.js) — hier lässt sich
|
||
// das jederzeit nachträglich korrigieren.
|
||
const gewerkeEl = document.getElementById('settingsGewerke');
|
||
if(gewerkeEl && typeof GEWERK_LABELS === 'object'){
|
||
const gesetzt = new Set(Array.isArray(p.gewerke) ? p.gewerke : []);
|
||
// 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
|
||
// Beschriftungen und Schreibweg an einer Stelle stehen (dieselbe Maske
|
||
// nutzt der Dialog "Neues Projekt").
|
||
const statusEl = document.getElementById('settingsAuftragsstatus');
|
||
if(statusEl && typeof auftragsstatusSelectHtml === 'function'){
|
||
statusEl.innerHTML = auftragsstatusSelectHtml(p, { disabled: locked });
|
||
}
|
||
|
||
|
||
// Honorar: das fokussierte Feld nicht überschreiben, sonst springt beim
|
||
// Tippen der Cursor (gleiche Vorsichtsmaßnahme wie im Honorar-Dialog).
|
||
const honorarEl = document.getElementById('settingsHonorar');
|
||
if(honorarEl){
|
||
if(document.activeElement !== honorarEl){
|
||
honorarEl.value = Number(p.honorar) > 0 ? p.honorar : '';
|
||
}
|
||
honorarEl.disabled = locked;
|
||
}
|
||
|
||
// 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 : []);
|
||
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) renderEinstTeam(p, locked);
|
||
|
||
renderEinstUebersicht();
|
||
};
|
||
|
||
// planCycleAssign: wechselt die Zuordnung einer Person zu DIESEM Projekt
|
||
// zyklisch durch 0=keine → 1=Haupt → 2=optional → 0 (dieselbe Zustandsfolge
|
||
// wie cycleAssign() in js/board/board.js, hier aber auf app.project statt
|
||
// currentWeek() — siehe Kopfkommentar). Parameter: personId, role. Keine
|
||
// Rückgabe.
|
||
window.planCycleAssign = function(personId, role){
|
||
const p = app.project;
|
||
if(!p || app.stammGesperrt()) return;
|
||
if(!p.assign) p.assign = {};
|
||
const key = assignKey(personId, role);
|
||
const cur = p.assign[key] || 0;
|
||
const next = (cur + 1) % 3;
|
||
if(next === 0) delete p.assign[key]; else p.assign[key] = next;
|
||
app.savePlan();
|
||
renderPlanSettings();
|
||
// Dieselbe Zuordnung erscheint im Projektboard und im Wochenbericht
|
||
// (dort jetzt nur noch zum Lesen) — beide sollen sofort nachziehen,
|
||
// nicht erst beim nächsten vollständigen Neuladen.
|
||
if(typeof renderBoard === 'function') renderBoard();
|
||
if(typeof renderWochenbericht === 'function') renderWochenbericht();
|
||
};
|
||
|
||
// setPlanProjNr: ändert die Projektnummer.
|
||
//
|
||
// Die Nummer ist mehr als eine Beschriftung: Hochgeladene Dateien hängen an
|
||
// (Abteilung, Projektnummer) — nicht an der Projekt-ID, weil die bei jedem
|
||
// Klonen einer Sitzung neu vergeben wird (siehe "File uploads" in
|
||
// CLAUDE.md). Wird die Nummer geändert, findet das Projekt seine bisherigen
|
||
// Dateien nicht mehr; deshalb die Rückfrage. Parameter: val. Keine Rückgabe.
|
||
window.setPlanProjNr = function(val){
|
||
const p = app.project;
|
||
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'
|
||
+ 'Bereits hochgeladene Dateien sind der alten Nummer zugeordnet und erscheinen danach nicht mehr in diesem Projekt.\n\nFortfahren?')){
|
||
renderPlanSettings();
|
||
return;
|
||
}
|
||
p.nr = neuVal;
|
||
app.savePlan();
|
||
renderPlanSettings();
|
||
};
|
||
|
||
// setPlanProjName: ändert die Projektbezeichnung. Parameter: val.
|
||
window.setPlanProjName = function(val){
|
||
const p = app.project;
|
||
if(!p || app.stammGesperrt()) return;
|
||
p.name = (val || '').trim();
|
||
app.savePlan();
|
||
// Die Bezeichnung steht an drei weiteren Stellen: in der Kopfzeile des
|
||
// Projektbereichs, im Eintrag der Seitenleiste und in der Adresse.
|
||
// renderPlanPage() würde alle drei erneuern, ist aber modulintern in
|
||
// plan-state.js und von hier nicht erreichbar — deshalb gezielt.
|
||
const titelEl = document.getElementById('planTitle');
|
||
if(titelEl) titelEl.textContent = p.name;
|
||
if(typeof syncPlanNavItem === 'function') syncPlanNavItem();
|
||
if(typeof updateRouteUrl === 'function' && typeof currentTab !== 'undefined' && currentTab === 'plan'){
|
||
if(window.PlanApp) PlanApp.projName = p.name;
|
||
updateRouteUrl('plan');
|
||
}
|
||
if(typeof renderBoard === 'function') renderBoard();
|
||
renderPlanSettings();
|
||
};
|
||
|
||
// 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.stammGesperrt()) return;
|
||
const tag = (event.target.value || '').trim();
|
||
if(!tag) return;
|
||
if(!Array.isArray(p.tags)) p.tags = [];
|
||
if(!p.tags.includes(tag)) p.tags.push(tag);
|
||
event.target.value = '';
|
||
app.savePlan();
|
||
renderPlanSettings();
|
||
};
|
||
|
||
// planRemoveTag: entfernt ein Schlagwort. Parameter: tag.
|
||
window.planRemoveTag = function(tag){
|
||
const p = app.project;
|
||
if(!p || app.stammGesperrt() || !Array.isArray(p.tags)) return;
|
||
p.tags = p.tags.filter(t => t !== tag);
|
||
app.savePlan();
|
||
renderPlanSettings();
|
||
};
|
||
|
||
// togglePlanGewerk: nimmt ein Gewerk in die Liste der bearbeitenden
|
||
// Fachbereiche auf bzw. entfernt es. Bestimmt, in welcher Gewerke-
|
||
// Besprechung des Wochenberichts dieses Projekt erscheint
|
||
// (berichtProjekte(), js/wochenbericht/wochenbericht.js). Reihenfolge wie
|
||
// 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.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);
|
||
p.gewerke = Object.keys(GEWERK_LABELS).filter(g => set.has(g));
|
||
app.savePlan();
|
||
renderPlanSettings();
|
||
};
|
||
})();
|