// ----------------------------- // 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. 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(''); } // 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 `${esc(entry.person.name)}`; }).join('') : `Niemand hinterlegt`; return `