Files
projektuebersicht/public/js/plan/plan-settings.js
erik 5b20adee48 All new
alle new
2026-10-01 06:46:51 +02:00

272 lines
13 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// -----------------------------
// 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 => `<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 : []);
gewerkeEl.innerHTML = Object.keys(GEWERK_LABELS).map(g => `<label class="beauftragte-lp-item" title="${esc(GEWERK_LABELS[g])}">
<input type="checkbox" value="${esc(g)}" ${gesetzt.has(g) ? 'checked' : ''} ${locked ? 'disabled' : ''}
onchange="togglePlanGewerk('${esc(g)}', this.checked)">
${esc(g)}
</label>`).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 => `<label class="beauftragte-lp-item">
<input type="checkbox" value="${esc(l)}" ${gesetzt.has(l) ? 'checked' : ''} ${locked ? 'disabled' : ''}
onchange="toggleBeauftragteLp('${esc(l)}', this.checked)">
${esc(l)}
</label>`).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);
};
})();