All new
alle new
This commit is contained in:
1 parent
da64472251
commit
5b20adee48
1605 files changed
+396517
-1674
No files matched your search
@@ -0,0 +1,271 @@
|
||||
// -----------------------------
|
||||
// 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);
|
||||
};
|
||||
})();
|
||||
Reference in new issue
Block a user