Beteiligten-Zuordnung verlagert: p.assign ist jetzt ein Stammdatum und nur noch in den Projekteinstellungen editierbar (planCycleAssign()). Wochenbericht und Projektboard zeigen sie nur noch an, mit Verweis dorthin (springZuEinstellungen()). cycleAssign() ist damit entfallen. Neu "TODOs aus Protokoll": ein externes Besprechungsprotokoll als PDF wird per KI eingelesen und daraus in einem zweiten KI-Schritt Aufgaben abgeleitet. Eigener Bereich in der Aufgaben-/GANTT-Leiste, bewusst unabhaengig von den gespeicherten Protokollen - nichts davon wird abgelegt. Drei Schritte in einem Dialog mit fester Fusszeile; die Vorschlagsliste als kompakte, aufklappbare Zeilen, weil ein echter Import leicht 20 Vorschlaege liefert. Sichtbarkeit je Aufgabe im GANTT: neue Spalte plan_todos.show_in_gantt (Vorgabe 1, idempotente Migration) und eine Checkbox im Aufgaben-Dialog. Eine terminierte Aufgabe kann damit gezielt aus dem Terminplan herausgehalten werden, bleibt sonst aber eine vollwertige Aufgabe. plan-build-gantt.js und plan-drag.js pruefen dieselbe Bedingung, damit Zeilenzahl und Umsortierung nicht auseinanderlaufen. KI-Timeout dritte Stufe: ein abweichender, aber entfernter Endpunkt (Drittanbieter-Proxy) bekommt 2 Minuten statt der 45 Sekunden der echten Anbieter-API. Gemessen: die Punkte-Erkennung eines realen Protokolls brauchte dort 39 Sekunden, lag also stets knapp unter dem alten Limit und brach bei jeder Schwankung ab. Bewegung: Hover- und Klick-Rueckmeldung als Token-Satz in :root, angewandt auf die anklickbaren Zeilen, Schalter und Dialoge. Die Akzentleiste ist ein inset-Schatten statt eines positionierten Elements, damit Drag-and-Drop unberuehrt bleibt; prefers-reduced-motion war bisher gar nicht vorhanden und kippt jetzt einfach die Tokens. Plan Checker: optionaler Projektabgleich des Plankopfs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
325 lines
16 KiB
JavaScript
325 lines
16 KiB
JavaScript
// -----------------------------
|
||
// 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 => `<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('');
|
||
}
|
||
|
||
// 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 `<span class="board-avatar ${app.planRoleClass(r)} state-${state}${locked ? ' locked-cell' : ''}" ${click} title="${esc(title)}">${esc(entry.person.name)}</span>`;
|
||
}).join('') : `<span class="hint" style="margin:0">Niemand hinterlegt</span>`;
|
||
return `<div class="board-role-row"><span class="board-role-label ${app.planRoleClass(r)}">${esc(r)}</span><div class="board-role-chips">${chips}</div></div>`;
|
||
}).join('');
|
||
}
|
||
};
|
||
|
||
// 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.isLocked()) 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.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);
|
||
};
|
||
})();
|