// ----------------------------- // 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, ebenfalls aus plan-info.js) // // 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. // // Gespeichert wird über den normalen Weg app.savePlan() (die Felder sind Teil // des Wochen-JSON, Tabellen projects.honorar 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. window.renderPlanSettings = function(){ const p = app.project; if(!p) return; const locked = app.isLocked(); 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, Auslastungs-Ersatzgröße) — 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'] .forEach(id => { const el = document.getElementById(id); const row = el && el.closest('.form-row'); if(row) row.style.display = sandbox ? 'none' : ''; }); // 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; } // 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){ 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 : []); gewerkeEl.innerHTML = Object.keys(GEWERK_LABELS).map(g => ``).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 }); } 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). const honorarEl = document.getElementById('settingsHonorar'); if(honorarEl){ if(document.activeElement !== honorarEl){ honorarEl.value = Number(p.honorar) > 0 ? p.honorar : ''; } honorarEl.disabled = locked; } // Beauftragte Leistungsphasen: dieselbe Kreuzchen-Liste wie im // Honorar-Dialog, 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 => ``).join(''); } }; // 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.isLocked()) 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.isLocked()) 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(); }; // 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; 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.isLocked() || !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.isLocked()) 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(); }; // 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); }; })();