Beteiligte in die Projekteinstellungen, TODOs aus Protokoll, Bewegung
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>
This commit is contained in:
1 parent
6ba93b2a92
commit
93505e0e48
17 files changed
+1883
-334
No files matched your search
@@ -281,7 +281,16 @@
|
||||
// Terminplan nicht mit leeren Zeilen ohne Balken zustellen. Nicht
|
||||
// jede Aufgabe aus "Aufgaben" landet also im GANTT, wohl aber jede
|
||||
// Aufgabe aus dem GANTT in "Aufgaben" (dieselbe Liste, siehe oben).
|
||||
if(todo.start && todo.end){
|
||||
// UND showInGantt darf nicht ausdrücklich false sein — die Checkbox
|
||||
// "Im Terminplan anzeigen" im Aufgaben-Dialog (plan-modals.js) blendet
|
||||
// eine terminierte Aufgabe trotz gültigem Zeitraum gezielt aus dem
|
||||
// GANTT aus (Erik: ein einzelner "TODOs aus Protokoll"-Import,
|
||||
// plan-protokoll-todos.js, legt leicht 15-25 datierte Aufgaben auf
|
||||
// einmal an, die sonst alle zwingend als Balken erschienen). Fehlt
|
||||
// das Feld (ältere Aufgaben vor dieser Spalte), gilt weiterhin
|
||||
// sichtbar — !== false statt === true, damit kein Bestandsverhalten
|
||||
// kippt.
|
||||
if(todo.start && todo.end && todo.showInGantt !== false){
|
||||
const todoRow = buildTodoRow(todo, tIdx);
|
||||
insertAfter.parentNode.insertBefore(todoRow, insertAfter.nextSibling);
|
||||
insertAfter = todoRow;
|
||||
|
||||
@@ -113,11 +113,17 @@
|
||||
const plan = app.ensurePlanLocal();
|
||||
const todos = Array.isArray(plan.todos) ? plan.todos : [];
|
||||
const milestones = Array.isArray(plan.milestones) ? plan.milestones : [];
|
||||
// Nur terminierte Aufgaben (Start UND Ende gesetzt) haben überhaupt eine
|
||||
// eigene GANTT-Zeile (siehe plan-build-gantt.js) und damit einen
|
||||
// Ziehgriff — Aufgaben ohne Termin bleiben an ihrer Stelle
|
||||
// in plan.todos stehen, sie können hier gar nicht bewegt worden sein.
|
||||
const scheduledIds = new Set(todos.filter(t => t.start && t.end).map(t => t.id));
|
||||
// Nur terminierte Aufgaben (Start UND Ende gesetzt) MIT showInGantt !==
|
||||
// false haben überhaupt eine eigene GANTT-Zeile (exakt dieselbe
|
||||
// Bedingung wie in plan-build-gantt.js) und damit einen Ziehgriff —
|
||||
// Aufgaben ohne Termin oder bewusst aus dem GANTT ausgeblendete bleiben
|
||||
// an ihrer Stelle in plan.todos stehen, sie können hier gar nicht bewegt
|
||||
// worden sein. Ohne diesen zweiten Teil der Bedingung würde scheduledIds
|
||||
// mehr Einträge zählen als tatsächlich .gantt-todo-Zeilen im DOM stehen,
|
||||
// und der Sicherheitscheck unten (todoRowIds.length !== scheduledIds.size)
|
||||
// würde JEDE Umsortierung stillschweigend verwerfen, sobald auch nur eine
|
||||
// Aufgabe ausgeblendet ist.
|
||||
const scheduledIds = new Set(todos.filter(t => t.start && t.end && t.showInGantt !== false).map(t => t.id));
|
||||
|
||||
// Alle beweglichen Zeilen in aktueller DOM-Reihenfolge
|
||||
const rows = [...chart.querySelectorAll(DRAGGABLE_GANTT_ROW_SELECTOR)];
|
||||
|
||||
+27
-33
@@ -4,16 +4,15 @@
|
||||
// Dieses Modul füllt die Projektangaben des Projektbereichs. Es unterscheidet
|
||||
// dabei zwei Arten von Angaben, und das ist der Kern der Sache:
|
||||
//
|
||||
// * NUR ANZEIGE — Kommentar der Sitzung (#planStatusEdit), Fortschritt je
|
||||
// Leistungsphase (#planLphList) und die Zuordnung der Beteiligten
|
||||
// (#planAssignMatrix). Diese drei entstehen in der wöchentlichen
|
||||
// * NUR ANZEIGE — Kommentar der Sitzung (#planStatusEdit) und Fortschritt
|
||||
// je Leistungsphase (#planLphList). Beide entstehen in der wöchentlichen
|
||||
// Besprechung und werden ausschließlich im Wochenbericht erfasst. Neben
|
||||
// jeder von ihnen steht ein Verweis dorthin (setzeBerichtVerweis()), der
|
||||
// hinüberwechselt UND dort dieses Projekt einstellt.
|
||||
// * STAMMDATEN, hier pflegbar — Gruppe (#planGroupSelect) und amtierende
|
||||
// Leistungsphase (#planActiveLpSelect), beide im Bereich "Einstellungen".
|
||||
//
|
||||
// Warum: Dieselben drei Angaben waren zuvor an DREI Stellen bearbeitbar —
|
||||
// Warum: Dieselben Angaben waren zuvor an DREI Stellen bearbeitbar —
|
||||
// Projektboard, Wochenbericht und hier. Niemand wusste, welche davon die
|
||||
// richtige ist, und zwei Personen konnten dasselbe Feld an verschiedenen
|
||||
// Stellen ändern. Seitdem hat jede Angabe genau einen Ort, an dem sie
|
||||
@@ -21,11 +20,20 @@
|
||||
// früheren Schreibfunktionen dieses Moduls (planStatusBlur(), setPlanLph(),
|
||||
// setPlanAssign()) sind damit entfallen.
|
||||
//
|
||||
// BEWUSST NICHT hier: Projektnummer, Bezeichnung, Schlagwörter, Honorar und
|
||||
// beauftragte Leistungsphasen. Sie sind ebenfalls Stammdaten, werden aber von
|
||||
// js/plan/plan-settings.js gerendert (Honorar und beauftragte Phasen
|
||||
// geschrieben von setHonorarBetrag()/toggleBeauftragteLp() aus
|
||||
// js/plan/plan-honorar.js).
|
||||
// Die Zuordnung der Beteiligten (p.assign) gehörte früher ebenfalls zu
|
||||
// dieser "nur Anzeige"-Gruppe — auf Eriks ausdrücklichen Wunsch ist sie
|
||||
// seitdem umgekehrt: kein reines Anzeigefeld mehr, sondern ein echtes
|
||||
// Stammdatum, und zwar NUR HIER im Projektbereich editierbar (Bereich
|
||||
// "Einstellungen", gerendert von js/plan/plan-settings.js —
|
||||
// #planAssignMatrix liegt dort im Markup, nicht im Dashboard). Der
|
||||
// Wochenbericht zeigt die Zuordnung seitdem nur noch zum Lesen, mit einem
|
||||
// Verweis hierher.
|
||||
//
|
||||
// BEWUSST NICHT hier: Projektnummer, Bezeichnung, Schlagwörter, Honorar,
|
||||
// beauftragte Leistungsphasen und die Beteiligten-Matrix. Sie sind
|
||||
// Stammdaten und werden von js/plan/plan-settings.js gerendert (Honorar und
|
||||
// beauftragte Phasen geschrieben von setHonorarBetrag()/toggleBeauftragteLp()
|
||||
// aus js/plan/plan-honorar.js, die Beteiligten-Matrix von planCycleAssign()).
|
||||
//
|
||||
// renderPlanInfo() wird von
|
||||
// renderPlanPage() (plan-state.js) nach jedem Laden aufgerufen; Änderungen
|
||||
@@ -128,25 +136,8 @@
|
||||
}).join('');
|
||||
}
|
||||
setzeBerichtVerweis('planLphVerweis', locked);
|
||||
|
||||
// Beteiligte — nur Anzeige. Nicht zugeordnete Personen erscheinen
|
||||
// weiterhin blass, damit erkennbar bleibt, wer überhaupt zur Auswahl
|
||||
// stünde; anklickbar sind sie hier aber nicht mehr.
|
||||
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 title = (entry.person.fullName || entry.person.name) + ' · ' + r;
|
||||
return `<span class="board-avatar ${app.planRoleClass(r)} state-${state} is-ansicht" 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('');
|
||||
}
|
||||
setzeBerichtVerweis('planAssignVerweis', locked);
|
||||
// Beteiligte: siehe js/plan/plan-settings.js (renderPlanSettings()) —
|
||||
// Stammdatum, nicht mehr hier.
|
||||
};
|
||||
|
||||
// setzeBerichtVerweis: füllt einen Hinweis-Platz mit dem Weg zur
|
||||
@@ -202,9 +193,12 @@
|
||||
};
|
||||
|
||||
// Die Schreibfunktionen setPlanLph()/setPlanAssign()/planStatusBlur() sind
|
||||
// entfallen. Fortschritt, Zuordnung und Kommentar werden allein im
|
||||
// Wochenbericht erfasst (siehe den Kopf dieser Datei); hier gab es damit
|
||||
// keinen Aufrufer mehr. Die gleichnamigen Funktionen des Projektboards
|
||||
// (editLph()/cycleAssign()/saveBoardComment() in js/board/board.js) sind
|
||||
// davon unberührt — dort hängt die Erfassung des Wochenberichts dran.
|
||||
// entfallen. Fortschritt und Kommentar werden allein im Wochenbericht
|
||||
// erfasst (siehe den Kopf dieser Datei); hier gab es damit keinen Aufrufer
|
||||
// mehr. Die gleichnamigen Funktionen des Projektboards
|
||||
// (editLph()/saveBoardComment() in js/board/board.js) sind davon
|
||||
// unberührt — dort hängt die Erfassung des Wochenberichts dran. Die
|
||||
// Zuordnung der Beteiligten (ehemals setPlanAssign()) läuft seit ihrer
|
||||
// Verlagerung in die Projekteinstellungen über planCycleAssign()
|
||||
// (js/plan/plan-settings.js) — ebenfalls kein Fall für dieses Modul.
|
||||
})();
|
||||
@@ -397,6 +397,14 @@
|
||||
}
|
||||
refreshTodoDuration();
|
||||
|
||||
// Im Terminplan anzeigen: Vorgabe sichtbar (!== false, nicht === true —
|
||||
// eine Aufgabe ohne das Feld, z. B. aus einem alten Bestand, gilt
|
||||
// weiterhin als sichtbar, kein Bestandsverhalten kippt rückwirkend).
|
||||
// Bei "Speichern & Neu" übernimmt pre.showInGantt den zuletzt gesetzten
|
||||
// Wert (dieselben Rahmendaten wie Zeitraum/Zuständige, s. saveTodoEdit()).
|
||||
const showInGanttEl = document.getElementById('todoEditShowInGantt');
|
||||
if(showInGanttEl) showInGanttEl.checked = isNew ? (pre.showInGantt !== undefined ? pre.showInGantt : true) : currentTodo.showInGantt !== false;
|
||||
|
||||
const storedProgress = isNew ? 0 : (parseInt(currentTodo.progress, 10) || 0);
|
||||
todoProgressMemo = (storedProgress > 0 && storedProgress < 100) ? storedProgress : 50;
|
||||
applyTodoStatus(todoStatusFrom(currentTodo), storedProgress);
|
||||
@@ -563,11 +571,14 @@
|
||||
const assigneesDiv = document.getElementById('todoEditAssignees');
|
||||
const assignees = assigneesDiv ? Array.from(assigneesDiv.querySelectorAll('.person-chip[aria-pressed="true"]')).map(btn => btn.dataset.personId) : [];
|
||||
|
||||
const showInGanttEl = document.getElementById('todoEditShowInGantt');
|
||||
const showInGantt = showInGanttEl ? showInGanttEl.checked : true;
|
||||
|
||||
const plan = app.ensurePlanLocal();
|
||||
if(!Array.isArray(plan.todos)) plan.todos = [];
|
||||
|
||||
if(app.currentTodoIndex === null){
|
||||
plan.todos.push({ id: uid(), title, start, end, done, progress, dependsOn, assignees });
|
||||
plan.todos.push({ id: uid(), title, start, end, done, progress, dependsOn, assignees, showInGantt });
|
||||
} else if(plan.todos[app.currentTodoIndex]){
|
||||
const t = plan.todos[app.currentTodoIndex];
|
||||
t.title = title;
|
||||
@@ -577,6 +588,7 @@
|
||||
t.progress = progress;
|
||||
t.dependsOn = dependsOn;
|
||||
t.assignees = assignees;
|
||||
t.showInGantt = showInGantt;
|
||||
}
|
||||
app.savePlan();
|
||||
// Erst schließen (setzt den Bearbeitungsindex zurück), dann ggf. direkt
|
||||
@@ -584,7 +596,7 @@
|
||||
todoModalSnapshot = todoFormSnapshot();
|
||||
closeTodoModal();
|
||||
renderPlanGantt();
|
||||
if(andNew) openTodoModal(null, { start, end, assignees });
|
||||
if(andNew) openTodoModal(null, { start, end, assignees, showInGantt });
|
||||
};
|
||||
|
||||
// deleteTodoEdit: löscht nach Bestätigung die aktuell bearbeitete Aufgabe,
|
||||
|
||||
@@ -0,0 +1,464 @@
|
||||
// -----------------------------
|
||||
// Projektplan — "TODOs aus Protokoll" (externes PDF per KI)
|
||||
// -----------------------------
|
||||
// Eigenständige Funktion im Bereich "Aufgaben" (+ GANTT-Toolbar, gleichrangig
|
||||
// neben "+ Vorlage"/"+ Terminplan"): ein EXTERNES Besprechungsprotokoll im
|
||||
// PDF-Format wird per KI eingelesen, und daraus werden — in einem zweiten,
|
||||
// eigenen KI-Schritt — Aufgaben für dieses Projekt abgeleitet. Bewusst
|
||||
// KOMPLETT unabhängig von js/plan/plan-protokoll.js: nichts hier liest aus
|
||||
// oder schreibt in project_protokolle/protokoll_punkte, nichts wird auf dem
|
||||
// Server dauerhaft abgelegt (weder das PDF noch die erkannten Punkte) — ein
|
||||
// reines Ad-hoc-Werkzeug wie der Plan Checker, kein Dokumentenarchiv. Wer das
|
||||
// externe Protokoll dauerhaft als Projektunterlage ablegen will, nutzt den
|
||||
// normalen Protokoll-Import (Bereich "Protokolle").
|
||||
//
|
||||
// Ein Dialog (#protokollTodosModal), DREI Schritte, feste Fußzeile
|
||||
// (Zurück/Abbrechen/Weiter) — Entwurfsrichtung "A — Schritt-Dialog" aus dem
|
||||
// Entwurfsvergleich (Taste-Skill), von Erik gewählt: ein sichtbarer
|
||||
// Fortschrittsbalken (#protokollTodosSteps) und derselbe Knopf-Platz über
|
||||
// alle Schritte hinweg, statt je Phase ein eigenes Knopf-Set. Nur der
|
||||
// Inhalt zwischen Fortschrittsbalken und Fußzeile (#protokollTodosBody)
|
||||
// wird je Schritt neu gerendert; Titel/Untertitel/Fortschritt/Fußzeile
|
||||
// bleiben derselbe Rahmen. Der rechte Fußzeilen-Knopf ("Weiter") führt
|
||||
// zugleich die KI-Aufrufe aus — ein Klick macht Schritt und Netzwerkaufruf
|
||||
// in einem:
|
||||
// 0. Hochladen — PDF + KI-Anbieter wählen → "Einlesen" ruft POST
|
||||
// /plan-todos-aus-protokoll/einlesen auf (server.js, nutzt dieselbe
|
||||
// Erkennung wie der Protokoll-Import: erkennePunkteMitKI()/
|
||||
// erkennePunkteAusText()).
|
||||
// 1. Punkte wählen — die erkannten Punkte als Checkliste (alle offenen
|
||||
// standardmäßig angehakt) → "Aufgaben ableiten" ruft POST
|
||||
// /plan-todos-aus-protokoll/aufgaben-vorschlag mit genau diesen
|
||||
// Punkten im Body (kein erneuter Server-Lookup, nichts ist
|
||||
// persistiert).
|
||||
// 2. Aufgaben bestätigen — die Vorschläge als kompakte Zeilenliste
|
||||
// (Entwurfsrichtung "B — Kompakte Zeilen" aus einem zweiten
|
||||
// Taste-Skill-Vergleich, nur für diesen Schritt; siehe
|
||||
// renderVorschlaegeHtml()): Titel/Termin-Abzeichen/Zuständig-Kürzel auf
|
||||
// einen Blick, Beschreibung/Bearbeitung erst nach Antippen der Zeile —
|
||||
// ein echter Testlauf lieferte bereits 19 Vorschläge, als ausführliche
|
||||
// Karten wäre das nur noch Scrollen gewesen. "N Aufgaben einfügen"
|
||||
// erzeugt normale plan_todos MIT Start- UND Enddatum (sonst keine
|
||||
// GANTT-Zeile, siehe CLAUDE.md) und ruft danach fitPlanGanttRange() wie
|
||||
// applyTemplate(), damit die neuen Balken im sichtbaren GANTT-Zeitraum
|
||||
// liegen.
|
||||
// "Zurück" springt ohne erneuten Serveraufruf auf den vorherigen Schritt
|
||||
// zurück (die Daten bleiben im Modul-Zustand); einzige Ausnahme: ein
|
||||
// Datei-Eingabefeld lässt sich aus Sicherheitsgründen nicht per Skript
|
||||
// erneut vorbelegen — wer von Schritt 1 zurück zu Schritt 0 geht, muss die
|
||||
// Datei bei Bedarf erneut auswählen.
|
||||
//
|
||||
// Läuft wie alle plan-Module in einem IIFE; Zustand: window.PlanApp
|
||||
// (plan-state.js).
|
||||
|
||||
(function(){
|
||||
const app = window.PlanApp;
|
||||
|
||||
// Modul-Zustand — lebt nur, während der Dialog offen ist; ein erneutes
|
||||
// Öffnen startet immer wieder bei Schritt 0.
|
||||
let step = 0;
|
||||
let erkanntePunkte = [];
|
||||
let punkteAuswahl = {}; // Index in erkanntePunkte -> angehakt (Boolean), überlebt Zurück/Weiter
|
||||
let gewaehlterAnbieter = '';
|
||||
let ausgewaehltePunkte = []; // die Punkte, die tatsächlich an die KI gingen (für die Quelle-Referenz in Schritt 2)
|
||||
let vorschlaege = [];
|
||||
|
||||
const STEP_TITEL = ['Externes Protokoll einlesen', 'Punkte wählen', 'Aufgaben bestätigen'];
|
||||
|
||||
// openProtokollTodosModal: öffnet den Dialog bei Schritt 0. Bei gesperrter
|
||||
// Sitzung gar nicht erst geöffnet (wie bei den anderen Bearbeitungs-
|
||||
// Dialogen, siehe plan-modals.js).
|
||||
window.openProtokollTodosModal = function(){
|
||||
if(app.isLocked()) return;
|
||||
step = 0;
|
||||
erkanntePunkte = [];
|
||||
punkteAuswahl = {};
|
||||
gewaehlterAnbieter = '';
|
||||
ausgewaehltePunkte = [];
|
||||
vorschlaege = [];
|
||||
renderStep();
|
||||
document.getElementById('protokollTodosModal').style.display = 'flex';
|
||||
};
|
||||
|
||||
window.closeProtokollTodosModal = function(){
|
||||
document.getElementById('protokollTodosModal').style.display = 'none';
|
||||
};
|
||||
|
||||
function zeigeFehler(msg){
|
||||
const el = document.getElementById('protokollTodosError');
|
||||
if(el){ el.textContent = msg; el.hidden = false; }
|
||||
}
|
||||
function verbergeFehler(){
|
||||
const el = document.getElementById('protokollTodosError');
|
||||
if(el){ el.textContent = ''; el.hidden = true; }
|
||||
}
|
||||
// zeigeFallbackHinweis: eigener, bernsteinfarbener Hinweis für einen
|
||||
// GLÜCKTEN, aber degradierten Durchlauf (KI nicht erreichbar, auf die
|
||||
// einfache Übernahme zurückgefallen) — bewusst NICHT dieselbe rote
|
||||
// Fehlermeldung wie zeigeFehler() oben, dieselbe Unterscheidung wie der
|
||||
// Protokoll-Import (.protokoll-ki-fallback-banner, plan-protokoll.js):
|
||||
// ein Fallback ist ein akzeptiertes Ergebnis, kein Fehlschlag.
|
||||
function zeigeFallbackHinweis(msg){
|
||||
const el = document.getElementById('protokollTodosFallback');
|
||||
if(el){ el.textContent = '⚠️ ' + msg; el.hidden = false; }
|
||||
}
|
||||
function verbergeFallbackHinweis(){
|
||||
const el = document.getElementById('protokollTodosFallback');
|
||||
if(el){ el.textContent = ''; el.hidden = true; }
|
||||
}
|
||||
|
||||
// setzeLadezustand: sperrt während eines laufenden KI-Aufrufs den
|
||||
// gesamten Dialog (Fußzeile inklusive, damit kein zweiter Aufruf
|
||||
// überlappt) — ohne das gäbe es während eines länger laufenden Aufrufs
|
||||
// (bis zu KI_TIMEOUT_MS_LOKAL = 30 Minuten bei einem eigenen Server,
|
||||
// server.js) keinerlei Rückmeldung, derselbe Grund wie bei
|
||||
// setzeProtokollImportLadezustand() in plan-protokoll.js.
|
||||
function setzeLadezustand(aktiv, text){
|
||||
const modal = document.getElementById('protokollTodosModal');
|
||||
if(modal) modal.querySelectorAll('input, select, button').forEach(el => { el.disabled = aktiv; });
|
||||
const ladeEl = document.getElementById('protokollTodosLoading');
|
||||
if(ladeEl) ladeEl.hidden = !aktiv;
|
||||
const textEl = document.getElementById('protokollTodosLoadingText');
|
||||
if(textEl && text) textEl.textContent = text;
|
||||
// setzeLadezustand(false) würde sonst auch "Zurück" auf Schritt 0
|
||||
// wieder aktivieren — das macht erst renderStep() richtig.
|
||||
if(!aktiv) renderStep();
|
||||
}
|
||||
|
||||
// anbieterOptionenHtml: baut die Options-Liste des KI-Anbieter-Felds aus
|
||||
// GET /my-ai-settings — derselbe Aufbau wie #protokollImportErkennung in
|
||||
// plan-protokoll.js.
|
||||
function anbieterOptionenHtml(){
|
||||
return fetch(baseUrl() + '/my-ai-settings')
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if(!d.ok) throw new Error('nicht ok');
|
||||
return `<option value="chatgpt">ChatGPT${d.chatgpt && d.chatgpt.configured ? '' : ' (kein Schlüssel hinterlegt)'}</option>
|
||||
<option value="claude">Claude${d.claude && d.claude.configured ? '' : ' (kein Schlüssel hinterlegt)'}</option>`;
|
||||
})
|
||||
.catch(() => '<option value="chatgpt">ChatGPT</option><option value="claude">Claude</option>');
|
||||
}
|
||||
|
||||
// renderStep: zeichnet Titel, Fortschrittsbalken, Fußzeilen-Beschriftung
|
||||
// und den schrittspezifischen Inhalt. Macht keinen Netzwerkaufruf — das
|
||||
// übernimmt protokollTodosWeiter(). Keine Parameter, keine Rückgabe.
|
||||
function renderStep(){
|
||||
verbergeFehler();
|
||||
verbergeFallbackHinweis();
|
||||
document.getElementById('protokollTodosTitle').textContent = STEP_TITEL[step];
|
||||
document.querySelectorAll('#protokollTodosSteps .seg').forEach(s => {
|
||||
s.classList.toggle('done', +s.dataset.i <= step);
|
||||
});
|
||||
document.getElementById('protokollTodosBackBtn').disabled = (step === 0);
|
||||
|
||||
const primaryBtn = document.getElementById('protokollTodosPrimaryBtn');
|
||||
const subtitleEl = document.getElementById('protokollTodosSubtitle');
|
||||
const body = document.getElementById('protokollTodosBody');
|
||||
|
||||
if(step === 0){
|
||||
subtitleEl.textContent = 'PDF hochladen, KI-Anbieter wählen.';
|
||||
primaryBtn.textContent = 'Einlesen';
|
||||
body.innerHTML = `
|
||||
<div class="form-row">
|
||||
<label for="protokollTodosFile">Externes Protokoll (PDF)</label>
|
||||
<input type="file" id="protokollTodosFile" accept=".pdf">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="protokollTodosAnbieter">KI-Anbieter</label>
|
||||
<select id="protokollTodosAnbieter"><option value="chatgpt">ChatGPT</option><option value="claude">Claude</option></select>
|
||||
</div>`;
|
||||
anbieterOptionenHtml().then(html => {
|
||||
const sel = document.getElementById('protokollTodosAnbieter');
|
||||
if(sel){ sel.innerHTML = html; if(gewaehlterAnbieter) sel.value = gewaehlterAnbieter; }
|
||||
});
|
||||
} else if(step === 1){
|
||||
subtitleEl.textContent = erkanntePunkte.length + ' Punkte erkannt — offene sind vorausgewählt.';
|
||||
primaryBtn.textContent = 'Aufgaben ableiten';
|
||||
body.innerHTML = `
|
||||
<div class="todo-depends-list" id="protokollTodosPunkteListe" style="max-height:280px">
|
||||
${erkanntePunkte.map((p, i) => {
|
||||
const checked = punkteAuswahl[i] !== undefined ? punkteAuswahl[i] : (p.status === 'offen');
|
||||
punkteAuswahl[i] = checked;
|
||||
return `<label class="todo-depends-item">
|
||||
<input type="checkbox" value="${i}" ${checked ? 'checked' : ''}>
|
||||
<span style="flex:1">
|
||||
<b>${esc(p.thema || 'Ohne Thema')}</b>
|
||||
${p.text ? `<br><span class="hint" style="margin:0">${esc(String(p.text).slice(0, 160))}</span>` : ''}
|
||||
</span>
|
||||
<span class="hint" style="margin:0">${esc(p.zustaendig || '')}${p.termin ? ' · ' + esc(formatDateDE(parseDateISO(p.termin)) || p.termin) : ''}</span>
|
||||
</label>`;
|
||||
}).join('')}
|
||||
</div>`;
|
||||
document.getElementById('protokollTodosPunkteListe').querySelectorAll('input[type=checkbox]').forEach(cb => {
|
||||
cb.addEventListener('change', () => { punkteAuswahl[cb.value] = cb.checked; });
|
||||
});
|
||||
} else {
|
||||
subtitleEl.textContent = vorschlaege.length + ' Vorschläge aus den gewählten Punkten.';
|
||||
body.innerHTML = renderVorschlaegeHtml();
|
||||
const liste = document.getElementById('protokollTodosVorschlaege');
|
||||
if(liste){
|
||||
// Abwahl blendet die Zeile ab (statt sie zu entfernen) — dieselbe
|
||||
// "noch da, aber erkennbar nicht dabei"-Behandlung wie bei den
|
||||
// Punkten in Schritt 1.
|
||||
liste.addEventListener('change', e => {
|
||||
if(!e.target.classList.contains('pt-vorschlag-check')) return;
|
||||
e.target.closest('.protokoll-todo-vorschlag').classList.toggle('off', !e.target.checked);
|
||||
aktualisierePrimaerKnopf();
|
||||
});
|
||||
// Klick auf die Zeile klappt Beschreibung/Termin/Zuständige auf —
|
||||
// außer der Klick galt der Checkbox oder dem Titel-Eingabefeld
|
||||
// selbst, die sollen nicht zugleich die Zeile zu-/aufklappen.
|
||||
liste.addEventListener('click', e => {
|
||||
if(e.target.closest('.pt-vorschlag-check, .pt-vorschlag-titel')) return;
|
||||
const main = e.target.closest('.pt-row-main');
|
||||
if(main) main.closest('.protokoll-todo-vorschlag').classList.toggle('open');
|
||||
});
|
||||
}
|
||||
aktualisierePrimaerKnopf();
|
||||
}
|
||||
}
|
||||
|
||||
function aktualisierePrimaerKnopf(){
|
||||
const checks = document.querySelectorAll('.pt-vorschlag-check');
|
||||
const n = checks.length ? Array.from(checks).filter(c => c.checked).length : vorschlaege.length;
|
||||
const btn = document.getElementById('protokollTodosPrimaryBtn');
|
||||
btn.textContent = n + ' Aufgabe' + (n === 1 ? '' : 'n') + ' einfügen';
|
||||
btn.disabled = n === 0;
|
||||
const selCount = document.getElementById('protokollTodosSelCount');
|
||||
if(selCount) selCount.innerHTML = `<b>${n}</b> von ${vorschlaege.length} ausgewählt`;
|
||||
}
|
||||
|
||||
// protokollTodosAuswahlAlle: markiert alle Vorschlagszeilen auf einen
|
||||
// Schlag an oder ab ("Alle"/"Keine" neben der Auswahl-Zählung) — bei 19
|
||||
// Vorschlägen (im echten Testlauf beobachtet) sonst 19 Einzelklicks.
|
||||
// Parameter: alle (Boolean). Keine Rückgabe.
|
||||
window.protokollTodosAuswahlAlle = function(alle){
|
||||
document.querySelectorAll('#protokollTodosVorschlaege .pt-vorschlag-check').forEach(cb => {
|
||||
cb.checked = alle;
|
||||
cb.closest('.protokoll-todo-vorschlag').classList.toggle('off', !alle);
|
||||
});
|
||||
aktualisierePrimaerKnopf();
|
||||
};
|
||||
|
||||
// matchePersonenVonName: einfacher Namensabgleich zwischen einem
|
||||
// KI-Textvorschlag ("zustaendig" aus dem externen Protokoll — oft eine
|
||||
// externe Firma, kein Teammitglied) und den diesem Projekt zugeordneten
|
||||
// Personen. Case-insensitive Teilstring auf Name/vollem Namen; kein
|
||||
// Treffer ist der Normalfall und bleibt als reiner Text sichtbar.
|
||||
function matchePersonenVonName(text, kandidaten){
|
||||
const t = String(text || '').trim().toLowerCase();
|
||||
if(!t) return [];
|
||||
const treffer = new Set();
|
||||
kandidaten.forEach(k => {
|
||||
const name = String(k.person.name || '').toLowerCase();
|
||||
const voll = String(k.person.fullName || '').toLowerCase();
|
||||
if((name && t.includes(name)) || (voll && t.includes(voll))) treffer.add(k.person.id);
|
||||
});
|
||||
return Array.from(treffer);
|
||||
}
|
||||
|
||||
// renderVorschlaegeHtml: die Vorschlagsliste aus Entwurfsrichtung
|
||||
// "B — Kompakte Zeilen" (Taste-Skill-Vergleich) — eine Zeile je Vorschlag
|
||||
// (Titel, Termin-Abzeichen, Zuständig-Kürzel), Beschreibung/Termin-Feld/
|
||||
// Zuständigen-Auswahl erst nach Antippen der Zeile sichtbar. Gewählt, weil
|
||||
// ein echter Testlauf bereits 19 Vorschläge aus einem Protokoll lieferte —
|
||||
// als ausführliche Karte wäre das ganze Fenster voll Scrollen gewesen,
|
||||
// als Zeilenliste passt die komplette Übersicht auf einen Blick, ohne
|
||||
// Termin/Zuständig ganz zu verstecken (beide stehen als Abzeichen schon
|
||||
// in der eingeklappten Zeile). Eine Auswahl-Zeile oben
|
||||
// ("N von M ausgewählt" + Alle/Keine) erspart bei vielen Vorschlägen das
|
||||
// einzelne Abwählen.
|
||||
function renderVorschlaegeHtml(){
|
||||
if(vorschlaege.length === 0){
|
||||
return '<p class="hint">Keine Aufgaben vorgeschlagen — die ausgewählten Punkte waren rein informativ.</p>';
|
||||
}
|
||||
// Kandidaten für die Zuständigen-Auswahl: die diesem Projekt bereits
|
||||
// zugeordneten Personen (projectAssignees(), js/gantt.js), einmalig
|
||||
// entdoppelt (eine Person kann in mehreren Rollen zugeordnet sein).
|
||||
const kandidaten = [];
|
||||
const gesehen = new Set();
|
||||
projectAssignees(app.project, app.week).forEach(k => {
|
||||
if(!gesehen.has(k.person.id)){ gesehen.add(k.person.id); kandidaten.push(k); }
|
||||
});
|
||||
|
||||
const zeilen = vorschlaege.map((v, idx) => {
|
||||
const quelle = (v.quellRefs || []).map(r => ausgewaehltePunkte[r] && ausgewaehltePunkte[r].thema).filter(Boolean).join(', ');
|
||||
const vorausgewaehlt = matchePersonenVonName(v.zustaendigVorschlag, kandidaten);
|
||||
const erstePerson = vorausgewaehlt.length ? kandidaten.find(k => k.person.id === vorausgewaehlt[0]) : null;
|
||||
return `<div class="protokoll-todo-vorschlag" data-idx="${idx}">
|
||||
<div class="pt-row-main">
|
||||
<input type="checkbox" class="pt-vorschlag-check" checked>
|
||||
<input type="text" class="pt-vorschlag-titel" value="${esc(v.titel)}">
|
||||
<span class="pt-row-badges">
|
||||
${v.faelligkeit ? `<span class="pt-badge-date">${esc(formatDateDE(parseDateISO(v.faelligkeit)) || v.faelligkeit)}</span>` : ''}
|
||||
${erstePerson ? `<span class="pt-badge-person" title="${esc(erstePerson.person.fullName || erstePerson.person.name)}">${esc(erstePerson.person.name || initialsForName(erstePerson.person.fullName || '') || '?')}</span>` : ''}
|
||||
</span>
|
||||
<span class="pt-chevron">▸</span>
|
||||
</div>
|
||||
<div class="pt-row-detail">
|
||||
${v.beschreibung ? `<div class="hint protokoll-todo-vorschlag-beschreibung">${esc(v.beschreibung)}</div>` : ''}
|
||||
<div class="protokoll-todo-vorschlag-felder">
|
||||
<label class="form-row" style="margin-bottom:6px">
|
||||
<span>Fällig am</span>
|
||||
<input type="date" class="pt-vorschlag-termin" value="${esc(v.faelligkeit)}" style="max-width:180px">
|
||||
</label>
|
||||
<!-- Dieselben .person-chip/.person-chip-ini-Chips mit
|
||||
aria-pressed und toggleTodoAssignee() (js/plan/plan-modals.js)
|
||||
wie im echten TODO-Dialog ("Zuständige") — die erste Fassung
|
||||
hier nutzte stattdessen eine rohe, unformatierte Checkbox
|
||||
neben dem Kürzel, was neben den übrigen, bewusst gestalteten
|
||||
Zeilen schlicht unfertig aussah (Eriks Rückmeldung). -->
|
||||
<div class="person-chips">
|
||||
${kandidaten.length ? kandidaten.map(k => {
|
||||
const kuerzel = k.person.name || initialsForName(k.person.fullName || '') || '?';
|
||||
const voll = k.person.fullName || k.person.name || '';
|
||||
return `<button type="button" class="person-chip pt-vorschlag-person" data-person-id="${esc(k.person.id)}"
|
||||
aria-pressed="${vorausgewaehlt.includes(k.person.id)}" onclick="toggleTodoAssignee(this)">
|
||||
<span class="person-chip-ini">${esc(kuerzel)}</span>${esc(voll)}
|
||||
</button>`;
|
||||
}).join('') : '<span class="hint" style="margin:0">Niemand diesem Projekt zugeordnet.</span>'}
|
||||
</div>
|
||||
${!vorausgewaehlt.length && v.zustaendigVorschlag ? `<span class="hint" style="margin:0">KI-Vorschlag „${esc(v.zustaendigVorschlag)}“ — kein passendes Teammitglied gefunden.</span>` : ''}
|
||||
${quelle ? `<span class="hint" style="margin:0">aus: ${esc(quelle)}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
return `
|
||||
<div class="pt-sel-bar">
|
||||
<span class="pt-sel-count" id="protokollTodosSelCount"></span>
|
||||
<span class="pt-sel-actions">
|
||||
<button type="button" onclick="protokollTodosAuswahlAlle(true)">Alle</button> ·
|
||||
<button type="button" onclick="protokollTodosAuswahlAlle(false)">Keine</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="todo-depends-list" id="protokollTodosVorschlaege" style="max-height:360px">${zeilen}</div>`;
|
||||
}
|
||||
|
||||
// protokollTodosZurueck: ein Schritt zurück, ohne erneuten Serveraufruf —
|
||||
// alle Daten bleiben im Modul-Zustand. Keine Parameter, keine Rückgabe.
|
||||
window.protokollTodosZurueck = function(){
|
||||
if(step === 0) return;
|
||||
step--;
|
||||
renderStep();
|
||||
};
|
||||
|
||||
// protokollTodosWeiter: der rechte Fußzeilen-Knopf führt Schritt UND
|
||||
// Netzwerkaufruf in einem aus — "Einlesen"/"Aufgaben ableiten" lösen den
|
||||
// jeweils nächsten KI-Schritt aus, der letzte Klick übernimmt die
|
||||
// Aufgaben. Keine Parameter, keine Rückgabe.
|
||||
window.protokollTodosWeiter = function(){
|
||||
if(step === 0) return schrittEinlesen();
|
||||
if(step === 1) return schrittAbleiten();
|
||||
return schrittUebernehmen();
|
||||
};
|
||||
|
||||
function schrittEinlesen(){
|
||||
verbergeFehler();
|
||||
const fileInput = document.getElementById('protokollTodosFile');
|
||||
const file = fileInput && fileInput.files && fileInput.files[0];
|
||||
if(!file) return zeigeFehler('Bitte eine PDF-Datei auswählen.');
|
||||
gewaehlterAnbieter = (document.getElementById('protokollTodosAnbieter') || {}).value || '';
|
||||
|
||||
setzeLadezustand(true, 'Wird eingelesen … bei KI-Erkennung über einen eigenen Server kann dies bis zu 30 Minuten dauern.');
|
||||
fetch(baseUrl() + '/plan-todos-aus-protokoll/einlesen?anbieter=' + encodeURIComponent(gewaehlterAnbieter), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': file.type || 'application/pdf' },
|
||||
body: file
|
||||
})
|
||||
.then(r => r.json().then(d => ({ ok: r.ok, d })))
|
||||
.then(({ ok, d }) => {
|
||||
if(!ok || !d.ok) throw new Error((d && d.error) || 'Einlesen fehlgeschlagen.');
|
||||
erkanntePunkte = Array.isArray(d.punkte) ? d.punkte : [];
|
||||
punkteAuswahl = {};
|
||||
if(erkanntePunkte.length === 0){ setzeLadezustand(false); zeigeFehler('Es wurden keine Punkte erkannt.'); return; }
|
||||
step = 1;
|
||||
setzeLadezustand(false);
|
||||
if(d.erkennungsart !== 'chatgpt' && d.erkennungsart !== 'claude' && d.erkennungshinweis){
|
||||
zeigeFallbackHinweis('KI-Erkennung nicht verfügbar (' + d.erkennungshinweis + ') — Punkte per Textmuster erkannt.');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('TODOs aus Protokoll (Einlesen) fehlgeschlagen:', err);
|
||||
setzeLadezustand(false);
|
||||
zeigeFehler(err.message || String(err));
|
||||
});
|
||||
}
|
||||
|
||||
function schrittAbleiten(){
|
||||
verbergeFehler();
|
||||
const listEl = document.getElementById('protokollTodosPunkteListe');
|
||||
const checkedIdx = listEl
|
||||
? Array.from(listEl.querySelectorAll('input[type="checkbox"]:checked')).map(cb => parseInt(cb.value, 10))
|
||||
: [];
|
||||
if(checkedIdx.length === 0) return zeigeFehler('Bitte mindestens einen Punkt auswählen.');
|
||||
|
||||
ausgewaehltePunkte = checkedIdx.map(i => erkanntePunkte[i]);
|
||||
setzeLadezustand(true, 'Aufgaben werden abgeleitet …');
|
||||
fetch(baseUrl() + '/plan-todos-aus-protokoll/aufgaben-vorschlag', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||||
body: JSON.stringify({ anbieter: gewaehlterAnbieter, punkte: ausgewaehltePunkte })
|
||||
})
|
||||
.then(r => r.json().then(d => ({ ok: r.ok, d })))
|
||||
.then(({ ok, d }) => {
|
||||
if(!ok || !d.ok) throw new Error((d && d.error) || 'Ableiten fehlgeschlagen.');
|
||||
vorschlaege = Array.isArray(d.vorschlaege) ? d.vorschlaege : [];
|
||||
step = 2;
|
||||
setzeLadezustand(false);
|
||||
if(d.erkennungsart === 'direkt' && d.erkennungshinweis){
|
||||
zeigeFallbackHinweis('KI-Ableitung nicht verfügbar (' + d.erkennungshinweis + ') — Punkte direkt übernommen, ohne Zusammenfassung.');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('TODOs aus Protokoll (Ableiten) fehlgeschlagen:', err);
|
||||
setzeLadezustand(false);
|
||||
zeigeFehler(err.message || String(err));
|
||||
});
|
||||
}
|
||||
|
||||
// schrittUebernehmen: legt für jede angehakte Vorschlagszeile einen
|
||||
// normalen plan_todo-Eintrag an — IMMER mit gesetztem Start UND Ende,
|
||||
// sonst bleibt die Aufgabe ein reiner Checklisten-Eintrag ohne GANTT-Zeile
|
||||
// (CLAUDE.md: "Ein TODO ist nur dann eine GANTT-Zeile, wenn es BEIDE Daten
|
||||
// hat"). Gleiche Form wie applyTemplate() (plan-templates.js) und
|
||||
// saveTodoAusKommentar() (wochenbericht.js). Kein Netzwerkaufruf hier
|
||||
// selbst — app.savePlan() übernimmt das Speichern wie überall sonst im
|
||||
// Projektbereich.
|
||||
function schrittUebernehmen(){
|
||||
if(app.isLocked()) return;
|
||||
const rows = document.querySelectorAll('#protokollTodosVorschlaege .protokoll-todo-vorschlag');
|
||||
const plan = app.ensurePlanLocal();
|
||||
const heute = formatDateISO(new Date());
|
||||
let anzahl = 0;
|
||||
rows.forEach(row => {
|
||||
const check = row.querySelector('.pt-vorschlag-check');
|
||||
if(!check || !check.checked) return;
|
||||
const titel = ((row.querySelector('.pt-vorschlag-titel') || {}).value || '').trim();
|
||||
if(!titel) return;
|
||||
const faelligkeit = (row.querySelector('.pt-vorschlag-termin') || {}).value || '';
|
||||
const personen = Array.from(row.querySelectorAll('.pt-vorschlag-person[aria-pressed="true"]')).map(el => el.dataset.personId);
|
||||
plan.todos.push({
|
||||
id: uid(),
|
||||
title: titel,
|
||||
start: heute,
|
||||
end: faelligkeit || heute,
|
||||
done: false,
|
||||
progress: 0,
|
||||
dependsOn: [],
|
||||
assignees: personen,
|
||||
});
|
||||
anzahl++;
|
||||
});
|
||||
|
||||
closeProtokollTodosModal();
|
||||
if(anzahl === 0) return;
|
||||
app.savePlan();
|
||||
// Wie applyTemplate(): der sichtbare GANTT-Zeitraum muss die neuen
|
||||
// Balken einschließen, sonst erscheinen sie zwar in der Aufgabenliste,
|
||||
// aber außerhalb des aktuellen Zooms unsichtbar im Terminplan.
|
||||
fitPlanGanttRange();
|
||||
app.showToast(anzahl + ' Aufgabe(n) aus Protokoll angelegt.');
|
||||
}
|
||||
})();
|
||||
@@ -13,7 +13,7 @@
|
||||
// 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)
|
||||
// * 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
|
||||
@@ -24,9 +24,21 @@
|
||||
// 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 bzw. project_beauftragte_lph) —
|
||||
// es gibt dafür keinen eigenen Endpunkt.
|
||||
// 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).
|
||||
@@ -151,6 +163,47 @@
|
||||
${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.
|
||||
|
||||
@@ -249,8 +249,8 @@ window.PlanApp = {
|
||||
// Sitzungen nicht möglich. Die Liste enthält die Knöpfe beider Bereiche:
|
||||
// Terminplan (…Btn) und Aufgaben (…BtnList), siehe App.html.
|
||||
const locked = isLocked();
|
||||
['addMilestoneBtn', 'addTodoBtn', 'templateBtn', 'honorarBtn', 'autoScheduleBtn',
|
||||
'addTodoBtnList', 'addMilestoneBtnList', 'templateBtnList', 'honorarBtnList', 'autoScheduleBtnList']
|
||||
['addMilestoneBtn', 'addTodoBtn', 'templateBtn', 'honorarBtn', 'protokollTodosBtn', 'autoScheduleBtn',
|
||||
'addTodoBtnList', 'addMilestoneBtnList', 'templateBtnList', 'honorarBtnList', 'protokollTodosBtnList', 'autoScheduleBtnList']
|
||||
.forEach(id => {
|
||||
const btn = document.getElementById(id);
|
||||
if(btn) btn.style.display = locked ? 'none' : '';
|
||||
|
||||
Reference in new issue
Block a user