// -----------------------------
// 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 `
`;
}).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();
};
})();