// ----------------------------- // 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 `
${esc(r)}${esc(text)}
`; }).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 ? `
Noch niemand zugeordnetÜber das Suchfeld Personen hinzufügen. Vorgeschlagen werden zuerst die Mitarbeiter aus den Gewerken des Projekts.
` : '
Niemand verfügbarIn dieser Sitzung ist niemand mit einer Fachrolle hinterlegt (Admin → Benutzerverwaltung).
'; } 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 `
${g ? `${esc(g)}` : ''} ${esc(titel)} ${liste.length === 1 ? '1 Person' : liste.length + ' Personen'}
` + 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 ``; }).join(''); return `
${esc(e.person.name)}${esc(name)} ${kapseln} ${locked ? '' : ``}
`; }).join(''); }).join(''); } const toast = document.getElementById('planEinstToast'); if(toast){ toast.hidden = !einstEntfernt || locked; toast.innerHTML = einstEntfernt ? `${esc(einstEntfernt.name)} aus dem Projekt entfernt` : ''; } } // 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() ? '
Niemand gefunden.
' : ''; box.hidden = !box.innerHTML; return; } box.innerHTML = gruppen.map(([titel, liste]) => `
${esc(titel)}
` + liste.map(e => ``).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.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 => `${esc(t)}${locked ? '' : ` ×`}`).join('') + (locked ? '' : ``); } // 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 ``; }).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 `
${esc(l)}${esc(LPH_BEZEICHNUNG[l] || '')}${l === laeuft ? 'läuft' : ''}
`; }).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(); }; })();